diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000000..6ba6ae82c8
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,18 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "cake.tool": {
+ "version": "0.35.0",
+ "commands": [
+ "dotnet-cake"
+ ]
+ },
+ "dotnet-format": {
+ "version": "3.1.37601",
+ "commands": [
+ "dotnet-format"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index 0dd7ef8ed1..8cdb92d11c 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -12,16 +12,183 @@ trim_trailing_whitespace = true
#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
-dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event,delegate
-dotnet_naming_rule.public_members_pascalcase.severity = suggestion
+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
+
dotnet_naming_symbols.private_members.applicable_accessibilities = private
-dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event,delegate
-dotnet_naming_rule.private_members_camelcase.severity = suggestion
+dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.private_members_camelcase.severity = warning
dotnet_naming_rule.private_members_camelcase.symbols = private_members
-dotnet_naming_rule.private_members_camelcase.style = camelcase
\ No newline at end of file
+dotnet_naming_rule.private_members_camelcase.style = camelcase
+
+dotnet_naming_symbols.local_function.applicable_kinds = local_function
+dotnet_naming_rule.local_function_camelcase.severity = warning
+dotnet_naming_rule.local_function_camelcase.symbols = local_function
+dotnet_naming_rule.local_function_camelcase.style = camelcase
+
+#all_lower for private and local constants/static readonlys
+dotnet_naming_style.all_lower.capitalization = all_lower
+dotnet_naming_style.all_lower.word_separator = _
+
+dotnet_naming_symbols.private_constants.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants.required_modifiers = const
+dotnet_naming_symbols.private_constants.applicable_kinds = field
+dotnet_naming_rule.private_const_all_lower.severity = warning
+dotnet_naming_rule.private_const_all_lower.symbols = private_constants
+dotnet_naming_rule.private_const_all_lower.style = all_lower
+
+dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly.applicable_kinds = field
+dotnet_naming_rule.private_static_readonly_all_lower.severity = warning
+dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly
+dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+dotnet_naming_rule.local_const_all_lower.severity = warning
+dotnet_naming_rule.local_const_all_lower.symbols = local_constants
+dotnet_naming_rule.local_const_all_lower.style = all_lower
+
+#ALL_UPPER for non private constants/static readonlys
+dotnet_naming_style.all_upper.capitalization = all_upper
+dotnet_naming_style.all_upper.word_separator = _
+
+dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_constants.required_modifiers = const
+dotnet_naming_symbols.public_constants.applicable_kinds = field
+dotnet_naming_rule.public_const_all_upper.severity = warning
+dotnet_naming_rule.public_const_all_upper.symbols = public_constants
+dotnet_naming_rule.public_const_all_upper.style = all_upper
+
+dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.public_static_readonly.applicable_kinds = field
+dotnet_naming_rule.public_static_readonly_all_upper.severity = warning
+dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly
+dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper
+
+#Roslyn formating options
+
+#Formatting - indentation options
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+#Formatting - new line options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_open_brace = all
+#csharp_new_line_before_members_in_anonymous_types = true
+#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing
+csharp_new_line_between_query_expression_clauses = true
+
+#Formatting - organize using options
+dotnet_sort_system_directives_first = true
+
+#Formatting - spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+
+#Formatting - wrapping options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#Roslyn language styles
+
+#Style - this. qualification
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+#Style - type names
+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_elsewhere = true:silent
+
+#Style - modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning
+
+#Style - parentheses
+# Skipped because roslyn cannot separate +-*/ with << >>
+
+#Style - expression bodies
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_methods = true:silent
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
+
+#Style - expression preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:warning
+
+#Style - null/type checks
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:warning
+
+#Style - unused
+dotnet_style_readonly_field = true:silent
+dotnet_code_quality_unused_parameters = non_public:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+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
+
+#Style - other C# 7.x features
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+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_switch_expression = false:none
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Private method is unused
+dotnet_diagnostic.IDE0051.severity = silent
+#Private member is unused
+dotnet_diagnostic.IDE0052.severity = silent
+
+#Rules for disposable
+dotnet_diagnostic.IDE0067.severity = none
+dotnet_diagnostic.IDE0068.severity = none
+dotnet_diagnostic.IDE0069.severity = none
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/00-mobile-issues.md b/.github/ISSUE_TEMPLATE/00-mobile-issues.md
new file mode 100644
index 0000000000..f171e80b8b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/00-mobile-issues.md
@@ -0,0 +1,8 @@
+---
+name: Mobile Report
+about: ⚠ Due to current development priorities we are not accepting mobile reports at this time (unless you're willing to fix them yourself!)
+---
+
+⚠ **PLEASE READ** ⚠: Due to prioritising finishing the client for desktop first we are not accepting reports related to mobile platforms for the time being, unless you're willing to fix them.
+If you'd like to report a problem or suggest a feature and then work on it, feel free to open an issue and highlight that you'd like to address it yourself in the issue body; mobile pull requests are also welcome.
+Otherwise, please check back in the future when the focus of development shifts towards mobile!
diff --git a/.github/ISSUE_TEMPLATE/bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/bug-issues.md
rename to .github/ISSUE_TEMPLATE/01-bug-issues.md
diff --git a/.github/ISSUE_TEMPLATE/crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/crash-issues.md
rename to .github/ISSUE_TEMPLATE/02-crash-issues.md
diff --git a/.github/ISSUE_TEMPLATE/feature-request-issues.md b/.github/ISSUE_TEMPLATE/03-feature-request-issues.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/feature-request-issues.md
rename to .github/ISSUE_TEMPLATE/03-feature-request-issues.md
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..69baeee60c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: osu!stable issues
+ url: https://github.com/ppy/osu-stable-issues
+ about: For issues regarding osu!stable (not osu!lazer), open them here.
diff --git a/.github/ISSUE_TEMPLATE/missing-for-live-issues.md b/.github/ISSUE_TEMPLATE/missing-for-live-issues.md
deleted file mode 100644
index 5822da9c65..0000000000
--- a/.github/ISSUE_TEMPLATE/missing-for-live-issues.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Missing for Live
-about: Features which are available in osu!stable but not yet in osu!lazer.
----
-**Describe the missing feature:**
-
-**Proposal designs of the feature:**
diff --git a/.gitignore b/.gitignore
index e60058ab35..732b171f69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,14 +10,8 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
-### Cake ###
-tools/**
-build/tools/**
-
-fastlane/report.xml
-
# Build results
-bin/[Dd]ebug/
+[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
@@ -104,7 +98,6 @@ $tf/
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
-inspectcode
# JustCode is a .NET coding add-in
.JustCode
@@ -254,20 +247,90 @@ paket-files/
# FAKE - F# Make
.fake/
-# JetBrains Rider
-.idea/.idea.osu/.idea/*.xml
-.idea/.idea.osu/.idea/codeStyles/*.xml
-.idea/.idea.osu/.idea/dataSources/*.xml
-.idea/.idea.osu/.idea/dictionaries/*.xml
-.idea/.idea.osu/*.iml
-*.sln.iml
-
-# CodeRush
-.cr/
-
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
-Staging/
+# Cake #
+/tools/**
+/build/tools/**
+/build/temp/**
+
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# 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
diff --git a/.gitmodules b/.idea/.gitignore
similarity index 100%
rename from .gitmodules
rename to .idea/.gitignore
diff --git a/.idea/.idea.osu.Desktop/.idea/.name b/.idea/.idea.osu.Desktop/.idea/.name
new file mode 100644
index 0000000000..12bf4aebba
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/.name
@@ -0,0 +1 @@
+osu.Desktop
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/codeStyles/codeStyleConfig.xml b/.idea/.idea.osu.Desktop/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000000..a55e7a179b
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/dataSources.xml b/.idea/.idea.osu.Desktop/.idea/dataSources.xml
new file mode 100644
index 0000000000..10f8c1c84d
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/dataSources.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ sqlite.xerial
+ true
+ org.sqlite.JDBC
+ jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/encodings.xml b/.idea/.idea.osu.Desktop/.idea/encodings.xml
new file mode 100644
index 0000000000..15a15b218a
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml
new file mode 100644
index 0000000000..27ba142e96
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/misc.xml b/.idea/.idea.osu.Desktop/.idea/misc.xml
new file mode 100644
index 0000000000..1d8c84d0af
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml
new file mode 100644
index 0000000000..fe63f5faf3
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
new file mode 100644
index 0000000000..7515e76054
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
new file mode 100644
index 0000000000..1815c271b4
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/runConfigurations/CatchRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
similarity index 88%
rename from .idea/.idea.osu/.idea/runConfigurations/CatchRuleset__Tests_.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
index 6463dd6ea5..a4154623b6 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/CatchRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
@@ -1,6 +1,6 @@
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu/.idea/runConfigurations/ManiaRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
similarity index 88%
rename from .idea/.idea.osu/.idea/runConfigurations/ManiaRuleset__Tests_.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
index 0b63b2d966..080dc04001 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/ManiaRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
@@ -1,6 +1,6 @@
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu/.idea/runConfigurations/OsuRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
similarity index 88%
rename from .idea/.idea.osu/.idea/runConfigurations/OsuRuleset__Tests_.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
index 750ece648b..3de6a7e609 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/OsuRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
@@ -1,6 +1,6 @@
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu/.idea/runConfigurations/TaikoRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
similarity index 88%
rename from .idea/.idea.osu/.idea/runConfigurations/TaikoRuleset__Tests_.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
index 7b359a1ca0..da14c2a29e 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/TaikoRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
@@ -1,6 +1,6 @@
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu/.idea/runConfigurations/Tournament.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
similarity index 90%
rename from .idea/.idea.osu/.idea/runConfigurations/Tournament.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
index 3722f3dc04..45d1ce25e9 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/Tournament.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
@@ -1,6 +1,6 @@
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu/.idea/runConfigurations/Tournament__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
similarity index 88%
rename from .idea/.idea.osu/.idea/runConfigurations/Tournament__Tests_.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
index e2628a1bb4..ba80f7c100 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/Tournament__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
@@ -1,6 +1,6 @@
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu/.idea/runConfigurations/osu_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
similarity index 89%
rename from .idea/.idea.osu/.idea/runConfigurations/osu_.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
index 7ac6bb745d..911c3ed9b7 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/osu_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
@@ -1,6 +1,6 @@
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
new file mode 100644
index 0000000000..d85a0ae44c
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/runConfigurations/osu___Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
similarity index 88%
rename from .idea/.idea.osu/.idea/runConfigurations/osu___Tests_.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
index 7fcb7c15ea..ec3c81f4cd 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/osu___Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
@@ -1,6 +1,6 @@
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/vcs.xml b/.idea/.idea.osu.Desktop/.idea/vcs.xml
new file mode 100644
index 0000000000..3de04b744c
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/vcs.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/indexLayout.xml b/.idea/.idea.osu/.idea/indexLayout.xml
new file mode 100644
index 0000000000..27ba142e96
--- /dev/null
+++ b/.idea/.idea.osu/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/modules.xml b/.idea/.idea.osu/.idea/modules.xml
new file mode 100644
index 0000000000..0360fdbc5e
--- /dev/null
+++ b/.idea/.idea.osu/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu/.idea/projectSettingsUpdater.xml
new file mode 100644
index 0000000000..7515e76054
--- /dev/null
+++ b/.idea/.idea.osu/.idea/projectSettingsUpdater.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/vcs.xml b/.idea/.idea.osu/.idea/vcs.xml
new file mode 100644
index 0000000000..94a25f7f4c
--- /dev/null
+++ b/.idea/.idea.osu/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ce353d9b27..0000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-language: csharp
-solution: osu.sln
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 57ff3e6b43..6480612b2e 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,18 +1,19 @@
{
"version": "0.2.0",
- "configurations": [{
+ "configurations": [
+ {
"name": "osu! (Debug)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.2/osu!.dll"
+ "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1/osu!.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
"linux": {
"env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -23,13 +24,13 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.2/osu!.dll"
+ "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1/osu!.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Release)",
"linux": {
"env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -40,29 +41,30 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.2/osu.Game.Tests.dll"
+ "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp3.1/osu.Game.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tests (Debug)",
"linux": {
"env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
- }, {
+ },
+ {
"name": "osu! (Tests, Release)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.2/osu.Game.Tests.dll"
+ "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp3.1/osu.Game.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tests (Release)",
"linux": {
"env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -73,14 +75,14 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.2/osu!.dll",
+ "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1/osu!.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
"linux": {
"env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -91,14 +93,14 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.2/osu!.dll",
+ "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1/osu!.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Release)",
"linux": {
"env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -109,14 +111,14 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp2.2/osu.Game.Tournament.Tests.dll",
+ "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1/osu.Game.Tournament.Tests.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tournament tests (Debug)",
"linux": {
"env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -127,18 +129,31 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp2.2/osu.Game.Tournament.Tests.dll",
+ "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1/osu.Game.Tournament.Tests.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tournament tests (Release)",
"linux": {
"env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
},
+ {
+ "name": "Benchmark",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/netcoreapp3.1/osu.Game.Benchmarks.dll",
+ "args": [
+ "--filter",
+ "*"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build benchmarks",
+ "console": "internalConsole"
+ },
{
"name": "Cake: Debug Script",
"type": "coreclr",
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index aba590f466..e638dec767 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -2,7 +2,8 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
- "tasks": [{
+ "tasks": [
+ {
"label": "Build osu! (Debug)",
"type": "shell",
"command": "dotnet",
@@ -78,7 +79,8 @@
],
"group": "build",
"problemMatcher": "$msCompile"
- }, {
+ },
+ {
"label": "Build tournament tests (Release)",
"type": "shell",
"command": "dotnet",
@@ -95,12 +97,28 @@
"problemMatcher": "$msCompile"
},
{
- "label": "Restore (netcoreapp2.2)",
+ "label": "Build benchmarks",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Benchmarks",
+ "/p:Configuration=Release",
+ "/p:GenerateFullPaths=true",
+ "/m",
+ "/verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore (netcoreapp3.1)",
"type": "shell",
"command": "dotnet",
"args": [
"restore",
- "osu.sln"
+ "build/Desktop.proj"
],
"problemMatcher": []
}
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
new file mode 100644
index 0000000000..a92191a439
--- /dev/null
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -0,0 +1,6 @@
+M:System.Object.Equals(System.Object,System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable or EqualityComparer.Default instead.
+M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable or EqualityComparer.Default instead.
+M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable or EqualityComparer.Default instead.
+M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
+T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
+M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset
new file mode 100644
index 0000000000..d497365f87
--- /dev/null
+++ b/CodeAnalysis/osu.ruleset
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000000..21b8b402e0
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,46 @@
+
+
+
+ 8.0
+ true
+
+
+ $(MSBuildThisFileDirectory)app.manifest
+
+
+
+ osu.licenseheader
+
+
+
+
+
+
+
+
+
+
+
+ $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset
+
+
+ true
+ $(NoWarn);CS1591
+
+
+
+ $(NoWarn);NU1701
+
+
+ false
+ ppy Pty Ltd
+ MIT
+ https://github.com/ppy/osu
+ https://github.com/ppy/osu
+ Automated release.
+ ppy Pty Ltd
+ Copyright (c) 2020 ppy Pty Ltd
+ osu game
+
+
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock
index 17c0db12e7..e3954c2681 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,12 +1,12 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.0)
- addressable (2.6.0)
- public_suffix (>= 2.0.2, < 4.0)
+ CFPropertyList (3.0.2)
+ addressable (2.7.0)
+ public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
- babosa (1.0.2)
- claide (1.0.2)
+ babosa (1.0.3)
+ claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
@@ -14,20 +14,20 @@ GEM
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.4.1)
- domain_name (0.5.20180417)
+ domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
- dotenv (2.7.1)
+ dotenv (2.7.5)
emoji_regex (1.0.1)
- excon (0.62.0)
- faraday (0.15.4)
+ excon (0.71.1)
+ faraday (0.17.3)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
- fastimage (2.1.5)
- fastlane (2.117.0)
+ fastimage (2.1.7)
+ fastlane (2.140.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
@@ -36,83 +36,85 @@ GEM
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
- excon (>= 0.45.0, < 1.0.0)
- faraday (~> 0.9)
+ excon (>= 0.71.0, < 1.0.0)
+ faraday (~> 0.17)
faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (~> 0.9)
+ faraday_middleware (~> 0.13.1)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
- google-api-client (>= 0.21.2, < 0.24.0)
+ google-api-client (>= 0.29.2, < 0.37.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
- mini_magick (~> 4.5.1)
- multi_json
+ jwt (~> 2.1.0)
+ mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
- rubyzip (>= 1.2.2, < 2.0.0)
+ rubyzip (>= 1.3.0, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
- terminal-notifier (>= 1.6.2, < 2.0.0)
+ terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
- xcodeproj (>= 1.6.0, < 2.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
- fastlane-plugin-clean_testflight_testers (0.2.0)
- fastlane-plugin-souyuz (0.8.1)
- souyuz (>= 0.8.1)
+ fastlane-plugin-clean_testflight_testers (0.3.0)
+ fastlane-plugin-souyuz (0.9.1)
+ souyuz (= 0.9.1)
fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3)
- google-api-client (0.23.9)
+ google-api-client (0.36.4)
addressable (~> 2.5, >= 2.5.1)
- googleauth (>= 0.5, < 0.7.0)
+ googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
- mime-types (~> 3.0)
+ mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
- signet (~> 0.9)
- google-cloud-core (1.3.0)
+ signet (~> 0.12)
+ google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
- google-cloud-env (1.0.5)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (1.3.0)
faraday (~> 0.11)
- google-cloud-storage (1.16.0)
+ google-cloud-errors (1.0.0)
+ google-cloud-storage (1.25.1)
+ addressable (~> 2.5)
digest-crc (~> 0.4)
- google-api-client (~> 0.23)
+ google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
- googleauth (>= 0.6.2, < 0.10.0)
- googleauth (0.6.7)
+ googleauth (~> 0.9)
+ mini_mime (~> 1.0)
+ googleauth (0.10.0)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
- signet (~> 0.7)
+ signet (~> 0.12)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
- json (2.2.0)
+ json (2.3.0)
jwt (2.1.0)
- memoist (0.16.0)
- mime-types (3.2.2)
- mime-types-data (~> 3.2015)
- mime-types-data (3.2018.0812)
- mini_magick (4.5.1)
+ memoist (0.16.2)
+ mini_magick (4.10.1)
+ mini_mime (1.0.2)
mini_portile2 (2.4.0)
- multi_json (1.13.1)
+ multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
naturally (2.2.0)
- nokogiri (1.10.1)
+ nokogiri (1.10.7)
mini_portile2 (~> 2.4.0)
- os (1.0.0)
+ os (1.0.1)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
@@ -121,35 +123,35 @@ GEM
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
- rubyzip (1.2.2)
+ rubyzip (1.3.0)
security (0.1.3)
- signet (0.11.0)
+ signet (0.12.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
- simctl (1.6.5)
+ simctl (1.6.7)
CFPropertyList
naturally
slack-notifier (2.3.2)
- souyuz (0.8.1)
- fastlane (>= 2.29.0)
+ souyuz (0.9.1)
+ fastlane (>= 1.103.0)
highline (~> 1.7)
nokogiri (~> 1.7)
- terminal-notifier (1.8.0)
+ terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
- tty-cursor (0.6.1)
- tty-screen (0.6.5)
- tty-spinner (0.9.0)
- tty-cursor (~> 0.6.0)
+ tty-cursor (0.7.0)
+ tty-screen (0.7.0)
+ tty-spinner (0.9.2)
+ tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.5)
- unicode-display_width (1.4.1)
+ unf_ext (0.0.7.6)
+ unicode-display_width (1.6.1)
word_wrap (1.0.0)
- xcodeproj (1.8.1)
+ xcodeproj (1.14.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
diff --git a/InspectCode.ps1 b/InspectCode.ps1
new file mode 100644
index 0000000000..6ed935fdbb
--- /dev/null
+++ b/InspectCode.ps1
@@ -0,0 +1,27 @@
+[CmdletBinding()]
+Param(
+ [string]$Target,
+ [string]$Configuration,
+ [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
+ [string]$Verbosity,
+ [switch]$ShowDescription,
+ [Alias("WhatIf", "Noop")]
+ [switch]$DryRun,
+ [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)]
+ [string[]]$ScriptArgs
+)
+
+# Build Cake arguments
+$cakeArguments = "";
+if ($Target) { $cakeArguments += "-target=$Target" }
+if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
+if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
+if ($ShowDescription) { $cakeArguments += "-showdescription" }
+if ($DryRun) { $cakeArguments += "-dryrun" }
+if ($Experimental) { $cakeArguments += "-experimental" }
+$cakeArguments += $ScriptArgs
+
+dotnet tool restore
+dotnet cake ./build/InspectCode.cake --bootstrap
+dotnet cake ./build/InspectCode.cake $cakeArguments
+exit $LASTEXITCODE
\ No newline at end of file
diff --git a/LICENCE b/LICENCE
index 21c6a7090f..2435c23545 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,4 +1,4 @@
-Copyright (c) 2019 ppy Pty Ltd .
+Copyright (c) 2020 ppy Pty Ltd .
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 2c330e403c..67027bb9f3 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,19 @@
-
+
# osu!
-[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
+[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu)
+[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)]()
+[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
+[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
-Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename "osu!lazer". Pew pew.
+Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
## Status
-This project is still heavily under development, but is in a state where users are encouraged to try it out and keep it installed alongside the stable osu! client. It will continue to evolve over the coming months and hopefully bring some new unique features to the table.
+This project is still heavily under development, but is in a state where users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve over the coming months and hopefully bring some new unique features to the table.
We are accepting bug reports (please report with as much detail as possible). Feature requests are welcome as long as you read and understand the contribution guidelines listed below.
@@ -18,9 +21,11 @@ Detailed changelogs are published on the [official osu! site](https://osu.ppy.sh
## Requirements
-- A desktop platform with the [.NET Core SDK 2.2](https://www.microsoft.com/net/learn/get-started) or higher installed.
-- When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio 2017+](https://visualstudio.microsoft.com/vs/), [Jetbrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
-- Note that there are **[additional requirements for Windows 7 and Windows 8.1](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore2x)** which you may need to manually install if your operating system is not up-to-date.
+- A desktop platform with the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) or higher installed.
+- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
+- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
+- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
+- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
## Running osu!
@@ -30,12 +35,10 @@ If you are not interested in developing the game, you can still consume our [bin
**Latest build:**
-| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) |
-| ------------- | ------------- |
+| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
+| ------------- | ------------- | ------------- | ------------- |
- **Linux** users are recommended to self-compile until we have official deployment in place.
-- **iOS** users can join the [TestFlight beta program](https://t.co/PasE1zrHhw) (note that due to high demand this is regularly full).
-- **Android** users can self-compile, and expect a public beta soon.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
@@ -56,31 +59,22 @@ git pull
### Building
-Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this provided [below](#contributing).
+Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
-> Visual Studio Code users must run the `Restore` task before any build attempt.
+- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations.
+- Visual Studio Code users must run the `Restore` task before any build attempt.
-You can also build and run osu! from the command-line with a single command:
+You can also build and run *osu!* from the command-line with a single command:
```shell
dotnet run --project osu.Desktop
```
-If you are not interested in debugging osu!, you can add `-c Release` to gain performance. In this case, you must replace `Debug` with `Release` in any commands mentioned in this document.
+If you are not interested in debugging *osu!*, you can add `-c Release` to gain performance. In this case, you must replace `Debug` with `Release` in any commands mentioned in this document.
-If the build fails, try to restore nuget packages with `dotnet restore`.
+If the build fails, try to restore NuGet packages with `dotnet restore`.
-#### A note for Linux users
-
-On Linux, the environment variable `LD_LIBRARY_PATH` must point to the build directory, located at `osu.Desktop/bin/Debug/$NETCORE_VERSION`.
-
-`$NETCORE_VERSION` is the version of the targeted .NET Core SDK. You can check it by running `grep TargetFramework osu.Desktop/osu.Desktop.csproj | sed -r 's/.*>(.*)<\/.*/\1/'`.
-
-For example, you can run osu! with the following command:
-
-```shell
-LD_LIBRARY_PATH="$(pwd)/osu.Desktop/bin/Debug/netcoreapp2.2" dotnet run --project osu.Desktop
-```
+_Due to a historical feature gap between .NET Core and Xamarin, running `dotnet` CLI from the root directory will not work for most commands. This can be resolved by specifying a target `.csproj` or the helper project at `build/Desktop.proj`. Configurations have been provided to work around this issue for all supported IDEs mentioned above._
### Testing with resource/framework modifications
@@ -88,23 +82,27 @@ Sometimes it may be necessary to cross-test changes in [osu-resources](https://g
### Code analysis
-Code analysis can be run with `powershell ./build.ps1` or `build.sh`. This is currently only supported under windows due to [resharper cli shortcomings](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install resharper or use rider to get inline support in your IDE of choice.
+Before committing your code, please run a code formatter. This can be achieved by running `dotnet format` in the command line, or using the `Format code` command in your IDE.
+
+We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
+
+JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`, which is [only supported on Windows](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## Contributing
-We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention on having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time, to ensure no effort is wasted.
+We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted.
If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) label).
Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**.
-Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as pain-free as possible.
+Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible.
-For those interested, we love to reward quality contributions via bounties, paid out via paypal or osu! supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
+For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
## Licence
-The osu! client code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
+*osu!*'s code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
Please note that this *does not cover* the usage of the "osu!" or "ppy" branding in any software, resources, advertising or promotion, as this is protected by trademark law.
diff --git a/appveyor.yml b/appveyor.yml
index 4dcaa7b45e..a4a0cedc66 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,8 +1,27 @@
clone_depth: 1
version: '{branch}-{build}'
-image: Previous Visual Studio 2017
-test: off
-install:
- - cmd: git submodule update --init --recursive --depth=5
-build_script:
- - cmd: PowerShell -Version 2.0 .\build.ps1
+image: Visual Studio 2019
+dotnet_csproj:
+ patch: true
+ file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
+ version: '0.0.{build}'
+cache:
+ - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
+before_build:
+ - ps: dotnet --info # Useful when version mismatch between CI and local
+ - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
+build:
+ project: osu.sln
+ parallel: true
+ verbosity: minimal
+ publish_nuget: true
+after_build:
+ - ps: dotnet tool restore
+ - ps: dotnet format --dry-run --check
+ - ps: .\InspectCode.ps1
+test:
+ assemblies:
+ except:
+ - '**\*Android*'
+ - '**\*iOS*'
+ - 'build\**\*'
diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml
new file mode 100644
index 0000000000..bb4482f501
--- /dev/null
+++ b/appveyor_deploy.yml
@@ -0,0 +1,21 @@
+clone_depth: 1
+version: '{build}'
+image: Visual Studio 2019
+dotnet_csproj:
+ patch: true
+ file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
+ version: $(APPVEYOR_REPO_TAG_NAME)
+before_build:
+ - ps: dotnet --info # Useful when version mismatch between CI and local
+ - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
+test: off
+skip_non_tags: true
+configuration: Release
+build:
+ project: build\Desktop.proj # Skipping Xamarin Release that's slow and covered by fastlane
+ parallel: true
+ verbosity: minimal
+ publish_nuget: true
+deploy:
+ - provider: Environment
+ name: nuget
diff --git a/assets/lazer-nuget.png b/assets/lazer-nuget.png
new file mode 100644
index 0000000000..c2a587fdc2
Binary files /dev/null and b/assets/lazer-nuget.png differ
diff --git a/assets/lazer.png b/assets/lazer.png
index 075a8e7184..1e40e844cc 100644
Binary files a/assets/lazer.png and b/assets/lazer.png differ
diff --git a/build.ps1 b/build.ps1
deleted file mode 100644
index c6a0bf6d4a..0000000000
--- a/build.ps1
+++ /dev/null
@@ -1,82 +0,0 @@
-##########################################################################
-# This is a customized Cake bootstrapper script for PowerShell.
-##########################################################################
-
-<#
-
-.SYNOPSIS
-This is a Powershell script to bootstrap a Cake build.
-
-.DESCRIPTION
-This Powershell script restores NuGet tools (including Cake)
-and execute your Cake build script with the parameters you provide.
-
-.PARAMETER Script
-The build script to execute.
-.PARAMETER Target
-The build script target to run.
-.PARAMETER Configuration
-The build configuration to use.
-.PARAMETER Verbosity
-Specifies the amount of information to be displayed.
-.PARAMETER ShowDescription
-Shows description about tasks.
-.PARAMETER DryRun
-Performs a dry run.
-.PARAMETER ScriptArgs
-Remaining arguments are added here.
-
-.LINK
-https://cakebuild.net
-
-#>
-
-[CmdletBinding()]
-Param(
- [string]$Script = "build.cake",
- [string]$Target,
- [string]$Configuration,
- [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
- [string]$Verbosity,
- [switch]$ShowDescription,
- [Alias("WhatIf", "Noop")]
- [switch]$DryRun,
- [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)]
- [string[]]$ScriptArgs
-)
-
-Write-Host "Preparing to run build script..."
-
-# Determine the script root for resolving other paths.
-if(!$PSScriptRoot) {
- $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
-}
-
-# Resolve the paths for resources used for debugging.
-$BUILD_DIR = Join-Path $PSScriptRoot "build"
-$TOOLS_DIR = Join-Path $BUILD_DIR "tools"
-$CAKE_CSPROJ = Join-Path $BUILD_DIR "cakebuild.csproj"
-
-# Install the required tools locally.
-Write-Host "Restoring cake tools..."
-Invoke-Expression "dotnet restore `"$CAKE_CSPROJ`" --packages `"$TOOLS_DIR`"" | Out-Null
-
-# Find the Cake executable
-$CAKE_EXECUTABLE = (Get-ChildItem -Path "$TOOLS_DIR/cake.coreclr/" -Filter Cake.dll -Recurse).FullName
-
-# Build Cake arguments
-$cakeArguments = @("$Script");
-if ($Target) { $cakeArguments += "-target=$Target" }
-if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
-if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
-if ($ShowDescription) { $cakeArguments += "-showdescription" }
-if ($DryRun) { $cakeArguments += "-dryrun" }
-if ($Experimental) { $cakeArguments += "-experimental" }
-$cakeArguments += $ScriptArgs
-
-# Start Cake
-Write-Host "Running build script..."
-Push-Location -Path $BUILD_DIR
-Invoke-Expression "dotnet `"$CAKE_EXECUTABLE`" $cakeArguments"
-Pop-Location
-exit $LASTEXITCODE
diff --git a/build.sh b/build.sh
deleted file mode 100755
index 8f1ef5b455..0000000000
--- a/build.sh
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env bash
-
-##########################################################################
-# This is a customized Cake bootstrapper script for Shell.
-##########################################################################
-
-echo "Preparing to run build script..."
-
-cd build
-SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
-TOOLS_DIR=$SCRIPT_DIR/tools
-CAKE_BINARY_PATH=$TOOLS_DIR/"cake.coreclr"
-
-SCRIPT="build.cake"
-CAKE_CSPROJ=$SCRIPT_DIR/"cakebuild.csproj"
-
-# Parse arguments.
-CAKE_ARGUMENTS=()
-for i in "$@"; do
- case $1 in
- -s|--script) SCRIPT="$2"; shift ;;
- --) shift; CAKE_ARGUMENTS+=("$@"); break ;;
- *) CAKE_ARGUMENTS+=("$1") ;;
- esac
- shift
-done
-
-# Install the required tools locally.
-echo "Restoring cake tools..."
-dotnet restore $CAKE_CSPROJ --packages $TOOLS_DIR > /dev/null 2>&1
-
-# Search for the CakeBuild binary.
-CAKE_BINARY=$(find $CAKE_BINARY_PATH -name "Cake.dll")
-
-# Start Cake
-echo "Running build script..."
-
-dotnet "$CAKE_BINARY" $SCRIPT "${CAKE_ARGUMENTS[@]}"
diff --git a/build/Desktop.proj b/build/Desktop.proj
new file mode 100644
index 0000000000..b1c6b065e8
--- /dev/null
+++ b/build/Desktop.proj
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build/build.cake b/build/InspectCode.cake
similarity index 57%
rename from build/build.cake
rename to build/InspectCode.cake
index 1d2588de49..06c56dce87 100644
--- a/build/build.cake
+++ b/build/InspectCode.cake
@@ -1,5 +1,5 @@
-#addin "nuget:?package=CodeFileSanity&version=0.0.21"
-#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.1.1"
+#addin "nuget:?package=CodeFileSanity&version=0.0.33"
+#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.3.0"
#tool "nuget:?package=NVika.MSBuild&version=1.0.1"
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
@@ -7,43 +7,29 @@ var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
// ARGUMENTS
///////////////////////////////////////////////////////////////////////////////
-var target = Argument("target", "Build");
+var target = Argument("target", "CodeAnalysis");
var configuration = Argument("configuration", "Release");
var rootDirectory = new DirectoryPath("..");
-var solution = rootDirectory.CombineWithFilePath("osu.sln");
+var sln = rootDirectory.CombineWithFilePath("osu.sln");
+var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////
-Task("Compile")
- .Does(() => {
- DotNetCoreBuild(solution.FullPath, new DotNetCoreBuildSettings {
- Configuration = configuration,
- });
- });
-
-Task("Test")
- .IsDependentOn("Compile")
- .Does(() => {
- var testAssemblies = GetFiles(rootDirectory + "/**/*.Tests/bin/**/*.Tests.dll");
-
- DotNetCoreVSTest(testAssemblies, new DotNetCoreVSTestSettings {
- Logger = AppVeyor.IsRunningOnAppVeyor ? "Appveyor" : $"trx",
- Parallel = true,
- ToolTimeout = TimeSpan.FromMinutes(10),
- });
- });
-
-// windows only because both inspectcore and nvika depend on net45
+// windows only because both inspectcode and nvika depend on net45
Task("InspectCode")
.WithCriteria(IsRunningOnWindows())
- .IsDependentOn("Compile")
.Does(() => {
- InspectCode(solution, new InspectCodeSettings {
+ InspectCode(desktopSlnf, new InspectCodeSettings {
CachesHome = "inspectcode",
OutputFile = "inspectcodereport.xml",
+ ArgumentCustomization = arg => {
+ if (AppVeyor.IsRunningOnAppVeyor) // Don't flood CI output
+ arg.Append("--verbosity:WARN");
+ return arg;
+ },
});
int returnCode = StartProcess(nVikaToolPath, $@"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors");
@@ -59,9 +45,8 @@ Task("CodeFileSanity")
});
});
-Task("Build")
+Task("CodeAnalysis")
.IsDependentOn("CodeFileSanity")
- .IsDependentOn("InspectCode")
- .IsDependentOn("Test");
+ .IsDependentOn("InspectCode");
RunTarget(target);
\ No newline at end of file
diff --git a/build/cakebuild.csproj b/build/cakebuild.csproj
deleted file mode 100644
index 8ccce35e26..0000000000
--- a/build/cakebuild.csproj
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- Exe
- true
- netcoreapp2.0
-
-
-
-
-
-
\ No newline at end of file
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 48c16caf0f..28a83fbbae 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -1,22 +1,84 @@
update_fastlane
-default_platform(:ios)
+platform :android do
+desc 'Deploy to play store'
+ lane :beta do |options|
-platform :ios do
- lane :testflight_prune_dry do
- clean_testflight_testers(days_of_inactivity:45, dry_run: true)
+ update_version(
+ version: options[:version],
+ build: options[:build],
+ )
+
+ build(options)
+
+ supply(
+ apk: './osu.Android/bin/Release/sh.ppy.osulazer-Signed.apk',
+ package_name: 'sh.ppy.osulazer',
+ track: 'alpha', # upload to alpha, we can promote it later
+ json_key: options[:json_key],
+ )
end
- # Specify a custom number for what's "inactive"
- lane :testflight_prune do
- clean_testflight_testers(days_of_inactivity: 45) # 120 days, so about 4 months
+ desc 'Deploy to github release'
+ lane :build_github do |options|
+
+ update_version(
+ version: options[:version],
+ build: options[:build],
+ )
+
+ build(options)
+
+ client = HTTPClient.new
+ changelog = client.get_content 'https://gist.githubusercontent.com/peppy/aaa2ec1a323554b619671cac6dbbb776/raw'
+ changelog.gsub!('$BUILD_ID', options[:build])
+
+ set_github_release(
+ repository_name: "ppy/osu",
+ api_token: ENV["GITHUB_TOKEN"],
+ name: options[:build],
+ tag_name: options[:build],
+ is_draft: true,
+ description: changelog,
+ commitish: "master",
+ upload_assets: ["osu.Android/bin/Release/sh.ppy.osulazer.apk"]
+ )
+
+ end
+
+ desc 'Compile the project'
+ lane :build do |options|
+ nuget_restore(
+ project_path: 'osu.sln'
+ )
+
+ souyuz(
+ build_configuration: 'Release',
+ solution_path: 'osu.sln',
+ platform: "android",
+ output_path: "osu.Android/bin/Release/",
+ keystore_path: options[:keystore_path],
+ keystore_alias: options[:keystore_alias],
+ keystore_password: ENV["KEYSTORE_PASSWORD"]
+ )
end
lane :update_version do |options|
- options[:plist_path] = '../osu.iOS/Info.plist'
- app_version(options)
+
+ split = options[:build].split('.')
+ split[1] = split[1].to_s.rjust(4, '0')
+ android_build = split.join('')
+
+ app_version(
+ solution_path: 'osu.sln',
+ version: options[:version],
+ build: android_build,
+ )
end
+end
+
+platform :ios do
desc 'Deploy to testflight'
lane :beta do |options|
update_version(options)
@@ -44,7 +106,7 @@ platform :ios do
desc 'Compile the project'
lane :build do
nuget_restore(
- project_path: 'osu.iOS.sln'
+ project_path: 'osu.sln'
)
souyuz(
@@ -62,4 +124,17 @@ platform :ios do
match(options)
end
+
+ lane :update_version do |options|
+ options[:plist_path] = '../osu.iOS/Info.plist'
+ app_version(options)
+ end
+
+ lane :testflight_prune_dry do
+ clean_testflight_testers(days_of_inactivity:45, dry_run: true)
+ end
+
+ lane :testflight_prune do
+ clean_testflight_testers(days_of_inactivity: 45)
+ end
end
diff --git a/fastlane/README.md b/fastlane/README.md
index 53bbc62cae..a400ed9516 100644
--- a/fastlane/README.md
+++ b/fastlane/README.md
@@ -15,22 +15,31 @@ Install _fastlane_ using
or alternatively using `brew cask install fastlane`
# Available Actions
+## Android
+### android beta
+```
+fastlane android beta
+```
+Deploy to play store
+### android build_github
+```
+fastlane android build_github
+```
+Deploy to github release
+### android build
+```
+fastlane android build
+```
+Compile the project
+### android update_version
+```
+fastlane android update_version
+```
+
+
+----
+
## iOS
-### ios testflight_prune_dry
-```
-fastlane ios testflight_prune_dry
-```
-
-### ios testflight_prune
-```
-fastlane ios testflight_prune
-```
-
-### ios update_version
-```
-fastlane ios update_version
-```
-
### ios beta
```
fastlane ios beta
@@ -46,6 +55,21 @@ Compile the project
fastlane ios provision
```
Install provisioning profiles using match
+### ios update_version
+```
+fastlane ios update_version
+```
+
+### ios testflight_prune_dry
+```
+fastlane ios testflight_prune_dry
+```
+
+### ios testflight_prune
+```
+fastlane ios testflight_prune
+```
+
----
diff --git a/global.json b/global.json
new file mode 100644
index 0000000000..6858d4044d
--- /dev/null
+++ b/global.json
@@ -0,0 +1,10 @@
+{
+ "sdk": {
+ "allowPrerelease": false,
+ "rollForward": "minor",
+ "version": "3.1.100"
+ },
+ "msbuild-sdks": {
+ "Microsoft.Build.Traversal": "2.0.24"
+ }
+}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 5ee0573c58..2ccba60424 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -1,57 +1,48 @@
+ 8.0
bin\$(Configuration)
4
2.0
false
false
- default
Library
512
Off
True
Xamarin.Android.Net.AndroidClientHandler
- v9.0
+ v10.0
false
+ true
+ armeabi-v7a;x86;arm64-v8a
+ true
+ cjk,mideast,other,rare,west
+ SdkOnly
+ prompt
-
+
True
portable
False
DEBUG;TRACE
- prompt
false
false
- SdkOnly
true
false
- cjk,mideast,other,rare,west
- true
- armeabi-v7a;x86;arm64-v8a
- true
-
+
false
None
True
- prompt
- true
+ true
false
- SdkOnly
False
true
- cjk,mideast,other,rare,west
- true
- armeabi-v7a;x86;arm64-v8a
- true
osu.licenseheader
-
- Always
-
@@ -62,7 +53,7 @@
-
-
+
+
diff --git a/osu.Android.sln b/osu.Android.sln
deleted file mode 100644
index ebf2c55cb4..0000000000
--- a/osu.Android.sln
+++ /dev/null
@@ -1,126 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.28516.95
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game", "osu.Game\osu.Game.csproj", "{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Osu", "osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj", "{C92A607B-1FDD-4954-9F92-03FF547D9080}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Catch", "osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj", "{58F6C80C-1253-4A0E-A465-B8C85EBEADF3}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Taiko", "osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj", "{F167E17A-7DE6-4AF5-B920-A5112296C695}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Mania", "osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj", "{48F4582B-7687-4621-9CBE-5C24197CB536}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Android", "osu.Android\osu.Android.csproj", "{D1D5F9A8-B40B-40E6-B02F-482D03346D3D}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Catch.Tests.Android", "osu.Game.Rulesets.Catch.Tests.Android\osu.Game.Rulesets.Catch.Tests.Android.csproj", "{C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Mania.Tests.Android", "osu.Game.Rulesets.Mania.Tests.Android\osu.Game.Rulesets.Mania.Tests.Android.csproj", "{531F1092-DB27-445D-AA33-2A77C7187C99}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Osu.Tests.Android", "osu.Game.Rulesets.Osu.Tests.Android\osu.Game.Rulesets.Osu.Tests.Android.csproj", "{90CAB706-39CB-4B93-9629-3218A6FF8E9B}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Taiko.Tests.Android", "osu.Game.Rulesets.Taiko.Tests.Android\osu.Game.Rulesets.Taiko.Tests.Android.csproj", "{3701A0A1-8476-42C6-B5C4-D24129B4A484}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Tests.Android", "osu.Game.Tests.Android\osu.Game.Tests.Android.csproj", "{5CC222DC-5716-4499-B897-DCBDDA4A5CF9}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.Build.0 = Release|Any CPU
- {C92A607B-1FDD-4954-9F92-03FF547D9080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C92A607B-1FDD-4954-9F92-03FF547D9080}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C92A607B-1FDD-4954-9F92-03FF547D9080}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C92A607B-1FDD-4954-9F92-03FF547D9080}.Release|Any CPU.Build.0 = Release|Any CPU
- {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Release|Any CPU.Build.0 = Release|Any CPU
- {F167E17A-7DE6-4AF5-B920-A5112296C695}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F167E17A-7DE6-4AF5-B920-A5112296C695}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F167E17A-7DE6-4AF5-B920-A5112296C695}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F167E17A-7DE6-4AF5-B920-A5112296C695}.Release|Any CPU.Build.0 = Release|Any CPU
- {48F4582B-7687-4621-9CBE-5C24197CB536}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {48F4582B-7687-4621-9CBE-5C24197CB536}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|Any CPU.Build.0 = Release|Any CPU
- {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|Any CPU.Build.0 = Release|Any CPU
- {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|Any CPU.Build.0 = Release|Any CPU
- {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|Any CPU.Build.0 = Release|Any CPU
- {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|Any CPU.Build.0 = Release|Any CPU
- {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|Any CPU.Build.0 = Release|Any CPU
- {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|Any CPU.Build.0 = Release|Any CPU
- {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|Any CPU.Deploy.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668}
- EndGlobalSection
- GlobalSection(MonoDevelopProperties) = preSolution
- Policies = $0
- $0.TextStylePolicy = $1
- $1.EolMarker = Windows
- $1.inheritsSet = VisualStudio
- $1.inheritsScope = text/plain
- $1.scope = text/x-csharp
- $0.CSharpFormattingPolicy = $2
- $2.IndentSwitchSection = True
- $2.NewLinesForBracesInProperties = True
- $2.NewLinesForBracesInAccessors = True
- $2.NewLinesForBracesInAnonymousMethods = True
- $2.NewLinesForBracesInControlBlocks = True
- $2.NewLinesForBracesInAnonymousTypes = True
- $2.NewLinesForBracesInObjectCollectionArrayInitializers = True
- $2.NewLinesForBracesInLambdaExpressionBody = True
- $2.NewLineForElse = True
- $2.NewLineForCatch = True
- $2.NewLineForFinally = True
- $2.NewLineForMembersInObjectInit = True
- $2.NewLineForMembersInAnonymousTypes = True
- $2.NewLineForClausesInQuery = True
- $2.SpacingAfterMethodDeclarationName = False
- $2.SpaceAfterMethodCallName = False
- $2.SpaceBeforeOpenSquareBracket = False
- $2.inheritsSet = Mono
- $2.inheritsScope = text/x-csharp
- $2.scope = text/x-csharp
- EndGlobalSection
-EndGlobal
diff --git a/osu.Android.sln.DotSettings b/osu.Android.sln.DotSettings
deleted file mode 100644
index 3f5bd9d34d..0000000000
--- a/osu.Android.sln.DotSettings
+++ /dev/null
@@ -1,815 +0,0 @@
-
- True
- True
- True
- True
- ExplicitlyExcluded
- ExplicitlyExcluded
- SOLUTION
- HINT
- WARNING
-
- True
- WARNING
- WARNING
- HINT
- HINT
- HINT
- HINT
- WARNING
- WARNING
- WARNING
- HINT
- WARNING
- HINT
- SUGGESTION
- HINT
- HINT
- HINT
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- HINT
- WARNING
- WARNING
- HINT
- WARNING
- WARNING
- DO_NOT_SHOW
- HINT
- WARNING
- DO_NOT_SHOW
- WARNING
- HINT
- HINT
- HINT
- ERROR
- HINT
- HINT
- HINT
- WARNING
- WARNING
- HINT
- DO_NOT_SHOW
- HINT
- HINT
- HINT
- HINT
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- HINT
- HINT
- HINT
- HINT
- HINT
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- HINT
- DO_NOT_SHOW
- DO_NOT_SHOW
- DO_NOT_SHOW
- WARNING
-
- WARNING
- WARNING
- WARNING
- ERROR
- WARNING
- WARNING
- HINT
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- HINT
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- HINT
- DO_NOT_SHOW
- DO_NOT_SHOW
- DO_NOT_SHOW
- WARNING
- WARNING
- HINT
- WARNING
- HINT
- HINT
- HINT
- HINT
- HINT
- HINT
- HINT
-
- HINT
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- WARNING
- HINT
- WARNING
- WARNING
- HINT
- HINT
- WARNING
- <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile>
- Code Cleanup (peppy)
- True
- True
- True
- True
- True
- True
- True
- True
- NEXT_LINE
- NEXT_LINE
- True
- NEVER
- NEVER
- False
- NEVER
- False
- True
- False
- False
- True
- True
- False
- CHOP_IF_LONG
- True
- 200
- CHOP_IF_LONG
- False
- False
- AABB
- API
- BPM
- GC
- GL
- GLSL
- HID
- HUD
- ID
- IP
- IPC
- LTRB
- MD5
- NS
- OS
- RGB
- RNG
- SHA
- SRGB
- TK
- SS
- PP
- GMT
- QAT
- BNG
- UI
- HINT
- <?xml version="1.0" encoding="utf-16"?>
-<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
- <TypePattern DisplayName="COM interfaces or structs">
- <TypePattern.Match>
- <Or>
- <And>
- <Kind Is="Interface" />
- <Or>
- <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" />
- <HasAttribute Name="System.Runtime.InteropServices.ComImport" />
- </Or>
- </And>
- <Kind Is="Struct" />
- </Or>
- </TypePattern.Match>
- </TypePattern>
- <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All">
- <TypePattern.Match>
- <And>
- <Kind Is="Class" />
- <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" />
- </And>
- </TypePattern.Match>
- <Entry DisplayName="Setup/Teardown Methods">
- <Entry.Match>
- <And>
- <Kind Is="Method" />
- <Or>
- <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" />
- <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" />
- <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" />
- <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" />
- </Or>
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="All other members" />
- <Entry Priority="100" DisplayName="Test Methods">
- <Entry.Match>
- <And>
- <Kind Is="Method" />
- <HasAttribute Name="NUnit.Framework.TestAttribute" />
- </And>
- </Entry.Match>
- <Entry.SortBy>
- <Name />
- </Entry.SortBy>
- </Entry>
- </TypePattern>
- <TypePattern DisplayName="Default Pattern">
- <Group DisplayName="Fields/Properties">
- <Group DisplayName="Public Fields">
- <Entry DisplayName="Constant Fields">
- <Entry.Match>
- <And>
- <Access Is="Public" />
- <Or>
- <Kind Is="Constant" />
- <Readonly />
- <And>
- <Static />
- <Readonly />
- </And>
- </Or>
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Static Fields">
- <Entry.Match>
- <And>
- <Access Is="Public" />
- <Static />
- <Not>
- <Readonly />
- </Not>
- <Kind Is="Field" />
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Normal Fields">
- <Entry.Match>
- <And>
- <Access Is="Public" />
- <Not>
- <Or>
- <Static />
- <Readonly />
- </Or>
- </Not>
- <Kind Is="Field" />
- </And>
- </Entry.Match>
- </Entry>
- </Group>
- <Entry DisplayName="Public Properties">
- <Entry.Match>
- <And>
- <Access Is="Public" />
- <Kind Is="Property" />
- </And>
- </Entry.Match>
- </Entry>
- <Group DisplayName="Internal Fields">
- <Entry DisplayName="Constant Fields">
- <Entry.Match>
- <And>
- <Access Is="Internal" />
- <Or>
- <Kind Is="Constant" />
- <Readonly />
- <And>
- <Static />
- <Readonly />
- </And>
- </Or>
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Static Fields">
- <Entry.Match>
- <And>
- <Access Is="Internal" />
- <Static />
- <Not>
- <Readonly />
- </Not>
- <Kind Is="Field" />
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Normal Fields">
- <Entry.Match>
- <And>
- <Access Is="Internal" />
- <Not>
- <Or>
- <Static />
- <Readonly />
- </Or>
- </Not>
- <Kind Is="Field" />
- </And>
- </Entry.Match>
- </Entry>
- </Group>
- <Entry DisplayName="Internal Properties">
- <Entry.Match>
- <And>
- <Access Is="Internal" />
- <Kind Is="Property" />
- </And>
- </Entry.Match>
- </Entry>
- <Group DisplayName="Protected Fields">
- <Entry DisplayName="Constant Fields">
- <Entry.Match>
- <And>
- <Access Is="Protected" />
- <Or>
- <Kind Is="Constant" />
- <Readonly />
- <And>
- <Static />
- <Readonly />
- </And>
- </Or>
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Static Fields">
- <Entry.Match>
- <And>
- <Access Is="Protected" />
- <Static />
- <Not>
- <Readonly />
- </Not>
- <Kind Is="Field" />
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Normal Fields">
- <Entry.Match>
- <And>
- <Access Is="Protected" />
- <Not>
- <Or>
- <Static />
- <Readonly />
- </Or>
- </Not>
- <Kind Is="Field" />
- </And>
- </Entry.Match>
- </Entry>
- </Group>
- <Entry DisplayName="Protected Properties">
- <Entry.Match>
- <And>
- <Access Is="Protected" />
- <Kind Is="Property" />
- </And>
- </Entry.Match>
- </Entry>
- <Group DisplayName="Private Fields">
- <Entry DisplayName="Constant Fields">
- <Entry.Match>
- <And>
- <Access Is="Private" />
- <Or>
- <Kind Is="Constant" />
- <Readonly />
- <And>
- <Static />
- <Readonly />
- </And>
- </Or>
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Static Fields">
- <Entry.Match>
- <And>
- <Access Is="Private" />
- <Static />
- <Not>
- <Readonly />
- </Not>
- <Kind Is="Field" />
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Normal Fields">
- <Entry.Match>
- <And>
- <Access Is="Private" />
- <Not>
- <Or>
- <Static />
- <Readonly />
- </Or>
- </Not>
- <Kind Is="Field" />
- </And>
- </Entry.Match>
- </Entry>
- </Group>
- <Entry DisplayName="Private Properties">
- <Entry.Match>
- <And>
- <Access Is="Private" />
- <Kind Is="Property" />
- </And>
- </Entry.Match>
- </Entry>
- </Group>
- <Group DisplayName="Constructor/Destructor">
- <Entry DisplayName="Ctor">
- <Entry.Match>
- <Kind Is="Constructor" />
- </Entry.Match>
- </Entry>
- <Region Name="Disposal">
- <Entry DisplayName="Dtor">
- <Entry.Match>
- <Kind Is="Destructor" />
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Dispose()">
- <Entry.Match>
- <And>
- <Access Is="Public" />
- <Kind Is="Method" />
- <Name Is="Dispose" />
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Dispose(true)">
- <Entry.Match>
- <And>
- <Access Is="Protected" />
- <Or>
- <Virtual />
- <Override />
- </Or>
- <Kind Is="Method" />
- <Name Is="Dispose" />
- </And>
- </Entry.Match>
- </Entry>
- </Region>
- </Group>
- <Group DisplayName="Methods">
- <Group DisplayName="Public">
- <Entry DisplayName="Static Methods">
- <Entry.Match>
- <And>
- <Access Is="Public" />
- <Static />
- <Kind Is="Method" />
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Methods">
- <Entry.Match>
- <And>
- <Access Is="Public" />
- <Not>
- <Static />
- </Not>
- <Kind Is="Method" />
- </And>
- </Entry.Match>
- </Entry>
- </Group>
- <Group DisplayName="Internal">
- <Entry DisplayName="Static Methods">
- <Entry.Match>
- <And>
- <Access Is="Internal" />
- <Static />
- <Kind Is="Method" />
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Methods">
- <Entry.Match>
- <And>
- <Access Is="Internal" />
- <Not>
- <Static />
- </Not>
- <Kind Is="Method" />
- </And>
- </Entry.Match>
- </Entry>
- </Group>
- <Group DisplayName="Protected">
- <Entry DisplayName="Static Methods">
- <Entry.Match>
- <And>
- <Access Is="Protected" />
- <Static />
- <Kind Is="Method" />
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Methods">
- <Entry.Match>
- <And>
- <Access Is="Protected" />
- <Not>
- <Static />
- </Not>
- <Kind Is="Method" />
- </And>
- </Entry.Match>
- </Entry>
- </Group>
- <Group DisplayName="Private">
- <Entry DisplayName="Static Methods">
- <Entry.Match>
- <And>
- <Access Is="Private" />
- <Static />
- <Kind Is="Method" />
- </And>
- </Entry.Match>
- </Entry>
- <Entry DisplayName="Methods">
- <Entry.Match>
- <And>
- <Access Is="Private" />
- <Not>
- <Static />
- </Not>
- <Kind Is="Method" />
- </And>
- </Entry.Match>
- </Entry>
- </Group>
- </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" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy>
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
- <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
- <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
- <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- o!f – Object Initializer: Anchor&Origin
- True
- constant("Centre")
- 0
- True
- True
- 2.0
- InCSharpFile
- ofao
- True
- Anchor = Anchor.$anchor$,
-Origin = Anchor.$anchor$,
- True
- True
- o!f – InternalChildren = []
- True
- True
- 2.0
- InCSharpFile
- ofic
- True
- InternalChildren = new Drawable[]
-{
- $END$
-};
- True
- True
- o!f – new GridContainer { .. }
- True
- True
- 2.0
- InCSharpFile
- ofgc
- True
- new GridContainer
-{
- RelativeSizeAxes = Axes.Both,
- Content = new[]
- {
- new Drawable[] { $END$ },
- new Drawable[] { }
- }
-};
- True
- True
- o!f – new FillFlowContainer { .. }
- True
- True
- 2.0
- InCSharpFile
- offf
- True
- new FillFlowContainer
-{
- RelativeSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- $END$
- }
-},
- True
- True
- o!f – new Container { .. }
- True
- True
- 2.0
- InCSharpFile
- ofcont
- True
- new Container
-{
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- $END$
- }
-},
- True
- True
- o!f – BackgroundDependencyLoader load()
- True
- True
- 2.0
- InCSharpFile
- ofbdl
- True
- [BackgroundDependencyLoader]
-private void load()
-{
- $END$
-}
- True
- True
- o!f – new Box { .. }
- True
- True
- 2.0
- InCSharpFile
- ofbox
- True
- new Box
-{
- Colour = Color4.Black,
- RelativeSizeAxes = Axes.Both,
-},
- True
- True
- o!f – Children = []
- True
- True
- 2.0
- InCSharpFile
- ofc
- True
- Children = new Drawable[]
-{
- $END$
-};
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
diff --git a/osu.Android.slnf b/osu.Android.slnf
new file mode 100644
index 0000000000..7d90f97eb9
--- /dev/null
+++ b/osu.Android.slnf
@@ -0,0 +1,19 @@
+{
+ "solution": {
+ "path": "osu.sln",
+ "projects": [
+ "osu.Android\\osu.Android.csproj",
+ "osu.Game.Rulesets.Catch.Tests.Android\\osu.Game.Rulesets.Catch.Tests.Android.csproj",
+ "osu.Game.Rulesets.Catch\\osu.Game.Rulesets.Catch.csproj",
+ "osu.Game.Rulesets.Mania.Tests.Android\\osu.Game.Rulesets.Mania.Tests.Android.csproj",
+ "osu.Game.Rulesets.Mania\\osu.Game.Rulesets.Mania.csproj",
+ "osu.Game.Rulesets.Osu.Tests.Android\\osu.Game.Rulesets.Osu.Tests.Android.csproj",
+ "osu.Game.Rulesets.Osu\\osu.Game.Rulesets.Osu.csproj",
+ "osu.Game.Rulesets.Taiko.Tests.Android\\osu.Game.Rulesets.Taiko.Tests.Android.csproj",
+ "osu.Game.Rulesets.Taiko\\osu.Game.Rulesets.Taiko.csproj",
+ "osu.Game.Tests.Android\\osu.Game.Tests.Android.csproj",
+ "osu.Game.Tests\\osu.Game.Tests.csproj",
+ "osu.Game\\osu.Game.csproj"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 762a9c418d..2e5fa59d20 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -16,6 +16,11 @@ namespace osu.Android
protected override void OnCreate(Bundle savedInstanceState)
{
+ // The default current directory on android is '/'.
+ // On some devices '/' maps to the app data directory. On others it maps to the root of the internal storage.
+ // In order to have a consistent current directory on all devices the full path of the app data directory is set as the current directory.
+ System.Environment.CurrentDirectory = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
+
base.OnCreate(savedInstanceState);
Window.AddFlags(WindowManagerFlags.Fullscreen);
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index d9bdd9c0c2..a91c010809 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -4,11 +4,37 @@
using System;
using Android.App;
using osu.Game;
+using osu.Game.Updater;
namespace osu.Android
{
public class OsuGameAndroid : OsuGame
{
- public override Version AssemblyVersion => new Version(Application.Context.ApplicationContext.PackageManager.GetPackageInfo(Application.Context.ApplicationContext.PackageName, 0).VersionName);
+ public override Version AssemblyVersion
+ {
+ get
+ {
+ var packageInfo = Application.Context.ApplicationContext.PackageManager.GetPackageInfo(Application.Context.ApplicationContext.PackageName, 0);
+
+ try
+ {
+ string versionName = packageInfo.VersionCode.ToString();
+ // undo play store version garbling
+ return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
+ }
+ catch
+ {
+ }
+
+ return new Version(packageInfo.VersionName);
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Add(new SimpleUpdateManager());
+ }
}
-}
+}
\ No newline at end of file
diff --git a/osu.Android/Properties/AndroidManifest.xml b/osu.Android/Properties/AndroidManifest.xml
index acd21f9587..770eaf2222 100644
--- a/osu.Android/Properties/AndroidManifest.xml
+++ b/osu.Android/Properties/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/osu.Android/Resources/drawable/lazer.png b/osu.Android/Resources/drawable/lazer.png
index 075a8e7184..fc7aa8a092 100644
Binary files a/osu.Android/Resources/drawable/lazer.png and b/osu.Android/Resources/drawable/lazer.png differ
diff --git a/osu.Android/lib/arm64-v8a/libbass.so b/osu.Android/lib/arm64-v8a/libbass.so
deleted file mode 100644
index d5c24b3e4b..0000000000
Binary files a/osu.Android/lib/arm64-v8a/libbass.so and /dev/null differ
diff --git a/osu.Android/lib/arm64-v8a/libbass_fx.so b/osu.Android/lib/arm64-v8a/libbass_fx.so
deleted file mode 100644
index e498b10117..0000000000
Binary files a/osu.Android/lib/arm64-v8a/libbass_fx.so and /dev/null differ
diff --git a/osu.Android/lib/armeabi-v7a/libbass.so b/osu.Android/lib/armeabi-v7a/libbass.so
deleted file mode 100644
index f7d23b9052..0000000000
Binary files a/osu.Android/lib/armeabi-v7a/libbass.so and /dev/null differ
diff --git a/osu.Android/lib/armeabi-v7a/libbass_fx.so b/osu.Android/lib/armeabi-v7a/libbass_fx.so
deleted file mode 100644
index 006e2feb30..0000000000
Binary files a/osu.Android/lib/armeabi-v7a/libbass_fx.so and /dev/null differ
diff --git a/osu.Android/lib/x86/libbass.so b/osu.Android/lib/x86/libbass.so
deleted file mode 100644
index b0f758a42b..0000000000
Binary files a/osu.Android/lib/x86/libbass.so and /dev/null differ
diff --git a/osu.Android/lib/x86/libbass_fx.so b/osu.Android/lib/x86/libbass_fx.so
deleted file mode 100644
index 526dca39ca..0000000000
Binary files a/osu.Android/lib/x86/libbass_fx.so and /dev/null differ
diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf
new file mode 100644
index 0000000000..d2c14d321a
--- /dev/null
+++ b/osu.Desktop.slnf
@@ -0,0 +1,21 @@
+{
+ "solution": {
+ "path": "osu.sln",
+ "projects": [
+ "osu.Desktop\\osu.Desktop.csproj",
+ "osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj",
+ "osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.csproj",
+ "osu.Game.Rulesets.Catch\\osu.Game.Rulesets.Catch.csproj",
+ "osu.Game.Rulesets.Mania.Tests\\osu.Game.Rulesets.Mania.Tests.csproj",
+ "osu.Game.Rulesets.Mania\\osu.Game.Rulesets.Mania.csproj",
+ "osu.Game.Rulesets.Osu.Tests\\osu.Game.Rulesets.Osu.Tests.csproj",
+ "osu.Game.Rulesets.Osu\\osu.Game.Rulesets.Osu.csproj",
+ "osu.Game.Rulesets.Taiko.Tests\\osu.Game.Rulesets.Taiko.Tests.csproj",
+ "osu.Game.Rulesets.Taiko\\osu.Game.Rulesets.Taiko.csproj",
+ "osu.Game.Tests\\osu.Game.Tests.csproj",
+ "osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
+ "osu.Game.Tournament\\osu.Game.Tournament.csproj",
+ "osu.Game\\osu.Game.csproj"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
new file mode 100644
index 0000000000..08cc0e7f5f
--- /dev/null
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -0,0 +1,149 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Text;
+using DiscordRPC;
+using DiscordRPC.Message;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Logging;
+using osu.Game.Online.API;
+using osu.Game.Rulesets;
+using osu.Game.Users;
+using LogLevel = osu.Framework.Logging.LogLevel;
+using User = osu.Game.Users.User;
+
+namespace osu.Desktop
+{
+ internal class DiscordRichPresence : Component
+ {
+ private const string client_id = "367827983903490050";
+
+ private DiscordRpcClient client;
+
+ [Resolved]
+ private IBindable ruleset { get; set; }
+
+ private Bindable user;
+
+ private readonly IBindable status = new Bindable();
+ private readonly IBindable activity = new Bindable();
+
+ private readonly RichPresence presence = new RichPresence
+ {
+ Assets = new Assets { LargeImageKey = "osu_logo_lazer", }
+ };
+
+ [BackgroundDependencyLoader]
+ private void load(IAPIProvider provider)
+ {
+ client = new DiscordRpcClient(client_id)
+ {
+ SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady.
+ };
+
+ client.OnReady += onReady;
+
+ // safety measure for now, until we performance test / improve backoff for failed connections.
+ client.OnConnectionFailed += (_, __) => client.Deinitialize();
+
+ client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
+
+ (user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u =>
+ {
+ status.UnbindBindings();
+ status.BindTo(u.NewValue.Status);
+
+ activity.UnbindBindings();
+ activity.BindTo(u.NewValue.Activity);
+ }, true);
+
+ ruleset.BindValueChanged(_ => updateStatus());
+ status.BindValueChanged(_ => updateStatus());
+ activity.BindValueChanged(_ => updateStatus());
+
+ client.Initialize();
+ }
+
+ private void onReady(object _, ReadyMessage __)
+ {
+ Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug);
+ updateStatus();
+ }
+
+ private void updateStatus()
+ {
+ if (!client.IsInitialized)
+ return;
+
+ if (status.Value is UserStatusOffline)
+ {
+ client.ClearPresence();
+ return;
+ }
+
+ if (status.Value is UserStatusOnline && activity.Value != null)
+ {
+ presence.State = truncate(activity.Value.Status);
+ presence.Details = truncate(getDetails(activity.Value));
+ }
+ else
+ {
+ presence.State = "Idle";
+ presence.Details = string.Empty;
+ }
+
+ // update user information
+ presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty);
+
+ // update ruleset
+ presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom";
+ presence.Assets.SmallImageText = ruleset.Value.Name;
+
+ client.SetPresence(presence);
+ }
+
+ private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
+
+ private string truncate(string str)
+ {
+ if (Encoding.UTF8.GetByteCount(str) <= 128)
+ return str;
+
+ ReadOnlyMemory strMem = str.AsMemory();
+
+ do
+ {
+ strMem = strMem[..^1];
+ } while (Encoding.UTF8.GetByteCount(strMem.Span) + ellipsis_length > 128);
+
+ return string.Create(strMem.Length + 1, strMem, (span, mem) =>
+ {
+ mem.Span.CopyTo(span);
+ span[^1] = '…';
+ });
+ }
+
+ private string getDetails(UserActivity activity)
+ {
+ switch (activity)
+ {
+ case UserActivity.SoloGame solo:
+ return solo.Beatmap.ToString();
+
+ case UserActivity.Editing edit:
+ return edit.Beatmap.ToString();
+ }
+
+ return string.Empty;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ client.Dispose();
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 975b7f9f5a..f70cc24159 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
@@ -17,6 +17,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform.Windows;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
+using osu.Game.Updater;
namespace osu.Desktop
{
@@ -38,9 +39,9 @@ namespace osu.Desktop
if (Host is DesktopGameHost desktopHost)
return new StableStorage(desktopHost);
}
- catch (Exception e)
+ catch (Exception)
{
- Logger.Error(e, "Error while searching for stable install");
+ Logger.Log("Could not find a stable install", LoggingTarget.Runtime, LogLevel.Important);
}
return null;
@@ -52,17 +53,15 @@ namespace osu.Desktop
if (!noVersionOverlay)
{
- LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, v =>
- {
- Add(v);
- v.Show();
- });
+ LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
Add(new SquirrelUpdateManager());
else
Add(new SimpleUpdateManager());
}
+
+ LoadComponentAsync(new DiscordRichPresence(), Add);
}
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)
@@ -71,7 +70,7 @@ namespace osu.Desktop
switch (newScreen)
{
- case Intro _:
+ case IntroScreen _:
case MainMenu _:
versionManager?.Show();
break;
@@ -115,14 +114,14 @@ namespace osu.Desktop
{
protected override string LocateBasePath()
{
- bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
+ static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
string stableInstallPath;
try
{
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(String.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
if (checkExists(stableInstallPath))
return stableInstallPath;
diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs
index 1f1d2cea5f..8c759f8487 100644
--- a/osu.Desktop/Overlays/VersionManager.cs
+++ b/osu.Desktop/Overlays/VersionManager.cs
@@ -8,29 +8,18 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game;
-using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Overlays;
-using osu.Game.Overlays.Notifications;
using osuTK;
using osuTK.Graphics;
namespace osu.Desktop.Overlays
{
- public class VersionManager : OverlayContainer
+ public class VersionManager : VisibilityContainer
{
- private OsuConfigManager config;
- private OsuGameBase game;
- private NotificationOverlay notificationOverlay;
-
[BackgroundDependencyLoader]
- private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game, OsuConfigManager config)
+ private void load(OsuColour colours, TextureStore textures, OsuGameBase game)
{
- notificationOverlay = notification;
- this.config = config;
- this.game = game;
-
AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
@@ -85,47 +74,6 @@ namespace osu.Desktop.Overlays
};
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- var version = game.Version;
- var lastVersion = config.Get(OsuSetting.Version);
-
- if (game.IsDeployedBuild && version != lastVersion)
- {
- config.Set(OsuSetting.Version, version);
-
- // only show a notification if we've previously saved a version to the config file (ie. not the first run).
- if (!string.IsNullOrEmpty(lastVersion))
- notificationOverlay.Post(new UpdateCompleteNotification(version));
- }
- }
-
- private class UpdateCompleteNotification : SimpleNotification
- {
- private readonly string version;
-
- public UpdateCompleteNotification(string version)
- {
- this.version = version;
- Text = $"You are now running osu!lazer {version}.\nClick to see what's new!";
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours, ChangelogOverlay changelog)
- {
- Icon = FontAwesome.Solid.CheckSquare;
- IconBackgound.Colour = colours.BlueDark;
-
- Activated = delegate
- {
- changelog.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
- return true;
- };
- }
- }
-
protected override void PopIn()
{
this.FadeIn(1400, Easing.OutQuint);
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index cb488fea52..bd91bcc933 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -22,36 +22,44 @@ namespace osu.Desktop
{
// Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory;
+ bool useSdl = args.Contains("--sdl");
- using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useSdl: useSdl))
{
host.ExceptionThrown += handleException;
if (!host.IsPrimaryInstance)
{
- var importer = new ArchiveImportIPCChannel(host);
- // Restore the cwd so relative paths given at the command line work correctly
- Directory.SetCurrentDirectory(cwd);
-
- foreach (var file in args)
+ if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
{
- Console.WriteLine(@"Importing {0}", file);
- if (!importer.ImportAsync(Path.GetFullPath(file)).Wait(3000))
- throw new TimeoutException(@"IPC took too long to send");
+ var importer = new ArchiveImportIPCChannel(host);
+ // Restore the cwd so relative paths given at the command line work correctly
+ Directory.SetCurrentDirectory(cwd);
+
+ foreach (var file in args)
+ {
+ Console.WriteLine(@"Importing {0}", file);
+ if (!importer.ImportAsync(Path.GetFullPath(file)).Wait(3000))
+ throw new TimeoutException(@"IPC took too long to send");
+ }
+
+ return 0;
}
+
+ // we want to allow multiple instances to be started when in debug.
+ if (!DebugUtils.IsDebugBuild)
+ return 0;
}
- else
- {
- switch (args.FirstOrDefault() ?? string.Empty)
- {
- default:
- host.Run(new OsuGameDesktop(args));
- break;
- case "--tournament":
- host.Run(new TournamentGame());
- break;
- }
+ switch (args.FirstOrDefault() ?? string.Empty)
+ {
+ default:
+ host.Run(new OsuGameDesktop(args));
+ break;
+
+ case "--tournament":
+ host.Run(new TournamentGame());
+ break;
}
return 0;
diff --git a/osu.Desktop/Properties/launchSettings.json b/osu.Desktop/Properties/launchSettings.json
new file mode 100644
index 0000000000..5e768ec9fa
--- /dev/null
+++ b/osu.Desktop/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "osu! Desktop": {
+ "commandName": "Project"
+ },
+ "osu! Tournament": {
+ "commandName": "Project",
+ "commandLineArgs": "--tournament"
+ }
+ }
+}
\ No newline at end of file
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 78a1e680ec..60b47a8b3a 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -20,13 +20,15 @@ using LogLevel = Splat.LogLevel;
namespace osu.Desktop.Updater
{
- public class SquirrelUpdateManager : Component
+ public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{
private UpdateManager updateManager;
private NotificationOverlay notificationOverlay;
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
+ private static readonly Logger logger = Logger.GetLogger("updater");
+
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification, OsuGameBase game)
{
@@ -77,7 +79,7 @@ namespace osu.Desktop.Updater
{
if (useDeltaPatching)
{
- Logger.Error(e, @"delta patching failed!");
+ logger.Add(@"delta patching failed; will attempt full download!");
//could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
//try again without deltas.
@@ -163,16 +165,11 @@ namespace osu.Desktop.Updater
{
public LogLevel Level { get; set; } = LogLevel.Info;
- private Logger logger;
-
public void Write(string message, LogLevel logLevel)
{
if (logLevel < Level)
return;
- if (logger == null)
- logger = Logger.GetLogger("updater");
-
logger.Add(message);
}
diff --git a/osu.Desktop/app.manifest b/osu.Desktop/app.manifest
new file mode 100644
index 0000000000..2e9127bf44
--- /dev/null
+++ b/osu.Desktop/app.manifest
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
\ No newline at end of file
diff --git a/osu.Desktop/lazer.ico b/osu.Desktop/lazer.ico
old mode 100644
new mode 100755
index 0c894dca41..a6aa8abb9f
Binary files a/osu.Desktop/lazer.ico and b/osu.Desktop/lazer.ico differ
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 8c9e1f279f..da47ad8223 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -1,15 +1,14 @@
-
- netcoreapp2.2
+ netcoreapp3.1
WinExe
- AnyCPU
true
click the circles. to the beat.
osu!
osu!lazer
osu!lazer
lazer.ico
+ app.manifest
0.0.0
0.0.0
@@ -23,13 +22,14 @@
-
-
+
-
-
+
+
+
+
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index a26b35fcd5..a919d54f38 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -12,7 +12,7 @@
click the circles. to the beat.
click the circles.
testing
- Copyright (c) 2019 ppy Pty Ltd
+ Copyright (c) 2020 ppy Pty Ltd
en-AU
diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
new file mode 100644
index 0000000000..394fd75488
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using BenchmarkDotNet.Attributes;
+using osu.Framework.IO.Stores;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
+using osu.Game.IO.Archives;
+using osu.Game.Resources;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkBeatmapParsing : BenchmarkTest
+ {
+ private readonly MemoryStream beatmapStream = new MemoryStream();
+
+ public override void SetUp()
+ {
+ using (var resources = new DllResourceStore(OsuResources.ResourceAssembly))
+ using (var archive = resources.GetStream("Beatmaps/241526 Soleily - Renatus.osz"))
+ using (var reader = new ZipArchiveReader(archive))
+ reader.GetStream("Soleily - Renatus (Gamu) [Insane].osu").CopyTo(beatmapStream);
+ }
+
+ [Benchmark]
+ public Beatmap BenchmarkBundledBeatmap()
+ {
+ beatmapStream.Seek(0, SeekOrigin.Begin);
+ var reader = new LineBufferedReader(beatmapStream); // no disposal
+
+ var decoder = Decoder.GetDecoder(reader);
+ return decoder.Decode(reader);
+ }
+ }
+}
diff --git a/osu.Game.Benchmarks/BenchmarkTest.cs b/osu.Game.Benchmarks/BenchmarkTest.cs
new file mode 100644
index 0000000000..34f5edd084
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkTest.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Running;
+using NUnit.Framework;
+
+namespace osu.Game.Benchmarks
+{
+ [TestFixture]
+ [MemoryDiagnoser]
+ public abstract class BenchmarkTest
+ {
+ [GlobalSetup]
+ [OneTimeSetUp]
+ public virtual void SetUp()
+ {
+ }
+
+ [Test]
+ public void RunBenchmark() => BenchmarkRunner.Run(GetType());
+ }
+}
diff --git a/osu.Game.Benchmarks/Program.cs b/osu.Game.Benchmarks/Program.cs
new file mode 100644
index 0000000000..c55075fea6
--- /dev/null
+++ b/osu.Game.Benchmarks/Program.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using BenchmarkDotNet.Running;
+
+namespace osu.Game.Benchmarks
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ BenchmarkSwitcher
+ .FromAssembly(typeof(Program).Assembly)
+ .Run(args);
+ }
+ }
+}
diff --git a/osu.Game.Benchmarks/Properties/launchSettings.json b/osu.Game.Benchmarks/Properties/launchSettings.json
new file mode 100644
index 0000000000..c1868088f9
--- /dev/null
+++ b/osu.Game.Benchmarks/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "All Benchmarks": {
+ "commandName": "Project",
+ "commandLineArgs": "--filter *"
+ }
+ }
+}
\ No newline at end of file
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
new file mode 100644
index 0000000000..f2e1c0ec3b
--- /dev/null
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netcoreapp3.1
+ Exe
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
index db95e18f13..f8c3fcd894 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,6 @@
-
-
+
+
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
index beca477943..f7f07ef938 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
@@ -5,7 +5,7 @@ using UIKit;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
- public class Application
+ public static class Application
{
public static void Main(string[] args)
{
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
index 37e7c45a4e..be6044bbd0 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
@@ -1,6 +1,5 @@
-
+
-
Debug
iPhoneSimulator
@@ -13,14 +12,6 @@
-
- libbass.a
- PreserveNewest
-
-
- libbass_fx.a
- PreserveNewest
-
Linker.xml
@@ -41,5 +32,4 @@
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
index 5dfaf5ec39..67d27c33eb 100644
--- a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
+++ b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Debug/netcoreapp2.2/osu.Game.Rulesets.Catch.Tests.dll"
+ "${workspaceRoot}/bin/Debug/netcoreapp3.1/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/netcoreapp2.2/osu.Game.Rulesets.Catch.Tests.dll"
+ "${workspaceRoot}/bin/Release/netcoreapp3.1/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index e45ed8c6f4..f4749be370 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -5,7 +5,8 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using NUnit.Framework;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
@@ -22,10 +23,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("spinner")]
[TestCase("spinner-and-circles")]
[TestCase("slider")]
- public new void Test(string name)
- {
- base.Test(name);
- }
+ [TestCase("hardrock-stream", new[] { typeof(CatchModHardRock) })]
+ [TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })]
+ [TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })]
+ public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
{
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs
new file mode 100644
index 0000000000..04e6dea376
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Game.Beatmaps.Legacy;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ [TestFixture]
+ public class CatchLegacyModConversionTest : LegacyModConversionTest
+ {
+ [TestCase(LegacyMods.Easy, new[] { typeof(CatchModEasy) })]
+ [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) })]
+ [TestCase(LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) })]
+ [TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })]
+ [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
+ [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModFlashlight), typeof(CatchModNightcore) })]
+ [TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })]
+ [TestCase(LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime), typeof(CatchModPerfect) })]
+ public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
+
+ protected override Ruleset CreateRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index ab3c040b4e..74a9c05bf9 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override Player CreatePlayer(Ruleset ruleset)
{
- Mods.Value = Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
+ SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
return base.CreatePlayer(ruleset);
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 33f93cdb4a..9b529a2e4c 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -11,10 +11,12 @@ using System;
using System.Collections.Generic;
using osu.Game.Skinning;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
using osuTK.Graphics;
using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+using osu.Game.Graphics.Sprites;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -64,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Tests
RelativeSizeAxes = Axes.Both,
Colour = Color4.Blue
},
- new SpriteText
+ new OsuSpriteText
{
Text = "custom"
}
@@ -81,25 +83,24 @@ namespace osu.Game.Rulesets.Catch.Tests
remove { }
}
- public Drawable GetDrawableComponent(string componentName)
+ public Drawable GetDrawableComponent(ISkinComponent component)
{
- switch (componentName)
+ switch (component.LookupName)
{
- case "Play/Catch/fruit-catcher-idle":
+ case "Gameplay/catch/fruit-catcher-idle":
return new CatcherCustomSkin();
}
return null;
}
- public SampleChannel GetSample(string sampleName) =>
+ public SampleChannel GetSample(ISampleInfo sampleInfo) =>
throw new NotImplementedException();
public Texture GetTexture(string componentName) =>
throw new NotImplementedException();
- public TValue GetValue(Func query) where TConfiguration : SkinConfiguration =>
- throw new NotImplementedException();
+ public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index 7a9b61c60c..1eb913e900 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void load()
{
var controlPointInfo = new ControlPointInfo();
- controlPointInfo.TimingPoints.Add(new TimingControlPoint());
+ controlPointInfo.Add(0, new TimingControlPoint());
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
{
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.Tests
RelativeSizeAxes = Axes.Both,
Children = new[]
{
- drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), beatmap, Array.Empty())
+ drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), beatmap.GetPlayableBeatmap(new CatchRuleset().RulesetInfo))
}
});
@@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void addToPlayfield(DrawableCatchHitObject drawable)
{
- foreach (var mod in Mods.Value.OfType())
+ foreach (var mod in SelectedMods.Value.OfType())
mod.ApplyToDrawableHitObjects(new[] { drawable });
drawableRuleset.Playfield.Add(drawable);
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs
index f6d26addaa..8c3dfef39c 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods;
namespace osu.Game.Rulesets.Catch.Tests
@@ -12,9 +13,10 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(CatchModHidden) }).ToList();
- public TestSceneDrawableHitObjectsHidden()
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
- Mods.Value = new[] { new CatchModHidden() };
- }
+ SelectedMods.Value = new[] { new CatchModHidden() };
+ });
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index a603d96201..da36673930 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
+ protected override bool Autoplay => true;
+
[Test]
public void TestHyperDash()
{
@@ -38,8 +40,10 @@ namespace osu.Game.Rulesets.Catch.Tests
beatmap.HitObjects.Add(new Fruit { StartTime = 1008, X = 56 / 512f, });
for (int i = 0; i < 512; i++)
+ {
if (i % 5 < 3)
beatmap.HitObjects.Add(new Fruit { X = i % 10 < 5 ? 0.02f : 0.98f, StartTime = 2000 + i * 100, NewCombo = i % 8 == 0 });
+ }
return beatmap;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 9acf47a67c..9559d13328 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,14 +2,14 @@
-
+
-
+
WinExe
- netcoreapp2.2
+ netcoreapp3.1
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 0d9a663b9f..90a6e609f0 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -4,66 +4,63 @@
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
-using System;
+using System.Linq;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
+using osu.Framework.Extensions.IEnumerableExtensions;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
public class CatchBeatmapConverter : BeatmapConverter
{
- public CatchBeatmapConverter(IBeatmap beatmap)
- : base(beatmap)
+ public CatchBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
{
}
- protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) };
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap)
{
- var curveData = obj as IHasCurve;
var positionData = obj as IHasXPosition;
var comboData = obj as IHasCombo;
- var endTime = obj as IHasEndTime;
- var legacyOffset = obj as IHasLegacyLastTickOffset;
- if (curveData != null)
+ switch (obj)
{
- yield return new JuiceStream
- {
- StartTime = obj.StartTime,
- Samples = obj.Samples,
- Path = curveData.Path,
- NodeSamples = curveData.NodeSamples,
- RepeatCount = curveData.RepeatCount,
- X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
- NewCombo = comboData?.NewCombo ?? false,
- ComboOffset = comboData?.ComboOffset ?? 0,
- LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0
- };
- }
- else if (endTime != null)
- {
- yield return new BananaShower
- {
- StartTime = obj.StartTime,
- Samples = obj.Samples,
- Duration = endTime.Duration,
- NewCombo = comboData?.NewCombo ?? false,
- ComboOffset = comboData?.ComboOffset ?? 0,
- };
- }
- else
- {
- yield return new Fruit
- {
- StartTime = obj.StartTime,
- Samples = obj.Samples,
- NewCombo = comboData?.NewCombo ?? false,
- ComboOffset = comboData?.ComboOffset ?? 0,
- X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
- };
+ case IHasCurve curveData:
+ return new JuiceStream
+ {
+ StartTime = obj.StartTime,
+ Samples = obj.Samples,
+ Path = curveData.Path,
+ NodeSamples = curveData.NodeSamples,
+ RepeatCount = curveData.RepeatCount,
+ X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
+ NewCombo = comboData?.NewCombo ?? false,
+ ComboOffset = comboData?.ComboOffset ?? 0,
+ LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
+ }.Yield();
+
+ case IHasEndTime endTime:
+ return new BananaShower
+ {
+ StartTime = obj.StartTime,
+ Samples = obj.Samples,
+ Duration = endTime.Duration,
+ NewCombo = comboData?.NewCombo ?? false,
+ ComboOffset = comboData?.ComboOffset ?? 0,
+ }.Yield();
+
+ default:
+ return new Fruit
+ {
+ StartTime = obj.StartTime,
+ Samples = obj.Samples,
+ NewCombo = comboData?.NewCombo ?? false,
+ ComboOffset = comboData?.ComboOffset ?? 0,
+ X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
+ }.Yield();
}
}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index 645cb5701a..db52fbac1b 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -8,8 +8,8 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
-using osuTK;
using osu.Game.Rulesets.Catch.MathUtils;
+using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
base.PostProcess();
- applyPositionOffsets();
+ ApplyPositionOffsets(Beatmap);
initialiseHyperDash((List)Beatmap.HitObjects);
@@ -40,19 +40,29 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
- private void applyPositionOffsets()
+ public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
{
var rng = new FastRandom(RNG_SEED);
- // todo: HardRock displacement should be applied here
- foreach (var obj in Beatmap.HitObjects)
+ bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
+ float? lastPosition = null;
+ double lastStartTime = 0;
+
+ foreach (var obj in beatmap.HitObjects.OfType())
{
+ obj.XOffset = 0;
+
switch (obj)
{
+ case Fruit fruit:
+ if (shouldApplyHardRockOffset)
+ applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
+ break;
+
case BananaShower bananaShower:
foreach (var banana in bananaShower.NestedHitObjects.OfType())
{
- banana.X = (float)rng.NextDouble();
+ banana.XOffset = (float)rng.NextDouble();
rng.Next(); // osu!stable retrieved a random banana type
rng.Next(); // osu!stable retrieved a random banana rotation
rng.Next(); // osu!stable retrieved a random banana colour
@@ -63,12 +73,13 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case JuiceStream juiceStream:
foreach (var nested in juiceStream.NestedHitObjects)
{
- var hitObject = (CatchHitObject)nested;
- if (hitObject is TinyDroplet)
- hitObject.X += rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH;
- else if (hitObject is Droplet)
+ var catchObject = (CatchHitObject)nested;
+ catchObject.XOffset = 0;
+
+ if (catchObject is TinyDroplet)
+ catchObject.XOffset = Math.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X);
+ else if (catchObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation
- hitObject.X = MathHelper.Clamp(hitObject.X, 0, 1);
}
break;
@@ -76,6 +87,105 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
+ private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng)
+ {
+ if (hitObject is JuiceStream stream)
+ {
+ lastPosition = stream.EndX;
+ lastStartTime = stream.EndTime;
+ return;
+ }
+
+ if (!(hitObject is Fruit))
+ return;
+
+ float offsetPosition = hitObject.X;
+ double startTime = hitObject.StartTime;
+
+ if (lastPosition == null)
+ {
+ lastPosition = offsetPosition;
+ lastStartTime = startTime;
+
+ return;
+ }
+
+ float positionDiff = offsetPosition - lastPosition.Value;
+ double timeDiff = startTime - lastStartTime;
+
+ if (timeDiff > 1000)
+ {
+ lastPosition = offsetPosition;
+ lastStartTime = startTime;
+ return;
+ }
+
+ if (positionDiff == 0)
+ {
+ applyRandomOffset(ref offsetPosition, timeDiff / 4d, rng);
+ hitObject.XOffset = offsetPosition - hitObject.X;
+ return;
+ }
+
+ if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d)
+ applyOffset(ref offsetPosition, positionDiff);
+
+ hitObject.XOffset = offsetPosition - hitObject.X;
+
+ lastPosition = offsetPosition;
+ lastStartTime = startTime;
+ }
+
+ ///
+ /// Applies a random offset in a random direction to a position, ensuring that the final position remains within the boundary of the playfield.
+ ///
+ /// The position which the offset should be applied to.
+ /// The maximum offset, cannot exceed 20px.
+ /// The random number generator.
+ private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng)
+ {
+ bool right = rng.NextBool();
+ float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH;
+
+ if (right)
+ {
+ // Clamp to the right bound
+ if (position + rand <= 1)
+ position += rand;
+ else
+ position -= rand;
+ }
+ else
+ {
+ // Clamp to the left bound
+ if (position - rand >= 0)
+ position -= rand;
+ else
+ position += rand;
+ }
+ }
+
+ ///
+ /// Applies an offset to a position, ensuring that the final position remains within the boundary of the playfield.
+ ///
+ /// The position which the offset should be applied to.
+ /// The amount to offset by.
+ private static void applyOffset(ref float position, float amount)
+ {
+ if (amount > 0)
+ {
+ // Clamp to the right bound
+ if (position + amount < 1)
+ position += amount;
+ }
+ else
+ {
+ // Clamp to the left bound
+ if (position + amount > 0)
+ position += amount;
+ }
+ }
+
private void initialiseHyperDash(List objects)
{
List objectWithDroplets = new List();
@@ -84,10 +194,15 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
if (currentObject is Fruit)
objectWithDroplets.Add(currentObject);
+
if (currentObject is JuiceStream)
+ {
foreach (var currentJuiceElement in currentObject.NestedHitObjects)
+ {
if (!(currentJuiceElement is TinyDroplet))
objectWithDroplets.Add((CatchHitObject)currentJuiceElement);
+ }
+ }
}
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
@@ -114,7 +229,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
else
{
currentObject.DistanceToHyperDash = distanceToHyper;
- lastExcess = MathHelper.Clamp(distanceToHyper, 0, halfCatcherWidth);
+ lastExcess = Math.Clamp(distanceToHyper, 0, halfCatcherWidth);
}
lastDirection = thisDirection;
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index ea9f225cc1..e5c3647f99 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -11,23 +11,31 @@ using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
-using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
+using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using System;
namespace osu.Game.Rulesets.Catch
{
- public class CatchRuleset : Ruleset
+ public class CatchRuleset : Ruleset, ILegacyRuleset
{
- public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableCatchRuleset(this, beatmap, mods);
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap);
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
+
+ public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this);
+
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
+ public const string SHORT_NAME = "fruits";
+
public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[]
{
new KeyBinding(InputKey.Z, CatchAction.MoveLeft),
@@ -45,7 +53,14 @@ namespace osu.Game.Rulesets.Catch
else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new CatchModDoubleTime();
- if (mods.HasFlag(LegacyMods.Autoplay))
+ if (mods.HasFlag(LegacyMods.Perfect))
+ yield return new CatchModPerfect();
+ else if (mods.HasFlag(LegacyMods.SuddenDeath))
+ yield return new CatchModSuddenDeath();
+
+ if (mods.HasFlag(LegacyMods.Cinema))
+ yield return new CatchModCinema();
+ else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new CatchModAutoplay();
if (mods.HasFlag(LegacyMods.Easy))
@@ -66,14 +81,8 @@ namespace osu.Game.Rulesets.Catch
if (mods.HasFlag(LegacyMods.NoFail))
yield return new CatchModNoFail();
- if (mods.HasFlag(LegacyMods.Perfect))
- yield return new CatchModPerfect();
-
if (mods.HasFlag(LegacyMods.Relax))
yield return new CatchModRelax();
-
- if (mods.HasFlag(LegacyMods.SuddenDeath))
- yield return new CatchModSuddenDeath();
}
public override IEnumerable GetModsFor(ModType type)
@@ -98,27 +107,35 @@ namespace osu.Game.Rulesets.Catch
new CatchModFlashlight(),
};
+ case ModType.Conversion:
+ return new Mod[]
+ {
+ new CatchModDifficultyAdjust(),
+ };
+
case ModType.Automation:
return new Mod[]
{
- new MultiMod(new CatchModAutoplay(), new ModCinema()),
+ new MultiMod(new CatchModAutoplay(), new CatchModCinema()),
new CatchModRelax(),
};
case ModType.Fun:
return new Mod[]
{
- new MultiMod(new ModWindUp(), new ModWindDown())
+ new MultiMod(new ModWindUp(), new ModWindDown())
};
default:
- return new Mod[] { };
+ return Array.Empty();
}
}
public override string Description => "osu!catch";
- public override string ShortName => "fruits";
+ public override string ShortName => SHORT_NAME;
+
+ public override string PlayingVerb => "Catching fruit";
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
@@ -126,13 +143,8 @@ namespace osu.Game.Rulesets.Catch
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);
- public override int? LegacyID => 2;
+ public int LegacyID => 2;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
-
- public CatchRuleset(RulesetInfo rulesetInfo = null)
- : base(rulesetInfo)
- {
- }
}
}
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponent.cs b/osu.Game.Rulesets.Catch/CatchSkinComponent.cs
new file mode 100644
index 0000000000..8bf53e53e3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponent.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Catch
+{
+ public class CatchSkinComponent : GameplaySkinComponent
+ {
+ public CatchSkinComponent(CatchSkinComponents component)
+ : base(component)
+ {
+ }
+
+ protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME;
+
+ protected override string ComponentName => Component.ToString().ToLower();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
new file mode 100644
index 0000000000..7e482d4045
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
@@ -0,0 +1,9 @@
+// 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.Catch
+{
+ public enum CatchSkinComponents
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index 5a640f6d1a..a6283eb7c4 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -10,7 +10,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
-using osuTK;
namespace osu.Game.Rulesets.Catch.Difficulty
{
@@ -35,12 +34,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
mods = Score.Mods;
- var legacyScore = Score as LegacyScoreInfo;
-
- fruitsHit = legacyScore?.Count300 ?? Score.Statistics[HitResult.Perfect];
- ticksHit = legacyScore?.Count100 ?? 0;
- tinyTicksHit = legacyScore?.Count50 ?? 0;
- tinyTicksMissed = legacyScore?.CountKatu ?? 0;
+ fruitsHit = Score?.GetCount300() ?? Score.Statistics[HitResult.Perfect];
+ ticksHit = Score?.GetCount100() ?? 0;
+ tinyTicksHit = Score?.GetCount50() ?? 0;
+ tinyTicksMissed = Score?.GetCountKatu() ?? 0;
misses = Score.Statistics[HitResult.Miss];
// Don't count scores made with supposedly unranked mods
@@ -48,55 +45,53 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return 0;
// We are heavily relying on aim in catch the beat
- double value = Math.Pow(5.0f * Math.Max(1.0f, Attributes.StarRating / 0.0049f) - 4.0f, 2.0f) / 100000.0f;
+ double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo
int numTotalHits = totalComboHits();
// Longer maps are worth more
- float lengthBonus =
- 0.95f + 0.4f * Math.Min(1.0f, numTotalHits / 3000.0f) +
- (numTotalHits > 3000 ? (float)Math.Log10(numTotalHits / 3000.0f) * 0.5f : 0.0f);
+ double lengthBonus =
+ 0.95 + 0.4 * Math.Min(1.0, numTotalHits / 3000.0) +
+ (numTotalHits > 3000 ? Math.Log10(numTotalHits / 3000.0) * 0.5 : 0.0);
// Longer maps are worth more
value *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
- value *= Math.Pow(0.97f, misses);
+ value *= Math.Pow(0.97, misses);
// Combo scaling
- float beatmapMaxCombo = Attributes.MaxCombo;
- if (beatmapMaxCombo > 0)
- value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
+ if (Attributes.MaxCombo > 0)
+ value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
- float approachRate = (float)Attributes.ApproachRate;
- float approachRateFactor = 1.0f;
- if (approachRate > 9.0f)
- approachRateFactor += 0.1f * (approachRate - 9.0f); // 10% for each AR above 9
- else if (approachRate < 8.0f)
- approachRateFactor += 0.025f * (8.0f - approachRate); // 2.5% for each AR below 8
+ double approachRateFactor = 1.0;
+ if (Attributes.ApproachRate > 9.0)
+ approachRateFactor += 0.1 * (Attributes.ApproachRate - 9.0); // 10% for each AR above 9
+ else if (Attributes.ApproachRate < 8.0)
+ approachRateFactor += 0.025 * (8.0 - Attributes.ApproachRate); // 2.5% for each AR below 8
value *= approachRateFactor;
if (mods.Any(m => m is ModHidden))
// Hiddens gives nothing on max approach rate, and more the lower it is
- value *= 1.05f + 0.075f * (10.0f - Math.Min(10.0f, approachRate)); // 7.5% for each AR below 10
+ value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
- value *= 1.35f * lengthBonus;
+ value *= 1.35 * lengthBonus;
// Scale the aim value with accuracy _slightly_
- value *= Math.Pow(accuracy(), 5.5f);
+ value *= Math.Pow(accuracy(), 5.5);
// Custom multipliers for NoFail. SpunOut is not applicable.
if (mods.Any(m => m is ModNoFail))
- value *= 0.90f;
+ value *= 0.90;
return value;
}
- private float accuracy() => totalHits() == 0 ? 0 : MathHelper.Clamp((float)totalSuccessfulHits() / totalHits(), 0f, 1f);
+ private float accuracy() => totalHits() == 0 ? 0 : Math.Clamp((float)totalSuccessfulHits() / totalHits(), 0, 1);
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
private int totalComboHits() => misses + ticksHit + fruitsHit;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index d146153294..7cd569035b 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -6,7 +6,6 @@ using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
-using osuTK;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
@@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
if (lastPlayerPosition == null)
lastPlayerPosition = catchCurrent.LastNormalizedPosition;
- float playerPosition = MathHelper.Clamp(
+ float playerPosition = Math.Clamp(
lastPlayerPosition.Value,
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
index 374dd50c11..fc030877f1 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
return 0;
case HitResult.Perfect:
- return 0.008;
+ return 0.01;
}
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
index f1399bb5c0..e87ecba749 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
@@ -18,17 +18,5 @@ namespace osu.Game.Rulesets.Catch.Judgements
return 30;
}
}
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return base.HealthIncreaseFor(result);
-
- case HitResult.Perfect:
- return 0.007;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
index 8fd9ac92ba..2149ed9712 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
@@ -23,18 +23,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
}
}
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return -0.02;
-
- case HitResult.Perfect:
- return 0.01;
- }
- }
-
///
/// Whether fruit on the platter should explode or drop.
/// Note that this is only checked if the owning object is also
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
index 3829b5e94f..d607b49ea4 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
return 0;
case HitResult.Perfect:
- return 0.004;
+ return 0.02;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs
index b3605b013b..46e427e1b7 100644
--- a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs
+++ b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs
@@ -12,14 +12,14 @@ namespace osu.Game.Rulesets.Catch.MathUtils
{
private const double int_to_real = 1.0 / (int.MaxValue + 1.0);
private const uint int_mask = 0x7FFFFFFF;
- private const uint y = 842502087;
- private const uint z = 3579807591;
- private const uint w = 273326509;
- private uint _x, _y = y, _z = z, _w = w;
+ private const uint y_initial = 842502087;
+ private const uint z_initial = 3579807591;
+ private const uint w_initial = 273326509;
+ private uint x, y = y_initial, z = z_initial, w = w_initial;
public FastRandom(int seed)
{
- _x = (uint)seed;
+ x = (uint)seed;
}
public FastRandom()
@@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Catch.MathUtils
/// The random value.
public uint NextUInt()
{
- uint t = _x ^ (_x << 11);
- _x = _y;
- _y = _z;
- _z = _w;
- return _w = _w ^ (_w >> 19) ^ t ^ (t >> 8);
+ uint t = x ^ (x << 11);
+ x = y;
+ y = z;
+ z = w;
+ return w = w ^ (w >> 19) ^ t ^ (t >> 8);
}
///
@@ -61,6 +61,14 @@ namespace osu.Game.Rulesets.Catch.MathUtils
/// The random value.
public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound));
+ ///
+ /// Generates a random integer value within the range [, ).
+ ///
+ /// The lower bound of the range.
+ /// The upper bound of the range.
+ /// The random value.
+ public int Next(double lowerBound, double upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound));
+
///
/// Generates a random double value within the range [0, 1).
///
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
new file mode 100644
index 0000000000..3bc1ee5bf5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.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.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Rulesets.Catch.Mods
+{
+ public class CatchModCinema : ModCinema
+ {
+ public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ {
+ ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
+ Replay = new CatchAutoGenerator(beatmap).Generate(),
+ };
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
new file mode 100644
index 0000000000..8377b3786a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Catch.Mods
+{
+ public class CatchModDifficultyAdjust : ModDifficultyAdjust
+ {
+ [SettingSource("Fruit Size", "Override a beatmap's set CS.")]
+ public BindableNumber CircleSize { get; } = new BindableFloat
+ {
+ Precision = 0.1f,
+ MinValue = 1,
+ MaxValue = 10,
+ Default = 5,
+ Value = 5,
+ };
+
+ [SettingSource("Approach Rate", "Override a beatmap's set AR.")]
+ public BindableNumber ApproachRate { get; } = new BindableFloat
+ {
+ Precision = 0.1f,
+ MinValue = 1,
+ MaxValue = 10,
+ Default = 5,
+ Value = 5,
+ };
+
+ protected override void TransferSettings(BeatmapDifficulty difficulty)
+ {
+ base.TransferSettings(difficulty);
+
+ TransferSetting(CircleSize, difficulty.CircleSize);
+ TransferSetting(ApproachRate, difficulty.ApproachRate);
+ }
+
+ protected override void ApplySettings(BeatmapDifficulty difficulty)
+ {
+ base.ApplySettings(difficulty);
+
+ difficulty.CircleSize = CircleSize.Value;
+ difficulty.ApproachRate = ApproachRate.Value;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
index 060e51e31d..ced1900ba9 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
@@ -1,121 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.MathUtils;
-using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
-using System;
-using osu.Game.Rulesets.Objects;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Beatmaps;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModHardRock : ModHardRock, IApplicableToHitObject
+ public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
{
public override double ScoreMultiplier => 1.12;
public override bool Ranked => true;
- private float? lastPosition;
- private double lastStartTime;
-
- public void ApplyToHitObject(HitObject hitObject)
- {
- if (hitObject is JuiceStream stream)
- {
- lastPosition = stream.EndX;
- lastStartTime = stream.EndTime;
- return;
- }
-
- if (!(hitObject is Fruit))
- return;
-
- var catchObject = (CatchHitObject)hitObject;
-
- float position = catchObject.X;
- double startTime = hitObject.StartTime;
-
- if (lastPosition == null)
- {
- lastPosition = position;
- lastStartTime = startTime;
-
- return;
- }
-
- float positionDiff = position - lastPosition.Value;
- double timeDiff = startTime - lastStartTime;
-
- if (timeDiff > 1000)
- {
- lastPosition = position;
- lastStartTime = startTime;
- return;
- }
-
- if (positionDiff == 0)
- {
- applyRandomOffset(ref position, timeDiff / 4d);
- catchObject.X = position;
- return;
- }
-
- if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d)
- applyOffset(ref position, positionDiff);
-
- catchObject.X = position;
-
- lastPosition = position;
- lastStartTime = startTime;
- }
-
- ///
- /// Applies a random offset in a random direction to a position, ensuring that the final position remains within the boundary of the playfield.
- ///
- /// The position which the offset should be applied to.
- /// The maximum offset, cannot exceed 20px.
- private void applyRandomOffset(ref float position, double maxOffset)
- {
- bool right = RNG.NextBool();
- float rand = Math.Min(20, (float)RNG.NextDouble(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH;
-
- if (right)
- {
- // Clamp to the right bound
- if (position + rand <= 1)
- position += rand;
- else
- position -= rand;
- }
- else
- {
- // Clamp to the left bound
- if (position - rand >= 0)
- position -= rand;
- else
- position += rand;
- }
- }
-
- ///
- /// Applies an offset to a position, ensuring that the final position remains within the boundary of the playfield.
- ///
- /// The position which the offset should be applied to.
- /// The amount to offset by.
- private void applyOffset(ref float position, float amount)
- {
- if (amount > 0)
- {
- // Clamp to the right bound
- if (position + amount < 1)
- position += amount;
- }
- else
- {
- // Clamp to the left bound
- if (position + amount > 0)
- position += amount;
- }
- }
+ public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
index da2edcee44..c07087efaf 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
@@ -1,11 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModNightcore : ModNightcore
+ public class CatchModNightcore : ModNightcore
{
public override double ScoreMultiplier => 1.06;
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 0454bc969d..4c72b9fd3e 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -1,12 +1,51 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModRelax : ModRelax
+ public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset
{
public override string Description => @"Use the mouse to control the catcher.";
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
+ }
+
+ private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition
+ {
+ private readonly CatcherArea.Catcher catcher;
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
+
+ public MouseInputHelper(CatchPlayfield playfield)
+ {
+ catcher = playfield.CatcherArea.MovableCatcher;
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ //disable keyboard controls
+ public bool OnPressed(CatchAction action) => true;
+
+ public void OnReleased(CatchAction action)
+ {
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ catcher.UpdatePosition(e.MousePosition.X / DrawSize.X);
+ return base.OnMouseMove(e);
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index 6d44e4660e..267e6d12c7 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -27,11 +27,13 @@ namespace osu.Game.Rulesets.Catch.Objects
return;
for (double i = StartTime; i <= EndTime; i += spacing)
+ {
AddNested(new Banana
{
Samples = Samples,
StartTime = i
});
+ }
}
public double EndTime => StartTime + Duration;
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index be76edc01b..e4ad49ea50 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -1,10 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Objects
{
@@ -12,7 +15,18 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public const double OBJECT_RADIUS = 44;
- public float X { get; set; }
+ private float x;
+
+ public float X
+ {
+ get => x + XOffset;
+ set => x = value;
+ }
+
+ ///
+ /// A random offset applied to , set by the .
+ ///
+ internal float XOffset { get; set; }
public double TimePreempt = 1000;
@@ -24,9 +38,21 @@ namespace osu.Game.Rulesets.Catch.Objects
public int ComboOffset { get; set; }
- public int IndexInCurrentCombo { get; set; }
+ public Bindable IndexInCurrentComboBindable { get; } = new Bindable();
- public int ComboIndex { get; set; }
+ public int IndexInCurrentCombo
+ {
+ get => IndexInCurrentComboBindable.Value;
+ set => IndexInCurrentComboBindable.Value = value;
+ }
+
+ public Bindable ComboIndexBindable { get; } = new Bindable();
+
+ public int ComboIndex
+ {
+ get => ComboIndexBindable.Value;
+ set => ComboIndexBindable.Value = value;
+ }
///
/// Difference between the distance to the next object
@@ -35,10 +61,16 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public float DistanceToHyperDash { get; set; }
+ public Bindable LastInComboBindable { get; } = new Bindable();
+
///
/// The next fruit starts a new combo. Used for explodey.
///
- public virtual bool LastInCombo { get; set; }
+ public virtual bool LastInCombo
+ {
+ get => LastInComboBindable.Value;
+ set => LastInComboBindable.Value = value;
+ }
public float Scale { get; set; } = 1;
@@ -61,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.Objects
Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5;
}
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
public enum FruitVisualRepresentation
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs
index 42646851d7..ea415e18fa 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs
@@ -2,35 +2,50 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public class DrawableBananaShower : DrawableCatchHitObject
{
+ private readonly Func> createDrawableRepresentation;
private readonly Container bananaContainer;
public DrawableBananaShower(BananaShower s, Func> createDrawableRepresentation = null)
: base(s)
{
+ this.createDrawableRepresentation = createDrawableRepresentation;
RelativeSizeAxes = Axes.X;
Origin = Anchor.BottomLeft;
X = 0;
AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both });
-
- foreach (var b in s.NestedHitObjects.Cast())
- AddNested(createDrawableRepresentation?.Invoke(b));
}
- protected override void AddNested(DrawableHitObject h)
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
- ((DrawableCatchHitObject)h).CheckPosition = o => CheckPosition?.Invoke(o) ?? false;
- bananaContainer.Add(h);
- base.AddNested(h);
+ base.AddNestedHitObject(hitObject);
+ bananaContainer.Add(hitObject);
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ bananaContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Banana banana:
+ return createDrawableRepresentation?.Invoke(banana)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
index 5785d9a9ca..b7c05392f3 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
@@ -3,12 +3,10 @@
using System;
using osuTK;
-using osuTK.Graphics;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
@@ -52,6 +50,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
public Func CheckPosition;
+ public bool IsOnPlate;
+
+ public override bool RemoveWhenNotAlive => IsOnPlate;
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (CheckPosition == null) return;
@@ -60,31 +62,24 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss);
}
- protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
+
+ protected override void UpdateInitialTransforms() => this.FadeInFromZero(200);
+
+ protected override void UpdateStateTransforms(ArmedState state)
{
- base.SkinChanged(skin, allowFallback);
-
- if (HitObject is IHasComboInformation combo)
- AccentColour = skin.GetValue(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White;
- }
-
- protected override void UpdateState(ArmedState state)
- {
- using (BeginAbsoluteSequence(HitObject.StartTime - HitObject.TimePreempt))
- this.FadeIn(200);
-
- var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;
+ var endTime = HitObject.GetEndTime();
using (BeginAbsoluteSequence(endTime, true))
{
switch (state)
{
case ArmedState.Miss:
- this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out).Expire();
+ this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out);
break;
case ArmedState.Hit:
- this.FadeOut().Expire();
+ this.FadeOut();
break;
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs
index 9cabdc3dd9..059310d671 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
@@ -27,16 +26,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
private void load()
{
AddInternal(pulp = new Pulp { Size = Size });
- }
- public override Color4 AccentColour
- {
- get => base.AccentColour;
- set
- {
- base.AccentColour = value;
- pulp.AccentColour = AccentColour;
- }
+ AccentColour.BindValueChanged(colour => { pulp.AccentColour = colour.NewValue; }, true);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs
index 77407def54..53a018c9f4 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs
@@ -8,7 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces;
using osuTK;
using osuTK.Graphics;
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
private void load()
{
// todo: this should come from the skin.
- AccentColour = colourForRepresentation(HitObject.VisualRepresentation);
+ AccentColour.Value = colourForRepresentation(HitObject.VisualRepresentation);
AddRangeInternal(new[]
{
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
Hollow = !HitObject.HyperDash,
Type = EdgeEffectType.Glow,
Radius = 4 * radius_adjust,
- Colour = HitObject.HyperDash ? Color4.Red : AccentColour.Darken(1).Opacity(0.6f)
+ Colour = HitObject.HyperDash ? Color4.Red : AccentColour.Value.Darken(1).Opacity(0.6f)
},
Size = new Vector2(Height),
Anchor = Anchor.Centre,
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
new Box
{
AlwaysPresent = true,
- Colour = AccentColour,
+ Colour = AccentColour.Value,
Alpha = 0,
RelativeSizeAxes = Axes.Both
}
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AccentColour = Color4.Red,
- Blending = BlendingMode.Additive,
+ Blending = BlendingParameters.Additive,
Alpha = 0.5f,
Scale = new Vector2(1.333f)
});
@@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
const float small_pulp = large_pulp_3 / 2;
- Vector2 positionAt(float angle, float distance) => new Vector2(
- distance * (float)Math.Sin(angle * Math.PI / 180),
- distance * (float)Math.Cos(angle * Math.PI / 180));
+ static Vector2 positionAt(float angle, float distance) => new Vector2(
+ distance * MathF.Sin(angle * MathF.PI / 180),
+ distance * MathF.Cos(angle * MathF.PI / 180));
switch (representation)
{
@@ -115,32 +115,32 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(small_pulp),
Y = -0.34f,
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_4),
Position = positionAt(0, distance_from_centre_4),
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_4),
Position = positionAt(90, distance_from_centre_4),
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_4),
Position = positionAt(180, distance_from_centre_4),
},
new Pulp
{
Size = new Vector2(large_pulp_4),
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Position = positionAt(270, distance_from_centre_4),
},
}
@@ -154,32 +154,32 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(small_pulp),
Y = -0.3f,
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_4),
Position = positionAt(45, distance_from_centre_4),
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_4),
Position = positionAt(135, distance_from_centre_4),
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_4),
Position = positionAt(225, distance_from_centre_4),
},
new Pulp
{
Size = new Vector2(large_pulp_4),
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Position = positionAt(315, distance_from_centre_4),
},
}
@@ -193,26 +193,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(small_pulp),
Y = -0.33f,
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_3),
Position = positionAt(60, distance_from_centre_3),
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_3),
Position = positionAt(180, distance_from_centre_3),
},
new Pulp
{
Size = new Vector2(large_pulp_3),
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Position = positionAt(300, distance_from_centre_3),
},
}
@@ -226,26 +226,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(small_pulp),
Y = -0.25f,
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_3),
Position = positionAt(0, distance_from_centre_3),
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_3),
Position = positionAt(120, distance_from_centre_3),
},
new Pulp
{
Size = new Vector2(large_pulp_3),
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Position = positionAt(240, distance_from_centre_3),
},
}
@@ -259,13 +259,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(small_pulp),
Y = -0.3f
},
new Pulp
{
- AccentColour = AccentColour,
+ AccentColour = AccentColour.Value,
Size = new Vector2(large_pulp_4 * 0.8f, large_pulp_4 * 2.5f),
Y = 0.05f,
},
@@ -278,7 +278,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
base.Update();
- border.Alpha = (float)MathHelper.Clamp((HitObject.StartTime - Time.Current) / 500, 0, 1);
+ border.Alpha = (float)Math.Clamp((HitObject.StartTime - Time.Current) / 500, 0, 1);
}
private Color4 colourForRepresentation(FruitVisualRepresentation representation)
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs
index 9e5e9f6a04..a24821b3ce 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs
@@ -2,38 +2,50 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public class DrawableJuiceStream : DrawableCatchHitObject
{
+ private readonly Func> createDrawableRepresentation;
private readonly Container dropletContainer;
public DrawableJuiceStream(JuiceStream s, Func> createDrawableRepresentation = null)
: base(s)
{
+ this.createDrawableRepresentation = createDrawableRepresentation;
RelativeSizeAxes = Axes.Both;
Origin = Anchor.BottomLeft;
X = 0;
AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, });
-
- foreach (var o in s.NestedHitObjects.Cast())
- AddNested(createDrawableRepresentation?.Invoke(o));
}
- protected override void AddNested(DrawableHitObject h)
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
- var catchObject = (DrawableCatchHitObject)h;
+ base.AddNestedHitObject(hitObject);
+ dropletContainer.Add(hitObject);
+ }
- catchObject.CheckPosition = o => CheckPosition?.Invoke(o) ?? false;
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ dropletContainer.Clear();
+ }
- dropletContainer.Add(h);
- base.AddNested(h);
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case CatchHitObject catchObject:
+ return createDrawableRepresentation?.Invoke(catchObject)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs
index b9b6d5b924..1e9daf18db 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
- Blending = BlendingMode.Additive;
+ Blending = BlendingParameters.Additive;
Colour = Color4.White.Opacity(0.9f);
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 0952e8981a..a4ed966abb 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -116,17 +116,27 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration => EndTime - StartTime;
- private SliderPath path;
+ private readonly SliderPath path = new SliderPath();
public SliderPath Path
{
get => path;
- set => path = value;
+ set
+ {
+ path.ControlPoints.Clear();
+ path.ExpectedDistance.Value = null;
+
+ if (value != null)
+ {
+ path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position.Value, c.Type.Value)));
+ path.ExpectedDistance.Value = value.ExpectedDistance.Value;
+ }
+ }
}
public double Distance => Path.Distance;
- public List> NodeSamples { get; set; } = new List>();
+ public List> NodeSamples { get; set; } = new List>();
public double? LegacyLastTickOffset { get; set; }
}
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 8dd00756f2..4649dcae90 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -3,7 +3,7 @@
using System;
using System.Linq;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Beatmaps;
@@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Catch.Replays
protected Replay Replay;
+ private CatchReplayFrame currentFrame;
+
public override Replay Generate()
{
// todo: add support for HT DT
@@ -35,9 +37,6 @@ namespace osu.Game.Rulesets.Catch.Replays
float lastPosition = 0.5f;
double lastTime = 0;
- // Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled
- Replay.Frames.Add(new CatchReplayFrame(-100000, lastPosition));
-
void moveToNext(CatchHitObject h)
{
float positionChange = Math.Abs(lastPosition - h.X);
@@ -58,18 +57,18 @@ namespace osu.Game.Rulesets.Catch.Replays
{
//we are already in the correct range.
lastTime = h.StartTime;
- Replay.Frames.Add(new CatchReplayFrame(h.StartTime, lastPosition));
+ addFrame(h.StartTime, lastPosition);
return;
}
if (impossibleJump)
{
- Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X));
+ addFrame(h.StartTime, h.X);
}
else if (h.HyperDash)
{
- Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable, lastPosition));
- Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X));
+ addFrame(h.StartTime - timeAvailable, lastPosition);
+ addFrame(h.StartTime, h.X);
}
else if (dashRequired)
{
@@ -81,16 +80,16 @@ namespace osu.Game.Rulesets.Catch.Replays
float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable);
//dash movement
- Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + 1, lastPosition, true));
- Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition));
- Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X));
+ addFrame(h.StartTime - timeAvailable + 1, lastPosition, true);
+ addFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition);
+ addFrame(h.StartTime, h.X);
}
else
{
double timeBefore = positionChange / movement_speed;
- Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeBefore, lastPosition));
- Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X));
+ addFrame(h.StartTime - timeBefore, lastPosition);
+ addFrame(h.StartTime, h.X);
}
lastTime = h.StartTime;
@@ -122,5 +121,16 @@ namespace osu.Game.Rulesets.Catch.Replays
return Replay;
}
+
+ private void addFrame(double time, float? position = null, bool dashing = false)
+ {
+ // todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
+ if (Replay.Frames.Count == 0)
+ Replay.Frames.Add(new CatchReplayFrame(time - 1, position, false, null));
+
+ var last = currentFrame;
+ currentFrame = new CatchReplayFrame(time, position, dashing, last);
+ Replay.Frames.Add(currentFrame);
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
index 103aa6c3f1..f122588a2b 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
@@ -3,8 +3,9 @@
using System.Collections.Generic;
using System.Diagnostics;
+using System.Linq;
using osu.Framework.Input.StateChanges;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Replays
{
}
- protected override bool IsImportant(CatchReplayFrame frame) => frame.Position > 0;
+ protected override bool IsImportant(CatchReplayFrame frame) => frame.Actions.Any();
protected float? Position
{
@@ -38,21 +39,11 @@ namespace osu.Game.Rulesets.Catch.Replays
{
if (!Position.HasValue) return new List();
- var actions = new List();
-
- if (CurrentFrame.Dashing)
- actions.Add(CatchAction.Dash);
-
- if (Position.Value > CurrentFrame.Position)
- actions.Add(CatchAction.MoveRight);
- else if (Position.Value < CurrentFrame.Position)
- actions.Add(CatchAction.MoveLeft);
-
return new List
{
new CatchReplayState
{
- PressedActions = actions,
+ PressedActions = CurrentFrame?.Actions ?? new List(),
CatcherX = Position.Value
},
};
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
index 1e88b35c3b..b41a5e0612 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.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 osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Catch.UI;
@@ -11,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Replays
{
public class CatchReplayFrame : ReplayFrame, IConvertibleReplayFrame
{
+ public List Actions = new List();
+
public float Position;
public bool Dashing;
@@ -18,17 +21,40 @@ namespace osu.Game.Rulesets.Catch.Replays
{
}
- public CatchReplayFrame(double time, float? position = null, bool dashing = false)
+ public CatchReplayFrame(double time, float? position = null, bool dashing = false, CatchReplayFrame lastFrame = null)
: base(time)
{
Position = position ?? -1;
Dashing = dashing;
+
+ if (Dashing)
+ Actions.Add(CatchAction.Dash);
+
+ if (lastFrame != null)
+ {
+ if (Position > lastFrame.Position)
+ lastFrame.Actions.Add(CatchAction.MoveRight);
+ else if (Position < lastFrame.Position)
+ lastFrame.Actions.Add(CatchAction.MoveLeft);
+ }
}
- public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
+ public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
- Position = legacyFrame.Position.X / CatchPlayfield.BASE_WIDTH;
- Dashing = legacyFrame.ButtonState == ReplayButtonState.Left1;
+ Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH;
+ Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;
+
+ if (Dashing)
+ Actions.Add(CatchAction.Dash);
+
+ // this probably needs some cross-checking with osu-stable to ensure it is actually correct.
+ if (lastFrame is CatchReplayFrame lastCatchFrame)
+ {
+ if (Position > lastCatchFrame.Position)
+ lastCatchFrame.Actions.Add(CatchAction.MoveRight);
+ else if (Position < lastCatchFrame.Position)
+ Actions.Add(CatchAction.MoveLeft);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json
new file mode 100644
index 0000000000..83f9e30800
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json
@@ -0,0 +1,150 @@
+{
+ "Mappings": [{
+ "StartTime": 369,
+ "Objects": [{
+ "StartTime": 369,
+ "Position": 177
+ },
+ {
+ "StartTime": 450,
+ "Position": 216.539276
+ },
+ {
+ "StartTime": 532,
+ "Position": 256.5667
+ },
+ {
+ "StartTime": 614,
+ "Position": 296.594116
+ },
+ {
+ "StartTime": 696,
+ "Position": 336.621521
+ },
+ {
+ "StartTime": 778,
+ "Position": 376.99762
+ },
+ {
+ "StartTime": 860,
+ "Position": 337.318878
+ },
+ {
+ "StartTime": 942,
+ "Position": 297.291443
+ },
+ {
+ "StartTime": 1024,
+ "Position": 257.264038
+ },
+ {
+ "StartTime": 1106,
+ "Position": 217.2366
+ },
+ {
+ "StartTime": 1188,
+ "Position": 177
+ },
+ {
+ "StartTime": 1270,
+ "Position": 216.818192
+ },
+ {
+ "StartTime": 1352,
+ "Position": 256.8456
+ },
+ {
+ "StartTime": 1434,
+ "Position": 296.873047
+ },
+ {
+ "StartTime": 1516,
+ "Position": 336.900452
+ },
+ {
+ "StartTime": 1598,
+ "Position": 376.99762
+ },
+ {
+ "StartTime": 1680,
+ "Position": 337.039948
+ },
+ {
+ "StartTime": 1762,
+ "Position": 297.0125
+ },
+ {
+ "StartTime": 1844,
+ "Position": 256.9851
+ },
+ {
+ "StartTime": 1926,
+ "Position": 216.957672
+ },
+ {
+ "StartTime": 2008,
+ "Position": 177
+ },
+ {
+ "StartTime": 2090,
+ "Position": 217.097137
+ },
+ {
+ "StartTime": 2172,
+ "Position": 257.124573
+ },
+ {
+ "StartTime": 2254,
+ "Position": 297.152
+ },
+ {
+ "StartTime": 2336,
+ "Position": 337.179443
+ },
+ {
+ "StartTime": 2418,
+ "Position": 376.99762
+ },
+ {
+ "StartTime": 2500,
+ "Position": 336.760956
+ },
+ {
+ "StartTime": 2582,
+ "Position": 296.733643
+ },
+ {
+ "StartTime": 2664,
+ "Position": 256.7062
+ },
+ {
+ "StartTime": 2746,
+ "Position": 216.678772
+ },
+ {
+ "StartTime": 2828,
+ "Position": 177
+ },
+ {
+ "StartTime": 2909,
+ "Position": 216.887909
+ },
+ {
+ "StartTime": 2991,
+ "Position": 256.915344
+ },
+ {
+ "StartTime": 3073,
+ "Position": 296.942749
+ },
+ {
+ "StartTime": 3155,
+ "Position": 336.970184
+ },
+ {
+ "StartTime": 3237,
+ "Position": 376.99762
+ }
+ ]
+ }]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu
new file mode 100644
index 0000000000..c1971edf2c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu
@@ -0,0 +1,18 @@
+osu file format v14
+
+[General]
+StackLeniency: 0.4
+Mode: 0
+
+[Difficulty]
+CircleSize:4
+OverallDifficulty:7
+ApproachRate:8
+SliderMultiplier:1.6
+SliderTickRate:4
+
+[TimingPoints]
+369,327.868852459016,4,2,2,32,1,0
+
+[HitObjects]
+177,191,369,6,0,L|382:192,7,200
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json
new file mode 100644
index 0000000000..7333b600fb
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json
@@ -0,0 +1,74 @@
+{
+ "Mappings": [{
+ "StartTime": 369,
+ "Objects": [{
+ "StartTime": 369,
+ "Position": 65
+ },
+ {
+ "StartTime": 450,
+ "Position": 482
+ },
+ {
+ "StartTime": 532,
+ "Position": 164
+ },
+ {
+ "StartTime": 614,
+ "Position": 315
+ },
+ {
+ "StartTime": 696,
+ "Position": 145
+ },
+ {
+ "StartTime": 778,
+ "Position": 159
+ },
+ {
+ "StartTime": 860,
+ "Position": 310
+ },
+ {
+ "StartTime": 942,
+ "Position": 441
+ },
+ {
+ "StartTime": 1024,
+ "Position": 428
+ },
+ {
+ "StartTime": 1106,
+ "Position": 243
+ },
+ {
+ "StartTime": 1188,
+ "Position": 422
+ },
+ {
+ "StartTime": 1270,
+ "Position": 481
+ },
+ {
+ "StartTime": 1352,
+ "Position": 104
+ },
+ {
+ "StartTime": 1434,
+ "Position": 473
+ },
+ {
+ "StartTime": 1516,
+ "Position": 135
+ },
+ {
+ "StartTime": 1598,
+ "Position": 360
+ },
+ {
+ "StartTime": 1680,
+ "Position": 123
+ }
+ ]
+ }]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner.osu
new file mode 100644
index 0000000000..208d21a344
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner.osu
@@ -0,0 +1,18 @@
+osu file format v14
+
+[General]
+StackLeniency: 0.4
+Mode: 0
+
+[Difficulty]
+CircleSize:4
+OverallDifficulty:7
+ApproachRate:8
+SliderMultiplier:1.6
+SliderTickRate:4
+
+[TimingPoints]
+369,327.868852459016,4,2,2,32,1,0
+
+[HitObjects]
+256,192,369,12,0,1680,0:0:0:0:
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json
new file mode 100644
index 0000000000..bbc16ab912
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json
@@ -0,0 +1,234 @@
+{
+ "Mappings": [{
+ "StartTime": 369,
+ "Objects": [{
+ "StartTime": 369,
+ "Position": 258
+ }]
+ },
+ {
+ "StartTime": 450,
+ "Objects": [{
+ "StartTime": 450,
+ "Position": 254
+ }]
+ },
+ {
+ "StartTime": 532,
+ "Objects": [{
+ "StartTime": 532,
+ "Position": 241
+ }]
+ },
+ {
+ "StartTime": 614,
+ "Objects": [{
+ "StartTime": 614,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 696,
+ "Objects": [{
+ "StartTime": 696,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 778,
+ "Objects": [{
+ "StartTime": 778,
+ "Position": 278
+ }]
+ },
+ {
+ "StartTime": 860,
+ "Objects": [{
+ "StartTime": 860,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 942,
+ "Objects": [{
+ "StartTime": 942,
+ "Position": 278
+ }]
+ },
+ {
+ "StartTime": 1024,
+ "Objects": [{
+ "StartTime": 1024,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 1106,
+ "Objects": [{
+ "StartTime": 1106,
+ "Position": 278
+ }]
+ },
+ {
+ "StartTime": 1188,
+ "Objects": [{
+ "StartTime": 1188,
+ "Position": 278
+ }]
+ },
+ {
+ "StartTime": 1270,
+ "Objects": [{
+ "StartTime": 1270,
+ "Position": 278
+ }]
+ },
+ {
+ "StartTime": 1352,
+ "Objects": [{
+ "StartTime": 1352,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 1434,
+ "Objects": [{
+ "StartTime": 1434,
+ "Position": 258
+ }]
+ },
+ {
+ "StartTime": 1516,
+ "Objects": [{
+ "StartTime": 1516,
+ "Position": 253
+ }]
+ },
+ {
+ "StartTime": 1598,
+ "Objects": [{
+ "StartTime": 1598,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 1680,
+ "Objects": [{
+ "StartTime": 1680,
+ "Position": 260
+ }]
+ },
+ {
+ "StartTime": 1762,
+ "Objects": [{
+ "StartTime": 1762,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 1844,
+ "Objects": [{
+ "StartTime": 1844,
+ "Position": 278
+ }]
+ },
+ {
+ "StartTime": 1926,
+ "Objects": [{
+ "StartTime": 1926,
+ "Position": 278
+ }]
+ },
+ {
+ "StartTime": 2008,
+ "Objects": [{
+ "StartTime": 2008,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 2090,
+ "Objects": [{
+ "StartTime": 2090,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 2172,
+ "Objects": [{
+ "StartTime": 2172,
+ "Position": 243
+ }]
+ },
+ {
+ "StartTime": 2254,
+ "Objects": [{
+ "StartTime": 2254,
+ "Position": 278
+ }]
+ },
+ {
+ "StartTime": 2336,
+ "Objects": [{
+ "StartTime": 2336,
+ "Position": 278
+ }]
+ },
+ {
+ "StartTime": 2418,
+ "Objects": [{
+ "StartTime": 2418,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 2500,
+ "Objects": [{
+ "StartTime": 2500,
+ "Position": 258
+ }]
+ },
+ {
+ "StartTime": 2582,
+ "Objects": [{
+ "StartTime": 2582,
+ "Position": 256
+ }]
+ },
+ {
+ "StartTime": 2664,
+ "Objects": [{
+ "StartTime": 2664,
+ "Position": 242
+ }]
+ },
+ {
+ "StartTime": 2746,
+ "Objects": [{
+ "StartTime": 2746,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 2828,
+ "Objects": [{
+ "StartTime": 2828,
+ "Position": 238
+ }]
+ },
+ {
+ "StartTime": 2909,
+ "Objects": [{
+ "StartTime": 2909,
+ "Position": 271
+ }]
+ },
+ {
+ "StartTime": 2991,
+ "Objects": [{
+ "StartTime": 2991,
+ "Position": 254
+ }]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream.osu
new file mode 100644
index 0000000000..321af4604b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream.osu
@@ -0,0 +1,50 @@
+osu file format v14
+
+[General]
+StackLeniency: 0.4
+Mode: 0
+
+[Difficulty]
+CircleSize:4
+OverallDifficulty:7
+ApproachRate:8
+SliderMultiplier:1.6
+SliderTickRate:1
+
+[TimingPoints]
+369,327.868852459016,4,2,2,32,1,0
+
+[HitObjects]
+258,189,369,1,0,0:0:0:0:
+258,189,450,1,0,0:0:0:0:
+258,189,532,1,0,0:0:0:0:
+258,189,614,1,0,0:0:0:0:
+258,189,696,1,0,0:0:0:0:
+258,189,778,1,0,0:0:0:0:
+258,189,860,1,0,0:0:0:0:
+258,189,942,1,0,0:0:0:0:
+258,189,1024,1,0,0:0:0:0:
+258,189,1106,1,0,0:0:0:0:
+258,189,1188,1,0,0:0:0:0:
+258,189,1270,1,0,0:0:0:0:
+258,189,1352,1,0,0:0:0:0:
+258,189,1434,1,0,0:0:0:0:
+258,189,1516,1,0,0:0:0:0:
+258,189,1598,1,0,0:0:0:0:
+258,189,1680,1,0,0:0:0:0:
+258,189,1762,1,0,0:0:0:0:
+258,189,1844,1,0,0:0:0:0:
+258,189,1926,1,0,0:0:0:0:
+258,189,2008,1,0,0:0:0:0:
+258,189,2090,1,0,0:0:0:0:
+258,189,2172,1,0,0:0:0:0:
+258,189,2254,1,0,0:0:0:0:
+258,189,2336,1,0,0:0:0:0:
+258,189,2418,1,0,0:0:0:0:
+258,189,2500,1,0,0:0:0:0:
+258,189,2582,1,0,0:0:0:0:
+258,189,2664,1,0,0:0:0:0:
+258,189,2746,1,0,0:0:0:0:
+258,189,2828,1,0,0:0:0:0:
+258,189,2909,1,0,0:0:0:0:
+258,189,2991,1,0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
similarity index 87%
rename from osu.Game.Rulesets.Catch/Objects/CatchHitWindows.cs
rename to osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
index 837662f5fe..ff793a372e 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitWindows.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
@@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
-namespace osu.Game.Rulesets.Catch.Objects
+namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchHitWindows : HitWindows
{
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index 99b22b2d56..4c7bc4ab73 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -1,43 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.Scoring
{
- public class CatchScoreProcessor : ScoreProcessor
+ public class CatchScoreProcessor : ScoreProcessor
{
- public CatchScoreProcessor(DrawableRuleset drawableRuleset)
- : base(drawableRuleset)
- {
- }
-
- private float hpDrainRate;
-
- protected override void ApplyBeatmap(Beatmap beatmap)
- {
- base.ApplyBeatmap(beatmap);
-
- hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate;
- }
-
- protected override double HealthAdjustmentFactorFor(JudgementResult result)
- {
- switch (result.Type)
- {
- case HitResult.Miss:
- return hpDrainRate;
-
- default:
- return 10.2 - hpDrainRate; // Award less HP as drain rate is increased
- }
- }
-
public override HitWindows CreateHitWindows() => new CatchHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index b6d8cf9cbe..589503c35b 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
@@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.UI
internal readonly CatcherArea CatcherArea;
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || CatcherArea.ReceivePositionalInputAt(screenSpacePos);
+
public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation)
{
Container explodingFruitContainer;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 0b06e958e6..1de0b6bfa3 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -7,7 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (lastPlateableFruit == null)
return;
- // this is required to make this run after the last caught fruit runs UpdateState at least once.
+ // this is required to make this run after the last caught fruit runs updateState() at least once.
// TODO: find a better alternative
if (lastPlateableFruit.IsLoaded)
action();
@@ -69,10 +69,12 @@ namespace osu.Game.Rulesets.Catch.UI
caughtFruit.RelativePositionAxes = Axes.None;
caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(fruit.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0);
+ caughtFruit.IsOnPlate = true;
caughtFruit.Anchor = Anchor.TopCentre;
caughtFruit.Origin = Anchor.Centre;
caughtFruit.Scale *= 0.7f;
+ caughtFruit.LifetimeStart = caughtFruit.HitObject.StartTime;
caughtFruit.LifetimeEnd = double.MaxValue;
MovableCatcher.Add(caughtFruit);
@@ -101,7 +103,9 @@ namespace osu.Game.Rulesets.Catch.UI
MovableCatcher.X = state.CatcherX.Value;
}
- public bool OnReleased(CatchAction action) => false;
+ public void OnReleased(CatchAction action)
+ {
+ }
public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj);
@@ -196,16 +200,17 @@ namespace osu.Game.Rulesets.Catch.UI
var additive = createCatcherSprite();
additive.Anchor = Anchor;
- additive.OriginPosition = additive.OriginPosition + new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly.
+ additive.OriginPosition += new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly.
additive.Position = Position;
additive.Scale = Scale;
additive.Colour = HyperDashing ? Color4.Red : Color4.White;
additive.RelativePositionAxes = RelativePositionAxes;
- additive.Blending = BlendingMode.Additive;
+ additive.Blending = BlendingParameters.Additive;
AdditiveTarget.Add(additive);
- additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint).Expire();
+ additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
+ additive.Expire(true);
Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50);
}
@@ -232,7 +237,7 @@ namespace osu.Game.Rulesets.Catch.UI
fruit.Y -= RNG.NextSingle() * diff;
}
- fruit.X = MathHelper.Clamp(fruit.X, -CATCHER_SIZE / 2, CATCHER_SIZE / 2);
+ fruit.X = Math.Clamp(fruit.X, -CATCHER_SIZE / 2, CATCHER_SIZE / 2);
caughtFruit.Add(fruit);
}
@@ -300,6 +305,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint);
this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint);
+ Trail &= Dashing;
}
}
else
@@ -337,24 +343,22 @@ namespace osu.Game.Rulesets.Catch.UI
return false;
}
- public bool OnReleased(CatchAction action)
+ public void OnReleased(CatchAction action)
{
switch (action)
{
case CatchAction.MoveLeft:
currentDirection++;
- return true;
+ break;
case CatchAction.MoveRight:
currentDirection--;
- return true;
+ break;
case CatchAction.Dash:
Dashing = false;
- return true;
+ break;
}
-
- return false;
}
///
@@ -373,8 +377,7 @@ namespace osu.Game.Rulesets.Catch.UI
double dashModifier = Dashing ? 1 : 0.5;
double speed = BASE_SPEED * dashModifier * hyperDashModifier;
- Scale = new Vector2(Math.Abs(Scale.X) * direction, Scale.Y);
- X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * speed, 0, 1);
+ UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed));
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
@@ -406,6 +409,9 @@ namespace osu.Game.Rulesets.Catch.UI
f.MoveToY(f.Y + 75, 750, Easing.InSine);
f.FadeOut(750);
+
+ // todo: this shouldn't exist once DrawableHitObject's ClearTransformsAfter overrides are repaired.
+ f.LifetimeStart = Time.Current;
f.Expire();
}
}
@@ -436,12 +442,26 @@ namespace osu.Game.Rulesets.Catch.UI
ExplodingFruitTarget.Add(fruit);
}
+ fruit.ClearTransforms();
fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine).Then().MoveToY(fruit.Y + 50, 500, Easing.InSine);
fruit.MoveToX(fruit.X + originalX * 6, 1000);
fruit.FadeOut(750);
+ // todo: this shouldn't exist once DrawableHitObject's ClearTransformsAfter overrides are repaired.
+ fruit.LifetimeStart = Time.Current;
fruit.Expire();
}
+
+ public void UpdatePosition(float position)
+ {
+ position = Math.Clamp(position, 0, 1);
+
+ if (position == X)
+ return;
+
+ Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
+ X = position;
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
index c0c1952064..025fa9c56e 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader]
private void load()
{
- InternalChild = new SkinnableSprite(@"Play/Catch/fruit-catcher-idle")
+ InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle", confineMode: ConfineMode.ScaleDownToFit)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index f48b84e344..fdd820b891 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -10,10 +10,8 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Catch.Replays;
-using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -25,15 +23,13 @@ namespace osu.Game.Rulesets.Catch.UI
protected override bool UserScrollSpeedAdjustment => false;
- public DrawableCatchRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ public DrawableCatchRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Down;
TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
}
- public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this);
-
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation);
diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
index 883cac67d1..b19affbf9f 100644
--- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
+++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
@@ -1,9 +1,7 @@
-
- netstandard2.0
+ netstandard2.1
Library
- AnyCPU
true
catch the fruit. to the beat.
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml
index e6728c801d..de7935b2ef 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
index 0362402320..c381ea585d 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
@@ -5,7 +5,7 @@ using UIKit;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
- public class Application
+ public static class Application
{
public static void Main(string[] args)
{
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
index 24abccb19d..88ad484bc1 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
@@ -1,6 +1,5 @@
-
+
-
Debug
iPhoneSimulator
@@ -13,14 +12,6 @@
-
- libbass.a
- PreserveNewest
-
-
- libbass_fx.a
- PreserveNewest
-
Linker.xml
@@ -41,5 +32,4 @@
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json
index 492f894484..0811c2724c 100644
--- a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json
+++ b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Debug/netcoreapp2.2/osu.Game.Rulesets.Mania.Tests.dll"
+ "${workspaceRoot}/bin/Debug/netcoreapp3.1/osu.Game.Rulesets.Mania.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/netcoreapp2.2/osu.Game.Rulesets.Mania.Tests.dll"
+ "${workspaceRoot}/bin/Release/netcoreapp3.1/osu.Game.Rulesets.Mania.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
index 6b95975059..d0ff1fab43 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
@@ -4,12 +4,11 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
@@ -20,26 +19,49 @@ namespace osu.Game.Rulesets.Mania.Tests
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase("basic")]
- public new void Test(string name)
- {
- base.Test(name);
- }
+ public void Test(string name) => base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
{
yield return new ConvertValue
{
StartTime = hitObject.StartTime,
- EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime,
+ EndTime = hitObject.GetEndTime(),
Column = ((ManiaHitObject)hitObject).Column
};
}
- protected override ManiaConvertMapping CreateConvertMapping() => new ManiaConvertMapping(Converter);
+ private readonly Dictionary rngSnapshots = new Dictionary();
+
+ protected override void OnConversionGenerated(HitObject original, IEnumerable result, IBeatmapConverter beatmapConverter)
+ {
+ base.OnConversionGenerated(original, result, beatmapConverter);
+
+ rngSnapshots[original] = new RngSnapshot(beatmapConverter);
+ }
+
+ protected override ManiaConvertMapping CreateConvertMapping(HitObject source) => new ManiaConvertMapping(rngSnapshots[source]);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
}
+ public class RngSnapshot
+ {
+ public readonly uint RandomW;
+ public readonly uint RandomX;
+ public readonly uint RandomY;
+ public readonly uint RandomZ;
+
+ public RngSnapshot(IBeatmapConverter converter)
+ {
+ var maniaConverter = (ManiaBeatmapConverter)converter;
+ RandomW = maniaConverter.Random.W;
+ RandomX = maniaConverter.Random.X;
+ RandomY = maniaConverter.Random.Y;
+ RandomZ = maniaConverter.Random.Z;
+ }
+ }
+
public class ManiaConvertMapping : ConvertMapping, IEquatable
{
public uint RandomW;
@@ -51,13 +73,12 @@ namespace osu.Game.Rulesets.Mania.Tests
{
}
- public ManiaConvertMapping(IBeatmapConverter converter)
+ public ManiaConvertMapping(RngSnapshot snapshot)
{
- var maniaConverter = (ManiaBeatmapConverter)converter;
- RandomW = maniaConverter.Random.W;
- RandomX = maniaConverter.Random.X;
- RandomY = maniaConverter.Random.Y;
- RandomZ = maniaConverter.Random.Z;
+ RandomW = snapshot.RandomW;
+ RandomX = snapshot.RandomX;
+ RandomY = snapshot.RandomY;
+ RandomZ = snapshot.RandomZ;
}
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
new file mode 100644
index 0000000000..957743c5f1
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Game.Beatmaps.Legacy;
+using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaLegacyModConversionTest : LegacyModConversionTest
+ {
+ [TestCase(LegacyMods.Easy, new[] { typeof(ManiaModEasy) })]
+ [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) })]
+ [TestCase(LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) })]
+ [TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })]
+ [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
+ [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModFlashlight), typeof(ManiaModNightcore) })]
+ [TestCase(LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) })]
+ [TestCase(LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })]
+ [TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })]
+ public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
+
+ protected override Ruleset CreateRuleset() => new ManiaRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
index 4b3786c30a..afde1c9521 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
@@ -27,8 +27,13 @@ namespace osu.Game.Rulesets.Mania.Tests
[Cached(typeof(IReadOnlyList))]
private IReadOnlyList mods { get; set; } = Array.Empty();
+ [Cached(typeof(IScrollingInfo))]
+ private IScrollingInfo scrollingInfo;
+
protected ManiaPlacementBlueprintTestScene()
{
+ scrollingInfo = ((ScrollingTestContainer)HitObjectContainer).ScrollingInfo;
+
Add(column = new Column(0)
{
Anchor = Anchor.Centre,
@@ -36,15 +41,8 @@ namespace osu.Game.Rulesets.Mania.Tests
AccentColour = Color4.OrangeRed,
Clock = new FramedClock(new StopwatchClock()), // No scroll
});
- }
- protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
- {
- var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
-
- dependencies.CacheAs(((ScrollingTestContainer)HitObjectContainer).ScrollingInfo);
-
- return dependencies;
+ AddStep("change direction", () => ((ScrollingTestContainer)HitObjectContainer).Flip());
}
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
diff --git a/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs
new file mode 100644
index 0000000000..80b1b3df8e
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public abstract class SkinnableTestScene : OsuGridTestScene
+ {
+ private Skin defaultSkin;
+
+ protected SkinnableTestScene()
+ : base(1, 2)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio, SkinManager skinManager)
+ {
+ defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
+ }
+
+ public void SetContents(Func creationFunction)
+ {
+ Cell(0).Child = createProvider(null, creationFunction);
+ Cell(1).Child = createProvider(defaultSkin, creationFunction);
+ }
+
+ private Drawable createProvider(Skin skin, Func creationFunction)
+ {
+ var mainProvider = new SkinProvidingContainer(skin);
+
+ return mainProvider
+ .WithChild(new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider))
+ {
+ Child = creationFunction()
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
index 20ac5eaa39..a5248c7712 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
@@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
@@ -12,8 +13,14 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
+ [HeadlessTest]
public class TestSceneAutoGeneration : OsuTestScene
{
+ ///
+ /// The number of frames which are generated at the start of a replay regardless of hitobject content.
+ ///
+ private const int frame_offset = 1;
+
[Test]
public void TestSingleNote()
{
@@ -26,11 +33,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames");
- Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time");
- Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time");
- Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed");
- Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released");
+ Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
+ Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
+ Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
}
[Test]
@@ -47,11 +54,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames");
- Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time");
- Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed");
- Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released");
+ Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
+ Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
+ Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
}
[Test]
@@ -67,11 +74,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames");
- Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time");
- Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time");
- Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
- Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
+ Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
+ Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
+ Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
}
[Test]
@@ -89,11 +96,13 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames");
- Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time");
- Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
- Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
+ Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
+
+ Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
+ Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
}
[Test]
@@ -110,15 +119,15 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames");
- Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time");
- Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect first note release time");
- Assert.AreEqual(2000, generated.Frames[3].Time, "Incorrect second note hit time");
- Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time");
- Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed");
- Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released");
- Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been pressed");
- Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released");
+ Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
+ Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
+ Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time");
+ Assert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time");
+ Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been pressed");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
}
[Test]
@@ -137,16 +146,16 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames");
- Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect first note release time");
- Assert.AreEqual(2000, generated.Frames[2].Time, "Incorrect second note hit time");
- Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time");
- Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed");
- Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
- Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key1), "Key1 has not been released");
- Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has been released");
- Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released");
+ Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
+ Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
+ Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
+ Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
+ Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has been released");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
}
[Test]
@@ -164,14 +173,14 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == 4, "Replay must have 4 frames");
- Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time");
- Assert.AreEqual(3000, generated.Frames[2].Time, "Incorrect second note press time + first note release time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect second note release time");
- Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed");
- Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released");
- Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key2), "Key2 has not been pressed");
- Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been released");
+ Assert.IsTrue(generated.Frames.Count == frame_offset + 3, "Replay must have 3 generated frames");
+ Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
+ Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time");
+ Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
+ Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key2), "Key2 has not been pressed");
+ Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been released");
}
private bool checkContains(ReplayFrame frame, params ManiaAction[] actions) => actions.All(action => ((ManiaReplayFrame)frame).Actions.Contains(action));
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
new file mode 100644
index 0000000000..eea1a36a19
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneDrawableJudgement : SkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(DrawableJudgement),
+ typeof(DrawableManiaJudgement)
+ };
+
+ public TestSceneDrawableJudgement()
+ {
+ foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1))
+ {
+ AddStep("Show " + result.GetDescription(), () => SetContents(() =>
+ new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs
new file mode 100644
index 0000000000..26a1b1b1ec
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs
@@ -0,0 +1,62 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class TestSceneHitExplosion : OsuTestScene
+ {
+ private ScrollingTestContainer scrolling;
+
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(DrawableNote),
+ typeof(DrawableManiaHitObject),
+ };
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Child = scrolling = new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativePositionAxes = Axes.Y,
+ Y = -0.25f,
+ Size = new Vector2(Column.COLUMN_WIDTH, NotePiece.NOTE_HEIGHT),
+ };
+
+ int runcount = 0;
+
+ AddRepeatStep("explode", () =>
+ {
+ runcount++;
+
+ if (runcount % 15 > 12)
+ return;
+
+ scrolling.AddRange(new Drawable[]
+ {
+ new HitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ });
+ }, 100);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
new file mode 100644
index 0000000000..7b0cf40d45
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -0,0 +1,314 @@
+// 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 NUnit.Framework;
+using osu.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneHoldNoteInput : RateAdjustedBeatmapTestScene
+ {
+ private const double time_before_head = 250;
+ private const double time_head = 1500;
+ private const double time_during_hold_1 = 2500;
+ private const double time_tail = 4000;
+ private const double time_after_tail = 5250;
+
+ private List judgementResults;
+ private bool allJudgedFired;
+
+ ///
+ /// -----[ ]-----
+ /// o o
+ ///
+ [Test]
+ public void TestNoInput()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ assertNoteJudgement(HitResult.Perfect);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestPressTooEarlyAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail, ManiaAction.Key1),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestPressTooEarlyAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o
+ ///
+ [Test]
+ public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_before_head + 10),
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o
+ ///
+ [Test]
+ public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_before_head + 10),
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Perfect);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAtStartAndBreak()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 10),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o
+ ///
+ [Test]
+ public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 10),
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o o
+ ///
+ [Test]
+ public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 10),
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestPressDuringNoteAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o o
+ ///
+ [Test]
+ public void TestPressDuringNoteAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_tail, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail + 10),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ private void assertHeadJudgement(HitResult result)
+ => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
+
+ private void assertTailJudgement(HitResult result)
+ => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result);
+
+ private void assertNoteJudgement(HitResult result)
+ => AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result);
+
+ private void assertTickJudgement(HitResult result)
+ => AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick
+
+ private ScoreAccessibleReplayPlayer currentPlayer;
+
+ private void performTest(List frames)
+ {
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = time_tail - time_head,
+ Column = 0,
+ }
+ },
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ });
+
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults.Add(result);
+ };
+ p.ScoreProcessor.AllJudged += () =>
+ {
+ if (currentPlayer == p) allJudgedFired = true;
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ allJudgedFired = false;
+ judgementResults = new List();
+ });
+
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+ AddUntilStep("Wait for all judged", () => allJudgedFired);
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, false, false)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs
index 04c5724f93..90394f3d1b 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs
@@ -6,7 +6,6 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@@ -36,9 +35,11 @@ namespace osu.Game.Rulesets.Mania.Tests
Child = drawableObject = new DrawableHoldNote(holdNote)
{
Height = 300,
- AccentColour = OsuColour.Gray(0.3f)
+ AccentColour = { Value = OsuColour.Gray(0.3f) }
}
};
+
+ AddBlueprint(new HoldNoteSelectionBlueprint(drawableObject));
}
protected override void Update()
@@ -51,7 +52,5 @@ namespace osu.Game.Rulesets.Mania.Tests
nested.Y = (float)(-finalPosition * content.DrawHeight);
}
}
-
- protected override SelectionBlueprint CreateBlueprint() => new HoldNoteSelectionBlueprint(drawableObject);
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs
index 6bb344f977..1514bdf0bd 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs
@@ -5,7 +5,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@@ -17,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{
- private readonly DrawableNote drawableObject;
-
protected override Container Content => content ?? base.Content;
private readonly Container content;
@@ -27,6 +24,8 @@ namespace osu.Game.Rulesets.Mania.Tests
var note = new Note { Column = 0 };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ DrawableNote drawableObject;
+
base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
{
Anchor = Anchor.Centre,
@@ -34,8 +33,8 @@ namespace osu.Game.Rulesets.Mania.Tests
Size = new Vector2(50, 20),
Child = drawableObject = new DrawableNote(note)
};
- }
- protected override SelectionBlueprint CreateBlueprint() => new NoteSelectionBlueprint(drawableObject);
+ AddBlueprint(new NoteSelectionBlueprint(drawableObject));
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
index b2613a59d5..8dae5e6d84 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
@@ -11,6 +11,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
@@ -40,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
Child = new FillFlowContainer
{
+ Clock = new FramedClock(new ManualClock()),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
@@ -62,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.Tests
private Drawable createNoteDisplay(ScrollingDirection direction, int identifier, out DrawableNote hitObject)
{
- var note = new Note { StartTime = 999999999 };
+ var note = new Note { StartTime = 0 };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return new ScrollingTestContainer(direction)
@@ -70,14 +72,14 @@ namespace osu.Game.Rulesets.Mania.Tests
AutoSizeAxes = Axes.Both,
Child = new NoteContainer(direction, $"note {identifier}, scrolling {direction.ToString().ToLowerInvariant()}")
{
- Child = hitObject = new DrawableNote(note) { AccentColour = Color4.OrangeRed }
+ Child = hitObject = new DrawableNote(note) { AccentColour = { Value = Color4.OrangeRed } }
}
};
}
private Drawable createHoldNoteDisplay(ScrollingDirection direction, int identifier, out DrawableHoldNote hitObject)
{
- var note = new HoldNote { StartTime = 999999999, Duration = 5000 };
+ var note = new HoldNote { StartTime = 0, Duration = 5000 };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return new ScrollingTestContainer(direction)
@@ -88,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests
Child = hitObject = new DrawableHoldNote(note)
{
RelativeSizeAxes = Axes.Both,
- AccentColour = Color4.OrangeRed,
+ AccentColour = { Value = Color4.OrangeRed },
}
}
};
@@ -133,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Tests
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Width = 1.25f,
- Colour = Color4.Black.Opacity(0.5f)
+ Colour = Color4.Green.Opacity(0.5f)
},
content = new Container { RelativeSizeAxes = Axes.Both }
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs
new file mode 100644
index 0000000000..cd25d162d0
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestScenePlayer : PlayerTestScene
+ {
+ public TestScenePlayer()
+ : base(new ManiaRuleset())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index 395e6daf0a..d5fd2808b8 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -66,6 +66,8 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.TopCentre));
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.BottomCentre));
+ AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[0], Anchor.TopCentre));
+ AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.BottomCentre));
AddStep("flip direction", () =>
{
@@ -75,10 +77,14 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.BottomCentre));
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.TopCentre));
+ AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[0], Anchor.BottomCentre));
+ AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.TopCentre));
}
private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
+ private bool barsInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
+
private void createNote()
{
foreach (var stage in stages)
@@ -114,8 +120,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var obj = new BarLine
{
StartTime = Time.Current + 2000,
- ControlPoint = new TimingControlPoint(),
- BeatIndex = major ? 0 : 1
+ Major = major,
};
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index df5131dd8b..dea6e6c0fb 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,14 +2,14 @@
-
+
-
+
WinExe
- netcoreapp2.2
+ netcoreapp3.1
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index e10602312e..d904474815 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mania.Objects;
using System;
using System.Linq;
using System.Collections.Generic;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
private const int max_notes_for_density = 7;
- protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) };
-
public int TargetColumns;
public bool Dual;
public readonly bool IsForCurrentRuleset;
@@ -37,10 +35,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
private ManiaBeatmap beatmap;
- public ManiaBeatmapConverter(IBeatmap beatmap)
- : base(beatmap)
+ public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
{
- IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
+ IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
@@ -51,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
if (TargetColumns >= 10)
{
- TargetColumns = TargetColumns / 2;
+ TargetColumns /= 2;
Dual = true;
}
}
@@ -69,11 +67,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
}
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject);
+
protected override Beatmap ConvertBeatmap(IBeatmap original)
{
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
- int seed = (int)Math.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)Math.Round(difficulty.ApproachRate);
+ int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed);
return base.ConvertBeatmap(original);
@@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
prevNoteTimes.RemoveAt(0);
prevNoteTimes.Add(newNoteTime);
- density = (prevNoteTimes[prevNoteTimes.Count - 1] - prevNoteTimes[0]) / prevNoteTimes.Count;
+ density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
}
private double lastTime;
@@ -156,37 +156,44 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// The hit objects generated.
private IEnumerable generateConverted(HitObject original, IBeatmap originalBeatmap)
{
- var endTimeData = original as IHasEndTime;
- var distanceData = original as IHasDistance;
- var positionData = original as IHasPosition;
-
Patterns.PatternGenerator conversion = null;
- if (distanceData != null)
+ switch (original)
{
- var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
- conversion = generator;
-
- for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration)
+ case IHasDistance _:
{
- recordNote(time, positionData?.Position ?? Vector2.Zero);
- computeDensity(time);
+ var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
+ conversion = generator;
+
+ var positionData = original as IHasPosition;
+
+ for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration)
+ {
+ recordNote(time, positionData?.Position ?? Vector2.Zero);
+ computeDensity(time);
+ }
+
+ break;
}
- }
- else if (endTimeData != null)
- {
- conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
- recordNote(endTimeData.EndTime, new Vector2(256, 192));
- computeDensity(endTimeData.EndTime);
- }
- else if (positionData != null)
- {
- computeDensity(original.StartTime);
+ case IHasEndTime endTimeData:
+ {
+ conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
- conversion = new HitObjectPatternGenerator(Random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap);
+ recordNote(endTimeData.EndTime, new Vector2(256, 192));
+ computeDensity(endTimeData.EndTime);
+ break;
+ }
- recordNote(original.StartTime, positionData.Position);
+ case IHasPosition positionData:
+ {
+ computeDensity(original.StartTime);
+
+ conversion = new HitObjectPatternGenerator(Random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap);
+
+ recordNote(original.StartTime, positionData.Position);
+ break;
+ }
}
if (conversion == null)
@@ -219,14 +226,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
private Pattern generate()
{
- var endTimeData = HitObject as IHasEndTime;
var positionData = HitObject as IHasXPosition;
int column = GetColumn(positionData?.X ?? 0);
var pattern = new Pattern();
- if (endTimeData != null)
+ if (HitObject is IHasEndTime endTimeData)
{
pattern.Add(new HoldNote
{
@@ -237,7 +243,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) },
});
}
- else if (positionData != null)
+ else if (HitObject is IHasXPosition)
{
pattern.Add(new Note
{
@@ -255,11 +261,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
/// The time to retrieve the sample info list from.
///
- private List sampleInfoListAt(double time)
+ private IList sampleInfoListAt(double time)
{
- var curveData = HitObject as IHasCurve;
-
- if (curveData == null)
+ if (!(HitObject is IHasCurve curveData))
return HitObject.Samples;
double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount();
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index ea418eedb4..315ef96e49 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
@@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in originalPattern.HitObjects)
{
- if (!Precision.AlmostEquals(EndTime, (obj as IHasEndTime)?.EndTime ?? obj.StartTime))
+ if (!Precision.AlmostEquals(EndTime, obj.GetEndTime()))
intermediatePattern.Add(obj);
else
endTimePattern.Add(obj);
@@ -364,7 +364,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
break;
}
- bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
+ static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);
@@ -472,11 +472,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The time to retrieve the sample info list from.
///
- private List sampleInfoListAt(double time)
+ private IList sampleInfoListAt(double time)
{
- var curveData = HitObject as IHasCurve;
-
- if (curveData == null)
+ if (!(HitObject is IHasCurve curveData))
return HitObject.Samples;
double segmentTime = (EndTime - HitObject.StartTime) / spanCount;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index decd159ee9..84f950997d 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
+using osu.Framework.Extensions.IEnumerableExtensions;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@@ -88,15 +89,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public override IEnumerable Generate()
{
- yield return generate();
- }
-
- private Pattern generate()
- {
- var pattern = new Pattern();
-
- try
+ Pattern generateCore()
{
+ var pattern = new Pattern();
+
if (TotalColumns == 1)
{
addToPattern(pattern, 0);
@@ -109,8 +105,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
// Generate a new pattern by copying the last hit objects in reverse-column order
for (int i = RandomStart; i < TotalColumns; i++)
+ {
if (PreviousPattern.ColumnHasObject(i))
addToPattern(pattern, RandomStart + TotalColumns - i - 1);
+ }
return pattern;
}
@@ -132,8 +130,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
// Generate a new pattern by placing on the already filled columns
for (int i = RandomStart; i < TotalColumns; i++)
+ {
if (PreviousPattern.ColumnHasObject(i))
addToPattern(pattern, i);
+ }
return pattern;
}
@@ -164,54 +164,56 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
if (convertType.HasFlag(PatternType.KeepSingle))
- return pattern = generateRandomNotes(1);
+ return generateRandomNotes(1);
if (convertType.HasFlag(PatternType.Mirror))
{
if (ConversionDifficulty > 6.5)
- return pattern = generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
+ return generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
if (ConversionDifficulty > 4)
- return pattern = generateRandomPatternWithMirrored(0.12, 0.17, 0);
+ return generateRandomPatternWithMirrored(0.12, 0.17, 0);
- return pattern = generateRandomPatternWithMirrored(0.12, 0, 0);
+ return generateRandomPatternWithMirrored(0.12, 0, 0);
}
if (ConversionDifficulty > 6.5)
{
if (convertType.HasFlag(PatternType.LowProbability))
- return pattern = generateRandomPattern(0.78, 0.42, 0, 0);
+ return generateRandomPattern(0.78, 0.42, 0, 0);
- return pattern = generateRandomPattern(1, 0.62, 0, 0);
+ return generateRandomPattern(1, 0.62, 0, 0);
}
if (ConversionDifficulty > 4)
{
if (convertType.HasFlag(PatternType.LowProbability))
- return pattern = generateRandomPattern(0.35, 0.08, 0, 0);
+ return generateRandomPattern(0.35, 0.08, 0, 0);
- return pattern = generateRandomPattern(0.52, 0.15, 0, 0);
+ return generateRandomPattern(0.52, 0.15, 0, 0);
}
if (ConversionDifficulty > 2)
{
if (convertType.HasFlag(PatternType.LowProbability))
- return pattern = generateRandomPattern(0.18, 0, 0, 0);
+ return generateRandomPattern(0.18, 0, 0, 0);
- return pattern = generateRandomPattern(0.45, 0, 0, 0);
+ return generateRandomPattern(0.45, 0, 0, 0);
}
- return pattern = generateRandomPattern(0, 0, 0, 0);
+ return generateRandomPattern(0, 0, 0, 0);
}
- finally
+
+ var p = generateCore();
+
+ foreach (var obj in p.HitObjects)
{
- foreach (var obj in pattern.HitObjects)
- {
- if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
- StairType = PatternType.ReverseStair;
- if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
- StairType = PatternType.Stair;
- }
+ if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
+ StairType = PatternType.ReverseStair;
+ if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
+ StairType = PatternType.Stair;
}
+
+ return p.Yield();
}
///
@@ -299,8 +301,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
- bool addToCentre;
- int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre);
+ int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out var addToCentre);
int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2;
int nextColumn = GetRandomColumn(upperBound: columnLimit);
@@ -380,8 +381,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The amount of notes to be generated. The note to be added to the centre column will NOT be part of this count.
private int getRandomNoteCountMirrored(double centreProbability, double p2, double p3, out bool addToCentre)
{
- addToCentre = false;
-
switch (TotalColumns)
{
case 2:
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index fba52dfc32..fb58d805a9 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
@@ -7,7 +7,6 @@ using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
-using osuTK;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@@ -54,11 +53,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (allowSpecial && TotalColumns == 8)
{
const float local_x_divisor = 512f / 7;
- return MathHelper.Clamp((int)Math.Floor(position / local_x_divisor), 0, 6) + 1;
+ return Math.Clamp((int)MathF.Floor(position / local_x_divisor), 0, 6) + 1;
}
float localXDivisor = 512f / TotalColumns;
- return MathHelper.Clamp((int)Math.Floor(position / localXDivisor), 0, TotalColumns - 1);
+ return Math.Clamp((int)MathF.Floor(position / localXDivisor), 0, TotalColumns - 1);
}
///
@@ -113,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
drainTime = 10000;
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
- conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
+ conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
return conversionDifficulty.Value;
@@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// A function to retrieve the next column. If null, a randomisation scheme will be used.
/// A function to perform additional validation checks to determine if a column is a valid candidate for a .
/// The minimum column index. If null, is used.
- /// The maximum column index. If null, is used.
+ /// The maximum column index. If null, TotalColumns is used.
/// A list of patterns for which the validity of a column should be checked against.
/// A column is not a valid candidate if a occupies the same column in any of the patterns.
/// A column which has passed the check and for which there are no
@@ -148,9 +147,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func nextColumn = null, [InstantHandle] Func validation = null,
params Pattern[] patterns)
{
- lowerBound = lowerBound ?? RandomStart;
- upperBound = upperBound ?? TotalColumns;
- nextColumn = nextColumn ?? (_ => GetRandomColumn(lowerBound, upperBound));
+ lowerBound ??= RandomStart;
+ upperBound ??= TotalColumns;
+ nextColumn ??= (_ => GetRandomColumn(lowerBound, upperBound));
// Check for the initial column
if (isValid(initialColumn))
@@ -184,7 +183,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Returns a random column index in the range [, ).
///
/// The minimum column index. If null, is used.
- /// The maximum column index. If null, is used.
+ /// The maximum column index. If null, is used.
protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
///
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs
index a3cd455886..e4a28167ec 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// Keep the same as last row.
///
- ForceStack = 1 << 0,
+ ForceStack = 1,
///
/// Keep different from last row.
diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
index b591f9da22..f5412dcfc5 100644
--- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
- Set(ManiaRulesetSetting.ScrollTime, 2250.0, 50.0, 10000.0, 50.0);
+ Set(ManiaRulesetSetting.ScrollTime, 1500.0, 50.0, 5000.0, 50.0);
Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index 4a9c22d339..37cba1fd3c 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -11,7 +11,9 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Difficulty
{
@@ -32,12 +34,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty
if (beatmap.HitObjects.Count == 0)
return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
+ HitWindows hitWindows = new ManiaHitWindows();
+ hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+
return new ManiaDifficultyAttributes
{
StarRating = difficultyValue(skills) * star_scaling_factor,
Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate,
+ GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
Skills = skills
};
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index b99bddee96..3f7a2baedd 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -37,12 +37,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
mods = Score.Mods;
scaledScore = Score.TotalScore;
- countPerfect = Convert.ToInt32(Score.Statistics[HitResult.Perfect]);
- countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
- countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
- countOk = Convert.ToInt32(Score.Statistics[HitResult.Ok]);
- countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
- countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
+ countPerfect = Score.Statistics[HitResult.Perfect];
+ countGreat = Score.Statistics[HitResult.Great];
+ countGood = Score.Statistics[HitResult.Good];
+ countOk = Score.Statistics[HitResult.Ok];
+ countMeh = Score.Statistics[HitResult.Meh];
+ countMiss = Score.Statistics[HitResult.Miss];
if (mods.Any(m => !m.Ranked))
return 0;
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
index 059cd39641..4f7ab87fad 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
@@ -5,7 +5,7 @@ using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
- var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime;
+ var endTime = maniaCurrent.BaseObject.GetEndTime();
try
{
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
index ed25173d38..bbbb93fd8b 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
@@ -4,7 +4,7 @@
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
- var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime;
+ var endTime = maniaCurrent.BaseObject.GetEndTime();
double holdFactor = 1.0; // Factor in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
new file mode 100644
index 0000000000..4e73883de0
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Mania.Edit.Blueprints
+{
+ public class HoldNoteNoteSelectionBlueprint : ManiaSelectionBlueprint
+ {
+ protected new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
+
+ private readonly HoldNotePosition position;
+
+ public HoldNoteNoteSelectionBlueprint(DrawableHoldNote holdNote, HoldNotePosition position)
+ : base(holdNote)
+ {
+ this.position = position;
+ InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
+
+ Select();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
+ if (DrawableObject.IsLoaded)
+ {
+ DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)DrawableObject.Head : DrawableObject.Tail;
+
+ Anchor = note.Anchor;
+ Origin = note.Origin;
+
+ Size = note.DrawSize;
+ Position = note.DrawPosition;
+ }
+ }
+
+ // Todo: This is temporary, since the note masks don't do anything special yet. In the future they will handle input.
+ public override bool HandlePositionalInput => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index 26115311f7..bcbc1ee527 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Graphics;
-using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK;
@@ -49,13 +48,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private double originalStartTime;
- protected override bool OnMouseMove(MouseMoveEvent e)
+ public override void UpdatePosition(Vector2 screenSpacePosition)
{
- base.OnMouseMove(e);
+ base.UpdatePosition(screenSpacePosition);
if (PlacementBegun)
{
- var endTime = TimeAt(e.ScreenSpaceMousePosition);
+ var endTime = TimeAt(screenSpacePosition);
HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime;
HitObject.Duration = Math.Abs(endTime - originalStartTime);
@@ -65,10 +64,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
headPiece.Width = tailPiece.Width = SnappedWidth;
headPiece.X = tailPiece.X = SnappedMousePosition.X;
- originalStartTime = HitObject.StartTime = TimeAt(e.ScreenSpaceMousePosition);
+ originalStartTime = HitObject.StartTime = TimeAt(screenSpacePosition);
}
-
- return true;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs
new file mode 100644
index 0000000000..219dad566d
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs
@@ -0,0 +1,11 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Mania.Edit.Blueprints
+{
+ public enum HoldNotePosition
+ {
+ Start,
+ End
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index d64c5dbc6a..56c0b671a0 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -16,69 +16,57 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
{
- public new DrawableHoldNote HitObject => (DrawableHoldNote)base.HitObject;
+ public new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
private readonly IBindable direction = new Bindable();
- private readonly BodyPiece body;
+ [Resolved]
+ private OsuColour colours { get; set; }
public HoldNoteSelectionBlueprint(DrawableHoldNote hold)
: base(hold)
{
- InternalChildren = new Drawable[]
- {
- new HoldNoteNoteSelectionBlueprint(hold.Head),
- new HoldNoteNoteSelectionBlueprint(hold.Tail),
- body = new BodyPiece
- {
- AccentColour = Color4.Transparent
- },
- };
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours, IScrollingInfo scrollingInfo)
+ private void load(IScrollingInfo scrollingInfo)
{
- body.BorderColour = colours.Yellow;
-
direction.BindTo(scrollingInfo.Direction);
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ InternalChildren = new Drawable[]
+ {
+ new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.Start),
+ new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.End),
+ new BodyPiece
+ {
+ AccentColour = Color4.Transparent,
+ BorderColour = colours.Yellow
+ },
+ };
+ }
+
protected override void Update()
{
base.Update();
- Size = HitObject.DrawSize + new Vector2(0, HitObject.Tail.DrawHeight);
+ // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
+ if (DrawableObject.IsLoaded)
+ {
+ Size = DrawableObject.DrawSize + new Vector2(0, DrawableObject.Tail.DrawHeight);
- // This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do
- // When scrolling upwards our origin is already at the top of the head note (which is the intended location),
- // but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note)
- if (direction.Value == ScrollingDirection.Down)
- Y -= HitObject.Tail.DrawHeight;
+ // This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do
+ // When scrolling upwards our origin is already at the top of the head note (which is the intended location),
+ // but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note)
+ if (direction.Value == ScrollingDirection.Down)
+ Y -= DrawableObject.Tail.DrawHeight;
+ }
}
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
-
- private class HoldNoteNoteSelectionBlueprint : NoteSelectionBlueprint
- {
- public HoldNoteNoteSelectionBlueprint(DrawableNote note)
- : base(note)
- {
- Select();
- }
-
- protected override void Update()
- {
- base.Update();
-
- Anchor = HitObject.Anchor;
- Origin = HitObject.Origin;
-
- Position = HitObject.DrawPosition;
- }
-
- // Todo: This is temporary, since the note masks don't do anything special yet. In the future they will handle input.
- public override bool HandlePositionalInput => false;
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index d3779e2e18..7a3b42914e 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -49,32 +49,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (Column == null)
return base.OnMouseDown(e);
- HitObject.StartTime = TimeAt(e.ScreenSpaceMousePosition);
HitObject.Column = Column.Index;
-
- BeginPlacement();
+ BeginPlacement(TimeAt(e.ScreenSpaceMousePosition));
return true;
}
- protected override bool OnMouseUp(MouseUpEvent e)
+ protected override void OnMouseUp(MouseUpEvent e)
{
EndPlacement();
- return base.OnMouseUp(e);
+ base.OnMouseUp(e);
}
- protected override bool OnMouseMove(MouseMoveEvent e)
+ public override void UpdatePosition(Vector2 screenSpacePosition)
{
if (!PlacementBegun)
- Column = ColumnAt(e.ScreenSpaceMousePosition);
+ Column = ColumnAt(screenSpacePosition);
- if (Column == null) return false;
+ if (Column == null) return;
SnappedWidth = Column.DrawWidth;
// Snap to the column
var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0)));
- SnappedMousePosition = new Vector2(parentPos.X, e.MousePosition.Y);
- return true;
+ SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(screenSpacePosition).Y);
}
protected double TimeAt(Vector2 screenSpacePosition)
@@ -86,7 +83,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
// If we're scrolling downwards, a position of 0 is actually further away from the hit target
// so we need to flip the vertical coordinate in the hitobject container's space
- var hitObjectPos = Column.HitObjectContainer.ToLocalSpace(applyPositionOffset(screenSpacePosition, false)).Y;
+ var hitObjectPos = mouseToHitObjectPosition(Column.HitObjectContainer.ToLocalSpace(screenSpacePosition)).Y;
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
hitObjectPos = hitObjectContainer.DrawHeight - hitObjectPos;
@@ -103,16 +100,58 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
scrollingInfo.TimeRange.Value,
Column.HitObjectContainer.DrawHeight);
- return applyPositionOffset(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent), true).Y;
+ if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
+ pos = Column.HitObjectContainer.DrawHeight - pos;
+
+ return hitObjectToMousePosition(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent)).Y;
}
protected Column ColumnAt(Vector2 screenSpacePosition)
- => composer.ColumnAt(applyPositionOffset(screenSpacePosition, false));
+ => composer.ColumnAt(screenSpacePosition);
- private Vector2 applyPositionOffset(Vector2 position, bool reverse)
+ ///
+ /// Converts a mouse position to a hitobject position.
+ ///
+ ///
+ /// Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction.
+ ///
+ /// The mouse position.
+ /// The resulting hitobject position, acnhored at the top or bottom of the blueprint depending on the scroll direction.
+ private Vector2 mouseToHitObjectPosition(Vector2 mousePosition)
{
- position.Y += (scrollingInfo.Direction.Value == ScrollingDirection.Up && !reverse ? -1 : 1) * NotePiece.NOTE_HEIGHT / 2;
- return position;
+ switch (scrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Up:
+ mousePosition.Y -= NotePiece.NOTE_HEIGHT / 2;
+ break;
+
+ case ScrollingDirection.Down:
+ mousePosition.Y += NotePiece.NOTE_HEIGHT / 2;
+ break;
+ }
+
+ return mousePosition;
+ }
+
+ ///
+ /// Converts a hitobject position to a mouse position.
+ ///
+ /// The hitobject position.
+ /// The resulting mouse position, anchored at the centre of the hitobject.
+ private Vector2 hitObjectToMousePosition(Vector2 hitObjectPosition)
+ {
+ switch (scrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Up:
+ hitObjectPosition.Y += NotePiece.NOTE_HEIGHT / 2;
+ break;
+
+ case ScrollingDirection.Down:
+ hitObjectPosition.Y -= NotePiece.NOTE_HEIGHT / 2;
+ break;
+ }
+
+ return hitObjectPosition;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index d3c12b1944..ff3dbe614a 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public Vector2 ScreenSpaceDragPosition { get; private set; }
public Vector2 DragPosition { get; private set; }
- protected new DrawableManiaHitObject HitObject => (DrawableManiaHitObject)base.HitObject;
+ public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
protected IClock EditorClock { get; private set; }
@@ -28,8 +28,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private IManiaHitObjectComposer composer { get; set; }
- public ManiaSelectionBlueprint(DrawableHitObject hitObject)
- : base(hitObject)
+ public ManiaSelectionBlueprint(DrawableHitObject drawableObject)
+ : base(drawableObject)
{
RelativeSizeAxes = Axes.None;
}
@@ -44,36 +44,34 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
- Position = Parent.ToLocalSpace(HitObject.ToScreenSpace(Vector2.Zero));
+ Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
}
protected override bool OnMouseDown(MouseDownEvent e)
{
ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = HitObject.ToLocalSpace(e.ScreenSpaceMousePosition);
+ DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
return base.OnMouseDown(e);
}
- protected override bool OnDrag(DragEvent e)
+ protected override void OnDrag(DragEvent e)
{
- var result = base.OnDrag(e);
+ base.OnDrag(e);
ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = HitObject.ToLocalSpace(e.ScreenSpaceMousePosition);
-
- return result;
+ DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
}
public override void Show()
{
- HitObject.AlwaysAlive = true;
+ DrawableObject.AlwaysAlive = true;
base.Show();
}
public override void Hide()
{
- HitObject.AlwaysAlive = false;
+ DrawableObject.AlwaysAlive = false;
base.Hide();
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
index d345b14e84..2bff33c4cf 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
@@ -19,7 +19,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
- Size = HitObject.DrawSize;
+ // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
+ if (DrawableObject.IsLoaded)
+ Size = DrawableObject.DrawSize;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
index e5f379f608..445df79f6f 100644
--- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
+++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
- public DrawableManiaEditRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ public DrawableManiaEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
new file mode 100644
index 0000000000..5f66ae7491
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Edit.Blueprints;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Mania.Edit
+{
+ public class ManiaBlueprintContainer : ComposeBlueprintContainer
+ {
+ public ManiaBlueprintContainer(IEnumerable drawableHitObjects)
+ : base(drawableHitObjects)
+ {
+ }
+
+ public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case DrawableNote note:
+ return new NoteSelectionBlueprint(note);
+
+ case DrawableHoldNote holdNote:
+ return new HoldNoteSelectionBlueprint(holdNote);
+ }
+
+ return base.CreateBlueprintFor(hitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index 2729621ab3..62b609610f 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -5,11 +5,8 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Drawables;
using System.Collections.Generic;
using osu.Framework.Allocation;
-using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@@ -21,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit
[Cached(Type = typeof(IManiaHitObjectComposer))]
public class ManiaHitObjectComposer : HitObjectComposer, IManiaHitObjectComposer
{
- protected new DrawableManiaEditRuleset DrawableRuleset { get; private set; }
+ private DrawableManiaEditRuleset drawableRuleset;
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
@@ -33,45 +30,31 @@ namespace osu.Game.Rulesets.Mania.Edit
///
/// The screen-space position.
/// The column which intersects with .
- public Column ColumnAt(Vector2 screenSpacePosition) => DrawableRuleset.GetColumnByPosition(screenSpacePosition);
+ public Column ColumnAt(Vector2 screenSpacePosition) => drawableRuleset.GetColumnByPosition(screenSpacePosition);
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
- public int TotalColumns => ((ManiaPlayfield)DrawableRuleset.Playfield).TotalColumns;
+ public int TotalColumns => ((ManiaPlayfield)drawableRuleset.Playfield).TotalColumns;
- protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
{
- DrawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods);
+ drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods);
// This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it
- dependencies.CacheAs(DrawableRuleset.ScrollingInfo);
+ dependencies.CacheAs(drawableRuleset.ScrollingInfo);
- return DrawableRuleset;
+ return drawableRuleset;
}
+ protected override ComposeBlueprintContainer CreateBlueprintContainer() => new ManiaBlueprintContainer(drawableRuleset.Playfield.AllHitObjects);
+
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
{
new NoteCompositionTool(),
new HoldNoteCompositionTool()
};
-
- public override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
-
- public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
- {
- switch (hitObject)
- {
- case DrawableNote note:
- return new NoteSelectionBlueprint(note);
-
- case DrawableHoldNote holdNote:
- return new HoldNoteSelectionBlueprint(holdNote);
- }
-
- return base.CreateBlueprintFor(hitObject);
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 6f49c7f0c4..618af3e772 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -1,17 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Input.Events;
using osu.Framework.Timing;
-using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
-using osuTK;
namespace osu.Game.Rulesets.Mania.Edit
{
@@ -31,13 +29,16 @@ namespace osu.Game.Rulesets.Mania.Edit
editorClock = clock;
}
- public override void HandleDrag(SelectionBlueprint blueprint, DragEvent dragEvent)
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
- adjustOrigins((ManiaSelectionBlueprint)blueprint);
- performDragMovement(dragEvent);
- performColumnMovement(dragEvent);
+ var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
+ int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
- base.HandleDrag(blueprint, dragEvent);
+ adjustOrigins(maniaBlueprint);
+ performDragMovement(moveEvent);
+ performColumnMovement(lastColumn, moveEvent);
+
+ return true;
}
///
@@ -47,41 +48,44 @@ namespace osu.Game.Rulesets.Mania.Edit
/// The that received the drag event.
private void adjustOrigins(ManiaSelectionBlueprint reference)
{
- var referenceParent = (HitObjectContainer)reference.HitObject.Parent;
+ var referenceParent = (HitObjectContainer)reference.DrawableObject.Parent;
- float offsetFromReferenceOrigin = reference.DragPosition.Y - reference.HitObject.OriginPosition.Y;
+ float offsetFromReferenceOrigin = reference.DragPosition.Y - reference.DrawableObject.OriginPosition.Y;
float targetPosition = referenceParent.ToLocalSpace(reference.ScreenSpaceDragPosition).Y - offsetFromReferenceOrigin;
// Flip the vertical coordinate space when scrolling downwards
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- targetPosition = targetPosition - referenceParent.DrawHeight;
+ targetPosition -= referenceParent.DrawHeight;
- float movementDelta = targetPosition - reference.HitObject.Position.Y;
+ float movementDelta = targetPosition - reference.DrawableObject.Position.Y;
foreach (var b in SelectedBlueprints.OfType())
- b.HitObject.Y += movementDelta;
+ b.DrawableObject.Y += movementDelta;
}
- private void performDragMovement(DragEvent dragEvent)
+ private void performDragMovement(MoveSelectionEvent moveEvent)
{
+ float delta = moveEvent.InstantDelta.Y;
+
+ // When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen.
+ // This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height.
+ if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
+ delta -= moveEvent.Blueprint.DrawableObject.Parent.DrawHeight;
+
foreach (var b in SelectedBlueprints)
{
- var hitObject = b.HitObject;
-
+ var hitObject = b.DrawableObject;
var objectParent = (HitObjectContainer)hitObject.Parent;
- // Using the hitobject position is required since AdjustPosition can be invoked multiple times per frame
- // without the position having been updated by the parenting ScrollingHitObjectContainer
- hitObject.Y += dragEvent.Delta.Y;
+ // StartTime could be used to adjust the position if only one movement event was received per frame.
+ // However this is not the case and ScrollingHitObjectContainer performs movement in UpdateAfterChildren() so the position must also be updated to be valid for further movement events
+ hitObject.Y += delta;
- float targetPosition;
+ float targetPosition = hitObject.Position.Y;
- // If we're scrolling downwards, a position of 0 is actually further away from the hit target
- // so we need to flip the vertical coordinate in the hitobject container's space
+ // The scrolling algorithm always assumes an anchor at the top of the screen, so the position must be flipped when scrolling downwards to reflect a top anchor
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- targetPosition = -hitObject.Position.Y;
- else
- targetPosition = hitObject.Position.Y;
+ targetPosition = -targetPosition;
objectParent.Remove(hitObject);
@@ -94,14 +98,13 @@ namespace osu.Game.Rulesets.Mania.Edit
}
}
- private void performColumnMovement(DragEvent dragEvent)
+ private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{
- var lastColumn = composer.ColumnAt(dragEvent.ScreenSpaceLastMousePosition);
- var currentColumn = composer.ColumnAt(dragEvent.ScreenSpaceMousePosition);
- if (lastColumn == null || currentColumn == null)
+ var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition);
+ if (currentColumn == null)
return;
- int columnDelta = currentColumn.Index - lastColumn.Index;
+ int columnDelta = currentColumn.Index - lastColumn;
if (columnDelta == 0)
return;
@@ -116,7 +119,7 @@ namespace osu.Game.Rulesets.Mania.Edit
maxColumn = obj.Column;
}
- columnDelta = MathHelper.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn);
+ columnDelta = Math.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn);
foreach (var obj in SelectedHitObjects.OfType())
obj.Column += columnDelta;
diff --git a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
index 30b0f09a94..ff8882124f 100644
--- a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
@@ -9,8 +9,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Masks
{
public abstract class ManiaSelectionBlueprint : SelectionBlueprint
{
- protected ManiaSelectionBlueprint(DrawableHitObject hitObject)
- : base(hitObject)
+ protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
+ : base(drawableObject)
{
RelativeSizeAxes = Axes.None;
}
diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
index b9c6e3a7f7..00b839f8ec 100644
--- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
@@ -15,11 +15,11 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
switch (result)
{
- case HitResult.Miss:
+ default:
return 0;
- default:
- return 0.040;
+ case HitResult.Perfect:
+ return 0.01;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
index 0e4c811945..c2f8fb8678 100644
--- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
@@ -29,32 +29,5 @@ namespace osu.Game.Rulesets.Mania.Judgements
return 300;
}
}
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- case HitResult.Miss:
- return -0.125;
-
- case HitResult.Meh:
- return 0.005;
-
- case HitResult.Ok:
- return 0.010;
-
- case HitResult.Good:
- return 0.035;
-
- case HitResult.Great:
- return 0.055;
-
- case HitResult.Perfect:
- return 0.065;
-
- default:
- return 0;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index d83033f9c6..02c2158383 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
@@ -26,18 +25,30 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Mania.Skinning;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania
{
- public class ManiaRuleset : Ruleset
+ public class ManiaRuleset : Ruleset, ILegacyRuleset
{
- public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableManiaRuleset(this, beatmap, mods);
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
+
+ public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
+
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score);
+ public const string SHORT_NAME = "mania";
+
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
+ public override ISkin CreateLegacySkinProvider(ISkinSource source) => new ManiaLegacySkinTransformer(source);
+
public override IEnumerable ConvertLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
@@ -45,7 +56,14 @@ namespace osu.Game.Rulesets.Mania
else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new ManiaModDoubleTime();
- if (mods.HasFlag(LegacyMods.Autoplay))
+ if (mods.HasFlag(LegacyMods.Perfect))
+ yield return new ManiaModPerfect();
+ else if (mods.HasFlag(LegacyMods.SuddenDeath))
+ yield return new ManiaModSuddenDeath();
+
+ if (mods.HasFlag(LegacyMods.Cinema))
+ yield return new ManiaModCinema();
+ else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new ManiaModAutoplay();
if (mods.HasFlag(LegacyMods.Easy))
@@ -96,14 +114,8 @@ namespace osu.Game.Rulesets.Mania
if (mods.HasFlag(LegacyMods.NoFail))
yield return new ManiaModNoFail();
- if (mods.HasFlag(LegacyMods.Perfect))
- yield return new ManiaModPerfect();
-
if (mods.HasFlag(LegacyMods.Random))
yield return new ManiaModRandom();
-
- if (mods.HasFlag(LegacyMods.SuddenDeath))
- yield return new ManiaModSuddenDeath();
}
public override IEnumerable GetModsFor(ModType type)
@@ -143,34 +155,37 @@ namespace osu.Game.Rulesets.Mania
new ManiaModRandom(),
new ManiaModDualStages(),
new ManiaModMirror(),
+ new ManiaModDifficultyAdjust(),
};
case ModType.Automation:
return new Mod[]
{
- new MultiMod(new ManiaModAutoplay(), new ModCinema()),
+ new MultiMod(new ManiaModAutoplay(), new ManiaModCinema()),
};
case ModType.Fun:
return new Mod[]
{
- new MultiMod(new ModWindUp(), new ModWindDown())
+ new MultiMod(new ModWindUp(), new ModWindDown())
};
default:
- return new Mod[] { };
+ return Array.Empty();
}
}
public override string Description => "osu!mania";
- public override string ShortName => "mania";
+ public override string ShortName => SHORT_NAME;
+
+ public override string PlayingVerb => "Smashing keys";
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetMania };
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(this, beatmap);
- public override int? LegacyID => 3;
+ public int LegacyID => 3;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
@@ -178,11 +193,6 @@ namespace osu.Game.Rulesets.Mania
public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this);
- public ManiaRuleset(RulesetInfo rulesetInfo = null)
- : base(rulesetInfo)
- {
- }
-
public override IEnumerable AvailableVariants
{
get
@@ -268,7 +278,7 @@ namespace osu.Game.Rulesets.Mania
return stage1Bindings.Concat(stage2Bindings);
}
- return new KeyBinding[0];
+ return Array.Empty();
}
public override string GetVariantName(int variant)
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
new file mode 100644
index 0000000000..69bd4b0ecf
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania
+{
+ public class ManiaSkinComponent : GameplaySkinComponent
+ {
+ public ManiaSkinComponent(ManiaSkinComponents component)
+ : base(component)
+ {
+ }
+
+ protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
+
+ protected override string ComponentName => Component.ToString().ToLower();
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponents.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponents.cs
new file mode 100644
index 0000000000..6d85816e5a
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponents.cs
@@ -0,0 +1,9 @@
+// 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.Mania
+{
+ public enum ManiaSkinComponents
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
new file mode 100644
index 0000000000..02c1fc1b79
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Rulesets.Mania.Mods
+{
+ public class ManiaModCinema : ModCinema
+ {
+ public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ {
+ ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
+ Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
+ };
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/APIMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs
similarity index 50%
rename from osu.Game/Online/API/Requests/Responses/APIMod.cs
rename to osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs
index b9da4f49ee..0817f8f9fc 100644
--- a/osu.Game/Online/API/Requests/Responses/APIMod.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs
@@ -3,12 +3,9 @@
using osu.Game.Rulesets.Mods;
-namespace osu.Game.Online.API.Requests.Responses
+namespace osu.Game.Rulesets.Mania.Mods
{
- public class APIMod : IMod
+ public class ManiaModDifficultyAdjust : ModDifficultyAdjust
{
- public string Acronym { get; set; }
-
- public bool Equals(IMod other) => Acronym == other?.Acronym;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
index 39185e6a57..4c125ad6ef 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Name => "Fade In";
public override string Acronym => "FI";
- public override IconUsage Icon => OsuIcon.ModHidden;
+ public override IconUsage? Icon => OsuIcon.ModHidden;
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Keys appear out of nowhere!";
public override double ScoreMultiplier => 1;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
index 17f4098420..485595cea9 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
@@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModMirror : Mod, IApplicableToBeatmap
+ public class ManiaModMirror : Mod, IApplicableToBeatmap
{
public override string Name => "Mirror";
public override string Acronym => "MR";
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1;
public override bool Ranked => true;
- public void ApplyToBeatmap(Beatmap beatmap)
+ public void ApplyToBeatmap(IBeatmap beatmap)
{
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
index 2d94fb6af5..4cc712060c 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
@@ -1,11 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModNightcore : ModNightcore
+ public class ManiaModNightcore : ModNightcore
{
public override double ScoreMultiplier => 1;
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
index ba16140644..14b36fb765 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
@@ -4,7 +4,7 @@
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -13,16 +13,16 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModRandom : Mod, IApplicableToBeatmap
+ public class ManiaModRandom : Mod, IApplicableToBeatmap
{
public override string Name => "Random";
public override string Acronym => "RD";
public override ModType Type => ModType.Conversion;
- public override IconUsage Icon => OsuIcon.Dice;
+ public override IconUsage? Icon => OsuIcon.Dice;
public override string Description => @"Shuffle around the keys!";
public override double ScoreMultiplier => 1;
- public void ApplyToBeatmap(Beatmap beatmap)
+ public void ApplyToBeatmap(IBeatmap beatmap)
{
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => RNG.Next()).ToList();
diff --git a/osu.Game.Rulesets.Mania/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
index 4c644a8f09..0981b028b2 100644
--- a/osu.Game.Rulesets.Mania/Objects/BarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
@@ -1,21 +1,12 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Objects
{
- public class BarLine : ManiaHitObject
+ public class BarLine : ManiaHitObject, IBarLine
{
- ///
- /// The control point which this bar line is part of.
- ///
- public TimingControlPoint ControlPoint;
-
- ///
- /// The index of the beat which this bar line represents within the control point.
- /// This is a "major" bar line if % == 0.
- ///
- public int BeatIndex;
+ public bool Major { get; set; }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
index 9c3197504f..56bc797c7f 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -40,9 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Colour = new Color4(255, 204, 33, 255),
});
- bool isMajor = barLine.BeatIndex % (int)barLine.ControlPoint.TimeSignature == 0;
-
- if (isMajor)
+ if (barLine.Major)
{
AddInternal(new EquilateralTriangle
{
@@ -65,11 +63,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
});
}
- if (!isMajor && barLine.BeatIndex % 2 == 1)
+ if (!barLine.Major)
Alpha = 0.2f;
}
- protected override void UpdateState(ArmedState state)
+ protected override void UpdateInitialTransforms()
+ {
+ }
+
+ protected override void UpdateStateTransforms(ArmedState state)
{
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 9368af987d..14a7c5fda3 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -1,14 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Linq;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
-using osuTK.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
@@ -21,22 +20,24 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public override bool DisplayResult => false;
- public readonly DrawableNote Head;
- public readonly DrawableNote Tail;
+ public DrawableHoldNoteHead Head => headContainer.Child;
+ public DrawableHoldNoteTail Tail => tailContainer.Child;
+
+ private readonly Container headContainer;
+ private readonly Container tailContainer;
+ private readonly Container tickContainer;
private readonly BodyPiece bodyPiece;
///
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
///
- private double? holdStartTime;
+ public double? HoldStartTime { get; private set; }
///
/// Whether the hold note has been released too early and shouldn't give full score for the release.
///
- private bool hasBroken;
-
- private readonly Container tickContainer;
+ public bool HasBroken { get; private set; }
public DrawableHoldNote(HoldNote hitObject)
: base(hitObject)
@@ -45,35 +46,75 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AddRangeInternal(new Drawable[]
{
- bodyPiece = new BodyPiece
- {
- RelativeSizeAxes = Axes.X,
- },
- tickContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- ChildrenEnumerable = HitObject.NestedHitObjects.OfType().Select(tick => new DrawableHoldNoteTick(tick)
- {
- HoldStartTime = () => holdStartTime
- })
- },
- Head = new DrawableHeadNote(this)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- },
- Tail = new DrawableTailNote(this)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- }
+ bodyPiece = new BodyPiece { RelativeSizeAxes = Axes.X },
+ tickContainer = new Container { RelativeSizeAxes = Axes.Both },
+ headContainer = new Container { RelativeSizeAxes = Axes.Both },
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
});
- foreach (var tick in tickContainer)
- AddNested(tick);
+ AccentColour.BindValueChanged(colour =>
+ {
+ bodyPiece.AccentColour = colour.NewValue;
+ }, true);
+ }
- AddNested(Head);
- AddNested(Tail);
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableHoldNoteHead head:
+ headContainer.Child = head;
+ break;
+
+ case DrawableHoldNoteTail tail:
+ tailContainer.Child = tail;
+ break;
+
+ case DrawableHoldNoteTick tick:
+ tickContainer.Add(tick);
+ break;
+ }
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ headContainer.Clear();
+ tailContainer.Clear();
+ tickContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case TailNote _:
+ return new DrawableHoldNoteTail(this)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AccentColour = { BindTarget = AccentColour }
+ };
+
+ case Note _:
+ return new DrawableHoldNoteHead(this)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AccentColour = { BindTarget = AccentColour }
+ };
+
+ case HoldNoteTick tick:
+ return new DrawableHoldNoteTick(tick)
+ {
+ HoldStartTime = () => HoldStartTime,
+ AccentColour = { BindTarget = AccentColour }
+ };
+ }
+
+ return base.CreateNestedHitObject(hitObject);
}
protected override void OnDirectionChanged(ValueChangedEvent e)
@@ -83,26 +124,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
}
- public override Color4 AccentColour
- {
- get => base.AccentColour;
- set
- {
- base.AccentColour = value;
-
- bodyPiece.AccentColour = value;
- Head.AccentColour = value;
- Tail.AccentColour = value;
- tickContainer.ForEach(t => t.AccentColour = value);
- }
- }
-
- protected override void CheckForResult(bool userTriggered, double timeOffset)
- {
- if (Tail.AllJudged)
- ApplyResult(r => r.Type = HitResult.Perfect);
- }
-
protected override void Update()
{
base.Update();
@@ -112,144 +133,68 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
}
- protected void BeginHold()
+ protected override void UpdateStateTransforms(ArmedState state)
{
- holdStartTime = Time.Current;
- bodyPiece.Hitting = true;
+ using (BeginDelayedSequence(HitObject.Duration, true))
+ base.UpdateStateTransforms(state);
}
- protected void EndHold()
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
{
- holdStartTime = null;
- bodyPiece.Hitting = false;
+ if (Tail.AllJudged)
+ ApplyResult(r => r.Type = HitResult.Perfect);
+
+ if (Tail.Result.Type == HitResult.Miss)
+ HasBroken = true;
}
public bool OnPressed(ManiaAction action)
{
- // Make sure the action happened within the body of the hold note
- if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime)
+ if (AllJudged)
return false;
if (action != Action.Value)
return false;
- // The user has pressed during the body of the hold note, after the head note and its hit windows have passed
- // and within the limited range of the above if-statement. This state will be managed by the head note if the
- // user has pressed during the hit windows of the head note.
- BeginHold();
+ beginHoldAt(Time.Current - Head.HitObject.StartTime);
+ Head.UpdateResult();
+
return true;
}
- public bool OnReleased(ManiaAction action)
+ private void beginHoldAt(double timeOffset)
{
- // Make sure that the user started holding the key during the hold note
- if (!holdStartTime.HasValue)
- return false;
+ if (timeOffset < -Head.HitObject.HitWindows.WindowFor(HitResult.Miss))
+ return;
+
+ HoldStartTime = Time.Current;
+ bodyPiece.Hitting = true;
+ }
+
+ public void OnReleased(ManiaAction action)
+ {
+ if (AllJudged)
+ return;
if (action != Action.Value)
- return false;
+ return;
- EndHold();
+ // Make sure a hold was started
+ if (HoldStartTime == null)
+ return;
+
+ Tail.UpdateResult();
+ endHold();
// If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit)
- hasBroken = true;
-
- return true;
+ HasBroken = true;
}
- ///
- /// The head note of a hold.
- ///
- private class DrawableHeadNote : DrawableNote
+ private void endHold()
{
- private readonly DrawableHoldNote holdNote;
-
- public DrawableHeadNote(DrawableHoldNote holdNote)
- : base(holdNote.HitObject.Head)
- {
- this.holdNote = holdNote;
- }
-
- public override bool OnPressed(ManiaAction action)
- {
- if (!base.OnPressed(action))
- return false;
-
- // If the key has been released too early, the user should not receive full score for the release
- if (Result.Type == HitResult.Miss)
- holdNote.hasBroken = true;
-
- // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held
- // The body doesn't handle these early early hits, so we have to explicitly set the holding state here
- holdNote.BeginHold();
-
- return true;
- }
- }
-
- ///
- /// The tail note of a hold.
- ///
- private class DrawableTailNote : DrawableNote
- {
- ///
- /// Lenience of release hit windows. This is to make cases where the hold note release
- /// is timed alongside presses of other hit objects less awkward.
- /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
- ///
- private const double release_window_lenience = 1.5;
-
- private readonly DrawableHoldNote holdNote;
-
- public DrawableTailNote(DrawableHoldNote holdNote)
- : base(holdNote.HitObject.Tail)
- {
- this.holdNote = holdNote;
- }
-
- protected override void CheckForResult(bool userTriggered, double timeOffset)
- {
- // Factor in the release lenience
- timeOffset /= release_window_lenience;
-
- if (!userTriggered)
- {
- if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = HitResult.Miss);
-
- return;
- }
-
- var result = HitObject.HitWindows.ResultFor(timeOffset);
- if (result == HitResult.None)
- return;
-
- ApplyResult(r =>
- {
- if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect))
- result = HitResult.Good;
-
- r.Type = result;
- });
- }
-
- public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down
-
- public override bool OnReleased(ManiaAction action)
- {
- // Make sure that the user started holding the key during the hold note
- if (!holdNote.holdStartTime.HasValue)
- return false;
-
- if (action != Action.Value)
- return false;
-
- UpdateResult(true);
-
- // Handled by the hold note, which will set holding = false
- return false;
- }
+ HoldStartTime = null;
+ bodyPiece.Hitting = false;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
new file mode 100644
index 0000000000..390c64c5e2
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -0,0 +1,24 @@
+// 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.Mania.Objects.Drawables
+{
+ ///
+ /// The head of a .
+ ///
+ public class DrawableHoldNoteHead : DrawableNote
+ {
+ public DrawableHoldNoteHead(DrawableHoldNote holdNote)
+ : base(holdNote.HitObject.Head)
+ {
+ }
+
+ public void UpdateResult() => base.UpdateResult(true);
+
+ public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
+
+ public override void OnReleased(ManiaAction action)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
new file mode 100644
index 0000000000..568b07c958
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -0,0 +1,66 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mania.Objects.Drawables
+{
+ ///
+ /// The tail of a .
+ ///
+ public class DrawableHoldNoteTail : DrawableNote
+ {
+ ///
+ /// Lenience of release hit windows. This is to make cases where the hold note release
+ /// is timed alongside presses of other hit objects less awkward.
+ /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
+ ///
+ private const double release_window_lenience = 1.5;
+
+ private readonly DrawableHoldNote holdNote;
+
+ public DrawableHoldNoteTail(DrawableHoldNote holdNote)
+ : base(holdNote.HitObject.Tail)
+ {
+ this.holdNote = holdNote;
+ }
+
+ public void UpdateResult() => base.UpdateResult(true);
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ Debug.Assert(HitObject.HitWindows != null);
+
+ // Factor in the release lenience
+ timeOffset /= release_window_lenience;
+
+ if (!userTriggered)
+ {
+ if (!HitObject.HitWindows.CanBeHit(timeOffset))
+ ApplyResult(r => r.Type = HitResult.Miss);
+
+ return;
+ }
+
+ var result = HitObject.HitWindows.ResultFor(timeOffset);
+ if (result == HitResult.None)
+ return;
+
+ ApplyResult(r =>
+ {
+ // If the head wasn't hit or the hold note was broken, cap the max score to Meh.
+ if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken))
+ result = HitResult.Meh;
+
+ r.Type = result;
+ });
+ }
+
+ public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
+
+ public override void OnReleased(ManiaAction action)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
index 9a29273282..9b0322a6cd 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
@@ -3,7 +3,6 @@
using System;
using osuTK;
-using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -23,11 +22,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public Func HoldStartTime;
- private readonly Container glowContainer;
-
public DrawableHoldNoteTick(HoldNoteTick hitObject)
: base(hitObject)
{
+ Container glowContainer;
+
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
@@ -53,23 +52,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
});
- }
- public override Color4 AccentColour
- {
- get => base.AccentColour;
- set
+ AccentColour.BindValueChanged(colour =>
{
- base.AccentColour = value;
-
glowContainer.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 2f,
Roundness = 15f,
- Colour = value.Opacity(0.3f)
+ Colour = colour.NewValue.Opacity(0.3f)
};
- }
+ }, true);
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 0873f753be..5bfa07bd14 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -45,6 +45,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}
+
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ switch (state)
+ {
+ case ArmedState.Miss:
+ this.FadeOut(150, Easing.In);
+ break;
+
+ case ArmedState.Hit:
+ this.FadeOut(150, Easing.OutQuint);
+ break;
+ }
+ }
}
public abstract class DrawableManiaHitObject : DrawableManiaHitObject
@@ -57,19 +71,5 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
HitObject = hitObject;
}
-
- protected override void UpdateState(ArmedState state)
- {
- switch (state)
- {
- case ArmedState.Miss:
- this.FadeOut(150, Easing.In).Expire();
- break;
-
- case ArmedState.Hit:
- this.FadeOut(150, Easing.OutQuint).Expire();
- break;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index afd7777861..85613d3afb 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
-using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Input.Bindings;
@@ -30,6 +30,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Masking = true;
AddInternal(headPiece = new NotePiece());
+
+ AccentColour.BindValueChanged(colour =>
+ {
+ headPiece.AccentColour = colour.NewValue;
+
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour.NewValue.Lighten(1f).Opacity(0.2f),
+ Radius = 10,
+ };
+ }, true);
}
protected override void OnDirectionChanged(ValueChangedEvent e)
@@ -39,25 +51,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
headPiece.Anchor = headPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}
- public override Color4 AccentColour
- {
- get => base.AccentColour;
- set
- {
- base.AccentColour = value;
- headPiece.AccentColour = AccentColour;
-
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = AccentColour.Lighten(1f).Opacity(0.6f),
- Radius = 10,
- };
- }
- }
-
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
+ Debug.Assert(HitObject.HitWindows != null);
+
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
@@ -80,6 +77,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
return UpdateResult(true);
}
- public virtual bool OnReleased(ManiaAction action) => false;
+ public virtual void OnReleased(ManiaAction action)
+ {
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs
index 8102718edf..31a4857805 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs
@@ -26,14 +26,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
public BodyPiece()
{
- Blending = BlendingMode.Additive;
+ Blending = BlendingParameters.Additive;
Children = new[]
{
Background = new Box { RelativeSizeAxes = Axes.Both },
Foreground = new BufferedContainer
{
- Blending = BlendingMode.Additive,
+ Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
CacheDrawnFrameBuffer = true,
Children = new Drawable[]
@@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
- private Cached subtractionCache = new Cached();
+ private readonly Cached subtractionCache = new Cached();
public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/GlowPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/GlowPiece.cs
deleted file mode 100644
index 1d25a0c966..0000000000
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/GlowPiece.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
-using osuTK.Graphics;
-
-namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
-{
- public class GlowPiece : CompositeDrawable, IHasAccentColour
- {
- private const float glow_alpha = 0.7f;
- private const float glow_radius = 5;
-
- public GlowPiece()
- {
- RelativeSizeAxes = Axes.Both;
- Masking = true;
-
- InternalChild = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
- updateGlow();
- }
-
- private Color4 accentColour;
-
- public Color4 AccentColour
- {
- get => accentColour;
- set
- {
- if (accentColour == value)
- return;
-
- accentColour = value;
-
- updateGlow();
- }
- }
-
- private void updateGlow()
- {
- if (!IsLoaded)
- return;
-
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = AccentColour.Opacity(glow_alpha),
- Radius = glow_radius,
- Hollow = true
- };
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs
deleted file mode 100644
index 9e0307c5c2..0000000000
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osuTK.Graphics;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Colour;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
-
-namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
-{
- public class LaneGlowPiece : CompositeDrawable, IHasAccentColour
- {
- private const float total_height = 100;
- private const float glow_height = 50;
- private const float glow_alpha = 0.4f;
- private const float edge_alpha = 0.3f;
-
- public LaneGlowPiece()
- {
- BypassAutoSizeAxes = Axes.Both;
- RelativeSizeAxes = Axes.X;
- Height = total_height;
-
- InternalChildren = new[]
- {
- new Container
- {
- Name = "Left edge",
- RelativeSizeAxes = Axes.Y,
- Width = 1,
- Children = createGradient(edge_alpha)
- },
- new Container
- {
- Name = "Right edge",
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- RelativeSizeAxes = Axes.Y,
- Width = 1,
- Children = createGradient(edge_alpha)
- },
- new Container
- {
- Name = "Glow",
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- Height = glow_height,
- Children = createGradient(glow_alpha)
- }
- };
- }
-
- private Drawable[] createGradient(float alpha) => new Drawable[]
- {
- new Box
- {
- Name = "Top",
- RelativeSizeAxes = Axes.Both,
- Height = 0.5f,
- Blending = BlendingMode.Additive,
- Colour = ColourInfo.GradientVertical(Color4.Transparent, Color4.White.Opacity(alpha))
- },
- new Box
- {
- Name = "Bottom",
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- RelativeSizeAxes = Axes.Both,
- Height = 0.5f,
- Blending = BlendingMode.Additive,
- Colour = ColourInfo.GradientVertical(Color4.White.Opacity(alpha), Color4.Transparent)
- }
- };
-
- public Color4 AccentColour
- {
- get => Colour;
- set => Colour = value;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs
index bb33693783..4521af7dfb 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs
@@ -18,8 +18,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
///
internal class NotePiece : Container, IHasAccentColour
{
- public const float NOTE_HEIGHT = 10;
- private const float head_colour_height = 6;
+ public const float NOTE_HEIGHT = 12;
private readonly IBindable direction = new Bindable();
@@ -39,8 +38,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
colouredBox = new Box
{
RelativeSizeAxes = Axes.X,
- Height = head_colour_height,
- Alpha = 0.2f
+ Height = NOTE_HEIGHT / 2,
+ Alpha = 0.1f
}
};
}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index 5e9f46d9c7..bdba813eed 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -6,6 +6,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
@@ -99,5 +100,7 @@ namespace osu.Game.Rulesets.Mania.Objects
}
public override Judgement CreateJudgement() => new HoldNoteJudgement();
+
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs
index c133ee73b1..ac6697a6dc 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs
@@ -3,6 +3,7 @@
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
@@ -12,5 +13,7 @@ namespace osu.Game.Rulesets.Mania.Objects
public class HoldNoteTick : ManiaHitObject
{
public override Judgement CreateJudgement() => new HoldNoteTickJudgement();
+
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index 70720a926b..995e1516cb 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -3,7 +3,9 @@
using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Objects.Types;
+using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs
deleted file mode 100644
index 5f2ceab48b..0000000000
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Mania.Objects
-{
- public class ManiaHitWindows : HitWindows
- {
- private static readonly IReadOnlyDictionary base_ranges = new Dictionary
- {
- { HitResult.Perfect, (44.8, 38.8, 27.8) },
- { HitResult.Great, (128, 98, 68) },
- { HitResult.Good, (194, 164, 134) },
- { HitResult.Ok, (254, 224, 194) },
- { HitResult.Meh, (302, 272, 242) },
- { HitResult.Miss, (376, 346, 316) },
- };
-
- public override bool IsHitResultAllowed(HitResult result) => true;
-
- public override void SetDifficulty(double difficulty)
- {
- Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]);
- Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]);
- Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]);
- Ok = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Ok]);
- Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]);
- Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index e5669816fa..483327d5b3 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -5,7 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
-using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Mania.Replays
@@ -46,9 +46,6 @@ namespace osu.Game.Rulesets.Mania.Replays
public override Replay Generate()
{
- // Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled
- Replay.Frames.Add(new ManiaReplayFrame(-100000, 0));
-
var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time);
var actions = new List();
@@ -69,6 +66,10 @@ namespace osu.Game.Rulesets.Mania.Replays
}
}
+ // todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
+ if (Replay.Frames.Count == 0)
+ Replay.Frames.Add(new ManiaReplayFrame(group.First().Time - 1));
+
Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray()));
}
@@ -77,13 +78,37 @@ namespace osu.Game.Rulesets.Mania.Replays
private IEnumerable generateActionPoints()
{
- foreach (var obj in Beatmap.HitObjects)
+ for (int i = 0; i < Beatmap.HitObjects.Count; i++)
{
- yield return new HitPoint { Time = obj.StartTime, Column = obj.Column };
- yield return new ReleasePoint { Time = ((obj as IHasEndTime)?.EndTime ?? obj.StartTime) + RELEASE_DELAY, Column = obj.Column };
+ var currentObject = Beatmap.HitObjects[i];
+ var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button
+
+ double endTime = currentObject.GetEndTime();
+
+ bool canDelayKeyUp = nextObjectInColumn == null ||
+ nextObjectInColumn.StartTime > endTime + RELEASE_DELAY;
+
+ double calculatedDelay = canDelayKeyUp ? RELEASE_DELAY : (nextObjectInColumn.StartTime - endTime) * 0.9;
+
+ yield return new HitPoint { Time = currentObject.StartTime, Column = currentObject.Column };
+
+ yield return new ReleasePoint { Time = endTime + calculatedDelay, Column = currentObject.Column };
}
}
+ protected override HitObject GetNextObject(int currentIndex)
+ {
+ int desiredColumn = Beatmap.HitObjects[currentIndex].Column;
+
+ for (int i = currentIndex + 1; i < Beatmap.HitObjects.Count; i++)
+ {
+ if (Beatmap.HitObjects[i].Column == desiredColumn)
+ return Beatmap.HitObjects[i];
+ }
+
+ return null;
+ }
+
private interface IActionPoint
{
double Time { get; set; }
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
index f7277d3669..877a9ee410 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
@@ -24,10 +24,10 @@ namespace osu.Game.Rulesets.Mania.Replays
Actions.AddRange(actions);
}
- public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
+ public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
// We don't need to fully convert, just create the converter
- var converter = new ManiaBeatmapConverter(beatmap);
+ var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset());
// NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling
// elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage.
diff --git a/osu.Game/Screens/Direct/OnlineListing.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs
similarity index 58%
rename from osu.Game/Screens/Direct/OnlineListing.cs
rename to osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs
index 8376383674..549f0f9214 100644
--- a/osu.Game/Screens/Direct/OnlineListing.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs
@@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-namespace osu.Game.Screens.Direct
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mania.Scoring
{
- public class OnlineListing : ScreenWhiteBox
+ public class ManiaHitWindows : HitWindows
{
}
}
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 5caf08fb1e..9b54b48de3 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -1,90 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Scoring
{
- internal class ManiaScoreProcessor : ScoreProcessor
+ internal class ManiaScoreProcessor : ScoreProcessor
{
- ///
- /// The hit HP multiplier at OD = 0.
- ///
- private const double hp_multiplier_min = 0.75;
-
- ///
- /// The hit HP multiplier at OD = 0.
- ///
- private const double hp_multiplier_mid = 0.85;
-
- ///
- /// The hit HP multiplier at OD = 0.
- ///
- private const double hp_multiplier_max = 1;
-
- ///
- /// The MISS HP multiplier at OD = 0.
- ///
- private const double hp_multiplier_miss_min = 0.5;
-
- ///
- /// The MISS HP multiplier at OD = 5.
- ///
- private const double hp_multiplier_miss_mid = 0.75;
-
- ///
- /// The MISS HP multiplier at OD = 10.
- ///
- private const double hp_multiplier_miss_max = 1;
-
- ///
- /// The MISS HP multiplier. This is multiplied to the miss hp increase.
- ///
- private double hpMissMultiplier = 1;
-
- ///
- /// The HIT HP multiplier. This is multiplied to hit hp increases.
- ///
- private double hpMultiplier = 1;
-
- public ManiaScoreProcessor(DrawableRuleset drawableRuleset)
- : base(drawableRuleset)
- {
- }
-
- protected override void ApplyBeatmap(Beatmap beatmap)
- {
- base.ApplyBeatmap(beatmap);
-
- BeatmapDifficulty difficulty = beatmap.BeatmapInfo.BaseDifficulty;
- hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max);
- hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max);
- }
-
- protected override void SimulateAutoplay(Beatmap beatmap)
- {
- while (true)
- {
- base.SimulateAutoplay(beatmap);
-
- if (!HasFailed)
- break;
-
- hpMultiplier *= 1.01;
- hpMissMultiplier *= 0.98;
-
- Reset(false);
- }
- }
-
- protected override double HealthAdjustmentFactorFor(JudgementResult result)
- => result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier;
-
public override HitWindows CreateHitWindows() => new ManiaHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
new file mode 100644
index 0000000000..f3739ce7c2
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
@@ -0,0 +1,67 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Audio;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class ManiaLegacySkinTransformer : ISkin
+ {
+ private readonly ISkin source;
+
+ public ManiaLegacySkinTransformer(ISkin source)
+ {
+ this.source = source;
+ }
+
+ public Drawable GetDrawableComponent(ISkinComponent component)
+ {
+ switch (component)
+ {
+ case GameplaySkinComponent resultComponent:
+ return getResult(resultComponent);
+ }
+
+ return null;
+ }
+
+ private Drawable getResult(GameplaySkinComponent resultComponent)
+ {
+ switch (resultComponent.Component)
+ {
+ case HitResult.Miss:
+ return this.GetAnimation("mania-hit0", true, true);
+
+ case HitResult.Meh:
+ return this.GetAnimation("mania-hit50", true, true);
+
+ case HitResult.Ok:
+ return this.GetAnimation("mania-hit100", true, true);
+
+ case HitResult.Good:
+ return this.GetAnimation("mania-hit200", true, true);
+
+ case HitResult.Great:
+ return this.GetAnimation("mania-hit300", true, true);
+
+ case HitResult.Perfect:
+ return this.GetAnimation("mania-hit300g", true, true);
+ }
+
+ return null;
+ }
+
+ public Texture GetTexture(string componentName) => source.GetTexture(componentName);
+
+ public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
+
+ public IBindable GetConfig(TLookup lookup) =>
+ source.GetConfig(lookup);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index c59bed4ea7..63c573d344 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
@@ -11,6 +11,8 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -19,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour
{
- private const float column_width = 45;
+ public const float COLUMN_WIDTH = 80;
private const float special_column_width = 70;
///
@@ -41,10 +43,7 @@ namespace osu.Game.Rulesets.Mania.UI
Index = index;
RelativeSizeAxes = Axes.Y;
- Width = column_width;
-
- Masking = true;
- CornerRadius = 5;
+ Width = COLUMN_WIDTH;
background = new ColumnBackground { RelativeSizeAxes = Axes.Both };
@@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.UI
explosionContainer = new Container
{
Name = "Hit explosions",
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
}
}
},
@@ -90,6 +89,12 @@ namespace osu.Game.Rulesets.Mania.UI
Bottom = dir.NewValue == ScrollingDirection.Down ? ManiaStage.HIT_TARGET_POSITION : 0,
};
+ explosionContainer.Padding = new MarginPadding
+ {
+ Top = dir.NewValue == ScrollingDirection.Up ? NotePiece.NOTE_HEIGHT / 2 : 0,
+ Bottom = dir.NewValue == ScrollingDirection.Down ? NotePiece.NOTE_HEIGHT / 2 : 0
+ };
+
keyArea.Anchor = keyArea.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
}, true);
}
@@ -108,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.UI
isSpecial = value;
- Width = isSpecial ? special_column_width : column_width;
+ Width = isSpecial ? special_column_width : COLUMN_WIDTH;
}
}
@@ -143,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// The DrawableHitObject to add.
public override void Add(DrawableHitObject hitObject)
{
- hitObject.AccentColour = AccentColour;
+ hitObject.AccentColour.Value = AccentColour;
hitObject.OnNewResult += OnNewResult;
HitObjectContainer.Add(hitObject);
@@ -163,9 +168,10 @@ namespace osu.Game.Rulesets.Mania.UI
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- explosionContainer.Add(new HitExplosion(judgedObject)
+ explosionContainer.Add(new HitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick)
{
- Anchor = Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre
+ Anchor = Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre,
+ Origin = Anchor.Centre
});
}
@@ -185,7 +191,9 @@ namespace osu.Game.Rulesets.Mania.UI
return true;
}
- public bool OnReleased(ManiaAction action) => false;
+ public void OnReleased(ManiaAction action)
+ {
+ }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
index b4e29ae9f9..75cc351310 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
@@ -35,14 +35,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
Name = "Background",
RelativeSizeAxes = Axes.Both,
- Alpha = 0.3f
},
backgroundOverlay = new Box
{
Name = "Background Gradient Overlay",
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
- Blending = BlendingMode.Additive,
+ Blending = BlendingParameters.Additive,
Alpha = 0
}
};
@@ -82,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
if (!IsLoaded)
return;
- background.Colour = AccentColour;
+ background.Colour = AccentColour.Darken(5);
var brightPoint = AccentColour.Opacity(0.6f);
var dimPoint = AccentColour.Opacity(0);
@@ -99,11 +98,10 @@ namespace osu.Game.Rulesets.Mania.UI.Components
return false;
}
- public bool OnReleased(ManiaAction action)
+ public void OnReleased(ManiaAction action)
{
if (action == this.action.Value)
backgroundOverlay.FadeTo(0, 250, Easing.OutQuint);
- return false;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
index a0d713067d..90e78c3899 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK.Graphics;
@@ -17,30 +18,17 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
public class ColumnHitObjectArea : CompositeDrawable, IHasAccentColour
{
- private const float hit_target_height = 10;
- private const float hit_target_bar_height = 2;
-
private readonly IBindable direction = new Bindable();
- private readonly Container hitTargetLine;
- private readonly Drawable hitTargetBar;
+ private readonly Drawable hitTarget;
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
{
InternalChildren = new[]
{
- hitTargetBar = new Box
+ hitTarget = new DefaultHitTarget
{
RelativeSizeAxes = Axes.X,
- Height = hit_target_height,
- Colour = Color4.Black
- },
- hitTargetLine = new Container
- {
- RelativeSizeAxes = Axes.X,
- Height = hit_target_bar_height,
- Masking = true,
- Child = new Box { RelativeSizeAxes = Axes.Both }
},
hitObjectContainer
};
@@ -54,17 +42,10 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
- hitTargetBar.Anchor = hitTargetBar.Origin = anchor;
- hitTargetLine.Anchor = hitTargetLine.Origin = anchor;
+ hitTarget.Anchor = hitTarget.Origin = anchor;
}, true);
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
- updateColours();
- }
-
private Color4 accentColour;
public Color4 AccentColour
@@ -77,21 +58,88 @@ namespace osu.Game.Rulesets.Mania.UI.Components
accentColour = value;
- updateColours();
+ if (hitTarget is IHasAccentColour colouredHitTarget)
+ colouredHitTarget.AccentColour = accentColour;
}
}
- private void updateColours()
+ private class DefaultHitTarget : CompositeDrawable, IHasAccentColour
{
- if (!IsLoaded)
- return;
+ private const float hit_target_bar_height = 2;
- hitTargetLine.EdgeEffect = new EdgeEffectParameters
+ private readonly IBindable direction = new Bindable();
+
+ private readonly Container hitTargetLine;
+ private readonly Drawable hitTargetBar;
+
+ public DefaultHitTarget()
{
- Type = EdgeEffectType.Glow,
- Radius = 5,
- Colour = accentColour.Opacity(0.5f),
- };
+ InternalChildren = new[]
+ {
+ hitTargetBar = new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = NotePiece.NOTE_HEIGHT,
+ Alpha = 0.6f,
+ Colour = Color4.Black
+ },
+ hitTargetLine = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = hit_target_bar_height,
+ Masking = true,
+ Child = new Box { RelativeSizeAxes = Axes.Both }
+ },
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(dir =>
+ {
+ Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
+
+ hitTargetBar.Anchor = hitTargetBar.Origin = anchor;
+ hitTargetLine.Anchor = hitTargetLine.Origin = anchor;
+ }, true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateColours();
+ }
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ if (accentColour == value)
+ return;
+
+ accentColour = value;
+
+ updateColours();
+ }
+ }
+
+ private void updateColours()
+ {
+ if (!IsLoaded)
+ return;
+
+ hitTargetLine.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 5,
+ Colour = accentColour.Opacity(0.5f),
+ };
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs
index 85880222d7..60fc2713b3 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs
@@ -115,11 +115,10 @@ namespace osu.Game.Rulesets.Mania.UI.Components
return false;
}
- public bool OnReleased(ManiaAction action)
+ public void OnReleased(ManiaAction action)
{
if (action == this.action.Value)
keyIcon.ScaleTo(1f, 125, Easing.OutQuint);
- return false;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index c8aeda8fe4..2c497541a8 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -2,14 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Input;
-using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -17,11 +14,9 @@ using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays;
-using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Types;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -36,40 +31,16 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable BarLines;
+ protected override bool RelativeScaleBeatLengths => true;
+
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
private readonly Bindable configDirection = new Bindable();
- public DrawableManiaRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
: base(ruleset, beatmap, mods)
{
- // Generate the bar lines
- double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
-
- var timingPoints = Beatmap.ControlPointInfo.TimingPoints;
- var barLines = new List();
-
- for (int i = 0; i < timingPoints.Count; i++)
- {
- TimingControlPoint point = timingPoints[i];
-
- // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
- double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - point.BeatLength : lastObjectTime + point.BeatLength * (int)point.TimeSignature;
-
- int index = 0;
-
- for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++)
- {
- barLines.Add(new BarLine
- {
- StartTime = t,
- ControlPoint = point,
- BeatIndex = index
- });
- }
- }
-
- BarLines = barLines;
+ BarLines = new BarLineGenerator(Beatmap).BarLines;
}
[BackgroundDependencyLoader]
@@ -94,8 +65,6 @@ namespace osu.Game.Rulesets.Mania.UI
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);
- public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
-
public override int Variant => (int)(Beatmap.Stages.Count == 1 ? PlayfieldType.Single : PlayfieldType.Dual) + Beatmap.TotalColumns;
protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
diff --git a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs
index 0ec1fc38d2..35de47e208 100644
--- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs
@@ -1,16 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK.Graphics;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.MathUtils;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
-using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -18,51 +16,112 @@ namespace osu.Game.Rulesets.Mania.UI
{
public override bool RemoveWhenNotAlive => true;
- private readonly CircularContainer circle;
+ private readonly CircularContainer largeFaint;
+ private readonly CircularContainer mainGlow1;
- public HitExplosion(DrawableHitObject judgedObject)
+ public HitExplosion(Color4 objectColour, bool isSmall = false)
{
- bool isTick = judgedObject is DrawableHoldNoteTick;
-
- Origin = Anchor.Centre;
-
RelativeSizeAxes = Axes.X;
- Y = NotePiece.NOTE_HEIGHT / 2;
Height = NotePiece.NOTE_HEIGHT;
// scale roughly in-line with visual appearance of notes
- Scale = new Vector2(isTick ? 0.4f : 0.8f);
+ Scale = new Vector2(1f, 0.6f);
- InternalChild = circle = new CircularContainer
+ if (isSmall)
+ Scale *= 0.5f;
+
+ const float angle_variangle = 15; // should be less than 45
+
+ const float roundness = 80;
+
+ const float initial_height = 10;
+
+ var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
+
+ InternalChildren = new Drawable[]
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- // we want our size to be very small so the glow dominates it.
- Size = new Vector2(0.1f),
- EdgeEffect = new EdgeEffectParameters
+ largeFaint = new CircularContainer
{
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.1f, judgedObject.AccentColour, Color4.White, 0, 1),
- Radius = 100,
- },
- Child = new Box
- {
- Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- AlwaysPresent = true
+ Masking = true,
+ // we want our size to be very small so the glow dominates it.
+ Size = new Vector2(0.8f),
+ Blending = BlendingParameters.Additive,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+ Roundness = 160,
+ Radius = 200,
+ },
+ },
+ mainGlow1 = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Blending = BlendingParameters.Additive,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+ Roundness = 20,
+ Radius = 50,
+ },
+ },
+ new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Size = new Vector2(0.01f, initial_height),
+ Blending = BlendingParameters.Additive,
+ Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour,
+ Roundness = roundness,
+ Radius = 40,
+ },
+ },
+ new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Size = new Vector2(0.01f, initial_height),
+ Blending = BlendingParameters.Additive,
+ Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour,
+ Roundness = roundness,
+ Radius = 40,
+ },
}
};
}
protected override void LoadComplete()
{
+ const double duration = 200;
+
base.LoadComplete();
- circle.ResizeTo(circle.Size * new Vector2(4, 20), 1000, Easing.OutQuint);
- this.FadeIn(16).Then().FadeOut(500, Easing.OutQuint);
+ largeFaint
+ .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
+ .FadeOut(duration * 2);
+ mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint);
+
+ this.FadeOut(duration, Easing.Out);
Expire(true);
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 5ab07416a6..08f6049782 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.UI
foreach (var stage in stages)
{
- sum = sum + stage.Columns.Count;
+ sum += stage.Columns.Count;
if (sum > column)
return stage;
}
diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
index a086da0565..07ef1022ae 100644
--- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
+++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
@@ -1,9 +1,7 @@
-
- netstandard2.0
+ netstandard2.1
Library
- AnyCPU
true
smash the keys. to the beat.
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml
index aad907b241..3ce17ccc27 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
index 3718264a42..b36d0b5728 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
@@ -5,7 +5,7 @@ using UIKit;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
- public class Application
+ public static class Application
{
public static void Main(string[] args)
{
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
index 9930a166e3..545abcec6c 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
@@ -1,6 +1,5 @@
-
+
-
Debug
iPhoneSimulator
@@ -13,14 +12,6 @@
-
- libbass.a
- PreserveNewest
-
-
- libbass_fx.a
- PreserveNewest
-
Linker.xml
@@ -41,5 +32,4 @@
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json
index ed03e99b9b..94568e3852 100644
--- a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json
+++ b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Debug/netcoreapp2.2/osu.Game.Rulesets.Osu.Tests.dll"
+ "${workspaceRoot}/bin/Debug/netcoreapp3.1/osu.Game.Rulesets.Osu.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/netcoreapp2.2/osu.Game.Rulesets.Osu.Tests.dll"
+ "${workspaceRoot}/bin/Release/netcoreapp3.1/osu.Game.Rulesets.Osu.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
index f98d63e6c7..cd3daf18a9 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
@@ -4,11 +4,9 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
@@ -21,10 +19,10 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase("basic")]
[TestCase("colinear-perfect-curve")]
[TestCase("slider-ticks")]
- public new void Test(string name)
- {
- base.Test(name);
- }
+ [TestCase("repeat-slider")]
+ [TestCase("uneven-repeat-slider")]
+ [TestCase("old-stacking")]
+ public void Test(string name) => base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
{
@@ -32,22 +30,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{
case Slider slider:
foreach (var nested in slider.NestedHitObjects)
- yield return createConvertValue(nested);
+ yield return createConvertValue((OsuHitObject)nested);
break;
default:
- yield return createConvertValue(hitObject);
+ yield return createConvertValue((OsuHitObject)hitObject);
break;
}
- ConvertValue createConvertValue(HitObject obj) => new ConvertValue
+ static ConvertValue createConvertValue(OsuHitObject obj) => new ConvertValue
{
StartTime = obj.StartTime,
- EndTime = (obj as IHasEndTime)?.EndTime ?? obj.StartTime,
- X = (obj as IHasPosition)?.X ?? OsuPlayfield.BASE_SIZE.X / 2,
- Y = (obj as IHasPosition)?.Y ?? OsuPlayfield.BASE_SIZE.Y / 2,
+ EndTime = obj.GetEndTime(),
+ X = obj.StackedPosition.X,
+ Y = obj.StackedPosition.Y
};
}
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index e55dc1f902..85a41137d4 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -14,7 +14,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.931145117263422, "diffcalc-test")]
+ [TestCase(6.9311451172608853d, "diffcalc-test")]
+ [TestCase(1.0736587013228804d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs
new file mode 100644
index 0000000000..495f2738b5
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Game.Beatmaps.Legacy;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class OsuLegacyModConversionTest : LegacyModConversionTest
+ {
+ [TestCase(LegacyMods.Easy, new[] { typeof(OsuModEasy) })]
+ [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) })]
+ [TestCase(LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) })]
+ [TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })]
+ [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
+ [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModFlashlight), typeof(OsuModFlashlight) })]
+ [TestCase(LegacyMods.Perfect, new[] { typeof(OsuModPerfect) })]
+ [TestCase(LegacyMods.SuddenDeath, new[] { typeof(OsuModSuddenDeath) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime), typeof(OsuModPerfect) })]
+ [TestCase(LegacyMods.SpunOut | LegacyMods.Easy, new[] { typeof(OsuModSpunOut), typeof(OsuModEasy) })]
+ public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
+
+ protected override Ruleset CreateRuleset() => new OsuRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/approachcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/approachcircle@2x.png
new file mode 100644
index 0000000000..72ef665478
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/approachcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit0@2x.png
new file mode 100644
index 0000000000..a91072eb5b
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit0@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit100@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit100@2x.png
new file mode 100644
index 0000000000..5eb202c021
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit100@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit300@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit300@2x.png
new file mode 100644
index 0000000000..878c11cd67
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit300@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit50@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit50@2x.png
new file mode 100644
index 0000000000..f64feded0c
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit50@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircle@2x.png
new file mode 100644
index 0000000000..043bfbfae1
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircleoverlay@2x.png
new file mode 100644
index 0000000000..4233d9bb6e
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircleoverlay@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/approachcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/approachcircle.png
new file mode 100644
index 0000000000..ff8b02ce80
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/approachcircle.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-0.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-0.png
new file mode 100644
index 0000000000..2af0569bcb
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-0.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-1.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-1.png
new file mode 100644
index 0000000000..e8b674d62a
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-1.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-2.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-2.png
new file mode 100644
index 0000000000..dbf7bc73bc
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-2.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-3.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-3.png
new file mode 100644
index 0000000000..43990c46e7
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-3.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-4.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-4.png
new file mode 100644
index 0000000000..4564f6d8bf
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-4.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-5.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-5.png
new file mode 100644
index 0000000000..dcef35eb59
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-5.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-6.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-6.png
new file mode 100644
index 0000000000..bfc0a01be5
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-6.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-7.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-7.png
new file mode 100644
index 0000000000..b9079ad5d5
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-7.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-8.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-8.png
new file mode 100644
index 0000000000..3a3d61b947
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-8.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-9.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-9.png
new file mode 100644
index 0000000000..3e703cd1cf
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-9.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit0.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit0.png
new file mode 100644
index 0000000000..3c3ebbfd0b
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit0.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit100.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit100.png
new file mode 100644
index 0000000000..9ecc302910
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit100.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit300.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit300.png
new file mode 100644
index 0000000000..24945f7d92
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit300.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit50.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit50.png
new file mode 100644
index 0000000000..d3f7eec5ee
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit50.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircle.png
new file mode 100644
index 0000000000..a5a3545abf
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircle.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircleoverlay.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircleoverlay.png
new file mode 100644
index 0000000000..b4062b8c62
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircleoverlay.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
new file mode 100644
index 0000000000..5369de24e9
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
@@ -0,0 +1,2 @@
+[General]
+Version: 1.0
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/approachcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/approachcircle@2x.png
new file mode 100755
index 0000000000..0a6ec6535c
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/approachcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-0@2x.png
new file mode 100644
index 0000000000..37e7e9143f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-0@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-1@2x.png
new file mode 100644
index 0000000000..b75c71927c
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-1@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-0@2x.png
new file mode 100644
index 0000000000..7932667408
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-0@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-1@2x.png
new file mode 100644
index 0000000000..0b0ae85972
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-1@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-0@2x.png
new file mode 100644
index 0000000000..441c3ec21f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-0@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-1@2x.png
new file mode 100644
index 0000000000..910d8e2231
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-1@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-0@2x.png
new file mode 100644
index 0000000000..6f92db28d3
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-0@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-1@2x.png
new file mode 100644
index 0000000000..b28503e9f2
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-1@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png
new file mode 100755
index 0000000000..919d8f405c
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png
new file mode 100755
index 0000000000..a9b2d95d88
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
new file mode 100644
index 0000000000..d4c3000d3f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Text.RegularExpressions;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public abstract class SkinnableTestScene : OsuGridTestScene
+ {
+ private Skin metricsSkin;
+ private Skin defaultSkin;
+ private Skin specialSkin;
+ private Skin oldSkin;
+
+ protected SkinnableTestScene()
+ : base(2, 3)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio, SkinManager skinManager)
+ {
+ var dllStore = new DllResourceStore(typeof(SkinnableTestScene).Assembly);
+
+ metricsSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true);
+ defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
+ specialSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/special_skin"), audio, true);
+ oldSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/old_skin"), audio, true);
+ }
+
+ public void SetContents(Func creationFunction)
+ {
+ Cell(0).Child = createProvider(null, creationFunction);
+ Cell(1).Child = createProvider(metricsSkin, creationFunction);
+ Cell(2).Child = createProvider(defaultSkin, creationFunction);
+ Cell(3).Child = createProvider(specialSkin, creationFunction);
+ Cell(4).Child = createProvider(oldSkin, creationFunction);
+ }
+
+ private Drawable createProvider(Skin skin, Func creationFunction)
+ {
+ var mainProvider = new SkinProvidingContainer(skin);
+
+ return mainProvider
+ .WithChild(new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider))
+ {
+ Child = creationFunction()
+ });
+ }
+
+ private class TestLegacySkin : LegacySkin
+ {
+ private readonly bool extrapolateAnimations;
+
+ public TestLegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, bool extrapolateAnimations)
+ : base(skin, storage, audioManager, "skin.ini")
+ {
+ this.extrapolateAnimations = extrapolateAnimations;
+ }
+
+ public override Texture GetTexture(string componentName)
+ {
+ // extrapolate frames to test longer animations
+ if (extrapolateAnimations)
+ {
+ var match = Regex.Match(componentName, "-([0-9]*)");
+
+ if (match.Length > 0 && int.TryParse(match.Groups[1].Value, out var number) && number < 60)
+ return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"));
+ }
+
+ return base.GetTexture(componentName);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs
index e8b99e86f9..871afdb09d 100644
--- a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Game.Beatmaps;
+using osu.Game.IO;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestStacking()
{
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(beatmap_data)))
- using (var reader = new StreamReader(stream))
+ using (var reader = new LineBufferedReader(stream))
{
var beatmap = Decoder.GetDecoder(reader).Decode(reader);
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty());
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
new file mode 100644
index 0000000000..46769f65fe
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -0,0 +1,132 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing.Input;
+using osu.Game.Audio;
+using osu.Game.Rulesets.Osu.Skinning;
+using osu.Game.Rulesets.Osu.UI.Cursor;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneCursorTrail : OsuTestScene
+ {
+ [Test]
+ public void TestSmoothCursorTrail()
+ {
+ Container scalingContainer = null;
+
+ createTest(() => scalingContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new CursorTrail()
+ });
+
+ AddStep("set large scale", () => scalingContainer.Scale = new Vector2(10));
+ }
+
+ [Test]
+ public void TestLegacySmoothCursorTrail()
+ {
+ createTest(() => new LegacySkinContainer(false)
+ {
+ Child = new LegacyCursorTrail()
+ });
+ }
+
+ [Test]
+ public void TestLegacyDisjointCursorTrail()
+ {
+ createTest(() => new LegacySkinContainer(true)
+ {
+ Child = new LegacyCursorTrail()
+ });
+ }
+
+ private void createTest(Func createContent) => AddStep("create trail", () =>
+ {
+ Clear();
+
+ Add(new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.8f),
+ Child = new MovingCursorInputManager { Child = createContent?.Invoke() }
+ });
+ });
+
+ [Cached(typeof(ISkinSource))]
+ private class LegacySkinContainer : Container, ISkinSource
+ {
+ private readonly bool disjoint;
+
+ public LegacySkinContainer(bool disjoint)
+ {
+ this.disjoint = disjoint;
+
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
+
+ public Texture GetTexture(string componentName)
+ {
+ switch (componentName)
+ {
+ case "cursortrail":
+ var tex = new Texture(Texture.WhitePixel.TextureGL);
+
+ if (disjoint)
+ tex.ScaleAdjust = 1 / 25f;
+ return tex;
+
+ case "cursormiddle":
+ return disjoint ? null : Texture.WhitePixel;
+ }
+
+ return null;
+ }
+
+ public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+
+ public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
+
+ public event Action SourceChanged
+ {
+ add { }
+ remove { }
+ }
+ }
+
+ private class MovingCursorInputManager : ManualInputManager
+ {
+ public MovingCursorInputManager()
+ {
+ UseParentInput = false;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ const double spin_duration = 1000;
+ double currentTime = Time.Current;
+
+ double angle = (currentTime % spin_duration) / spin_duration * 2 * Math.PI;
+ Vector2 rPos = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
+
+ MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
new file mode 100644
index 0000000000..ac627aa23e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneDrawableJudgement : SkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(DrawableJudgement),
+ typeof(DrawableOsuJudgement)
+ };
+
+ public TestSceneDrawableJudgement()
+ {
+ foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1))
+ {
+ AddStep("Show " + result.GetDescription(), () => SetContents(() =>
+ new DrawableOsuJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
new file mode 100644
index 0000000000..94ca2d4cd1
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
@@ -0,0 +1,230 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneFollowPoints : OsuTestScene
+ {
+ private Container hitObjectContainer;
+ private FollowPointRenderer followPointRenderer;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Children = new Drawable[]
+ {
+ hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both },
+ followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }
+ };
+ });
+
+ [Test]
+ public void TestAddObject()
+ {
+ addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveObject()
+ {
+ addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
+
+ removeObjectStep(() => getObject(0));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestAddMultipleObjects()
+ {
+ addMultipleObjectsStep();
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveEndObject()
+ {
+ addMultipleObjectsStep();
+
+ removeObjectStep(() => getObject(4));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveStartObject()
+ {
+ addMultipleObjectsStep();
+
+ removeObjectStep(() => getObject(0));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveMiddleObject()
+ {
+ addMultipleObjectsStep();
+
+ removeObjectStep(() => getObject(2));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestMoveObject()
+ {
+ addMultipleObjectsStep();
+
+ AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100));
+
+ assertGroups();
+ }
+
+ [TestCase(0, 0)] // Start -> Start
+ [TestCase(0, 2)] // Start -> Middle
+ [TestCase(0, 5)] // Start -> End
+ [TestCase(2, 0)] // Middle -> Start
+ [TestCase(1, 3)] // Middle -> Middle (forwards)
+ [TestCase(3, 1)] // Middle -> Middle (backwards)
+ [TestCase(4, 0)] // End -> Start
+ [TestCase(4, 2)] // End -> Middle
+ [TestCase(4, 4)] // End -> End
+ public void TestReorderObjects(int startIndex, int endIndex)
+ {
+ addMultipleObjectsStep();
+
+ reorderObjectStep(startIndex, endIndex);
+
+ assertGroups();
+ }
+
+ private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[]
+ {
+ new HitCircle { Position = new Vector2(100, 100) },
+ new HitCircle { Position = new Vector2(200, 200) },
+ new HitCircle { Position = new Vector2(300, 300) },
+ new HitCircle { Position = new Vector2(400, 400) },
+ new HitCircle { Position = new Vector2(500, 500) },
+ });
+
+ private void addObjectsStep(Func ctorFunc)
+ {
+ AddStep("add hitobjects", () =>
+ {
+ var objects = ctorFunc();
+
+ for (int i = 0; i < objects.Length; i++)
+ {
+ objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1);
+ objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ DrawableOsuHitObject drawableObject = null;
+
+ switch (objects[i])
+ {
+ case HitCircle circle:
+ drawableObject = new DrawableHitCircle(circle);
+ break;
+
+ case Slider slider:
+ drawableObject = new DrawableSlider(slider);
+ break;
+
+ case Spinner spinner:
+ drawableObject = new DrawableSpinner(spinner);
+ break;
+ }
+
+ hitObjectContainer.Add(drawableObject);
+ followPointRenderer.AddFollowPoints(drawableObject);
+ }
+ });
+ }
+
+ private void removeObjectStep(Func getFunc)
+ {
+ AddStep("remove hitobject", () =>
+ {
+ var drawableObject = getFunc?.Invoke();
+
+ hitObjectContainer.Remove(drawableObject);
+ followPointRenderer.RemoveFollowPoints(drawableObject);
+ });
+ }
+
+ private void reorderObjectStep(int startIndex, int endIndex)
+ {
+ AddStep($"move object {startIndex} to {endIndex}", () =>
+ {
+ DrawableOsuHitObject toReorder = getObject(startIndex);
+
+ double targetTime;
+ if (endIndex < hitObjectContainer.Count)
+ targetTime = getObject(endIndex).HitObject.StartTime - 1;
+ else
+ targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1;
+
+ hitObjectContainer.Remove(toReorder);
+ toReorder.HitObject.StartTime = targetTime;
+ hitObjectContainer.Add(toReorder);
+ });
+ }
+
+ private void assertGroups()
+ {
+ AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count);
+ AddAssert("group endpoints are correct", () =>
+ {
+ for (int i = 0; i < hitObjectContainer.Count; i++)
+ {
+ DrawableOsuHitObject expectedStart = getObject(i);
+ DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
+
+ if (getGroup(i).Start != expectedStart)
+ throw new AssertionException($"Object {i} expected to be the start of group {i}.");
+
+ if (getGroup(i).End != expectedEnd)
+ throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
+ }
+
+ return true;
+ });
+ }
+
+ private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
+
+ private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
+
+ private class TestHitObjectContainer : Container
+ {
+ protected override int Compare(Drawable x, Drawable y)
+ {
+ var osuX = (DrawableOsuHitObject)x;
+ var osuY = (DrawableOsuHitObject)y;
+
+ int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime);
+
+ if (compare == 0)
+ return base.Compare(x, y);
+
+ return compare;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 1b1cfa89c0..aa170eae1e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -6,29 +6,68 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Cursor;
-using osu.Game.Graphics.Cursor;
+using osu.Framework.Testing.Input;
using osu.Game.Rulesets.Osu.UI.Cursor;
-using osu.Game.Rulesets.UI;
-using osu.Game.Tests.Visual;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneGameplayCursor : OsuTestScene, IProvideCursor
+ public class TestSceneGameplayCursor : SkinnableTestScene
{
- private GameplayCursorContainer cursorContainer;
-
- public override IReadOnlyList RequiredTypes => new[] { typeof(CursorTrail) };
-
- public CursorContainer Cursor => cursorContainer;
-
- public bool ProvidingUserCursor => true;
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(OsuCursorContainer),
+ typeof(CursorTrail)
+ };
[BackgroundDependencyLoader]
private void load()
{
- Add(cursorContainer = new OsuCursorContainer { RelativeSizeAxes = Axes.Both });
+ SetContents(() => new MovingCursorInputManager
+ {
+ Child = new ClickingCursorContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ }
+ });
+ }
+
+ private class ClickingCursorContainer : OsuCursorContainer
+ {
+ protected override void Update()
+ {
+ base.Update();
+
+ double currentTime = Time.Current;
+
+ if (((int)(currentTime / 1000)) % 2 == 0)
+ OnPressed(OsuAction.LeftButton);
+ else
+ OnReleased(OsuAction.LeftButton);
+ }
+ }
+
+ private class MovingCursorInputManager : ManualInputManager
+ {
+ public MovingCursorInputManager()
+ {
+ UseParentInput = false;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ const double spin_duration = 5000;
+ double currentTime = Time.Current;
+
+ double angle = (currentTime % spin_duration) / spin_duration * 2 * Math.PI;
+ Vector2 rPos = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
+
+ MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index d44a0cd841..098e277fff 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -7,7 +7,6 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Tests.Visual;
using osuTK;
using System.Collections.Generic;
using System;
@@ -19,39 +18,34 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneHitCircle : OsuTestScene
+ public class TestSceneHitCircle : SkinnableTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
typeof(DrawableHitCircle)
};
- private readonly Container content;
- protected override Container Content => content;
-
private int depthIndex;
public TestSceneHitCircle()
{
- base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
-
- AddStep("Miss Big Single", () => testSingle(2));
- AddStep("Miss Medium Single", () => testSingle(5));
- AddStep("Miss Small Single", () => testSingle(7));
- AddStep("Hit Big Single", () => testSingle(2, true));
- AddStep("Hit Medium Single", () => testSingle(5, true));
- AddStep("Hit Small Single", () => testSingle(7, true));
- AddStep("Miss Big Stream", () => testStream(2));
- AddStep("Miss Medium Stream", () => testStream(5));
- AddStep("Miss Small Stream", () => testStream(7));
- AddStep("Hit Big Stream", () => testStream(2, true));
- AddStep("Hit Medium Stream", () => testStream(5, true));
- AddStep("Hit Small Stream", () => testStream(7, true));
+ AddStep("Miss Big Single", () => SetContents(() => testSingle(2)));
+ AddStep("Miss Medium Single", () => SetContents(() => testSingle(5)));
+ AddStep("Miss Small Single", () => SetContents(() => testSingle(7)));
+ AddStep("Hit Big Single", () => SetContents(() => testSingle(2, true)));
+ AddStep("Hit Medium Single", () => SetContents(() => testSingle(5, true)));
+ AddStep("Hit Small Single", () => SetContents(() => testSingle(7, true)));
+ AddStep("Miss Big Stream", () => SetContents(() => testStream(2)));
+ AddStep("Miss Medium Stream", () => SetContents(() => testStream(5)));
+ AddStep("Miss Small Stream", () => SetContents(() => testStream(7)));
+ AddStep("Hit Big Stream", () => SetContents(() => testStream(2, true)));
+ AddStep("Hit Medium Stream", () => SetContents(() => testStream(5, true)));
+ AddStep("Hit Small Stream", () => SetContents(() => testStream(7, true)));
}
- private void testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
+ private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
{
- positionOffset = positionOffset ?? Vector2.Zero;
+ positionOffset ??= Vector2.Zero;
var circle = new HitCircle
{
@@ -61,27 +55,33 @@ namespace osu.Game.Rulesets.Osu.Tests
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
- var drawable = new TestDrawableHitCircle(circle, auto)
- {
- Anchor = Anchor.Centre,
- Depth = depthIndex++
- };
+ var drawable = CreateDrawableHitCircle(circle, auto);
- foreach (var mod in Mods.Value.OfType())
+ foreach (var mod in SelectedMods.Value.OfType())
mod.ApplyToDrawableHitObjects(new[] { drawable });
- Add(drawable);
+ return drawable;
}
- private void testStream(float circleSize, bool auto = false)
+ protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto)
{
+ Anchor = Anchor.Centre,
+ Depth = depthIndex++
+ };
+
+ private Drawable testStream(float circleSize, bool auto = false)
+ {
+ var container = new Container { RelativeSizeAxes = Axes.Both };
+
Vector2 pos = new Vector2(-250, 0);
for (int i = 0; i <= 1000; i += 100)
{
- testSingle(circleSize, auto, i, pos);
+ container.Add(testSingle(circleSize, auto, i, pos));
pos.X += 50;
}
+
+ return container;
}
protected class TestDrawableHitCircle : DrawableHitCircle
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs
new file mode 100644
index 0000000000..5695462859
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneHitCircleComboChange : TestSceneHitCircle
+ {
+ private readonly Bindable comboIndex = new Bindable();
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Scheduler.AddDelayed(() => comboIndex.Value++, 250, true);
+ }
+
+ protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
+ {
+ circle.ComboIndexBindable.BindTo(comboIndex);
+ circle.IndexInCurrentComboBindable.BindTo(comboIndex);
+ return base.CreateDrawableHitCircle(circle, auto);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs
index 55c6b22146..21ebce8c23 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs
@@ -14,9 +14,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList();
- public TestSceneHitCircleHidden()
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
- Mods.Value = new[] { new OsuModHidden() };
- }
+ SelectedMods.Value = new[] { new OsuModHidden() };
+ });
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
index 399cf22599..b99cd523ff 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
@@ -29,7 +29,10 @@ namespace osu.Game.Rulesets.Osu.Tests
};
for (int i = 0; i < 512; i++)
- beatmap.HitObjects.Add(new HitCircle { Position = new Vector2(256, 192), StartTime = i * 100 });
+ {
+ if (i % 32 < 20)
+ beatmap.HitObjects.Add(new HitCircle { Position = new Vector2(256, 192), StartTime = i * 100 });
+ }
return beatmap;
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs
index 32043bf5d7..0ecce42e88 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs
@@ -1,10 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
@@ -14,16 +15,58 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneHitCircleSelectionBlueprint : SelectionBlueprintTestScene
{
- private readonly DrawableHitCircle drawableObject;
+ private HitCircle hitCircle;
+ private DrawableHitCircle drawableObject;
+ private TestBlueprint blueprint;
- public TestSceneHitCircleSelectionBlueprint()
+ [SetUp]
+ public void Setup() => Schedule(() =>
{
- var hitCircle = new HitCircle { Position = new Vector2(256, 192) };
+ Clear();
+
+ hitCircle = new HitCircle { Position = new Vector2(256, 192) };
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
Add(drawableObject = new DrawableHitCircle(hitCircle));
+ AddBlueprint(blueprint = new TestBlueprint(drawableObject));
+ });
+
+ [Test]
+ public void TestInitialState()
+ {
+ AddAssert("blueprint positioned over hitobject", () => blueprint.CirclePiece.Position == hitCircle.Position);
}
- protected override SelectionBlueprint CreateBlueprint() => new HitCircleSelectionBlueprint(drawableObject);
+ [Test]
+ public void TestMoveHitObject()
+ {
+ AddStep("move hitobject", () => hitCircle.Position = new Vector2(300, 225));
+ AddAssert("blueprint positioned over hitobject", () => blueprint.CirclePiece.Position == hitCircle.Position);
+ }
+
+ [Test]
+ public void TestMoveAfterApplyingDefaults()
+ {
+ AddStep("apply defaults", () => hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 }));
+ AddStep("move hitobject", () => hitCircle.Position = new Vector2(300, 225));
+ AddAssert("blueprint positioned over hitobject", () => blueprint.CirclePiece.Position == hitCircle.Position);
+ }
+
+ [Test]
+ public void TestStackedHitObject()
+ {
+ AddStep("set stacking", () => hitCircle.StackHeight = 5);
+ AddAssert("blueprint positioned over hitobject", () => blueprint.CirclePiece.Position == hitCircle.StackedPosition);
+ }
+
+ private class TestBlueprint : HitCircleSelectionBlueprint
+ {
+ public new HitCirclePiece CirclePiece => base.CirclePiece;
+
+ public TestBlueprint(DrawableHitCircle drawableCircle)
+ : base(drawableCircle)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs
new file mode 100644
index 0000000000..4676f14655
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs
@@ -0,0 +1,155 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Graphics;
+using osu.Framework.IO.Stores;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneLegacyBeatmapSkin : OsuTestScene
+ {
+ [Resolved]
+ private AudioManager audio { get; set; }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestBeatmapComboColours(bool customSkinColoursPresent)
+ {
+ ExposedPlayer player = null;
+
+ AddStep("load coloured beatmap", () => player = loadBeatmap(customSkinColoursPresent, true));
+ AddUntilStep("wait for player", () => player.IsLoaded);
+
+ AddAssert("is beatmap skin colours", () => player.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours));
+ }
+
+ [Test]
+ public void TestBeatmapNoComboColours()
+ {
+ ExposedPlayer player = null;
+
+ AddStep("load no-colour beatmap", () => player = loadBeatmap(false, false));
+ AddUntilStep("wait for player", () => player.IsLoaded);
+
+ AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
+ }
+
+ [Test]
+ public void TestBeatmapNoComboColoursSkinOverride()
+ {
+ ExposedPlayer player = null;
+
+ AddStep("load custom-skin colour", () => player = loadBeatmap(true, false));
+ AddUntilStep("wait for player", () => player.IsLoaded);
+
+ AddAssert("is custom user skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours));
+ }
+
+ private ExposedPlayer loadBeatmap(bool userHasCustomColours, bool beatmapHasColours)
+ {
+ ExposedPlayer player;
+
+ Beatmap.Value = new CustomSkinWorkingBeatmap(audio, beatmapHasColours);
+ Child = new OsuScreenStack(player = new ExposedPlayer(userHasCustomColours)) { RelativeSizeAxes = Axes.Both };
+
+ return player;
+ }
+
+ private class ExposedPlayer : Player
+ {
+ private readonly bool userHasCustomColours;
+
+ public ExposedPlayer(bool userHasCustomColours)
+ : base(false, false)
+ {
+ this.userHasCustomColours = userHasCustomColours;
+ }
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+ dependencies.CacheAs(new TestSkin(userHasCustomColours));
+ return dependencies;
+ }
+
+ public IReadOnlyList UsableComboColours =>
+ GameplayClockContainer.ChildrenOfType()
+ .First()
+ .GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value;
+ }
+
+ private class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
+ {
+ private readonly bool hasColours;
+
+ public CustomSkinWorkingBeatmap(AudioManager audio, bool hasColours)
+ : base(new Beatmap
+ {
+ BeatmapInfo =
+ {
+ BeatmapSet = new BeatmapSetInfo(),
+ Ruleset = new OsuRuleset().RulesetInfo,
+ },
+ HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
+ }, null, null, audio)
+ {
+ this.hasColours = hasColours;
+ }
+
+ protected override ISkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours);
+ }
+
+ private class TestBeatmapSkin : LegacyBeatmapSkin
+ {
+ public static Color4[] Colours { get; } =
+ {
+ new Color4(50, 100, 150, 255),
+ new Color4(40, 80, 120, 255),
+ };
+
+ public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours)
+ : base(beatmap, new ResourceStore(), null)
+ {
+ if (hasColours)
+ Configuration.AddComboColours(Colours);
+ }
+ }
+
+ private class TestSkin : LegacySkin, ISkinSource
+ {
+ public static Color4[] Colours { get; } =
+ {
+ new Color4(150, 100, 50, 255),
+ new Color4(20, 20, 20, 255),
+ };
+
+ public TestSkin(bool hasCustomColours)
+ : base(new SkinInfo(), null, null, string.Empty)
+ {
+ if (hasCustomColours)
+ Configuration.AddComboColours(Colours);
+ }
+
+ public event Action SourceChanged
+ {
+ add { }
+ remove { }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
new file mode 100644
index 0000000000..4af4d5f966
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
@@ -0,0 +1,197 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Edit;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneOsuDistanceSnapGrid : ManualInputManagerTestScene
+ {
+ private const double beat_length = 100;
+ private static readonly Vector2 grid_position = new Vector2(512, 384);
+
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(CircularDistanceSnapGrid)
+ };
+
+ [Cached(typeof(EditorBeatmap))]
+ private readonly EditorBeatmap editorBeatmap;
+
+ [Cached]
+ private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
+
+ [Cached(typeof(IDistanceSnapProvider))]
+ private readonly SnapProvider snapProvider = new SnapProvider();
+
+ private TestOsuDistanceSnapGrid grid;
+
+ public TestSceneOsuDistanceSnapGrid()
+ {
+ editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
+ editorBeatmap.ControlPointInfo.Clear();
+ editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray
+ },
+ grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
+ new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
+ };
+ });
+
+ [TestCase(1)]
+ [TestCase(2)]
+ [TestCase(3)]
+ [TestCase(4)]
+ [TestCase(6)]
+ [TestCase(8)]
+ [TestCase(12)]
+ [TestCase(16)]
+ public void TestBeatDivisor(int divisor)
+ {
+ AddStep($"set beat divisor = {divisor}", () => beatDivisor.Value = divisor);
+ }
+
+ [Test]
+ public void TestCursorInCentre()
+ {
+ AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position)));
+ assertSnappedDistance((float)beat_length);
+ }
+
+ [Test]
+ public void TestCursorBeforeMovementPoint()
+ {
+ AddStep("move mouse to just before movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.49f)));
+ assertSnappedDistance((float)beat_length);
+ }
+
+ [Test]
+ public void TestCursorAfterMovementPoint()
+ {
+ AddStep("move mouse to just after movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.51f)));
+ assertSnappedDistance((float)beat_length * 2);
+ }
+
+ [Test]
+ public void TestLimitedDistance()
+ {
+ AddStep("create limited grid", () =>
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray
+ },
+ grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
+ new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
+ };
+ });
+
+ AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 3f)));
+ assertSnappedDistance((float)beat_length * 2);
+ }
+
+ private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
+ {
+ Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position;
+
+ return Precision.AlmostEquals(expectedDistance, Vector2.Distance(snappedPosition, grid_position));
+ });
+
+ private class SnappingCursorContainer : CompositeDrawable
+ {
+ public Func GetSnapPosition;
+
+ private readonly Drawable cursor;
+
+ public SnappingCursorContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = cursor = new Circle
+ {
+ Origin = Anchor.Centre,
+ Size = new Vector2(50),
+ Colour = Color4.Red
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ updatePosition(GetContainingInputManager().CurrentState.Mouse.Position);
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ base.OnMouseMove(e);
+
+ updatePosition(e.ScreenSpaceMousePosition);
+ return true;
+ }
+
+ private void updatePosition(Vector2 screenSpacePosition)
+ {
+ cursor.Position = GetSnapPosition.Invoke(screenSpacePosition);
+ }
+ }
+
+ private class TestOsuDistanceSnapGrid : OsuDistanceSnapGrid
+ {
+ public new float DistanceSpacing => base.DistanceSpacing;
+
+ public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
+ : base(hitObject, nextHitObject)
+ {
+ }
+ }
+
+ private class SnapProvider : IDistanceSnapProvider
+ {
+ public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time);
+
+ public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
+
+ public float DurationToDistance(double referenceTime, double duration) => (float)duration;
+
+ public double DistanceToDuration(double referenceTime, float distance) => distance;
+
+ public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
+
+ public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
index 64e7632b1b..412effe176 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override Player CreatePlayer(Ruleset ruleset)
{
- Mods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), };
+ SelectedMods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), };
return base.CreatePlayer(ruleset);
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
index 3d8afd66f4..d692be89b2 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
@@ -1,23 +1,25 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Graphics;
-using osu.Framework.MathUtils;
+using System.Diagnostics;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneShaking : TestSceneHitCircle
{
- public override void Add(Drawable drawable)
+ protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
{
- base.Add(drawable);
+ var drawableHitObject = base.CreateDrawableHitCircle(circle, auto);
- if (drawable is TestDrawableHitCircle hitObject)
- {
- Scheduler.AddDelayed(() => hitObject.TriggerJudgement(),
- hitObject.HitObject.StartTime - (hitObject.HitObject.HitWindows.HalfWindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current);
- }
+ Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
+
+ double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current;
+ Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay);
+
+ return drawableHitObject;
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
new file mode 100644
index 0000000000..4da1b1dae0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -0,0 +1,161 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Timing;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class TestSceneSkinFallbacks : PlayerTestScene
+ {
+ private readonly TestSource testUserSkin;
+ private readonly TestSource testBeatmapSkin;
+
+ public TestSceneSkinFallbacks()
+ : base(new OsuRuleset())
+ {
+ testUserSkin = new TestSource("user");
+ testBeatmapSkin = new TestSource("beatmap");
+ }
+
+ [Test]
+ public void TestBeatmapSkinDefault()
+ {
+ AddStep("enable user provider", () => testUserSkin.Enabled = true);
+
+ AddStep("enable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, true));
+ checkNextHitObject("beatmap");
+
+ AddStep("disable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, false));
+ checkNextHitObject("user");
+
+ AddStep("disable user provider", () => testUserSkin.Enabled = false);
+ checkNextHitObject(null);
+ }
+
+ private void checkNextHitObject(string skin) =>
+ AddUntilStep($"check skin from {skin}", () =>
+ {
+ var firstObject = ((TestPlayer)Player).DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.OfType().FirstOrDefault();
+
+ if (firstObject == null)
+ return false;
+
+ var skinnable = firstObject.ApproachCircle.Child as SkinnableDrawable;
+
+ if (skin == null && skinnable?.Drawable is Sprite)
+ // check for default skin provider
+ return true;
+
+ var text = skinnable?.Drawable as SpriteText;
+
+ return text?.Text == skin;
+ });
+
+ [Resolved]
+ private AudioManager audio { get; set; }
+
+ protected override Player CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin);
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, audio, testBeatmapSkin);
+
+ public class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
+ {
+ private readonly ISkinSource skin;
+
+ public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin)
+ : base(beatmap, storyboard, frameBasedClock, audio)
+ {
+ this.skin = skin;
+ }
+
+ protected override ISkin GetSkin() => skin;
+ }
+
+ public class SkinProvidingPlayer : TestPlayer
+ {
+ private readonly TestSource userSkin;
+
+ public SkinProvidingPlayer(TestSource userSkin)
+ {
+ this.userSkin = userSkin;
+ }
+
+ private DependencyContainer dependencies;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ dependencies.CacheAs(userSkin);
+
+ return dependencies;
+ }
+ }
+
+ public class TestSource : ISkinSource
+ {
+ private readonly string identifier;
+
+ public TestSource(string identifier)
+ {
+ this.identifier = identifier;
+ }
+
+ public Drawable GetDrawableComponent(ISkinComponent component)
+ {
+ if (!enabled) return null;
+
+ return new OsuSpriteText
+ {
+ Text = identifier,
+ Font = OsuFont.Default.With(size: 30),
+ };
+ }
+
+ public Texture GetTexture(string componentName) => null;
+
+ public SampleChannel GetSample(ISampleInfo sampleInfo) => null;
+
+ public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default;
+ public IBindable GetConfig(TLookup lookup) => null;
+
+ public event Action SourceChanged;
+
+ private bool enabled = true;
+
+ public bool Enabled
+ {
+ get => enabled;
+ set
+ {
+ if (value == enabled)
+ return;
+
+ enabled = value;
+ SourceChanged?.Invoke();
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index c5a27205d6..e8386363be 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -10,7 +10,6 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
using osu.Game.Rulesets.Mods;
@@ -27,83 +26,173 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneSlider : OsuTestScene
+ public class TestSceneSlider : SkinnableTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
+ typeof(Slider),
+ typeof(SliderTick),
+ typeof(SliderTailCircle),
typeof(SliderBall),
typeof(SliderBody),
- typeof(SliderTick),
+ typeof(SnakingSliderBody),
typeof(DrawableSlider),
typeof(DrawableSliderTick),
+ typeof(DrawableSliderTail),
+ typeof(DrawableSliderHead),
typeof(DrawableRepeatPoint),
typeof(DrawableOsuHitObject)
};
- private readonly Container content;
- protected override Container Content => content;
+ private Container content;
+
+ protected override Container Content
+ {
+ get
+ {
+ if (content == null)
+ base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
+
+ return content;
+ }
+ }
private int depthIndex;
public TestSceneSlider()
{
- base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
+ AddStep("Big Single", () => SetContents(() => testSimpleBig()));
+ AddStep("Medium Single", () => SetContents(() => testSimpleMedium()));
+ AddStep("Small Single", () => SetContents(() => testSimpleSmall()));
+ AddStep("Big 1 Repeat", () => SetContents(() => testSimpleBig(1)));
+ AddStep("Medium 1 Repeat", () => SetContents(() => testSimpleMedium(1)));
+ AddStep("Small 1 Repeat", () => SetContents(() => testSimpleSmall(1)));
+ AddStep("Big 2 Repeats", () => SetContents(() => testSimpleBig(2)));
+ AddStep("Medium 2 Repeats", () => SetContents(() => testSimpleMedium(2)));
+ AddStep("Small 2 Repeats", () => SetContents(() => testSimpleSmall(2)));
- AddStep("Big Single", () => testSimpleBig());
- AddStep("Medium Single", () => testSimpleMedium());
- AddStep("Small Single", () => testSimpleSmall());
- AddStep("Big 1 Repeat", () => testSimpleBig(1));
- AddStep("Medium 1 Repeat", () => testSimpleMedium(1));
- AddStep("Small 1 Repeat", () => testSimpleSmall(1));
- AddStep("Big 2 Repeats", () => testSimpleBig(2));
- AddStep("Medium 2 Repeats", () => testSimpleMedium(2));
- AddStep("Small 2 Repeats", () => testSimpleSmall(2));
+ AddStep("Slow Slider", () => SetContents(testSlowSpeed)); // slow long sliders take ages already so no repeat steps
+ AddStep("Slow Short Slider", () => SetContents(() => testShortSlowSpeed()));
+ AddStep("Slow Short Slider 1 Repeats", () => SetContents(() => testShortSlowSpeed(1)));
+ AddStep("Slow Short Slider 2 Repeats", () => SetContents(() => testShortSlowSpeed(2)));
- AddStep("Slow Slider", testSlowSpeed); // slow long sliders take ages already so no repeat steps
- AddStep("Slow Short Slider", () => testShortSlowSpeed());
- AddStep("Slow Short Slider 1 Repeats", () => testShortSlowSpeed(1));
- AddStep("Slow Short Slider 2 Repeats", () => testShortSlowSpeed(2));
+ AddStep("Fast Slider", () => SetContents(() => testHighSpeed()));
+ AddStep("Fast Slider 1 Repeat", () => SetContents(() => testHighSpeed(1)));
+ AddStep("Fast Slider 2 Repeats", () => SetContents(() => testHighSpeed(2)));
+ AddStep("Fast Short Slider", () => SetContents(() => testShortHighSpeed()));
+ AddStep("Fast Short Slider 1 Repeat", () => SetContents(() => testShortHighSpeed(1)));
+ AddStep("Fast Short Slider 2 Repeats", () => SetContents(() => testShortHighSpeed(2)));
+ AddStep("Fast Short Slider 6 Repeats", () => SetContents(() => testShortHighSpeed(6)));
- AddStep("Fast Slider", () => testHighSpeed());
- AddStep("Fast Slider 1 Repeat", () => testHighSpeed(1));
- AddStep("Fast Slider 2 Repeats", () => testHighSpeed(2));
- AddStep("Fast Short Slider", () => testShortHighSpeed());
- AddStep("Fast Short Slider 1 Repeat", () => testShortHighSpeed(1));
- AddStep("Fast Short Slider 2 Repeats", () => testShortHighSpeed(2));
- AddStep("Fast Short Slider 6 Repeats", () => testShortHighSpeed(6));
+ AddStep("Perfect Curve", () => SetContents(() => testPerfect()));
+ AddStep("Perfect Curve 1 Repeat", () => SetContents(() => testPerfect(1)));
+ AddStep("Perfect Curve 2 Repeats", () => SetContents(() => testPerfect(2)));
- AddStep("Perfect Curve", () => testPerfect());
- AddStep("Perfect Curve 1 Repeat", () => testPerfect(1));
- AddStep("Perfect Curve 2 Repeats", () => testPerfect(2));
+ AddStep("Linear Slider", () => SetContents(() => testLinear()));
+ AddStep("Linear Slider 1 Repeat", () => SetContents(() => testLinear(1)));
+ AddStep("Linear Slider 2 Repeats", () => SetContents(() => testLinear(2)));
- AddStep("Linear Slider", () => testLinear());
- AddStep("Linear Slider 1 Repeat", () => testLinear(1));
- AddStep("Linear Slider 2 Repeats", () => testLinear(2));
+ AddStep("Bezier Slider", () => SetContents(() => testBezier()));
+ AddStep("Bezier Slider 1 Repeat", () => SetContents(() => testBezier(1)));
+ AddStep("Bezier Slider 2 Repeats", () => SetContents(() => testBezier(2)));
- AddStep("Bezier Slider", () => testBezier());
- AddStep("Bezier Slider 1 Repeat", () => testBezier(1));
- AddStep("Bezier Slider 2 Repeats", () => testBezier(2));
+ AddStep("Linear Overlapping", () => SetContents(() => testLinearOverlapping()));
+ AddStep("Linear Overlapping 1 Repeat", () => SetContents(() => testLinearOverlapping(1)));
+ AddStep("Linear Overlapping 2 Repeats", () => SetContents(() => testLinearOverlapping(2)));
- AddStep("Linear Overlapping", () => testLinearOverlapping());
- AddStep("Linear Overlapping 1 Repeat", () => testLinearOverlapping(1));
- AddStep("Linear Overlapping 2 Repeats", () => testLinearOverlapping(2));
+ AddStep("Catmull Slider", () => SetContents(() => testCatmull()));
+ AddStep("Catmull Slider 1 Repeat", () => SetContents(() => testCatmull(1)));
+ AddStep("Catmull Slider 2 Repeats", () => SetContents(() => testCatmull(2)));
- AddStep("Catmull Slider", () => testCatmull());
- AddStep("Catmull Slider 1 Repeat", () => testCatmull(1));
- AddStep("Catmull Slider 2 Repeats", () => testCatmull(2));
+ AddStep("Big Single, Large StackOffset", () => SetContents(() => testSimpleBigLargeStackOffset()));
+ AddStep("Big 1 Repeat, Large StackOffset", () => SetContents(() => testSimpleBigLargeStackOffset(1)));
- AddStep("Big Single, Large StackOffset", () => testSimpleBigLargeStackOffset());
- AddStep("Big 1 Repeat, Large StackOffset", () => testSimpleBigLargeStackOffset(1));
-
- AddStep("Distance Overflow", () => testDistanceOverflow());
- AddStep("Distance Overflow 1 Repeat", () => testDistanceOverflow(1));
+ AddStep("Distance Overflow", () => SetContents(() => testDistanceOverflow()));
+ AddStep("Distance Overflow 1 Repeat", () => SetContents(() => testDistanceOverflow(1)));
}
- private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
+ [Test]
+ public void TestChangeStackHeight()
+ {
+ DrawableSlider slider = null;
- private void testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
+ AddStep("create slider", () =>
+ {
+ slider = (DrawableSlider)createSlider(repeats: 1);
+ Add(slider);
+ });
- private void testDistanceOverflow(int repeats = 0)
+ AddStep("change stack height", () => slider.HitObject.StackHeight = 10);
+ AddAssert("body positioned correctly", () => slider.Position == slider.HitObject.StackedPosition);
+ }
+
+ [Test]
+ public void TestChangeSamplesWithNoNodeSamples()
+ {
+ DrawableSlider slider = null;
+
+ AddStep("create slider", () =>
+ {
+ slider = (DrawableSlider)createSlider(repeats: 1);
+ Add(slider);
+ });
+
+ AddStep("change samples", () => slider.HitObject.Samples = new[]
+ {
+ new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP },
+ new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE },
+ });
+
+ AddAssert("head samples updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle));
+ AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples));
+ AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples));
+ AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0);
+
+ static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
+
+ static bool assertSamples(HitObject hitObject)
+ {
+ return hitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)
+ && hitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE);
+ }
+ }
+
+ [Test]
+ public void TestChangeSamplesWithNodeSamples()
+ {
+ DrawableSlider slider = null;
+
+ AddStep("create slider", () =>
+ {
+ slider = (DrawableSlider)createSlider(repeats: 1);
+
+ for (int i = 0; i < 2; i++)
+ ((Slider)slider.HitObject).NodeSamples.Add(new List { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } });
+
+ Add(slider);
+ });
+
+ AddStep("change samples", () => slider.HitObject.Samples = new[]
+ {
+ new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP },
+ new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE },
+ });
+
+ AddAssert("head samples not updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle));
+ AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples));
+ AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples));
+ AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0);
+
+ static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
+
+ static bool assertSamples(HitObject hitObject) => hitObject.Samples.All(s => s.Name != HitSampleInfo.HIT_CLAP && s.Name != HitSampleInfo.HIT_WHISTLE);
+ }
+
+ private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
+
+ private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
+
+ private Drawable testDistanceOverflow(int repeats = 0)
{
var slider = new Slider
{
@@ -116,26 +205,25 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(52, -34)
}, 700),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats),
StackHeight = 10
};
- addSlider(slider, 2, 2);
+ return createDrawable(slider, 2, 2);
}
- private void testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
+ private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
- private void testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats);
+ private Drawable testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats);
- private void testSlowSpeed() => createSlider(speedMultiplier: 0.5);
+ private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5);
- private void testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5);
+ private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5);
- private void testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15);
+ private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15);
- private void testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15);
+ private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15);
- private void createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
+ private Drawable createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
{
var slider = new Slider
{
@@ -147,14 +235,13 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(distance, 0),
}, distance),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats),
StackHeight = stackHeight
};
- addSlider(slider, circleSize, speedMultiplier);
+ return createDrawable(slider, circleSize, speedMultiplier);
}
- private void testPerfect(int repeats = 0)
+ private Drawable testPerfect(int repeats = 0)
{
var slider = new Slider
{
@@ -167,15 +254,14 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(400, 0)
}, 600),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats)
};
- addSlider(slider, 2, 3);
+ return createDrawable(slider, 2, 3);
}
- private void testLinear(int repeats = 0) => createLinear(repeats);
+ private Drawable testLinear(int repeats = 0) => createLinear(repeats);
- private void createLinear(int repeats)
+ private Drawable createLinear(int repeats)
{
var slider = new Slider
{
@@ -191,15 +277,14 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(430, 0)
}),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats)
};
- addSlider(slider, 2, 3);
+ return createDrawable(slider, 2, 3);
}
- private void testBezier(int repeats = 0) => createBezier(repeats);
+ private Drawable testBezier(int repeats = 0) => createBezier(repeats);
- private void createBezier(int repeats)
+ private Drawable createBezier(int repeats)
{
var slider = new Slider
{
@@ -214,15 +299,14 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(430, 0)
}),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats)
};
- addSlider(slider, 2, 3);
+ return createDrawable(slider, 2, 3);
}
- private void testLinearOverlapping(int repeats = 0) => createOverlapping(repeats);
+ private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats);
- private void createOverlapping(int repeats)
+ private Drawable createOverlapping(int repeats)
{
var slider = new Slider
{
@@ -238,17 +322,16 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(0, -200)
}),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats)
};
- addSlider(slider, 2, 3);
+ return createDrawable(slider, 2, 3);
}
- private void testCatmull(int repeats = 0) => createCatmull(repeats);
+ private Drawable testCatmull(int repeats = 0) => createCatmull(repeats);
- private void createCatmull(int repeats = 0)
+ private Drawable createCatmull(int repeats = 0)
{
- var repeatSamples = new List>();
+ var repeatSamples = new List>();
for (int i = 0; i < repeats; i++)
repeatSamples.Add(new List());
@@ -267,44 +350,37 @@ namespace osu.Game.Rulesets.Osu.Tests
NodeSamples = repeatSamples
};
- addSlider(slider, 3, 1);
+ return createDrawable(slider, 3, 1);
}
- private List> createEmptySamples(int repeats)
- {
- var repeatSamples = new List>();
- for (int i = 0; i < repeats; i++)
- repeatSamples.Add(new List());
- return repeatSamples;
- }
-
- private void addSlider(Slider slider, float circleSize, double speedMultiplier)
+ private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
{
var cpi = new ControlPointInfo();
- cpi.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
+ cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
- var drawable = new DrawableSlider(slider)
- {
- Anchor = Anchor.Centre,
- Depth = depthIndex++
- };
+ var drawable = CreateDrawableSlider(slider);
- foreach (var mod in Mods.Value.OfType())
+ foreach (var mod in SelectedMods.Value.OfType())
mod.ApplyToDrawableHitObjects(new[] { drawable });
drawable.OnNewResult += onNewResult;
- Add(drawable);
+ return drawable;
}
+ protected virtual DrawableSlider CreateDrawableSlider(Slider slider) => new DrawableSlider(slider)
+ {
+ Anchor = Anchor.Centre,
+ Depth = depthIndex++
+ };
+
private float judgementOffsetDirection = 1;
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
- var osuObject = judgedObject as DrawableOsuHitObject;
- if (osuObject == null)
+ if (!(judgedObject is DrawableOsuHitObject osuObject))
return;
OsuSpriteText text;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs
new file mode 100644
index 0000000000..13ced3019e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneSliderComboChange : TestSceneSlider
+ {
+ private readonly Bindable comboIndex = new Bindable();
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Scheduler.AddDelayed(() => comboIndex.Value++, 250, true);
+ }
+
+ protected override DrawableSlider CreateDrawableSlider(Slider slider)
+ {
+ slider.ComboIndexBindable.BindTo(comboIndex);
+ slider.IndexInCurrentComboBindable.BindTo(comboIndex);
+
+ return base.CreateDrawableSlider(slider);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs
index 2a9c1d167b..d0ee1bddb5 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs
@@ -14,9 +14,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList();
- public TestSceneSliderHidden()
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
- Mods.Value = new[] { new OsuModHidden() };
- }
+ SelectedMods.Value = new[] { new OsuModHidden() };
+ });
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 2eb783233a..b6fc9821a4 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -286,11 +286,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private bool assertGreatJudge() => judgementResults.Last().Type == HitResult.Great;
- private bool assertHeadMissTailTracked() => judgementResults[judgementResults.Count - 2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss;
+ private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss;
- private bool assertMidSliderJudgements() => judgementResults[judgementResults.Count - 2].Type == HitResult.Great;
+ private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.Great;
- private bool assertMidSliderJudgementFail() => judgementResults[judgementResults.Count - 2].Type == HitResult.Miss;
+ private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.Miss;
private ScoreAccessibleReplayPlayer currentPlayer;
@@ -313,10 +313,6 @@ namespace osu.Game.Rulesets.Osu.Tests
}, 25),
}
},
- ControlPointInfo =
- {
- DifficultyPoints = { new DifficultyControlPoint { SpeedMultiplier = 0.1f } }
- },
BeatmapInfo =
{
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
@@ -324,6 +320,8 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
index 8cf5a2f33e..5dd2bd18a8 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
@@ -3,17 +3,20 @@
using System;
using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -29,11 +32,16 @@ namespace osu.Game.Rulesets.Osu.Tests
typeof(PathControlPointPiece)
};
- private readonly DrawableSlider drawableObject;
+ private Slider slider;
+ private DrawableSlider drawableObject;
+ private TestSliderBlueprint blueprint;
- public TestSceneSliderSelectionBlueprint()
+ [SetUp]
+ public void Setup() => Schedule(() =>
{
- var slider = new Slider
+ Clear();
+
+ slider = new Slider
{
Position = new Vector2(256, 192),
Path = new SliderPath(PathType.Bezier, new[]
@@ -47,8 +55,178 @@ namespace osu.Game.Rulesets.Osu.Tests
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
Add(drawableObject = new DrawableSlider(slider));
+ AddBlueprint(blueprint = new TestSliderBlueprint(drawableObject));
+ });
+
+ [Test]
+ public void TestInitialState()
+ {
+ checkPositions();
}
- protected override SelectionBlueprint CreateBlueprint() => new SliderSelectionBlueprint(drawableObject);
+ [Test]
+ public void TestMoveHitObject()
+ {
+ moveHitObject();
+ checkPositions();
+ }
+
+ [Test]
+ public void TestMoveAfterApplyingDefaults()
+ {
+ AddStep("apply defaults", () => slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 }));
+ moveHitObject();
+ checkPositions();
+ }
+
+ [Test]
+ public void TestStackedHitObject()
+ {
+ AddStep("set stacking", () => slider.StackHeight = 5);
+ checkPositions();
+ }
+
+ [Test]
+ public void TestSingleControlPointSelection()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, true);
+ checkControlPointSelected(1, false);
+ }
+
+ [Test]
+ public void TestSingleControlPointDeselectionViaOtherControlPoint()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ moveMouseToControlPoint(1);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, false);
+ checkControlPointSelected(1, true);
+ }
+
+ [Test]
+ public void TestSingleControlPointDeselectionViaClickOutside()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, false);
+ checkControlPointSelected(1, false);
+ }
+
+ [Test]
+ public void TestMultipleControlPointSelection()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ moveMouseToControlPoint(1);
+ AddStep("ctrl + click", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ checkControlPointSelected(0, true);
+ checkControlPointSelected(1, true);
+ }
+
+ [Test]
+ public void TestMultipleControlPointDeselectionViaOtherControlPoint()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ moveMouseToControlPoint(1);
+ AddStep("ctrl + click", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ moveMouseToControlPoint(2);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, false);
+ checkControlPointSelected(1, false);
+ }
+
+ [Test]
+ public void TestMultipleControlPointDeselectionViaClickOutside()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ moveMouseToControlPoint(1);
+ AddStep("ctrl + click", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, false);
+ checkControlPointSelected(1, false);
+ }
+
+ private void moveHitObject()
+ {
+ AddStep("move hitobject", () =>
+ {
+ slider.Position = new Vector2(300, 225);
+ });
+ }
+
+ private void checkPositions()
+ {
+ AddAssert("body positioned correctly", () => blueprint.BodyPiece.Position == slider.StackedPosition);
+
+ AddAssert("head positioned correctly",
+ () => Precision.AlmostEquals(blueprint.HeadBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.HeadCircle.ScreenSpaceDrawQuad.Centre));
+
+ AddAssert("tail positioned correctly",
+ () => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
+ }
+
+ private void moveMouseToControlPoint(int index)
+ {
+ AddStep($"move mouse to control point {index}", () =>
+ {
+ Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
+ InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
+ });
+ }
+
+ private void checkControlPointSelected(int index, bool selected)
+ => AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
+
+ private class TestSliderBlueprint : SliderSelectionBlueprint
+ {
+ public new SliderBodyPiece BodyPiece => base.BodyPiece;
+ public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
+ public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
+ public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
+
+ public TestSliderBlueprint(DrawableSlider slider)
+ : base(slider)
+ {
+ }
+
+ protected override SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new TestSliderCircleBlueprint(slider, position);
+ }
+
+ private class TestSliderCircleBlueprint : SliderCircleSelectionBlueprint
+ {
+ public new HitCirclePiece CirclePiece => base.CirclePiece;
+
+ public TestSliderCircleBlueprint(DrawableSlider slider, SliderPosition position)
+ : base(slider, position)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index 3ed3f3e981..f53b64c729 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Depth = depthIndex++
};
- foreach (var mod in Mods.Value.OfType())
+ foreach (var mod in SelectedMods.Value.OfType())
mod.ApplyToDrawableHitObjects(new[] { drawable });
Add(drawable);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs
index a0ab1908d6..dd863deed2 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs
@@ -14,9 +14,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList();
- public TestSceneSpinnerHidden()
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
- Mods.Value = new[] { new OsuModHidden() };
- }
+ SelectedMods.Value = new[] { new OsuModHidden() };
+ });
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
new file mode 100644
index 0000000000..5cf571d961
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -0,0 +1,112 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Utils;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Tests.Visual;
+using osuTK;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Storyboards;
+using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneSpinnerRotation : TestSceneOsuPlayer
+ {
+ [Resolved]
+ private AudioManager audioManager { get; set; }
+
+ private TrackVirtualManual track;
+
+ protected override bool Autoplay => true;
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
+ {
+ var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
+ track = (TrackVirtualManual)working.Track;
+ return working;
+ }
+
+ private DrawableSpinner drawableSpinner;
+
+ [SetUpSteps]
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddUntilStep("wait for track to start running", () => track.IsRunning);
+ AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)((TestPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.First());
+ }
+
+ [Test]
+ public void TestSpinnerRewindingRotation()
+ {
+ addSeekStep(5000);
+ AddAssert("is rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100));
+
+ addSeekStep(0);
+ AddAssert("is rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100));
+ }
+
+ [Test]
+ public void TestSpinnerMiddleRewindingRotation()
+ {
+ double estimatedRotation = 0;
+
+ addSeekStep(5000);
+ AddStep("retrieve rotation", () => estimatedRotation = drawableSpinner.Disc.RotationAbsolute);
+
+ addSeekStep(2500);
+ addSeekStep(5000);
+ AddAssert("is rotation absolute almost same", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, estimatedRotation, 100));
+ }
+
+ [Test]
+ public void TestSpinPerMinuteOnRewind()
+ {
+ double estimatedSpm = 0;
+
+ addSeekStep(2500);
+ AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
+
+ addSeekStep(5000);
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+
+ addSeekStep(2500);
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+ }
+
+ private void addSeekStep(double time)
+ {
+ AddStep($"seek to {time}", () => track.Seek(time));
+
+ AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, ((TestPlayer)Player).DrawableRuleset.FrameStableClock.CurrentTime, 100));
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ EndTime = 6000,
+ },
+ // placeholder object to avoid hitting the results screen
+ new HitCircle
+ {
+ StartTime = 99999,
+ }
+ }
+ };
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs
index c5cea76b14..d777ca3610 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs
@@ -7,7 +7,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
using osu.Game.Rulesets.Osu.Objects;
@@ -25,8 +24,6 @@ namespace osu.Game.Rulesets.Osu.Tests
typeof(SpinnerPiece)
};
- private readonly DrawableSpinner drawableSpinner;
-
public TestSceneSpinnerSelectionBlueprint()
{
var spinner = new Spinner
@@ -35,16 +32,19 @@ namespace osu.Game.Rulesets.Osu.Tests
StartTime = -1000,
EndTime = 2000
};
+
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
+ DrawableSpinner drawableSpinner;
+
Add(new Container
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
Child = drawableSpinner = new DrawableSpinner(spinner)
});
- }
- protected override SelectionBlueprint CreateBlueprint() => new SpinnerSelectionBlueprint(drawableSpinner) { Size = new Vector2(0.5f) };
+ AddBlueprint(new SpinnerSelectionBlueprint(drawableSpinner) { Size = new Vector2(0.5f) });
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index bb3e5a66f3..1d8c4708c1 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,14 +2,14 @@
-
+
-
+
WinExe
- netcoreapp2.2
+ netcoreapp3.1
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index 6a41e93c35..147d74c929 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -7,68 +7,65 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects.Types;
-using System;
+using System.Linq;
using osu.Game.Rulesets.Osu.UI;
+using osu.Framework.Extensions.IEnumerableExtensions;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
public class OsuBeatmapConverter : BeatmapConverter
{
- public OsuBeatmapConverter(IBeatmap beatmap)
- : base(beatmap)
+ public OsuBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
{
}
- protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasPosition) };
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition);
protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap)
{
- var curveData = original as IHasCurve;
- var endTimeData = original as IHasEndTime;
var positionData = original as IHasPosition;
var comboData = original as IHasCombo;
- var legacyOffset = original as IHasLegacyLastTickOffset;
- if (curveData != null)
+ switch (original)
{
- yield return new Slider
- {
- StartTime = original.StartTime,
- Samples = original.Samples,
- Path = curveData.Path,
- NodeSamples = curveData.NodeSamples,
- RepeatCount = curveData.RepeatCount,
- Position = positionData?.Position ?? Vector2.Zero,
- NewCombo = comboData?.NewCombo ?? false,
- ComboOffset = comboData?.ComboOffset ?? 0,
- LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset,
- // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
- // this results in more (or less) ticks being generated in stackThreshold)
@@ -120,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
OsuHitObject objectN = beatmap.HitObjects[n];
if (objectN is Spinner) continue;
- double endTime = (objectN as IHasEndTime)?.EndTime ?? objectN.StartTime;
+ double endTime = objectN.GetEndTime();
if (objectI.StartTime - endTime > stackThreshold)
//We are no longer within stacking range of the previous object.
@@ -198,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
if (currHitObject.StackHeight != 0 && !(currHitObject is Slider))
continue;
- double startTime = (currHitObject as IHasEndTime)?.EndTime ?? currHitObject.StartTime;
+ double startTime = currHitObject.GetEndTime();
int sliderStack = 0;
for (int j = i + 1; j < beatmap.HitObjects.Count; j++)
@@ -208,17 +209,22 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime)
break;
+ // The start position of the hitobject, or the position at the end of the path if the hitobject is a slider
+ Vector2 position2 = currHitObject is Slider currSlider
+ ? currSlider.Position + currSlider.Path.PositionAt(1)
+ : currHitObject.Position;
+
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
{
currHitObject.StackHeight++;
- startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
+ startTime = beatmap.HitObjects[j].GetEndTime();
}
- else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.EndPosition) < stack_distance)
+ else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
{
//Case for sliders - bump notes down and right, rather than up and left.
sliderStack++;
beatmap.HitObjects[j].StackHeight -= sliderStack;
- startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
+ startTime = beatmap.HitObjects[j].GetEndTime();
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index c197933233..b0d261a1cc 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -13,6 +13,8 @@ using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Difficulty
{
@@ -34,8 +36,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2;
+ HitWindows hitWindows = new OsuHitWindows();
+ hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate;
+ double hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
int maxCombo = beatmap.HitObjects.Count;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 093081b6a1..ce8ecf02ac 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -45,32 +45,32 @@ namespace osu.Game.Rulesets.Osu.Difficulty
mods = Score.Mods;
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo;
- countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
- countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
- countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
- countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
+ countGreat = Score.Statistics[HitResult.Great];
+ countGood = Score.Statistics[HitResult.Good];
+ countMeh = Score.Statistics[HitResult.Meh];
+ countMiss = Score.Statistics[HitResult.Miss];
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
return 0;
// Custom multipliers for NoFail and SpunOut.
- double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
+ double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is OsuModNoFail))
- multiplier *= 0.90f;
+ multiplier *= 0.90;
if (mods.Any(m => m is OsuModSpunOut))
- multiplier *= 0.95f;
+ multiplier *= 0.95;
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue();
double totalValue =
Math.Pow(
- Math.Pow(aimValue, 1.1f) +
- Math.Pow(speedValue, 1.1f) +
- Math.Pow(accuracyValue, 1.1f), 1.0f / 1.1f
+ Math.Pow(aimValue, 1.1) +
+ Math.Pow(speedValue, 1.1) +
+ Math.Pow(accuracyValue, 1.1), 1.0 / 1.1
) * multiplier;
if (categoryRatings != null)
@@ -93,82 +93,82 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8);
- double aimValue = Math.Pow(5.0f * Math.Max(1.0f, rawAim / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
+ double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more
- double lengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +
- (totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
+ double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
+ (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
aimValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
- aimValue *= Math.Pow(0.97f, countMiss);
+ aimValue *= Math.Pow(0.97, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
- aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
+ aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
- double approachRateFactor = 1.0f;
+ double approachRateFactor = 1.0;
- if (Attributes.ApproachRate > 10.33f)
- approachRateFactor += 0.3f * (Attributes.ApproachRate - 10.33f);
- else if (Attributes.ApproachRate < 8.0f)
+ if (Attributes.ApproachRate > 10.33)
+ approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
+ else if (Attributes.ApproachRate < 8.0)
{
- approachRateFactor += 0.01f * (8.0f - Attributes.ApproachRate);
+ approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate);
}
aimValue *= approachRateFactor;
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden))
- aimValue *= 1.0f + 0.04f * (12.0f - Attributes.ApproachRate);
+ aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
if (mods.Any(h => h is OsuModFlashlight))
{
// Apply object-based bonus for flashlight.
- aimValue *= 1.0f + 0.35f * Math.Min(1.0f, totalHits / 200.0f) +
+ aimValue *= 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
(totalHits > 200
- ? 0.3f * Math.Min(1.0f, (totalHits - 200) / 300.0f) +
- (totalHits > 500 ? (totalHits - 500) / 1200.0f : 0.0f)
- : 0.0f);
+ ? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
+ (totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
+ : 0.0);
}
// Scale the aim value with accuracy _slightly_
- aimValue *= 0.5f + accuracy / 2.0f;
+ aimValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that
- aimValue *= 0.98f + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
+ aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
return aimValue;
}
private double computeSpeedValue()
{
- double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes.SpeedStrain / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
+ double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more
- speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +
- (totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
+ speedValue *= 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
+ (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
- speedValue *= Math.Pow(0.97f, countMiss);
+ speedValue *= Math.Pow(0.97, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
- speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
+ speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
- double approachRateFactor = 1.0f;
- if (Attributes.ApproachRate > 10.33f)
- approachRateFactor += 0.3f * (Attributes.ApproachRate - 10.33f);
+ double approachRateFactor = 1.0;
+ if (Attributes.ApproachRate > 10.33)
+ approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
speedValue *= approachRateFactor;
if (mods.Any(m => m is OsuModHidden))
- speedValue *= 1.0f + 0.04f * (12.0f - Attributes.ApproachRate);
+ speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
// Scale the speed value with accuracy _slightly_
- speedValue *= 0.02f + accuracy;
+ speedValue *= 0.02 + accuracy;
// It is important to also consider accuracy difficulty when doing that
- speedValue *= 0.96f + Math.Pow(Attributes.OverallDifficulty, 2) / 1600;
+ speedValue *= 0.96 + Math.Pow(Attributes.OverallDifficulty, 2) / 1600;
return speedValue;
}
@@ -190,15 +190,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
- double accuracyValue = Math.Pow(1.52163f, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f;
+ double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
- accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f));
+ accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
if (mods.Any(m => m is OsuModHidden))
- accuracyValue *= 1.08f;
+ accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight))
- accuracyValue *= 1.02f;
+ accuracyValue *= 1.02;
return accuracyValue;
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index eacac7ae6a..fa6c5c4d9c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (progress % 2 >= 1)
progress = 1 - progress % 1;
else
- progress = progress % 1;
+ progress %= 1;
// ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs
new file mode 100644
index 0000000000..b9c77d3f56
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints
+{
+ ///
+ /// A piece of a selection or placement blueprint which visualises an .
+ ///
+ /// The type of which this visualises.
+ public abstract class BlueprintPiece : CompositeDrawable
+ where T : OsuHitObject
+ {
+ ///
+ /// Updates this using the properties of a .
+ ///
+ /// The to reference properties from.
+ public virtual void UpdateFrom(T hitObject)
+ {
+ Position = hitObject.StackedPosition;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs
index 7f6a60c400..2868ddeaa4 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs
@@ -10,19 +10,16 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
{
- public class HitCirclePiece : HitObjectPiece
+ public class HitCirclePiece : BlueprintPiece
{
- private readonly HitCircle hitCircle;
-
- public HitCirclePiece(HitCircle hitCircle)
- : base(hitCircle)
+ public HitCirclePiece()
{
- this.hitCircle = hitCircle;
Origin = Anchor.Centre;
- Size = new Vector2((float)OsuHitObject.OBJECT_RADIUS * 2);
- Scale = new Vector2(hitCircle.Scale);
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+
CornerRadius = Size.X / 2;
+ CornerExponent = 2;
InternalChild = new RingPiece();
}
@@ -31,12 +28,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
private void load(OsuColour colours)
{
Colour = colours.Yellow;
-
- PositionBindable.BindValueChanged(_ => UpdatePosition(), true);
- StackHeightBindable.BindValueChanged(_ => UpdatePosition());
- ScaleBindable.BindValueChanged(scale => Scale = new Vector2(scale.NewValue), true);
}
- protected virtual void UpdatePosition() => Position = hitCircle.StackedPosition;
+ public override void UpdateFrom(HitCircle hitObject)
+ {
+ base.UpdateFrom(hitObject);
+
+ Scale = new Vector2(hitObject.Scale);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index a4050f0c31..bb47c7e464 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -13,31 +13,30 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
{
public new HitCircle HitObject => (HitCircle)base.HitObject;
+ private readonly HitCirclePiece circlePiece;
+
public HitCirclePlacementBlueprint()
: base(new HitCircle())
{
- InternalChild = new HitCirclePiece(HitObject);
+ InternalChild = circlePiece = new HitCirclePiece();
}
- protected override void LoadComplete()
+ protected override void Update()
{
- base.LoadComplete();
+ base.Update();
- // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
- HitObject.Position = Parent?.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position) ?? Vector2.Zero;
+ circlePiece.UpdateFrom(HitObject);
}
protected override bool OnClick(ClickEvent e)
{
- HitObject.StartTime = EditorClock.CurrentTime;
EndPlacement();
return true;
}
- protected override bool OnMouseMove(MouseMoveEvent e)
+ public override void UpdatePosition(Vector2 screenSpacePosition)
{
- HitObject.Position = e.MousePosition;
- return true;
+ HitObject.Position = ToLocalSpace(screenSpacePosition);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
index 83787e2219..093bae854e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
@@ -1,18 +1,35 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
{
- public class HitCircleSelectionBlueprint : OsuSelectionBlueprint
+ public class HitCircleSelectionBlueprint : OsuSelectionBlueprint
{
- public HitCircleSelectionBlueprint(DrawableHitCircle hitCircle)
- : base(hitCircle)
+ protected new DrawableHitCircle DrawableObject => (DrawableHitCircle)base.DrawableObject;
+
+ protected readonly HitCirclePiece CirclePiece;
+
+ public HitCircleSelectionBlueprint(DrawableHitCircle drawableCircle)
+ : base(drawableCircle)
{
- InternalChild = new HitCirclePiece((HitCircle)hitCircle.HitObject);
+ InternalChild = CirclePiece = new HitCirclePiece();
}
+
+ protected override void Update()
+ {
+ base.Update();
+
+ CirclePiece.UpdateFrom(HitObject);
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos);
+
+ public override Quad SelectionQuad => DrawableObject.HitArea.ScreenSpaceDrawQuad;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitObjectPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitObjectPiece.cs
deleted file mode 100644
index 315a5a2b9d..0000000000
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitObjectPiece.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Osu.Objects;
-using osuTK;
-
-namespace osu.Game.Rulesets.Osu.Edit.Blueprints
-{
- ///
- /// A piece of a blueprint which responds to changes in the state of a .
- ///
- public abstract class HitObjectPiece : CompositeDrawable
- {
- protected readonly IBindable PositionBindable = new Bindable();
- protected readonly IBindable StackHeightBindable = new Bindable();
- protected readonly IBindable ScaleBindable = new Bindable();
-
- private readonly OsuHitObject hitObject;
-
- protected HitObjectPiece(OsuHitObject hitObject)
- {
- this.hitObject = hitObject;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- PositionBindable.BindTo(hitObject.PositionBindable);
- StackHeightBindable.BindTo(hitObject.StackHeightBindable);
- ScaleBindable.BindTo(hitObject.ScaleBindable);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
index dd524252f3..a864257274 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
@@ -7,12 +7,13 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints
{
- public class OsuSelectionBlueprint : SelectionBlueprint
+ public abstract class OsuSelectionBlueprint : SelectionBlueprint
+ where T : OsuHitObject
{
- protected OsuHitObject OsuObject => (OsuHitObject)HitObject.HitObject;
+ protected T HitObject => (T)DrawableObject.HitObject;
- public OsuSelectionBlueprint(DrawableHitObject hitObject)
- : base(hitObject)
+ protected OsuSelectionBlueprint(DrawableHitObject drawableObject)
+ : base(drawableObject)
{
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/SliderPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/SliderPiece.cs
deleted file mode 100644
index 8fd1d6d6f9..0000000000
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/SliderPiece.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Osu.Objects;
-
-namespace osu.Game.Rulesets.Osu.Edit.Blueprints
-{
- ///
- /// A piece of a blueprint which responds to changes in the state of a .
- ///
- public abstract class SliderPiece : HitObjectPiece
- {
- protected readonly IBindable PathBindable = new Bindable();
-
- private readonly Slider slider;
-
- protected SliderPiece(Slider slider)
- : base(slider)
- {
- this.slider = slider;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- PathBindable.BindTo(slider.PathBindable);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
new file mode 100644
index 0000000000..0fc441fec6
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
@@ -0,0 +1,75 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Lines;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
+{
+ ///
+ /// A visualisation of the line between two s.
+ ///
+ public class PathControlPointConnectionPiece : CompositeDrawable
+ {
+ public PathControlPoint ControlPoint;
+
+ private readonly Path path;
+ private readonly Slider slider;
+
+ private IBindable sliderPosition;
+ private IBindable pathVersion;
+
+ public PathControlPointConnectionPiece(Slider slider, PathControlPoint controlPoint)
+ {
+ this.slider = slider;
+ ControlPoint = controlPoint;
+
+ Origin = Anchor.Centre;
+ AutoSizeAxes = Axes.Both;
+
+ InternalChild = path = new SmoothPath
+ {
+ Anchor = Anchor.Centre,
+ PathRadius = 1
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ sliderPosition = slider.PositionBindable.GetBoundCopy();
+ sliderPosition.BindValueChanged(_ => updateConnectingPath());
+
+ pathVersion = slider.Path.Version.GetBoundCopy();
+ pathVersion.BindValueChanged(_ => updateConnectingPath());
+
+ updateConnectingPath();
+ }
+
+ ///
+ /// Updates the path connecting this control point to the next one.
+ ///
+ private void updateConnectingPath()
+ {
+ Position = slider.StackedPosition + ControlPoint.Position.Value;
+
+ path.ClearVertices();
+
+ int index = slider.Path.ControlPoints.IndexOf(ControlPoint) + 1;
+
+ if (index == 0 || index == slider.Path.ControlPoints.Count)
+ return;
+
+ path.AddVertex(Vector2.Zero);
+ path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value);
+
+ path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index e257369ad9..af4da5e853 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -1,112 +1,176 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
- public class PathControlPointPiece : CompositeDrawable
+ ///
+ /// A visualisation of a single in a .
+ ///
+ public class PathControlPointPiece : BlueprintPiece
{
- private readonly Slider slider;
- private readonly int index;
+ public Action RequestSelection;
- private readonly Path path;
- private readonly CircularContainer marker;
+ public readonly BindableBool IsSelected = new BindableBool();
+
+ public readonly PathControlPoint ControlPoint;
+
+ private readonly Slider slider;
+ private readonly Container marker;
+ private readonly Drawable markerRing;
+
+ [Resolved(CanBeNull = true)]
+ private IDistanceSnapProvider snapProvider { get; set; }
[Resolved]
private OsuColour colours { get; set; }
- public PathControlPointPiece(Slider slider, int index)
+ private IBindable sliderPosition;
+ private IBindable controlPointPosition;
+
+ public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
{
this.slider = slider;
- this.index = index;
+ ControlPoint = controlPoint;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
- path = new SmoothPath
- {
- Anchor = Anchor.Centre,
- PathRadius = 1
- },
- marker = new CircularContainer
+ marker = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = new Vector2(10),
- Masking = true,
- Child = new Box { RelativeSizeAxes = Axes.Both }
+ AutoSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(10),
+ },
+ markerRing = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(14),
+ Masking = true,
+ BorderThickness = 2,
+ BorderColour = Color4.White,
+ Alpha = 0,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ }
+ }
+ }
}
};
}
- protected override void Update()
+ protected override void LoadComplete()
{
- base.Update();
+ base.LoadComplete();
- Position = slider.StackedPosition + slider.Path.ControlPoints[index];
+ sliderPosition = slider.PositionBindable.GetBoundCopy();
+ sliderPosition.BindValueChanged(_ => updateMarkerDisplay());
- marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow;
+ controlPointPosition = ControlPoint.Position.GetBoundCopy();
+ controlPointPosition.BindValueChanged(_ => updateMarkerDisplay());
- path.ClearVertices();
+ IsSelected.BindValueChanged(_ => updateMarkerDisplay());
- if (index != slider.Path.ControlPoints.Length - 1)
- {
- path.AddVertex(Vector2.Zero);
- path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]);
- }
-
- path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
+ updateMarkerDisplay();
}
+ // The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
- protected override bool OnDragStart(DragStartEvent e) => true;
-
- protected override bool OnDrag(DragEvent e)
+ protected override bool OnHover(HoverEvent e)
{
- var newControlPoints = slider.Path.ControlPoints.ToArray();
-
- if (index == 0)
- {
- // Special handling for the head - only the position of the slider changes
- slider.Position += e.Delta;
-
- // Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
- for (int i = 1; i < newControlPoints.Length; i++)
- newControlPoints[i] -= e.Delta;
- }
- else
- newControlPoints[index] += e.Delta;
-
- if (isSegmentSeparatorWithNext)
- newControlPoints[index + 1] = newControlPoints[index];
-
- if (isSegmentSeparatorWithPrevious)
- newControlPoints[index - 1] = newControlPoints[index];
-
- slider.Path = new SliderPath(slider.Path.Type, newControlPoints);
-
- return true;
+ updateMarkerDisplay();
+ return false;
}
- protected override bool OnDragEnd(DragEndEvent e) => true;
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateMarkerDisplay();
+ }
- private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (RequestSelection == null)
+ return false;
- private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index];
+ switch (e.Button)
+ {
+ case MouseButton.Left:
+ RequestSelection.Invoke(this, e);
+ return true;
- private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index];
+ case MouseButton.Right:
+ if (!IsSelected.Value)
+ RequestSelection.Invoke(this, e);
+ return false; // Allow context menu to show
+ }
+
+ return false;
+ }
+
+ protected override bool OnClick(ClickEvent e) => RequestSelection != null;
+
+ protected override bool OnDragStart(DragStartEvent e) => e.Button == MouseButton.Left;
+
+ protected override void OnDrag(DragEvent e)
+ {
+ if (ControlPoint == slider.Path.ControlPoints[0])
+ {
+ // Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
+ (Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime);
+ Vector2 movementDelta = snappedPosition - slider.Position;
+
+ slider.Position += movementDelta;
+ slider.StartTime = snappedTime;
+
+ // Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
+ for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
+ slider.Path.ControlPoints[i].Position.Value -= movementDelta;
+ }
+ else
+ ControlPoint.Position.Value += e.Delta;
+ }
+
+ ///
+ /// Updates the state of the circular control point marker.
+ ///
+ private void updateMarkerDisplay()
+ {
+ Position = slider.StackedPosition + ControlPoint.Position.Value;
+
+ markerRing.Alpha = IsSelected.Value ? 1 : 0;
+
+ Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
+ if (IsHovered || IsSelected.Value)
+ colour = Color4.White;
+ marker.Colour = colour;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index df846b5d5b..e293eba9d7 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -1,39 +1,209 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Humanizer;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
- public class PathControlPointVisualiser : SliderPiece
+ public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu
{
+ internal readonly Container Pieces;
+
+ private readonly Container connections;
+
private readonly Slider slider;
- private readonly Container pieces;
+ private readonly bool allowSelection;
- public PathControlPointVisualiser(Slider slider)
- : base(slider)
+ private InputManager inputManager;
+
+ private IBindableList controlPoints;
+
+ public Action> RemoveControlPointsRequested;
+
+ public PathControlPointVisualiser(Slider slider, bool allowSelection)
{
this.slider = slider;
+ this.allowSelection = allowSelection;
- InternalChild = pieces = new Container { RelativeSizeAxes = Axes.Both };
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ connections = new Container { RelativeSizeAxes = Axes.Both },
+ Pieces = new Container { RelativeSizeAxes = Axes.Both }
+ };
}
- [BackgroundDependencyLoader]
- private void load()
+ protected override void LoadComplete()
{
- PathBindable.BindValueChanged(_ => updatePathControlPoints(), true);
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+
+ controlPoints = slider.Path.ControlPoints.GetBoundCopy();
+ controlPoints.ItemsAdded += addControlPoints;
+ controlPoints.ItemsRemoved += removeControlPoints;
+
+ addControlPoints(controlPoints);
}
- private void updatePathControlPoints()
+ private void addControlPoints(IEnumerable controlPoints)
{
- while (slider.Path.ControlPoints.Length > pieces.Count)
- pieces.Add(new PathControlPointPiece(slider, pieces.Count));
- while (slider.Path.ControlPoints.Length < pieces.Count)
- pieces.Remove(pieces[pieces.Count - 1]);
+ foreach (var point in controlPoints)
+ {
+ Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
+ {
+ if (allowSelection)
+ d.RequestSelection = selectPiece;
+ }));
+
+ connections.Add(new PathControlPointConnectionPiece(slider, point));
+ }
+ }
+
+ private void removeControlPoints(IEnumerable controlPoints)
+ {
+ foreach (var point in controlPoints)
+ {
+ Pieces.RemoveAll(p => p.ControlPoint == point);
+ connections.RemoveAll(c => c.ControlPoint == point);
+ }
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ foreach (var piece in Pieces)
+ {
+ piece.IsSelected.Value = false;
+ }
+
+ return false;
+ }
+
+ public bool OnPressed(PlatformAction action)
+ {
+ switch (action.ActionMethod)
+ {
+ case PlatformActionMethod.Delete:
+ return deleteSelected();
+ }
+
+ return false;
+ }
+
+ public void OnReleased(PlatformAction action)
+ {
+ }
+
+ private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e)
+ {
+ if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
+ piece.IsSelected.Toggle();
+ else
+ {
+ foreach (var p in Pieces)
+ p.IsSelected.Value = p == piece;
+ }
+ }
+
+ private bool deleteSelected()
+ {
+ List toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
+
+ // Ensure that there are any points to be deleted
+ if (toRemove.Count == 0)
+ return false;
+
+ RemoveControlPointsRequested?.Invoke(toRemove);
+
+ // Since pieces are re-used, they will not point to the deleted control points while remaining selected
+ foreach (var piece in Pieces)
+ piece.IsSelected.Value = false;
+
+ return true;
+ }
+
+ public MenuItem[] ContextMenuItems
+ {
+ get
+ {
+ if (!Pieces.Any(p => p.IsHovered))
+ return null;
+
+ var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToList();
+ int count = selectedPieces.Count;
+
+ if (count == 0)
+ return null;
+
+ List