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..2c000d3881 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -12,16 +12,171 @@ 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 - type names
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+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:silent
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_methods = true:silent
+csharp_style_expression_bodied_operators = true:silent
+csharp_style_expression_bodied_properties = 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:silent
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:silent
+
+#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:silent
+csharp_style_pattern_matching_over_as_with_null_check = true:silent
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:suggestion
+
+#Style - unused
+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:silent
+
+#Style - variable declaration
+csharp_style_inlined_variable_declaration = true:silent
+csharp_style_deconstructed_variable_declaration = true:silent
+
+#Style - other C# 7.x features
+csharp_style_expression_bodied_local_functions = true:silent
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:silent
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Field can be readonly
+dotnet_diagnostic.IDE0044.severity = silent
+#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..e6b5db5904 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,87 @@ 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
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000000..e69de29bb2
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/.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..5372b6f28a 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..45a94f37c0 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..1f09381e08 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..ba530f0ddf 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..89d5b45f67 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 100%
rename from .idea/.idea.osu/.idea/runConfigurations/Tournament__Tests_.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
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..f1d0957b8e 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/.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..23b49abcad 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/.vscode/launch.json b/.vscode/launch.json
index 57ff3e6b43..5940df2191 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,13 +6,13 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.2/osu!.dll"
+ "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.0/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.0:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -23,13 +23,13 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.2/osu!.dll"
+ "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.0/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.0:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -40,13 +40,13 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.2/osu.Game.Tests.dll"
+ "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp3.0/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.0:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -56,13 +56,13 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.2/osu.Game.Tests.dll"
+ "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp3.0/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.0:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -73,14 +73,14 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.2/osu!.dll",
+ "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.0/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.0:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -91,14 +91,14 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.2/osu!.dll",
+ "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.0/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.0:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -109,14 +109,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.0/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.0:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
@@ -127,14 +127,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.0/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.0:${env:LD_LIBRARY_PATH}"
}
},
"console": "internalConsole"
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index aba590f466..04ff7c1bea 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -95,12 +95,12 @@
"problemMatcher": "$msCompile"
},
{
- "label": "Restore (netcoreapp2.2)",
+ "label": "Restore (netcoreapp3.0)",
"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..9fb86485d2
--- /dev/null
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -0,0 +1,4 @@
+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.
+T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000000..838851b712
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,40 @@
+
+
+
+ 7.3
+
+
+ $(MSBuildThisFileDirectory)app.manifest
+
+
+
+ osu.licenseheader
+
+
+
+
+
+
+
+
+
+
+ true
+ $(NoWarn);CS1591
+
+
+
+ $(NoWarn);NU1701
+
+
+ ppy Pty Ltd
+ MIT
+ https://github.com/ppy/osu
+ https://github.com/ppy/osu
+ Automated release.
+ ppy Pty Ltd
+ Copyright (c) 2019 ppy Pty Ltd
+ osu game
+
+
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock
index f7c19064b4..ab594aee74 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,11 +1,11 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.0)
- addressable (2.6.0)
- public_suffix (>= 2.0.2, < 4.0)
+ CFPropertyList (3.0.1)
+ addressable (2.7.0)
+ public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
- babosa (1.0.2)
+ babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
@@ -18,7 +18,7 @@ GEM
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
- excon (0.66.0)
+ excon (0.67.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
@@ -26,8 +26,8 @@ GEM
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
- fastimage (2.1.5)
- fastlane (2.129.0)
+ fastimage (2.1.7)
+ fastlane (2.133.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
@@ -37,9 +37,9 @@ GEM
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.45.0, < 1.0.0)
- faraday (~> 0.9)
+ faraday (< 0.16.0)
faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (~> 0.9)
+ faraday_middleware (< 0.16.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.21.2, < 0.24.0)
@@ -52,7 +52,7 @@ GEM
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)
@@ -77,9 +77,9 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.9)
- google-cloud-core (1.3.0)
+ google-cloud-core (1.3.1)
google-cloud-env (~> 1.0)
- google-cloud-env (1.2.0)
+ google-cloud-env (1.2.1)
faraday (~> 0.11)
google-cloud-storage (1.16.0)
digest-crc (~> 0.4)
@@ -100,9 +100,9 @@ GEM
json (2.2.0)
jwt (2.1.0)
memoist (0.16.0)
- mime-types (3.2.2)
+ mime-types (3.3)
mime-types-data (~> 3.2015)
- mime-types-data (3.2019.0331)
+ mime-types-data (3.2019.1009)
mini_magick (4.9.5)
mini_portile2 (2.4.0)
multi_json (1.13.1)
@@ -121,14 +121,14 @@ GEM
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
- rubyzip (1.2.3)
+ 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.6)
CFPropertyList
naturally
slack-notifier (2.3.2)
diff --git a/README.md b/README.md
index 56491a4be4..65fb97eb5d 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,10 @@
# osu!
-[](https://ci.appveyor.com/project/peppy/osu) [](https://www.codefactor.io/repository/github/ppy/osu) [](https://discord.gg/ppy)
+[](https://ci.appveyor.com/project/peppy/osu)
+[]()
+[](https://www.codefactor.io/repository/github/ppy/osu)
+[](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.
@@ -18,10 +21,10 @@ Detailed changelogs are published on the [official osu! site](https://osu.ppy.sh
## Requirements
-- A desktop platform with the [.NET Core SDK 2.2](https://www.microsoft.com/net/learn/get-started) or higher installed.
-- When running on linux, please have a system-wide ffmpeg installation available to support video decoding.
+- A desktop platform with the [.NET Core SDK 3.0](https://www.microsoft.com/net/learn/get-started) or higher installed.
+- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore2x)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
-- When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio 2017+](https://visualstudio.microsoft.com/vs/), [Jetbrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
+- 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!
@@ -31,12 +34,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://testflight.apple.com/join/2tLcjWlF) | [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://testflight.apple.com/join/2tLcjWlF) (note that due to high demand this is regularly full).
-- **Android** users can self-compile, and expect a public beta soon.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
@@ -59,7 +60,8 @@ git pull
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).
-> 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:
@@ -69,19 +71,7 @@ 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 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
-```
+If the build fails, try to restore NuGet packages with `dotnet restore`.
### Testing with resource/framework modifications
@@ -89,11 +79,11 @@ 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.
+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.
## 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).
@@ -101,7 +91,7 @@ Before starting, please make sure you are familiar with the [development and tes
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as pain-free as possible.
-For those interested, we love to reward quality contributions via [bounties](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.
+For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
## Licence
diff --git a/appveyor.yml b/appveyor.yml
index be1727e7d7..f911d67c6e 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,6 +1,6 @@
clone_depth: 1
version: '{branch}-{build}'
-image: Previous Visual Studio 2017
+image: Visual Studio 2019
test: off
build_script:
- cmd: PowerShell -Version 2.0 .\build.ps1
diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml
index d36298175b..fb7825b31d 100644
--- a/appveyor_deploy.yml
+++ b/appveyor_deploy.yml
@@ -1,6 +1,6 @@
clone_depth: 1
version: '{build}'
-image: Previous Visual Studio 2017
+image: Visual Studio 2019
test: off
skip_non_tags: true
build_script:
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/build.ps1 b/build.ps1
old mode 100644
new mode 100755
index c6a0bf6d4a..4b3b1f717a
--- a/build.ps1
+++ b/build.ps1
@@ -1,39 +1,5 @@
-##########################################################################
-# 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")]
@@ -45,27 +11,8 @@ Param(
[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");
+$cakeArguments = "";
if ($Target) { $cakeArguments += "-target=$Target" }
if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
@@ -74,9 +21,7 @@ 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
+dotnet tool restore
+dotnet cake ./build/build.cake --bootstrap
+dotnet cake ./build/build.cake $cakeArguments
+exit $LASTEXITCODE
\ No newline at end of file
diff --git a/build.sh b/build.sh
index 8f1ef5b455..2c22f08574 100755
--- a/build.sh
+++ b/build.sh
@@ -1,18 +1,5 @@
-#!/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"
+echo "Installing Cake.Tool..."
+dotnet tool restore
# Parse arguments.
CAKE_ARGUMENTS=()
@@ -25,14 +12,6 @@ for i in "$@"; do
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[@]}"
+dotnet cake ./build/build.cake --bootstrap
+dotnet cake ./build/build.cake "${CAKE_ARGUMENTS[@]}"
\ No newline at end of file
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/build.cake
index 1d2588de49..274e57ef4e 100644
--- a/build/build.cake
+++ b/build/build.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.2.1"
#tool "nuget:?package=NVika.MSBuild&version=1.0.1"
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
@@ -11,7 +11,9 @@ var target = Argument("target", "Build");
var configuration = Argument("configuration", "Release");
var rootDirectory = new DirectoryPath("..");
-var solution = rootDirectory.CombineWithFilePath("osu.sln");
+var sln = rootDirectory.CombineWithFilePath("osu.sln");
+var desktopBuilds = rootDirectory.CombineWithFilePath("build/Desktop.proj");
+var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
///////////////////////////////////////////////////////////////////////////////
// TASKS
@@ -19,7 +21,7 @@ var solution = rootDirectory.CombineWithFilePath("osu.sln");
Task("Compile")
.Does(() => {
- DotNetCoreBuild(solution.FullPath, new DotNetCoreBuildSettings {
+ DotNetCoreBuild(desktopBuilds.FullPath, new DotNetCoreBuildSettings {
Configuration = configuration,
});
});
@@ -41,7 +43,7 @@ Task("InspectCode")
.WithCriteria(IsRunningOnWindows())
.IsDependentOn("Compile")
.Does(() => {
- InspectCode(solution, new InspectCodeSettings {
+ InspectCode(desktopSlnf, new InspectCodeSettings {
CachesHome = "inspectcode",
OutputFile = "inspectcodereport.xml",
});
@@ -59,8 +61,12 @@ Task("CodeFileSanity")
});
});
+Task("DotnetFormat")
+ .Does(() => DotNetCoreTool(sln.FullPath, "format", "--dry-run --check"));
+
Task("Build")
.IsDependentOn("CodeFileSanity")
+ .IsDependentOn("DotnetFormat")
.IsDependentOn("InspectCode")
.IsDependentOn("Test");
diff --git a/build/cakebuild.csproj b/build/cakebuild.csproj
deleted file mode 100644
index d5a6d44740..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 0b60e28b0f..28a83fbbae 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -1,5 +1,83 @@
update_fastlane
+platform :android do
+desc 'Deploy to play store'
+ lane :beta do |options|
+
+ update_version(
+ version: options[:version],
+ build: options[:build],
+ )
+
+ build(options)
+
+ supply(
+ apk: './osu.Android/bin/Release/sh.ppy.osulazer-Signed.apk',
+ package_name: 'sh.ppy.osulazer',
+ track: 'alpha', # upload to alpha, we can promote it later
+ json_key: options[:json_key],
+ )
+ end
+
+ desc 'Deploy to github release'
+ lane :build_github do |options|
+
+ update_version(
+ version: options[:version],
+ build: options[:build],
+ )
+
+ build(options)
+
+ client = HTTPClient.new
+ changelog = client.get_content 'https://gist.githubusercontent.com/peppy/aaa2ec1a323554b619671cac6dbbb776/raw'
+ changelog.gsub!('$BUILD_ID', options[:build])
+
+ set_github_release(
+ repository_name: "ppy/osu",
+ api_token: ENV["GITHUB_TOKEN"],
+ name: options[:build],
+ tag_name: options[:build],
+ is_draft: true,
+ description: changelog,
+ commitish: "master",
+ upload_assets: ["osu.Android/bin/Release/sh.ppy.osulazer.apk"]
+ )
+
+ end
+
+ desc 'Compile the project'
+ lane :build do |options|
+ nuget_restore(
+ project_path: 'osu.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|
+
+ 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|
@@ -28,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(
diff --git a/fastlane/README.md b/fastlane/README.md
index fbccf1c8c0..a400ed9516 100644
--- a/fastlane/README.md
+++ b/fastlane/README.md
@@ -15,6 +15,30 @@ 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 beta
```
diff --git a/global.json b/global.json
new file mode 100644
index 0000000000..d8b8d14c36
--- /dev/null
+++ b/global.json
@@ -0,0 +1,5 @@
+{
+ "msbuild-sdks": {
+ "Microsoft.Build.Traversal": "2.0.19"
+ }
+}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index c57fc342ba..0f0e82d56a 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -1,51 +1,44 @@
-
- Debug
- AnyCPU
+
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
+ System.Net.Http.HttpClientHandler
+ legacy
+ 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
false
- SdkOnly
False
true
- cjk,mideast,other,rare,west
- true
- armeabi-v7a;x86;arm64-v8a
- true
@@ -61,7 +54,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 5a97fc7518..0000000000
--- a/osu.Android.sln.DotSettings
+++ /dev/null
@@ -1,834 +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)
- Required
- Required
- Required
- Explicit
- ExpressionBody
- ExpressionBody
- True
- NEXT_LINE
- True
- True
- True
- True
- True
- True
- True
- True
- NEXT_LINE
- 1
- 1
- NEXT_LINE
- MULTILINE
- NEXT_LINE
- 1
- 1
- True
- NEXT_LINE
- NEVER
- NEVER
- True
- False
- True
- NEVER
- False
- False
- True
- False
- False
- True
- True
- False
- 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.Desktop.slnf b/osu.Desktop.slnf
new file mode 100644
index 0000000000..e6b6446f72
--- /dev/null
+++ b/osu.Desktop.slnf
@@ -0,0 +1,20 @@
+{
+ "solution": {
+ "path": "osu.sln",
+ "projects": [
+ "osu.Desktop\\osu.Desktop.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/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 761f52f961..7725ee6451 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -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
{
diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs
index 51e801c185..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,48 +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, NotificationOverlay notificationOverlay)
- {
- Icon = FontAwesome.Solid.CheckSquare;
- IconBackgound.Colour = colours.BlueDark;
-
- Activated = delegate
- {
- notificationOverlay.Hide();
- changelog.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
- return true;
- };
- }
- }
-
protected override void PopIn()
{
this.FadeIn(1400, Easing.OutQuint);
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index fa41c061b5..60b47a8b3a 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -20,7 +20,7 @@ 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;
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 538aaf2d7a..453cf6f94d 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -1,9 +1,7 @@
-
- netcoreapp2.2
+ netcoreapp3.0
WinExe
- AnyCPU
true
click the circles. to the beat.
osu!
@@ -23,13 +21,13 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
index db95e18f13..0fa3b7730d 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
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 7990c35e09..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
@@ -33,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..4030d2d9e7 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.0/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.0/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index 7a9b61c60c..0369b6db4e 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
{
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index 7b8c699f2c..da36673930 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -40,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 36342024b0..1dbe9b39ee 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.0
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index 5ab47c1611..db52fbac1b 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -8,7 +8,6 @@ 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;
@@ -78,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
catchObject.XOffset = 0;
if (catchObject is TinyDroplet)
- catchObject.XOffset = MathHelper.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X);
+ 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
}
@@ -195,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));
@@ -225,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/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index 5a640f6d1a..a7f0d358ed 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
{
@@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
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(), 0f, 1f);
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/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 a25d9cb67e..e4ad49ea50 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.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 osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
@@ -37,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
@@ -48,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;
@@ -74,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/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs
index 1af77b75fc..eae652573b 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs
@@ -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/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 0952e8981a..80a3af0aa0 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Objects
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/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 56c8b33e02..d330add1c4 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -235,7 +235,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);
}
@@ -378,7 +378,7 @@ namespace osu.Game.Rulesets.Catch.UI
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);
+ X = (float)Math.Clamp(X + direction * Clock.ElapsedFrameTime * speed, 0, 1);
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
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/osu.Game.Rulesets.Mania.Tests.iOS.csproj b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
index 58c2e2aa5a..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
@@ -33,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..779eb4f277 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.0/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.0/osu.Game.Rulesets.Mania.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
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/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs
index 622d840a0c..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;
@@ -39,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests
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/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index e7fd601abe..d5fd2808b8 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -15,7 +15,6 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
@@ -67,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", () =>
{
@@ -76,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)
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 09bf9241f2..8fc4dbfe72 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.0
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index e10602312e..6c5bb304bf 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -255,7 +255,7 @@ 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;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index ea418eedb4..6297a68e08 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -472,7 +472,7 @@ 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;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index decd159ee9..ada960a78d 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -109,8 +109,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 +134,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;
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index fba52dfc32..b9984a8b90 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)Math.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)Math.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
@@ -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/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
new file mode 100644
index 0000000000..acce41db6f
--- /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 ? 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..b28d8bb0e6 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -49,10 +49,8 @@ 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;
}
@@ -62,19 +60,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return 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..3bd7fb2d49 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,13 +44,13 @@ 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);
}
@@ -60,20 +60,20 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
var result = base.OnDrag(e);
ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = HitObject.ToLocalSpace(e.ScreenSpaceMousePosition);
+ DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
return result;
}
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/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 6f49c7f0c4..53f7e30dfd 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;
- 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/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
new file mode 100644
index 0000000000..0981b028b2
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
@@ -0,0 +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.Objects;
+
+namespace osu.Game.Rulesets.Mania.Objects
+{
+ public class BarLine : ManiaHitObject, IBarLine
+ {
+ 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 be21610525..56bc797c7f 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -4,7 +4,6 @@
using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
@@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// Visualises a . Although this derives DrawableManiaHitObject,
/// this does not handle input/sound like a normal hit object.
///
- public class DrawableBarLine : DrawableHitObject
+ public class DrawableBarLine : DrawableManiaHitObject
{
///
/// Height of major bar line triangles.
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index c5c157608f..87b9633c80 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -2,13 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
-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 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;
@@ -22,8 +21,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public override bool DisplayResult => false;
- public readonly DrawableNote Head;
- public readonly DrawableNote Tail;
+ public DrawableNote Head => headContainer.Child;
+ public DrawableNote Tail => tailContainer.Child;
+
+ private readonly Container headContainer;
+ private readonly Container tailContainer;
+ private readonly Container tickContainer;
private readonly BodyPiece bodyPiece;
@@ -40,50 +43,81 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public DrawableHoldNote(HoldNote hitObject)
: base(hitObject)
{
- Container tickContainer;
RelativeSizeAxes = Axes.X;
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);
-
- AddNested(Head);
- AddNested(Tail);
-
AccentColour.BindValueChanged(colour =>
{
bodyPiece.AccentColour = colour.NewValue;
- Head.AccentColour.Value = colour.NewValue;
- Tail.AccentColour.Value = colour.NewValue;
- tickContainer.ForEach(t => t.AccentColour.Value = colour.NewValue);
}, true);
}
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableHeadNote head:
+ headContainer.Child = head;
+ break;
+
+ case DrawableTailNote 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 DrawableTailNote(this)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AccentColour = { BindTarget = AccentColour }
+ };
+
+ case Note _:
+ return new DrawableHeadNote(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)
{
base.OnDirectionChanged(e);
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index 0c82cf7bbc..bdba813eed 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -101,6 +101,6 @@ namespace osu.Game.Rulesets.Mania.Objects
public override Judgement CreateJudgement() => new HoldNoteJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ 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 d0125f8793..ac6697a6dc 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs
@@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Mania.Objects
{
public override Judgement CreateJudgement() => new HoldNoteTickJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 29863fba2e..d371c1f7a8 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI
public DrawableManiaRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
- BarLines = new BarLineGenerator(Beatmap).BarLines;
+ BarLines = new BarLineGenerator(Beatmap).BarLines;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 12faa499ad..5ab07416a6 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
index 98a4b7d0b6..a28de7ea58 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
@@ -12,7 +12,6 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
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/osu.Game.Rulesets.Osu.Tests.iOS.csproj b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
index c7787bd162..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
@@ -33,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..67338b7bbe 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.0/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.0/osu.Game.Rulesets.Osu.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
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
index 685a51d208..46769f65fe 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -101,7 +101,11 @@ namespace osu.Game.Rulesets.Osu.Tests
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
- public event Action SourceChanged;
+ public event Action SourceChanged
+ {
+ add { }
+ remove { }
+ }
}
private class MovingCursorInputManager : ManualInputManager
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index 433ec6bd25..ac627aa23e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -24,12 +24,14 @@ namespace osu.Game.Rulesets.Osu.Tests
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/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/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
index 95c2810e94..b99cd523ff 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
@@ -29,8 +29,10 @@ namespace osu.Game.Rulesets.Osu.Tests
};
for (int i = 0; i < 512; i++)
+ {
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/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
new file mode 100644
index 0000000000..eff4d919b0
--- /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.MathUtils;
+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(IEditorBeatmap))]
+ 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/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index 29c71a8903..5c656bf594 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -111,6 +111,82 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("Distance Overflow 1 Repeat", () => SetContents(() => testDistanceOverflow(1)));
}
+ [Test]
+ public void TestChangeStackHeight()
+ {
+ DrawableSlider slider = null;
+
+ AddStep("create slider", () =>
+ {
+ slider = (DrawableSlider)createSlider(repeats: 1);
+ Add(slider);
+ });
+
+ 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);
+
+ bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
+
+ 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);
+
+ bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
+ 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);
@@ -128,7 +204,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(52, -34)
}, 700),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats),
StackHeight = 10
};
@@ -159,7 +234,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(distance, 0),
}, distance),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats),
StackHeight = stackHeight
};
@@ -179,7 +253,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(400, 0)
}, 600),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats)
};
return createDrawable(slider, 2, 3);
@@ -203,7 +276,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(430, 0)
}),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats)
};
return createDrawable(slider, 2, 3);
@@ -226,7 +298,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(430, 0)
}),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats)
};
return createDrawable(slider, 2, 3);
@@ -250,7 +321,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(0, -200)
}),
RepeatCount = repeats,
- NodeSamples = createEmptySamples(repeats)
};
return createDrawable(slider, 2, 3);
@@ -260,7 +330,7 @@ namespace osu.Game.Rulesets.Osu.Tests
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());
@@ -282,26 +352,14 @@ namespace osu.Game.Rulesets.Osu.Tests
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 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())
mod.ApplyToDrawableHitObjects(new[] { drawable });
@@ -311,6 +369,12 @@ namespace osu.Game.Rulesets.Osu.Tests
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)
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/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 2eb783233a..5f75cbabec 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -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..dde2aa53e0 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.MathUtils;
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];
+ 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/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
new file mode 100644
index 0000000000..cded7f0e95
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -0,0 +1,96 @@
+// 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.MathUtils;
+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 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)
+ {
+ var working = new ClockBackedTestWorkingBeatmap(beatmap, 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));
+ }
+
+ 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 = 5000,
+ },
+ // placeholder object to avoid hitting the results screen
+ new HitObject
+ {
+ 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 791043bcc6..fddf176fd0 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.0
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 fe11ead94d..2b6b93a590 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs
@@ -10,18 +10,13 @@ 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(OsuHitObject.OBJECT_RADIUS * 2);
- Scale = new Vector2(hitCircle.Scale);
CornerRadius = Size.X / 2;
InternalChild = new RingPiece();
@@ -31,12 +26,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/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index e257369ad9..0ccf020300 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -1,26 +1,38 @@
// 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.Objects;
+using osu.Game.Rulesets.Edit;
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
+ public class PathControlPointPiece : BlueprintPiece
{
- private readonly Slider slider;
- private readonly int index;
+ public Action RequestSelection;
+ public Action ControlPointsChanged;
+ public readonly BindableBool IsSelected = new BindableBool();
+ public readonly int Index;
+
+ private readonly Slider slider;
private readonly Path path;
- private readonly CircularContainer marker;
+ private readonly Container marker;
+ private readonly Drawable markerRing;
+
+ [Resolved(CanBeNull = true)]
+ private IDistanceSnapProvider snapProvider { get; set; }
[Resolved]
private OsuColour colours { get; set; }
@@ -28,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointPiece(Slider slider, int index)
{
this.slider = slider;
- this.index = index;
+ Index = index;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@@ -40,13 +52,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
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
+ }
+ }
+ }
}
};
}
@@ -55,48 +90,97 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
base.Update();
- Position = slider.StackedPosition + slider.Path.ControlPoints[index];
+ Position = slider.StackedPosition + slider.Path.ControlPoints[Index];
- marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow;
+ updateMarkerDisplay();
+ updateConnectingPath();
+ }
+ ///
+ /// Updates the state of the circular control point marker.
+ ///
+ private void updateMarkerDisplay()
+ {
+ markerRing.Alpha = IsSelected.Value ? 1 : 0;
+
+ Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
+ if (IsHovered || IsSelected.Value)
+ colour = Color4.White;
+ marker.Colour = colour;
+ }
+
+ ///
+ /// Updates the path connecting this control point to the previous one.
+ ///
+ private void updateConnectingPath()
+ {
path.ClearVertices();
- if (index != slider.Path.ControlPoints.Length - 1)
+ if (Index != slider.Path.ControlPoints.Length - 1)
{
path.AddVertex(Vector2.Zero);
- path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]);
+ path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]);
}
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
+ // 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 OnMouseDown(MouseDownEvent e)
+ {
+ if (RequestSelection == null)
+ return false;
+
+ switch (e.Button)
+ {
+ case MouseButton.Left:
+ RequestSelection.Invoke(Index, e);
+ return true;
+
+ case MouseButton.Right:
+ if (!IsSelected.Value)
+ RequestSelection.Invoke(Index, e);
+ return false; // Allow context menu to show
+ }
+
+ return false;
+ }
+
+ protected override bool OnMouseUp(MouseUpEvent e) => RequestSelection != null;
+
+ protected override bool OnClick(ClickEvent e) => RequestSelection != null;
+
+ protected override bool OnDragStart(DragStartEvent e) => e.Button == MouseButton.Left;
protected override bool OnDrag(DragEvent e)
{
var newControlPoints = slider.Path.ControlPoints.ToArray();
- if (index == 0)
+ if (Index == 0)
{
- // Special handling for the head - only the position of the slider changes
- slider.Position += e.Delta;
+ // 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 < newControlPoints.Length; i++)
- newControlPoints[i] -= e.Delta;
+ newControlPoints[i] -= movementDelta;
}
else
- newControlPoints[index] += e.Delta;
+ newControlPoints[Index] += e.Delta;
if (isSegmentSeparatorWithNext)
- newControlPoints[index + 1] = newControlPoints[index];
+ newControlPoints[Index + 1] = newControlPoints[Index];
if (isSegmentSeparatorWithPrevious)
- newControlPoints[index - 1] = newControlPoints[index];
+ newControlPoints[Index - 1] = newControlPoints[Index];
- slider.Path = new SliderPath(slider.Path.Type, newControlPoints);
+ ControlPointsChanged?.Invoke(newControlPoints);
return true;
}
@@ -105,8 +189,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
- private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index];
+ private bool isSegmentSeparatorWithNext => Index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[Index + 1] == slider.Path.ControlPoints[Index];
- private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index];
+ private bool isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
}
}
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..2bcce99c89 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,162 @@
// 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 Humanizer;
using osu.Framework.Allocation;
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.Osu.Objects;
+using osu.Game.Screens.Edit.Compose;
+using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
- public class PathControlPointVisualiser : SliderPiece
+ public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu
{
+ public Action ControlPointsChanged;
+
+ internal readonly Container Pieces;
private readonly Slider slider;
+ private readonly bool allowSelection;
- private readonly Container pieces;
+ private InputManager inputManager;
- public PathControlPointVisualiser(Slider slider)
- : base(slider)
+ [Resolved(CanBeNull = true)]
+ private IPlacementHandler placementHandler { get; set; }
+
+ public PathControlPointVisualiser(Slider slider, bool allowSelection)
{
this.slider = slider;
+ this.allowSelection = allowSelection;
- InternalChild = pieces = new Container { RelativeSizeAxes = Axes.Both };
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = Pieces = new Container { RelativeSizeAxes = Axes.Both };
}
- [BackgroundDependencyLoader]
- private void load()
+ protected override void LoadComplete()
{
- PathBindable.BindValueChanged(_ => updatePathControlPoints(), true);
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
}
- private void updatePathControlPoints()
+ protected override void Update()
{
- 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]);
+ base.Update();
+
+ while (slider.Path.ControlPoints.Length > Pieces.Count)
+ {
+ var piece = new PathControlPointPiece(slider, Pieces.Count)
+ {
+ ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
+ };
+
+ if (allowSelection)
+ piece.RequestSelection = selectPiece;
+
+ Pieces.Add(piece);
+ }
+
+ while (slider.Path.ControlPoints.Length < Pieces.Count)
+ Pieces.Remove(Pieces[Pieces.Count - 1]);
+ }
+
+ 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 bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
+
+ private void selectPiece(int index, MouseButtonEvent e)
+ {
+ if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
+ Pieces[index].IsSelected.Toggle();
+ else
+ {
+ foreach (var piece in Pieces)
+ piece.IsSelected.Value = piece.Index == index;
+ }
+ }
+
+ private bool deleteSelected()
+ {
+ var newControlPoints = new List();
+
+ foreach (var piece in Pieces)
+ {
+ if (!piece.IsSelected.Value)
+ newControlPoints.Add(slider.Path.ControlPoints[piece.Index]);
+ }
+
+ // Ensure that there are any points to be deleted
+ if (newControlPoints.Count == slider.Path.ControlPoints.Length)
+ return false;
+
+ // If there are 0 remaining control points, treat the slider as being deleted
+ if (newControlPoints.Count == 0)
+ {
+ placementHandler?.Delete(slider);
+ return true;
+ }
+
+ // Make control points relative
+ Vector2 first = newControlPoints[0];
+ for (int i = 0; i < newControlPoints.Count; i++)
+ newControlPoints[i] = newControlPoints[i] - first;
+
+ // The slider's position defines the position of the first control point, and all further control points are relative to that point
+ slider.Position = slider.Position + first;
+
+ // 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;
+
+ ControlPointsChanged?.Invoke(newControlPoints.ToArray());
+ return true;
+ }
+
+ public MenuItem[] ContextMenuItems
+ {
+ get
+ {
+ if (!Pieces.Any(p => p.IsHovered))
+ return null;
+
+ int selectedPoints = Pieces.Count(p => p.IsSelected.Value);
+
+ if (selectedPoints == 0)
+ return null;
+
+ return new MenuItem[]
+ {
+ new OsuMenuItem($"Delete {"control point".ToQuantity(selectedPoints)}", MenuItemType.Destructive, () => deleteSelected())
+ };
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index f1f55731b6..78f4c4d992 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -11,19 +11,15 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
- public class SliderBodyPiece : SliderPiece
+ public class SliderBodyPiece : BlueprintPiece
{
- private readonly Slider slider;
private readonly ManualSliderBody body;
- public SliderBodyPiece(Slider slider)
- : base(slider)
+ public SliderBodyPiece()
{
- this.slider = slider;
-
InternalChild = body = new ManualSliderBody
{
- AccentColour = Color4.Transparent,
+ AccentColour = Color4.Transparent
};
}
@@ -31,24 +27,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private void load(OsuColour colours)
{
body.BorderColour = colours.Yellow;
-
- PositionBindable.BindValueChanged(_ => updatePosition(), true);
- ScaleBindable.BindValueChanged(scale => body.PathRadius = scale.NewValue * OsuHitObject.OBJECT_RADIUS, true);
}
- private void updatePosition() => Position = slider.StackedPosition;
-
- protected override void Update()
+ public override void UpdateFrom(Slider hitObject)
{
- base.Update();
+ base.UpdateFrom(hitObject);
+
+ body.PathRadius = hitObject.Scale * OsuHitObject.OBJECT_RADIUS;
var vertices = new List();
- slider.Path.GetPathToProgress(vertices, 0, 1);
+ hitObject.Path.GetPathToProgress(vertices, 0, 1);
body.SetVertices(vertices);
Size = body.Size;
OriginPosition = body.PathOffset;
}
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderCirclePiece.cs
deleted file mode 100644
index 2ecfea2e3e..0000000000
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderCirclePiece.cs
+++ /dev/null
@@ -1,47 +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.Edit.Blueprints.HitCircles.Components;
-using osu.Game.Rulesets.Osu.Objects;
-
-namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
-{
- public class SliderCirclePiece : HitCirclePiece
- {
- private readonly IBindable pathBindable = new Bindable();
-
- private readonly Slider slider;
- private readonly SliderPosition position;
-
- public SliderCirclePiece(Slider slider, SliderPosition position)
- : base(slider.HeadCircle)
- {
- this.slider = slider;
- this.position = position;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- pathBindable.BindTo(slider.PathBindable);
- pathBindable.BindValueChanged(_ => UpdatePosition(), true);
- }
-
- protected override void UpdatePosition()
- {
- switch (position)
- {
- case SliderPosition.Start:
- Position = slider.StackedPosition + slider.Path.PositionAt(0);
- break;
-
- case SliderPosition.End:
- Position = slider.StackedPosition + slider.Path.PositionAt(1);
- break;
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
index c9f005495c..f09279ed73 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
@@ -1,22 +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 osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
- public class SliderCircleSelectionBlueprint : OsuSelectionBlueprint
+ public class SliderCircleSelectionBlueprint : OsuSelectionBlueprint
{
- public SliderCircleSelectionBlueprint(DrawableOsuHitObject hitObject, Slider slider, SliderPosition position)
- : base(hitObject)
+ protected readonly HitCirclePiece CirclePiece;
+
+ private readonly SliderPosition position;
+
+ public SliderCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position)
+ : base(slider)
{
- InternalChild = new SliderCirclePiece(slider, position);
+ this.position = position;
+ InternalChild = CirclePiece = new HitCirclePiece();
Select();
}
+ protected override void Update()
+ {
+ base.Update();
+
+ CirclePiece.UpdateFrom(position == SliderPosition.Start ? HitObject.HeadCircle : HitObject.TailCircle);
+ }
+
// Todo: This is temporary, since the slider circle 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.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 55de626d7d..9c0afada29 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -6,11 +6,13 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
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.Components;
using osuTK;
using osuTK.Input;
@@ -21,11 +23,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public new Objects.Slider HitObject => (Objects.Slider)base.HitObject;
+ private SliderBodyPiece bodyPiece;
+ private HitCirclePiece headCirclePiece;
+ private HitCirclePiece tailCirclePiece;
+
private readonly List segments = new List();
private Vector2 cursor;
+ private InputManager inputManager;
private PlacementState state;
+ [Resolved(CanBeNull = true)]
+ private HitObjectComposer composer { get; set; }
+
public SliderPlacementBlueprint()
: base(new Objects.Slider())
{
@@ -38,10 +48,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
InternalChildren = new Drawable[]
{
- new SliderBodyPiece(HitObject),
- new SliderCirclePiece(HitObject, SliderPosition.Start),
- new SliderCirclePiece(HitObject, SliderPosition.End),
- new PathControlPointVisualiser(HitObject),
+ bodyPiece = new SliderBodyPiece(),
+ headCirclePiece = new HitCirclePiece(),
+ tailCirclePiece = new HitCirclePiece(),
+ new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
};
setState(PlacementState.Initial);
@@ -50,25 +60,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void LoadComplete()
{
base.LoadComplete();
-
- // 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;
+ inputManager = GetContainingInputManager();
}
- protected override bool OnMouseMove(MouseMoveEvent e)
+ public override void UpdatePosition(Vector2 screenSpacePosition)
{
switch (state)
{
case PlacementState.Initial:
- HitObject.Position = e.MousePosition;
- return true;
+ HitObject.Position = ToLocalSpace(screenSpacePosition);
+ break;
case PlacementState.Body:
- cursor = e.MousePosition - HitObject.Position;
- return true;
+ // The given screen-space position may have been externally snapped, but the unsnapped position from the input manager
+ // is used instead since snapping control points doesn't make much sense
+ cursor = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
+ break;
}
-
- return false;
}
protected override bool OnClick(ClickEvent e)
@@ -109,8 +117,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void beginCurve()
{
BeginPlacement();
-
- HitObject.StartTime = EditorClock.CurrentTime;
setState(PlacementState.Body);
}
@@ -128,8 +134,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
- var newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
- HitObject.Path = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
+ Vector2[] newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
+
+ var unsnappedPath = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
+ var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
+
+ HitObject.Path = new SliderPath(unsnappedPath.Type, newControlPoints, snappedDistance);
+
+ bodyPiece.UpdateFrom(HitObject);
+ headCirclePiece.UpdateFrom(HitObject.HeadCircle);
+ tailCirclePiece.UpdateFrom(HitObject.TailCircle);
}
private void setState(PlacementState newState)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index fb8c081ff7..820d6c92d7 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -1,17 +1,34 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Diagnostics;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
- public class SliderSelectionBlueprint : OsuSelectionBlueprint
+ public class SliderSelectionBlueprint : OsuSelectionBlueprint
{
- private readonly SliderCircleSelectionBlueprint headBlueprint;
+ protected readonly SliderBodyPiece BodyPiece;
+ protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
+ protected readonly SliderCircleSelectionBlueprint TailBlueprint;
+ protected readonly PathControlPointVisualiser ControlPointVisualiser;
+
+ [Resolved(CanBeNull = true)]
+ private HitObjectComposer composer { get; set; }
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
@@ -20,13 +37,111 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
InternalChildren = new Drawable[]
{
- new SliderBodyPiece(sliderObject),
- headBlueprint = new SliderCircleSelectionBlueprint(slider.HeadCircle, sliderObject, SliderPosition.Start),
- new SliderCircleSelectionBlueprint(slider.TailCircle, sliderObject, SliderPosition.End),
- new PathControlPointVisualiser(sliderObject),
+ BodyPiece = new SliderBodyPiece(),
+ HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
+ TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
+ ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints },
};
}
- public override Vector2 SelectionPoint => headBlueprint.SelectionPoint;
+ protected override void Update()
+ {
+ base.Update();
+
+ BodyPiece.UpdateFrom(HitObject);
+ }
+
+ private Vector2 rightClickPosition;
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ switch (e.Button)
+ {
+ case MouseButton.Right:
+ rightClickPosition = e.MouseDownPosition;
+ return false; // Allow right click to be handled by context menu
+
+ case MouseButton.Left when e.ControlPressed && IsSelected:
+ placementControlPointIndex = addControlPoint(e.MousePosition);
+ return true; // Stop input from being handled and modifying the selection
+ }
+
+ return false;
+ }
+
+ private int? placementControlPointIndex;
+
+ protected override bool OnDragStart(DragStartEvent e) => placementControlPointIndex != null;
+
+ protected override bool OnDrag(DragEvent e)
+ {
+ Debug.Assert(placementControlPointIndex != null);
+
+ Vector2 position = e.MousePosition - HitObject.Position;
+
+ var controlPoints = HitObject.Path.ControlPoints.ToArray();
+ controlPoints[placementControlPointIndex.Value] = position;
+
+ onNewControlPoints(controlPoints);
+
+ return true;
+ }
+
+ protected override bool OnDragEnd(DragEndEvent e)
+ {
+ placementControlPointIndex = null;
+ return true;
+ }
+
+ private int addControlPoint(Vector2 position)
+ {
+ position -= HitObject.Position;
+
+ var controlPoints = new Vector2[HitObject.Path.ControlPoints.Length + 1];
+ HitObject.Path.ControlPoints.CopyTo(controlPoints);
+
+ int insertionIndex = 0;
+ float minDistance = float.MaxValue;
+
+ for (int i = 0; i < controlPoints.Length - 2; i++)
+ {
+ float dist = new Line(controlPoints[i], controlPoints[i + 1]).DistanceToPoint(position);
+
+ if (dist < minDistance)
+ {
+ insertionIndex = i + 1;
+ minDistance = dist;
+ }
+ }
+
+ // Move the control points from the insertion index onwards to make room for the insertion
+ Array.Copy(controlPoints, insertionIndex, controlPoints, insertionIndex + 1, controlPoints.Length - insertionIndex - 1);
+ controlPoints[insertionIndex] = position;
+
+ onNewControlPoints(controlPoints);
+
+ return insertionIndex;
+ }
+
+ private void onNewControlPoints(Vector2[] controlPoints)
+ {
+ var unsnappedPath = new SliderPath(controlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, controlPoints);
+ var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
+
+ HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance);
+
+ UpdateHitObject();
+ }
+
+ public override MenuItem[] ContextMenuItems => new MenuItem[]
+ {
+ new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
+ };
+
+ public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint;
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos);
+
+ protected virtual SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new SliderCircleSelectionBlueprint(slider, position);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
index ae94848c81..65c8720031 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
@@ -12,17 +12,13 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
{
- public class SpinnerPiece : HitObjectPiece
+ public class SpinnerPiece : BlueprintPiece
{
- private readonly Spinner spinner;
private readonly CircularContainer circle;
private readonly RingPiece ring;
- public SpinnerPiece(Spinner spinner)
- : base(spinner)
+ public SpinnerPiece()
{
- this.spinner = spinner;
-
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
@@ -44,21 +40,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
Origin = Anchor.Centre
}
};
-
- ring.Scale = new Vector2(spinner.Scale);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Yellow;
-
- PositionBindable.BindValueChanged(_ => updatePosition(), true);
- StackHeightBindable.BindValueChanged(_ => updatePosition());
- ScaleBindable.BindValueChanged(scale => ring.Scale = new Vector2(scale.NewValue), true);
}
- private void updatePosition() => Position = spinner.Position;
+ public override void UpdateFrom(Spinner hitObject)
+ {
+ base.UpdateFrom(hitObject);
+
+ ring.Scale = new Vector2(hitObject.Scale);
+ }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos);
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
index 03d761c67f..5525b8936e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
@@ -7,6 +7,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
{
@@ -21,7 +22,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
public SpinnerPlacementBlueprint()
: base(new Spinner { Position = OsuPlayfield.BASE_SIZE / 2 })
{
- InternalChild = piece = new SpinnerPiece(HitObject) { Alpha = 0.5f };
+ InternalChild = piece = new SpinnerPiece { Alpha = 0.5f };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ piece.UpdateFrom(HitObject);
}
protected override bool OnClick(ClickEvent e)
@@ -33,8 +41,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
}
else
{
- HitObject.StartTime = EditorClock.CurrentTime;
-
isPlacingEnd = true;
piece.FadeTo(1f, 150, Easing.OutQuint);
@@ -43,5 +49,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return true;
}
+
+ public override void UpdatePosition(Vector2 screenSpacePosition)
+ {
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs
index 25cef3b251..f05d4f8435 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs
@@ -8,14 +8,21 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
{
- public class SpinnerSelectionBlueprint : OsuSelectionBlueprint
+ public class SpinnerSelectionBlueprint : OsuSelectionBlueprint
{
private readonly SpinnerPiece piece;
public SpinnerSelectionBlueprint(DrawableSpinner spinner)
: base(spinner)
{
- InternalChild = piece = new SpinnerPiece((Spinner)spinner.HitObject);
+ InternalChild = piece = new SpinnerPiece();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ piece.UpdateFrom(HitObject);
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => piece.ReceivePositionalInputAt(screenSpacePos);
diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
index cc08d356f9..3437af8c1e 100644
--- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
+++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
@@ -2,8 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
@@ -12,11 +16,36 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditRuleset : DrawableOsuRuleset
{
+ ///
+ /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
+ /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
+ ///
+ private const double editor_hit_object_fade_out_extension = 500;
+
public DrawableOsuEditRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
}
+ public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h)
+ => base.CreateDrawableRepresentation(h)?.With(d => d.ApplyCustomUpdateState += updateState);
+
+ private void updateState(DrawableHitObject hitObject, ArmedState state)
+ {
+ switch (state)
+ {
+ case ArmedState.Miss:
+ // Get the existing fade out transform
+ var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
+ if (existing == null)
+ return;
+
+ using (hitObject.BeginAbsoluteSequence(existing.StartTime))
+ hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
+ break;
+ }
+ }
+
protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor();
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { Size = Vector2.One };
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
new file mode 100644
index 0000000000..9b00204d51
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.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 osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
+ {
+ public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
+ : base(hitObject, nextHitObject, hitObject.StackedEndPosition)
+ {
+ Masking = true;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 1c040e9dee..812afaaa24 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -1,11 +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 System;
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
@@ -52,5 +55,46 @@ namespace osu.Game.Rulesets.Osu.Edit
return base.CreateBlueprintFor(hitObject);
}
+
+ protected override DistanceSnapGrid CreateDistanceSnapGrid(IEnumerable selectedHitObjects)
+ {
+ var objects = selectedHitObjects.ToList();
+
+ if (objects.Count == 0)
+ return createGrid(h => h.StartTime <= EditorClock.CurrentTime);
+
+ double minTime = objects.Min(h => h.StartTime);
+ return createGrid(h => h.StartTime < minTime, objects.Count + 1);
+ }
+
+ ///
+ /// Creates a grid from the last matching a predicate to a target .
+ ///
+ /// A predicate that matches s where the grid can start from.
+ /// Only the last matching the predicate is used.
+ /// An offset from the selected via at which the grid should stop.
+ /// The from a selected to a target .
+ private OsuDistanceSnapGrid createGrid(Func sourceSelector, int targetOffset = 1)
+ {
+ if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
+
+ int sourceIndex = -1;
+
+ for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
+ {
+ if (!sourceSelector(EditorBeatmap.HitObjects[i]))
+ break;
+
+ sourceIndex = i;
+ }
+
+ if (sourceIndex == -1)
+ return null;
+
+ OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
+ OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null;
+
+ return new OsuDistanceSnapGrid(sourceObject, targetObject);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 1ab1219ab0..9418565907 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -2,17 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
-using osu.Framework.Input.Events;
-using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : SelectionHandler
{
- public override void HandleDrag(SelectionBlueprint blueprint, DragEvent dragEvent)
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
+ Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
+ Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
+
+ // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var h in SelectedHitObjects.OfType())
{
if (h is Spinner)
@@ -21,10 +24,26 @@ namespace osu.Game.Rulesets.Osu.Edit
continue;
}
- h.Position += dragEvent.Delta;
+ // Stacking is not considered
+ minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
+ maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
}
- base.HandleDrag(blueprint, dragEvent);
+ if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
+ return false;
+
+ foreach (var h in SelectedHitObjects.OfType())
+ {
+ if (h is Spinner)
+ {
+ // Spinners don't support position adjustments
+ continue;
+ }
+
+ h.Position += moveEvent.InstantDelta;
+ }
+
+ return true;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index 1eb37f8119..63110b2797 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -13,7 +14,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Mods
};
}
- private float calculateGap(float value) => MathHelper.Clamp(value, 0, target_clamp) * targetBreakMultiplier;
+ private float calculateGap(float value) => Math.Clamp(value, 0, target_clamp) * targetBreakMultiplier;
// lagrange polinominal for (0,0) (0.6,0.4) (1,1) should make a good curve
private static float applyAdjustmentCurve(float value) => 0.6f * value * value + 0.4f * value;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index 7fa3dbe07e..778c2f7d43 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
@@ -55,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods
var destination = e.MousePosition;
FlashlightPosition = Interpolation.ValueAt(
- MathHelper.Clamp(Clock.ElapsedFrameTime, 0, follow_delay), position, destination, 0, follow_delay, Easing.Out);
+ Math.Clamp(Clock.ElapsedFrameTime, 0, follow_delay), position, destination, 0, follow_delay, Easing.Out);
return base.OnMouseMove(e);
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index 17fcd03dd5..1664a37a66 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -55,8 +55,10 @@ namespace osu.Game.Rulesets.Osu.Mods
}
for (int i = 0; i < amountWiggles; i++)
+ {
using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration, true))
wiggle();
+ }
// Keep wiggling sliders and spinners for their duration
if (!(osuObject is IHasEndTime endTime))
@@ -65,8 +67,10 @@ namespace osu.Game.Rulesets.Osu.Mods
amountWiggles = (int)(endTime.Duration / wiggle_duration);
for (int i = 0; i < amountWiggles; i++)
+ {
using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration, true))
wiggle();
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/ConnectionRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/ConnectionRenderer.cs
deleted file mode 100644
index 9106f4c7bd..0000000000
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/ConnectionRenderer.cs
+++ /dev/null
@@ -1,21 +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.Graphics.Containers;
-using osu.Game.Rulesets.Objects;
-using System.Collections.Generic;
-
-namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
-{
- ///
- /// Connects hit objects visually, for example with follow points.
- ///
- public abstract class ConnectionRenderer : LifetimeManagementContainer
- where T : HitObject
- {
- ///
- /// Hit objects to create connections for
- ///
- public abstract IEnumerable HitObjects { get; set; }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
index 89ffddf4cb..db34ae1d87 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
@@ -12,6 +12,9 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
+ ///
+ /// A single follow point positioned between two adjacent s.
+ ///
public class FollowPoint : Container
{
private const float width = 8;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
new file mode 100644
index 0000000000..1e032eb977
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -0,0 +1,140 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
+{
+ ///
+ /// Visualises the s between two s.
+ ///
+ public class FollowPointConnection : CompositeDrawable
+ {
+ // Todo: These shouldn't be constants
+ private const int spacing = 32;
+ private const double preempt = 800;
+
+ ///
+ /// The start time of .
+ ///
+ public readonly Bindable StartTime = new Bindable();
+
+ ///
+ /// The which s will exit from.
+ ///
+ [NotNull]
+ public readonly DrawableOsuHitObject Start;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The which s will exit from.
+ public FollowPointConnection([NotNull] DrawableOsuHitObject start)
+ {
+ Start = start;
+
+ RelativeSizeAxes = Axes.Both;
+
+ StartTime.BindTo(Start.HitObject.StartTimeBindable);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ bindEvents(Start);
+ }
+
+ private DrawableOsuHitObject end;
+
+ ///
+ /// The which s will enter.
+ ///
+ [CanBeNull]
+ public DrawableOsuHitObject End
+ {
+ get => end;
+ set
+ {
+ end = value;
+
+ if (end != null)
+ bindEvents(end);
+
+ if (IsLoaded)
+ scheduleRefresh();
+ else
+ refresh();
+ }
+ }
+
+ private void bindEvents(DrawableOsuHitObject drawableObject)
+ {
+ drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh());
+ drawableObject.HitObject.DefaultsApplied += scheduleRefresh;
+ }
+
+ private void scheduleRefresh() => Scheduler.AddOnce(refresh);
+
+ private void refresh()
+ {
+ ClearInternal();
+
+ if (End == null)
+ return;
+
+ OsuHitObject osuStart = Start.HitObject;
+ OsuHitObject osuEnd = End.HitObject;
+
+ if (osuEnd.NewCombo)
+ return;
+
+ if (osuStart is Spinner || osuEnd is Spinner)
+ return;
+
+ Vector2 startPosition = osuStart.EndPosition;
+ Vector2 endPosition = osuEnd.Position;
+ double startTime = (osuStart as IHasEndTime)?.EndTime ?? osuStart.StartTime;
+ double endTime = osuEnd.StartTime;
+
+ Vector2 distanceVector = endPosition - startPosition;
+ int distance = (int)distanceVector.Length;
+ float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
+ double duration = endTime - startTime;
+
+ for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
+ {
+ float fraction = (float)d / distance;
+ Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
+ Vector2 pointEndPosition = startPosition + fraction * distanceVector;
+ double fadeOutTime = startTime + fraction * duration;
+ double fadeInTime = fadeOutTime - preempt;
+
+ FollowPoint fp;
+
+ AddInternal(fp = new FollowPoint
+ {
+ Position = pointStartPosition,
+ Rotation = rotation,
+ Alpha = 0,
+ Scale = new Vector2(1.5f * osuEnd.Scale),
+ });
+
+ using (fp.BeginAbsoluteSequence(fadeInTime))
+ {
+ fp.FadeIn(osuEnd.TimeFadeIn);
+ fp.ScaleTo(osuEnd.Scale, osuEnd.TimeFadeIn, Easing.Out);
+ fp.MoveTo(pointEndPosition, osuEnd.TimeFadeIn, Easing.Out);
+ fp.Delay(fadeOutTime - fadeInTime).FadeOut(osuEnd.TimeFadeIn);
+ }
+
+ fp.Expire(true);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
index a269b87c75..be192080f9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
@@ -1,121 +1,110 @@
// 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 osuTK;
+using System.Linq;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Objects.Types;
+using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
- public class FollowPointRenderer : ConnectionRenderer
+ ///
+ /// Visualises connections between s.
+ ///
+ public class FollowPointRenderer : CompositeDrawable
{
- private int pointDistance = 32;
-
///
- /// Determines how much space there is between points.
+ /// All the s contained by this .
///
- public int PointDistance
- {
- get => pointDistance;
- set
- {
- if (pointDistance == value) return;
+ internal IReadOnlyList Connections => connections;
- pointDistance = value;
- update();
- }
- }
-
- private int preEmpt = 800;
-
- ///
- /// Follow points to the next hitobject start appearing for this many milliseconds before an hitobject's end time.
- ///
- public int PreEmpt
- {
- get => preEmpt;
- set
- {
- if (preEmpt == value) return;
-
- preEmpt = value;
- update();
- }
- }
-
- private IEnumerable hitObjects;
-
- public override IEnumerable HitObjects
- {
- get => hitObjects;
- set
- {
- hitObjects = value;
- update();
- }
- }
+ private readonly List connections = new List();
public override bool RemoveCompletedTransforms => false;
- private void update()
+ ///
+ /// Adds the s around a .
+ /// This includes s leading into , and s exiting .
+ ///
+ /// The to add s for.
+ public void AddFollowPoints(DrawableOsuHitObject hitObject)
+ => addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
+
+ ///
+ /// Removes the s around a .
+ /// This includes s leading into , and s exiting .
+ ///
+ /// The to remove s for.
+ public void RemoveFollowPoints(DrawableOsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
+
+ ///
+ /// Adds a to this .
+ ///
+ /// The to add.
+ /// The index of in .
+ private void addConnection(FollowPointConnection connection)
{
- ClearInternal();
+ AddInternal(connection);
- if (hitObjects == null)
- return;
+ // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
+ int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
- OsuHitObject prevHitObject = null;
-
- foreach (var currHitObject in hitObjects)
+ if (index < connections.Count - 1)
{
- if (prevHitObject != null && !currHitObject.NewCombo && !(prevHitObject is Spinner) && !(currHitObject is Spinner))
- {
- Vector2 startPosition = prevHitObject.EndPosition;
- Vector2 endPosition = currHitObject.Position;
- double startTime = (prevHitObject as IHasEndTime)?.EndTime ?? prevHitObject.StartTime;
- double endTime = currHitObject.StartTime;
+ // Update the connection's end point to the next connection's start point
+ // h1 -> -> -> h2
+ // connection nextGroup
- Vector2 distanceVector = endPosition - startPosition;
- int distance = (int)distanceVector.Length;
- float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
- double duration = endTime - startTime;
-
- for (int d = (int)(PointDistance * 1.5); d < distance - PointDistance; d += PointDistance)
- {
- float fraction = (float)d / distance;
- Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
- Vector2 pointEndPosition = startPosition + fraction * distanceVector;
- double fadeOutTime = startTime + fraction * duration;
- double fadeInTime = fadeOutTime - PreEmpt;
-
- FollowPoint fp;
-
- AddInternal(fp = new FollowPoint
- {
- Position = pointStartPosition,
- Rotation = rotation,
- Alpha = 0,
- Scale = new Vector2(1.5f * currHitObject.Scale),
- });
-
- using (fp.BeginAbsoluteSequence(fadeInTime))
- {
- fp.FadeIn(currHitObject.TimeFadeIn);
- fp.ScaleTo(currHitObject.Scale, currHitObject.TimeFadeIn, Easing.Out);
-
- fp.MoveTo(pointEndPosition, currHitObject.TimeFadeIn, Easing.Out);
-
- fp.Delay(fadeOutTime - fadeInTime).FadeOut(currHitObject.TimeFadeIn);
- }
-
- fp.Expire(true);
- }
- }
-
- prevHitObject = currHitObject;
+ FollowPointConnection nextConnection = connections[index + 1];
+ connection.End = nextConnection.Start;
}
+ else
+ {
+ // The end point may be non-null during re-ordering
+ connection.End = null;
+ }
+
+ if (index > 0)
+ {
+ // Update the previous connection's end point to the current connection's start point
+ // h1 -> -> -> h2
+ // prevGroup connection
+
+ FollowPointConnection previousConnection = connections[index - 1];
+ previousConnection.End = connection.Start;
+ }
+ }
+
+ ///
+ /// Removes a from this .
+ ///
+ /// The to remove.
+ /// Whether was removed.
+ private void removeGroup(FollowPointConnection connection)
+ {
+ RemoveInternal(connection);
+
+ int index = connections.IndexOf(connection);
+
+ if (index > 0)
+ {
+ // Update the previous connection's end point to the next connection's start point
+ // h1 -> -> -> h2 -> -> -> h3
+ // prevGroup connection nextGroup
+ // The current connection's end point is used since there may not be a next connection
+ FollowPointConnection previousConnection = connections[index - 1];
+ previousConnection.End = connection.End;
+ }
+
+ connections.Remove(connection);
+ }
+
+ private void onStartTimeChanged(FollowPointConnection connection)
+ {
+ // Naive but can be improved if performance becomes an issue
+ removeGroup(connection);
+ addConnection(connection);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index c90f230f93..f74f2d7bc5 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -24,14 +24,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable stackHeightBindable = new Bindable();
private readonly IBindable scaleBindable = new Bindable();
- public OsuAction? HitAction => hitArea.HitAction;
+ public OsuAction? HitAction => HitArea.HitAction;
+ public readonly HitReceptor HitArea;
+ public readonly SkinnableDrawable CirclePiece;
private readonly Container scaleContainer;
- private readonly HitArea hitArea;
-
- public SkinnableDrawable CirclePiece { get; }
-
public DrawableHitCircle(HitCircle h)
: base(h)
{
@@ -48,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Anchor = Anchor.Centre,
Children = new Drawable[]
{
- hitArea = new HitArea
+ HitArea = new HitReceptor
{
Hit = () =>
{
@@ -59,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
- CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece(HitObject.IndexInCurrentCombo)),
+ CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece()),
ApproachCircle = new ApproachCircle
{
Alpha = 0,
@@ -69,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
},
};
- Size = hitArea.DrawSize;
+ Size = HitArea.DrawSize;
}
[BackgroundDependencyLoader]
@@ -153,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Expire(true);
- hitArea.HitAction = null;
+ HitArea.HitAction = null;
break;
case ArmedState.Miss:
@@ -172,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Drawable ProxiedLayer => ApproachCircle;
- private class HitArea : Drawable, IKeyBindingHandler
+ public class HitReceptor : Drawable, IKeyBindingHandler
{
// IsHovered is used
public override bool HandlePositionalInput => true;
@@ -181,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public OsuAction? HitAction;
- public HitArea()
+ public HitReceptor()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs
index 84d2a4af9b..122975d55e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs
@@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
else
{
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
- Rotation = Interpolation.ValueAt(MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
+ Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 9e8ad9851c..69189758a6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -1,11 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -21,16 +21,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach
{
- private readonly Slider slider;
- private readonly List components = new List();
-
- public readonly DrawableHitCircle HeadCircle;
- public readonly DrawableSliderTail TailCircle;
+ public DrawableSliderHead HeadCircle => headContainer.Child;
+ public DrawableSliderTail TailCircle => tailContainer.Child;
public readonly SnakingSliderBody Body;
public readonly SliderBall Ball;
+ private readonly Container headContainer;
+ private readonly Container tailContainer;
+ private readonly Container tickContainer;
+ private readonly Container repeatContainer;
+
+ private readonly Slider slider;
+
private readonly IBindable positionBindable = new Bindable();
+ private readonly IBindable stackHeightBindable = new Bindable();
private readonly IBindable scaleBindable = new Bindable();
private readonly IBindable pathBindable = new Bindable();
@@ -44,14 +49,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Position = s.StackedPosition;
- Container ticks;
- Container repeatPoints;
-
InternalChildren = new Drawable[]
{
Body = new SnakingSliderBody(s),
- ticks = new Container { RelativeSizeAxes = Axes.Both },
- repeatPoints = new Container { RelativeSizeAxes = Axes.Both },
+ tickContainer = new Container { RelativeSizeAxes = Axes.Both },
+ repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s, this)
{
GetInitialHitAction = () => HeadCircle.HitAction,
@@ -60,45 +62,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AlwaysPresent = true,
Alpha = 0
},
- HeadCircle = new DrawableSliderHead(s, s.HeadCircle)
- {
- OnShake = Shake
- },
- TailCircle = new DrawableSliderTail(s, s.TailCircle)
+ headContainer = new Container { RelativeSizeAxes = Axes.Both },
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
};
-
- components.Add(Body);
- components.Add(Ball);
-
- AddNested(HeadCircle);
-
- AddNested(TailCircle);
- components.Add(TailCircle);
-
- foreach (var tick in s.NestedHitObjects.OfType())
- {
- var drawableTick = new DrawableSliderTick(tick) { Position = tick.Position - s.Position };
-
- ticks.Add(drawableTick);
- components.Add(drawableTick);
- AddNested(drawableTick);
- }
-
- foreach (var repeatPoint in s.NestedHitObjects.OfType())
- {
- var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) { Position = repeatPoint.Position - s.Position };
-
- repeatPoints.Add(drawableRepeatPoint);
- components.Add(drawableRepeatPoint);
- AddNested(drawableRepeatPoint);
- }
- }
-
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- Body.FadeInFromZero(HitObject.TimeFadeIn);
}
[BackgroundDependencyLoader]
@@ -108,6 +74,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, Body.SnakingOut);
positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
+ stackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
scaleBindable.BindValueChanged(scale =>
{
updatePathRadius();
@@ -115,6 +82,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
positionBindable.BindTo(HitObject.PositionBindable);
+ stackHeightBindable.BindTo(HitObject.StackHeightBindable);
scaleBindable.BindTo(HitObject.ScaleBindable);
pathBindable.BindTo(slider.PathBindable);
@@ -129,6 +97,67 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}, true);
}
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableSliderHead head:
+ headContainer.Child = head;
+ break;
+
+ case DrawableSliderTail tail:
+ tailContainer.Child = tail;
+ break;
+
+ case DrawableSliderTick tick:
+ tickContainer.Add(tick);
+ break;
+
+ case DrawableRepeatPoint repeat:
+ repeatContainer.Add(repeat);
+ break;
+ }
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+
+ headContainer.Clear();
+ tailContainer.Clear();
+ repeatContainer.Clear();
+ tickContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case SliderTailCircle tail:
+ return new DrawableSliderTail(slider, tail);
+
+ case HitCircle head:
+ return new DrawableSliderHead(slider, head) { OnShake = Shake };
+
+ case SliderTick tick:
+ return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position };
+
+ case RepeatPoint repeat:
+ return new DrawableRepeatPoint(repeat, this) { Position = repeat.Position - slider.Position };
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ Body.FadeInFromZero(HitObject.TimeFadeIn);
+ }
+
public readonly Bindable Tracking = new Bindable();
protected override void Update()
@@ -137,11 +166,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.Value = Ball.Tracking;
- double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
+ double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
- foreach (var c in components.OfType()) c.UpdateProgress(completionProgress);
- foreach (var c in components.OfType()) c.UpdateSnakingPosition(slider.Path.PositionAt(Body.SnakedStart ?? 0), slider.Path.PositionAt(Body.SnakedEnd ?? 0));
- foreach (var t in components.OfType()) t.Tracking = Ball.Tracking;
+ Ball.UpdateProgress(completionProgress);
+ Body.UpdateProgress(completionProgress);
+
+ foreach (DrawableHitObject hitObject in NestedHitObjects)
+ {
+ if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(slider.Path.PositionAt(Body.SnakedStart ?? 0), slider.Path.PositionAt(Body.SnakedEnd ?? 0));
+ if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking;
+ }
Size = Body.Size;
OriginPosition = Body.PathOffset;
@@ -187,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r =>
{
- var judgementsCount = NestedHitObjects.Count();
+ var judgementsCount = NestedHitObjects.Count;
var judgementsHit = NestedHitObjects.Count(h => h.IsHit);
var hitFraction = (double)judgementsHit / judgementsCount;
@@ -228,7 +262,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
- public Drawable ProxiedLayer => HeadCircle.ApproachCircle;
+ public Drawable ProxiedLayer => HeadCircle.ProxiedLayer;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Body.ReceivePositionalInputAt(screenSpacePos);
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index 66b6f0f9ac..a10c66d1df 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.Update();
- double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
+ double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
if (!IsHit)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index 23c5494cf5..42bf5e4d21 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindValueChanged(_ => updatePosition());
pathBindable.BindValueChanged(_ => updatePosition(), true);
+
+ // TODO: This has no drawable content. Support for skins should be added.
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index d1b9ee6cb4..1261d3d19a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -136,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindTo(HitObject.PositionBindable);
}
- public float Progress => MathHelper.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1);
+ public float Progress => Math.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs
index 944c93bb6d..e364c96426 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly NumberPiece number;
private readonly GlowPiece glow;
- public MainCirclePiece(int index)
+ public MainCirclePiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@@ -31,10 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
glow = new GlowPiece(),
circle = new CirclePiece(),
- number = new NumberPiece
- {
- Text = (index + 1).ToString(),
- },
+ number = new NumberPiece(),
ring = new RingPiece(),
flash = new FlashPiece(),
explode = new ExplodePiece(),
@@ -42,12 +39,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
private readonly IBindable state = new Bindable();
-
- private readonly Bindable accentColour = new Bindable();
+ private readonly IBindable accentColour = new Bindable();
+ private readonly IBindable indexInCurrentCombo = new Bindable();
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
+ OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
+
state.BindTo(drawableObject.State);
state.BindValueChanged(updateState, true);
@@ -58,6 +57,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
glow.Colour = colour.NewValue;
circle.Colour = colour.NewValue;
}, true);
+
+ indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
+ indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
}
private void updateState(ValueChangedEvent state)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs
index 62c4ba5ee3..7c94568835 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs
@@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Skinning;
@@ -30,17 +29,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Children = new Drawable[]
{
- new CircularContainer
+ new Container
{
Masking = true,
- Origin = Anchor.Centre,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 60,
Colour = Color4.White.Opacity(0.5f),
},
- Child = new Box()
},
number = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs
index 70a1bad4a3..f2150280b3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
var spanProgress = slider.ProgressAt(completionProgress);
double start = 0;
- double end = SnakingIn.Value ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1;
+ double end = SnakingIn.Value ? Math.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1;
if (span >= slider.SpanCount() - 1)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
index 448a2eada7..c45e98cc76 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
lastAngle -= 360;
currentRotation += thisAngle - lastAngle;
- RotationAbsolute += Math.Abs(thisAngle - lastAngle);
+ RotationAbsolute += Math.Abs(thisAngle - lastAngle) * Math.Sign(Clock.ElapsedFrameTime);
}
lastAngle = thisAngle;
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 2cf877b000..0ba712a83f 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.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.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
@@ -14,8 +15,16 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition
{
+ ///
+ /// The radius of hit objects (ie. the radius of a ).
+ ///
public const float OBJECT_RADIUS = 64;
+ ///
+ /// Scoring distance with a speed-adjusted beat length of 1 second (ie. the speed slider balls move through their track).
+ ///
+ internal const float BASE_SCORING_DISTANCE = 100;
+
public double TimePreempt = 600;
public double TimeFadeIn = 400;
@@ -58,13 +67,46 @@ namespace osu.Game.Rulesets.Osu.Objects
public virtual bool NewCombo { get; set; }
- public int ComboOffset { get; set; }
+ public readonly Bindable ComboOffsetBindable = new Bindable();
- public virtual int IndexInCurrentCombo { get; set; }
+ public int ComboOffset
+ {
+ get => ComboOffsetBindable.Value;
+ set => ComboOffsetBindable.Value = value;
+ }
- public virtual int ComboIndex { get; set; }
+ public Bindable IndexInCurrentComboBindable { get; } = new Bindable();
- public bool LastInCombo { get; set; }
+ public virtual int IndexInCurrentCombo
+ {
+ get => IndexInCurrentComboBindable.Value;
+ set => IndexInCurrentComboBindable.Value = value;
+ }
+
+ public Bindable ComboIndexBindable { get; } = new Bindable();
+
+ public virtual int ComboIndex
+ {
+ get => ComboIndexBindable.Value;
+ set => ComboIndexBindable.Value = value;
+ }
+
+ public Bindable LastInComboBindable { get; } = new Bindable();
+
+ public bool LastInCombo
+ {
+ get => LastInComboBindable.Value;
+ set => LastInComboBindable.Value = value;
+ }
+
+ protected OsuHitObject()
+ {
+ StackHeightBindable.BindValueChanged(height =>
+ {
+ foreach (var nested in NestedHitObjects.OfType())
+ nested.StackHeight = height.NewValue;
+ });
+ }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs
index a794e57c9e..a277517f9f 100644
--- a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs
+++ b/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs
@@ -30,6 +30,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public override Judgement CreateJudgement() => new OsuJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 2805494021..c6f5a075e0 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -19,11 +19,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class Slider : OsuHitObject, IHasCurve
{
- ///
- /// Scoring distance with a speed-adjusted beat length of 1 second.
- ///
- private const float base_scoring_distance = 100;
-
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
public double Duration => EndTime - StartTime;
@@ -33,28 +28,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t);
- public override int ComboIndex
- {
- get => base.ComboIndex;
- set
- {
- base.ComboIndex = value;
- foreach (var n in NestedHitObjects.OfType())
- n.ComboIndex = value;
- }
- }
-
- public override int IndexInCurrentCombo
- {
- get => base.IndexInCurrentCombo;
- set
- {
- base.IndexInCurrentCombo = value;
- foreach (var n in NestedHitObjects.OfType())
- n.IndexInCurrentCombo = value;
- }
- }
-
public readonly Bindable PathBindable = new Bindable();
public SliderPath Path
@@ -64,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Objects
{
PathBindable.Value = value;
endPositionCache.Invalidate();
+
+ updateNestedPositions();
}
}
@@ -75,14 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
base.Position = value;
-
- if (HeadCircle != null)
- HeadCircle.Position = value;
-
- if (TailCircle != null)
- TailCircle.Position = EndPosition;
-
endPositionCache.Invalidate();
+
+ updateNestedPositions();
}
}
@@ -100,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Objects
///
internal float LazyTravelDistance;
- public List> NodeSamples { get; set; } = new List>();
+ public List> NodeSamples { get; set; } = new List>();
private int repeatCount;
@@ -138,6 +108,12 @@ namespace osu.Game.Rulesets.Osu.Objects
public HitCircle HeadCircle;
public SliderTailCircle TailCircle;
+ public Slider()
+ {
+ SamplesBindable.ItemsAdded += _ => updateNestedSamples();
+ SamplesBindable.ItemsRemoved += _ => updateNestedSamples();
+ }
+
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -145,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
@@ -158,18 +134,6 @@ namespace osu.Game.Rulesets.Osu.Objects
foreach (var e in
SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
{
- var firstSample = Samples.Find(s => s.Name == HitSampleInfo.HIT_NORMAL)
- ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
- var sampleList = new List();
-
- if (firstSample != null)
- sampleList.Add(new HitSampleInfo
- {
- Bank = firstSample.Bank,
- Volume = firstSample.Volume,
- Name = @"slidertick",
- });
-
switch (e.Type)
{
case SliderEventType.Tick:
@@ -181,7 +145,6 @@ namespace osu.Game.Rulesets.Osu.Objects
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
- Samples = sampleList
});
break;
@@ -190,10 +153,8 @@ namespace osu.Game.Rulesets.Osu.Objects
{
StartTime = e.Time,
Position = Position,
- Samples = getNodeSamples(0),
+ StackHeight = StackHeight,
SampleControlPoint = SampleControlPoint,
- IndexInCurrentCombo = IndexInCurrentCombo,
- ComboIndex = ComboIndex,
});
break;
@@ -205,8 +166,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
StartTime = e.Time,
Position = EndPosition,
- IndexInCurrentCombo = IndexInCurrentCombo,
- ComboIndex = ComboIndex,
+ StackHeight = StackHeight
});
break;
@@ -219,18 +179,54 @@ namespace osu.Game.Rulesets.Osu.Objects
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
- Samples = getNodeSamples(e.SpanIndex + 1)
});
break;
}
}
+
+ updateNestedSamples();
}
- private List getNodeSamples(int nodeIndex) =>
+ private void updateNestedPositions()
+ {
+ if (HeadCircle != null)
+ HeadCircle.Position = Position;
+
+ if (TailCircle != null)
+ TailCircle.Position = EndPosition;
+ }
+
+ private void updateNestedSamples()
+ {
+ var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
+ ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
+ var sampleList = new List();
+
+ if (firstSample != null)
+ {
+ sampleList.Add(new HitSampleInfo
+ {
+ Bank = firstSample.Bank,
+ Volume = firstSample.Volume,
+ Name = @"slidertick",
+ });
+ }
+
+ foreach (var tick in NestedHitObjects.OfType())
+ tick.Samples = sampleList;
+
+ foreach (var repeat in NestedHitObjects.OfType())
+ repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1);
+
+ if (HeadCircle != null)
+ HeadCircle.Samples = getNodeSamples(0);
+ }
+
+ private IList getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
public override Judgement CreateJudgement() => new OsuJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index 7e540a577b..14c3369967 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
@@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public override Judgement CreateJudgement() => new OsuSliderTailJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
index af7cf5b144..a49f4cef8b 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
@@ -32,6 +32,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public override Judgement CreateJudgement() => new OsuJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 2e7b763966..2441a1449d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -33,6 +33,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public override Judgement CreateJudgement() => new OsuJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json
index b994cbd85a..004e7940d1 100644
--- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json
+++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json
@@ -143,14 +143,14 @@
"Objects": [{
"StartTime": 34989,
"EndTime": 34989,
- "X": 163,
- "Y": 138
+ "X": 156.597382,
+ "Y": 131.597382
},
{
"StartTime": 35018,
"EndTime": 35018,
- "X": 188,
- "Y": 138
+ "X": 181.597382,
+ "Y": 131.597382
}
]
},
@@ -159,14 +159,14 @@
"Objects": [{
"StartTime": 35106,
"EndTime": 35106,
- "X": 163,
- "Y": 138
+ "X": 159.798691,
+ "Y": 134.798691
},
{
"StartTime": 35135,
"EndTime": 35135,
- "X": 188,
- "Y": 138
+ "X": 184.798691,
+ "Y": 134.798691
}
]
},
@@ -191,20 +191,20 @@
"Objects": [{
"StartTime": 35695,
"EndTime": 35695,
- "X": 166,
- "Y": 76
+ "X": 162.798691,
+ "Y": 72.79869
},
{
"StartTime": 35871,
"EndTime": 35871,
- "X": 240.99855,
- "Y": 75.53417
+ "X": 237.797241,
+ "Y": 72.33286
},
{
"StartTime": 36011,
"EndTime": 36011,
- "X": 315.9971,
- "Y": 75.0683441
+ "X": 312.795776,
+ "Y": 71.8670349
}
]
},
@@ -235,20 +235,20 @@
"Objects": [{
"StartTime": 36518,
"EndTime": 36518,
- "X": 166,
- "Y": 76
+ "X": 169.201309,
+ "Y": 79.20131
},
{
"StartTime": 36694,
"EndTime": 36694,
- "X": 240.99855,
- "Y": 75.53417
+ "X": 244.19986,
+ "Y": 78.73548
},
{
"StartTime": 36834,
"EndTime": 36834,
- "X": 315.9971,
- "Y": 75.0683441
+ "X": 319.198425,
+ "Y": 78.26965
}
]
},
@@ -257,20 +257,20 @@
"Objects": [{
"StartTime": 36929,
"EndTime": 36929,
- "X": 315,
- "Y": 75
+ "X": 324.603943,
+ "Y": 84.6039352
},
{
"StartTime": 37105,
"EndTime": 37105,
- "X": 240.001526,
- "Y": 75.47769
+ "X": 249.605469,
+ "Y": 85.08163
},
{
"StartTime": 37245,
"EndTime": 37245,
- "X": 165.003052,
- "Y": 75.95539
+ "X": 174.607,
+ "Y": 85.5593262
}
]
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
index 83d507f64b..93ae0371df 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Skinning;
using osuTK;
@@ -25,13 +24,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
private readonly IBindable state = new Bindable();
-
private readonly Bindable accentColour = new Bindable();
+ private readonly IBindable indexInCurrentCombo = new Bindable();
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
{
+ OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
+
Sprite hitCircleSprite;
+ SkinnableSpriteText hitCircleText;
InternalChildren = new Drawable[]
{
@@ -42,14 +44,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
- new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
+ hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
- }, confineMode: ConfineMode.NoScaling)
- {
- Text = (((IHasComboInformation)drawableObject.HitObject).IndexInCurrentCombo + 1).ToString()
- },
+ }, confineMode: ConfineMode.NoScaling),
new Sprite
{
Texture = skin.GetTexture("hitcircleoverlay"),
@@ -63,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
accentColour.BindTo(drawableObject.AccentColour);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
+
+ indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
+ indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
}
private void updateState(ValueChangedEvent state)
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index b32dfd483f..80291c002e 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -40,9 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
for (int i = 0; i < max_sprites; i++)
{
- // InvalidationID 1 forces an update of each part of the cursor trail the first time ApplyState is run on the draw node
- // This is to prevent garbage data from being sent to the vertex shader, resulting in visual issues on some platforms
- parts[i].InvalidationID = 1;
+ // -1 signals that the part is unusable, and should not be drawn
+ parts[i].InvalidationID = -1;
}
}
@@ -112,7 +111,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
for (int i = 0; i < parts.Length; ++i)
{
parts[i].Time -= time;
- ++parts[i].InvalidationID;
+
+ if (parts[i].InvalidationID != -1)
+ ++parts[i].InvalidationID;
}
time = 0;
@@ -205,8 +206,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public TrailDrawNode(CursorTrail source)
: base(source)
{
- for (int i = 0; i < max_sprites; i++)
- parts[i].InvalidationID = 0;
}
public override void ApplyState()
@@ -218,11 +217,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
size = Source.partSize;
time = Source.time;
- for (int i = 0; i < Source.parts.Length; ++i)
- {
- if (Source.parts[i].InvalidationID > parts[i].InvalidationID)
- parts[i] = Source.parts[i];
- }
+ Source.parts.CopyTo(parts, 0);
}
public override void Draw(Action vertexAction)
@@ -234,6 +229,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
for (int i = 0; i < parts.Length; ++i)
{
+ if (parts[i].InvalidationID == -1)
+ continue;
+
vertexBatch.DrawTime = parts[i].Time;
Vector2 pos = parts[i].Position;
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
index 41a02deaca..0aa8661fd3 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
@@ -2,14 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Beatmaps;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Skinning;
using osuTK;
@@ -23,12 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private bool cursorExpand;
- private Bindable cursorScale;
- private Bindable autoCursorScale;
- private readonly IBindable beatmap = new Bindable();
-
private Container expandTarget;
- private Drawable scaleTarget;
public OsuCursor()
{
@@ -43,43 +35,19 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
[BackgroundDependencyLoader]
- private void load(OsuConfigManager config, IBindable beatmap)
+ private void load()
{
InternalChild = expandTarget = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Child = scaleTarget = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
+ Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
}
};
-
- this.beatmap.BindTo(beatmap);
- this.beatmap.ValueChanged += _ => calculateScale();
-
- cursorScale = config.GetBindable(OsuSetting.GameplayCursorSize);
- cursorScale.ValueChanged += _ => calculateScale();
-
- autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
- autoCursorScale.ValueChanged += _ => calculateScale();
-
- calculateScale();
- }
-
- private void calculateScale()
- {
- float scale = cursorScale.Value;
-
- if (autoCursorScale.Value && beatmap.Value != null)
- {
- // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
- scale *= 1f - 0.7f * (1f + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
- }
-
- scaleTarget.Scale = new Vector2(scale);
}
private const float pressed_scale = 1.2f;
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index a944ff88c6..6433ced624 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -8,6 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
@@ -27,6 +29,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Drawable cursorTrail;
+ public Bindable CursorScale;
+ private Bindable userCursorScale;
+ private Bindable autoCursorScale;
+ private readonly IBindable beatmap = new Bindable();
+
public OsuCursorContainer()
{
InternalChild = fadeContainer = new Container
@@ -37,9 +44,41 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
[BackgroundDependencyLoader(true)]
- private void load(OsuRulesetConfigManager config)
+ private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig, IBindable beatmap)
{
- config?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
+ rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
+
+ this.beatmap.BindTo(beatmap);
+ this.beatmap.ValueChanged += _ => calculateScale();
+
+ userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize);
+ userCursorScale.ValueChanged += _ => calculateScale();
+
+ autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
+ autoCursorScale.ValueChanged += _ => calculateScale();
+
+ CursorScale = new Bindable();
+ CursorScale.ValueChanged += e => ActiveCursor.Scale = cursorTrail.Scale = new Vector2(e.NewValue);
+
+ calculateScale();
+ }
+
+ private void calculateScale()
+ {
+ float scale = userCursorScale.Value;
+
+ if (autoCursorScale.Value && beatmap.Value != null)
+ {
+ // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
+ scale *= 1f - 0.7f * (1f + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
+ }
+
+ CursorScale.Value = scale;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
showTrail.BindValueChanged(v => cursorTrail.FadeTo(v.NewValue ? 1 : 0, 200), true);
}
@@ -90,13 +129,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected override void PopIn()
{
fadeContainer.FadeTo(1, 300, Easing.OutQuint);
- ActiveCursor.ScaleTo(1, 400, Easing.OutQuint);
+ ActiveCursor.ScaleTo(CursorScale.Value, 400, Easing.OutQuint);
}
protected override void PopOut()
{
fadeContainer.FadeTo(0.05f, 450, Easing.OutQuint);
- ActiveCursor.ScaleTo(0.8f, 450, Easing.OutQuint);
+ ActiveCursor.ScaleTo(CursorScale.Value * 0.8f, 450, Easing.OutQuint);
}
private class DefaultCursorTrail : CursorTrail
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index d1757de445..6d1ea4bbfc 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -9,7 +9,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.UI;
-using System.Linq;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
@@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
private readonly ApproachCircleProxyContainer approachCircles;
private readonly JudgementContainer judgementLayer;
- private readonly ConnectionRenderer connectionLayer;
+ private readonly FollowPointRenderer followPoints;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
InternalChildren = new Drawable[]
{
- connectionLayer = new FollowPointRenderer
+ followPoints = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
@@ -57,24 +56,25 @@ namespace osu.Game.Rulesets.Osu.UI
public override void Add(DrawableHitObject h)
{
h.OnNewResult += onNewResult;
-
- if (h is IDrawableHitObjectWithProxiedApproach c)
+ h.OnLoadComplete += d =>
{
- var original = c.ProxiedLayer;
-
- // Hitobjects only have lifetimes set on LoadComplete. For nested hitobjects (e.g. SliderHeads), this only happens when the parenting slider becomes visible.
- // This delegation is required to make sure that the approach circles for those not-yet-loaded objects aren't added prematurely.
- original.OnLoadComplete += addApproachCircleProxy;
- }
+ if (d is IDrawableHitObjectWithProxiedApproach c)
+ approachCircles.Add(c.ProxiedLayer.CreateProxy());
+ };
base.Add(h);
+
+ followPoints.AddFollowPoints((DrawableOsuHitObject)h);
}
- private void addApproachCircleProxy(Drawable d) => approachCircles.Add(d.CreateProxy());
-
- public override void PostProcess()
+ public override bool Remove(DrawableHitObject h)
{
- connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType();
+ bool result = base.Remove(h);
+
+ if (result)
+ followPoints.RemoveFollowPoints((DrawableOsuHitObject)h);
+
+ return result;
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
index 9e5df0d6b1..3b18e41f30 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
@@ -1,15 +1,15 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.UI.Cursor;
-using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
@@ -18,9 +18,11 @@ namespace osu.Game.Rulesets.Osu.UI
{
public class OsuResumeOverlay : ResumeOverlay
{
+ private Container cursorScaleContainer;
private OsuClickToResumeCursor clickToResumeCursor;
- private GameplayCursorContainer localCursorContainer;
+ private OsuCursorContainer localCursorContainer;
+ private Bindable localCursorScale;
public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
@@ -29,24 +31,38 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader]
private void load()
{
- Add(clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume });
+ Add(cursorScaleContainer = new Container
+ {
+ RelativePositionAxes = Axes.Both,
+ Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
+ });
}
- public override void Show()
+ protected override void PopIn()
{
- base.Show();
- clickToResumeCursor.ShowAt(GameplayCursor.ActiveCursor.Position);
+ base.PopIn();
+
+ GameplayCursor.ActiveCursor.Hide();
+ cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
+ clickToResumeCursor.Appear();
if (localCursorContainer == null)
+ {
Add(localCursorContainer = new OsuCursorContainer());
+
+ localCursorScale = new Bindable();
+ localCursorScale.BindTo(localCursorContainer.CursorScale);
+ localCursorScale.BindValueChanged(scale => cursorScaleContainer.Scale = new Vector2(scale.NewValue), true);
+ }
}
- public override void Hide()
+ protected override void PopOut()
{
+ base.PopOut();
+
localCursorContainer?.Expire();
localCursorContainer = null;
-
- base.Hide();
+ GameplayCursor?.ActiveCursor?.Show();
}
protected override bool OnHover(HoverEvent e) => true;
@@ -82,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.UI
case OsuAction.RightButton:
if (!IsHovered) return false;
- this.ScaleTo(new Vector2(2), TRANSITION_TIME, Easing.OutQuint);
+ this.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
return true;
@@ -93,11 +109,10 @@ namespace osu.Game.Rulesets.Osu.UI
public bool OnReleased(OsuAction action) => false;
- public void ShowAt(Vector2 activeCursorPosition) => Schedule(() =>
+ public void Appear() => Schedule(() =>
{
updateColour();
- this.MoveTo(activeCursorPosition);
- this.ScaleTo(new Vector2(4)).Then().ScaleTo(Vector2.One, 1000, Easing.OutQuint);
+ this.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint);
});
private void updateColour()
diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
index b0ca314551..bffeaabb55 100644
--- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
+++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
@@ -1,9 +1,7 @@
-
- netstandard2.0
+ netstandard2.1
Library
- AnyCPU
true
click the circles. to the beat.
diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml
index cd4b74aa16..d9de0fde4e 100644
--- a/osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj b/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj
index 3e46bb89af..8ee640cd99 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj
@@ -1,6 +1,5 @@
-
+
-
Debug
iPhoneSimulator
@@ -33,5 +32,4 @@
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json
index 3ef78bf6c8..7d929e6bbf 100644
--- a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json
+++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Debug/netcoreapp2.2/osu.Game.Rulesets.Taiko.Tests.dll"
+ "${workspaceRoot}/bin/Debug/netcoreapp3.0/osu.Game.Rulesets.Taiko.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/netcoreapp2.2/osu.Game.Rulesets.Taiko.Tests.dll"
+ "${workspaceRoot}/bin/Release/netcoreapp3.0/osu.Game.Rulesets.Taiko.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
index 3aa461e779..8522a42739 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
@@ -53,6 +53,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Strong Rim", () => addRimHit(true));
AddStep("Add bar line", () => addBarLine(false));
AddStep("Add major bar line", () => addBarLine(true));
+ AddStep("Add centre w/ bar line", () =>
+ {
+ addCentreHit(false);
+ addBarLine(true);
+ });
AddStep("Height test 1", () => changePlayfieldSize(1));
AddStep("Height test 2", () => changePlayfieldSize(2));
AddStep("Height test 3", () => changePlayfieldSize(3));
@@ -61,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Reset height", () => changePlayfieldSize(6));
var controlPointInfo = new ControlPointInfo();
- controlPointInfo.TimingPoints.Add(new TimingControlPoint());
+ controlPointInfo.Add(0, new TimingControlPoint());
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
{
@@ -137,7 +142,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
var cpi = new ControlPointInfo();
- cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai });
+ cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
@@ -152,7 +157,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
var cpi = new ControlPointInfo();
- cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai });
+ cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
@@ -234,7 +239,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
private class TestStrongNestedHit : DrawableStrongNestedHit
{
public TestStrongNestedHit(DrawableHitObject mainObject)
- : base(null, mainObject)
+ : base(new StrongHitObject { StartTime = mainObject.HitObject.StartTime }, mainObject)
{
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index b0e0efdc68..b5bd384e05 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,14 +2,14 @@
-
+
WinExe
- netcoreapp2.2
+ netcoreapp3.0
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
index ad2596931d..aaf113f216 100644
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
+++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
@@ -19,12 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Audio
{
this.controlPoints = controlPoints;
- IEnumerable samplePoints;
- if (controlPoints.SamplePoints.Count == 0)
- // Get the default sample point
- samplePoints = new[] { controlPoints.SamplePointAt(double.MinValue) };
- else
- samplePoints = controlPoints.SamplePoints;
+ IEnumerable samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
foreach (var s in samplePoints)
{
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index f0cf8d9c7d..180e0d8309 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
var curveData = obj as IHasCurve;
// Old osu! used hit sounding to determine various hit type information
- List samples = obj.Samples;
+ IList samples = obj.Samples;
bool strong = samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
@@ -117,13 +117,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
{
- List> allSamples = curveData != null ? curveData.NodeSamples : new List>(new[] { samples });
+ List> allSamples = curveData != null ? curveData.NodeSamples : new List>(new[] { samples });
int i = 0;
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
{
- List currentSamples = allSamples[i];
+ IList currentSamples = allSamples[i];
bool isRim = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
strong = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
diff --git a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
new file mode 100644
index 0000000000..2afbbc737c
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
@@ -0,0 +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.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Objects
+{
+ public class BarLine : TaikoHitObject, IBarLine
+ {
+ public bool Major { get; set; }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
index f5b75a781b..4d3a1a3f8a 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
@@ -5,7 +5,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index 8e16a21199..338fd9e20f 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -1,17 +1,18 @@
// 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.MathUtils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
-using osuTK;
using osuTK.Graphics;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
@@ -28,31 +29,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
private int rollingHits;
+ private readonly Container tickContainer;
+
+ private Color4 colourIdle;
+ private Color4 colourEngaged;
+
public DrawableDrumRoll(DrumRoll drumRoll)
: base(drumRoll)
{
RelativeSizeAxes = Axes.Y;
-
- Container tickContainer;
MainPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both });
-
- foreach (var tick in drumRoll.NestedHitObjects.OfType())
- {
- var newTick = new DrawableDrumRollTick(tick);
- newTick.OnNewResult += onNewTickResult;
-
- AddNested(newTick);
- tickContainer.Add(newTick);
- }
}
- protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece();
-
- public override bool OnPressed(TaikoAction action) => false;
-
- private Color4 colourIdle;
- private Color4 colourEngaged;
-
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -60,14 +48,57 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
colourEngaged = colours.YellowDarker;
}
- private void onNewTickResult(DrawableHitObject obj, JudgementResult result)
+ protected override void LoadComplete()
{
+ base.LoadComplete();
+
+ OnNewResult += onNewResult;
+ }
+
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableDrumRollTick tick:
+ tickContainer.Add(tick);
+ break;
+ }
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ tickContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case DrumRollTick tick:
+ return new DrawableDrumRollTick(tick);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
+ protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece();
+
+ public override bool OnPressed(TaikoAction action) => false;
+
+ private void onNewResult(DrawableHitObject obj, JudgementResult result)
+ {
+ if (!(obj is DrawableDrumRollTick))
+ return;
+
if (result.Type > HitResult.Miss)
rollingHits++;
else
rollingHits--;
- rollingHits = MathHelper.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
+ rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
MainPiece.FadeAccent(newColour, 100);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
index 07af7fe7e0..fa39819199 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@@ -11,9 +10,9 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
-using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
@@ -30,8 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
private const double ring_appear_offset = 100;
- private readonly List ticks = new List();
-
+ private readonly Container ticks;
private readonly Container bodyContainer;
private readonly CircularContainer targetRing;
private readonly CircularContainer expandingRing;
@@ -108,16 +106,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
});
+ AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both });
+
MainPiece.Add(symbol = new SwellSymbolPiece());
-
- foreach (var tick in HitObject.NestedHitObjects.OfType())
- {
- var vis = new DrawableSwellTick(tick);
-
- ticks.Add(vis);
- AddInternal(vis);
- AddNested(vis);
- }
}
[BackgroundDependencyLoader]
@@ -136,11 +127,49 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Width *= Parent.RelativeChildSize.X;
}
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableSwellTick tick:
+ ticks.Add(tick);
+ break;
+ }
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ ticks.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case SwellTick tick:
+ return new DrawableSwellTick(tick);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (userTriggered)
{
- var nextTick = ticks.Find(j => !j.IsHit);
+ DrawableSwellTick nextTick = null;
+
+ foreach (var t in ticks)
+ {
+ if (!t.IsHit)
+ {
+ nextTick = t;
+ break;
+ }
+ }
nextTick?.TriggerResult(HitResult.Great);
@@ -149,7 +178,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
var completion = (float)numHits / HitObject.RequiredHits;
expandingRing
- .FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50)
+ .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50)
.Then()
.FadeTo(completion / 8, 2000, Easing.OutQuint);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 423f65b2d3..0db6498c12 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -11,6 +11,7 @@ using osu.Game.Audio;
using System.Collections.Generic;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -109,11 +110,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public override Vector2 OriginPosition => new Vector2(DrawHeight / 2);
- protected readonly Vector2 BaseSize;
+ public new TaikoHitType HitObject;
+ protected readonly Vector2 BaseSize;
protected readonly TaikoPiece MainPiece;
- public new TaikoHitType HitObject;
+ private readonly Container strongHitContainer;
protected DrawableTaikoHitObject(TaikoHitType hitObject)
: base(hitObject)
@@ -129,17 +131,38 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Content.Add(MainPiece = CreateMainPiece());
MainPiece.KiaiMode = HitObject.Kiai;
- var strongObject = HitObject.NestedHitObjects.OfType().FirstOrDefault();
+ AddInternal(strongHitContainer = new Container());
+ }
- if (strongObject != null)
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
{
- var strongHit = CreateStrongHit(strongObject);
-
- AddNested(strongHit);
- AddInternal(strongHit);
+ case DrawableStrongNestedHit strong:
+ strongHitContainer.Add(strong);
+ break;
}
}
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ strongHitContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case StrongHitObject strong:
+ return CreateStrongHit(strong);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
// Normal and clap samples are handled by the drum
protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 4e02c76a8b..8956ca9c19 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -88,6 +88,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override Judgement CreateJudgement() => new TaikoDrumRollJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs
index c466ca7c8a..8a8be3e38d 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs
@@ -27,6 +27,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs
index d660149528..72a04698be 100644
--- a/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs
@@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
public override Judgement CreateJudgement() => new TaikoStrongJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs
index f96c033dce..e60984596d 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs
@@ -35,6 +35,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override Judgement CreateJudgement() => new TaikoSwellJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs
index 68212e8f12..91f4d3dbe7 100644
--- a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs
@@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
public override Judgement CreateJudgement() => new TaikoSwellTickJudgement();
- protected override HitWindows CreateHitWindows() => null;
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 5caa9e4626..fc109bf6a6 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
- new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
+ new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
}
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 84464b199e..980f5ea340 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI;
using osuTK;
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
base.Update();
- float aspectAdjust = MathHelper.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
+ float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Size = new Vector2(1, default_relative_height * aspectAdjust);
}
}
diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj
index 656ebcc7c2..ebed8c6d7c 100644
--- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj
+++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj
@@ -1,9 +1,7 @@
-
- netstandard2.0
+ netstandard2.1
Library
- AnyCPU
true
bash the drum. to the beat.
diff --git a/osu.Game.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Tests.Android/Properties/AndroidManifest.xml
index bb996dc5ca..4a63f0c357 100644
--- a/osu.Game.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Tests.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
index 5c0713b895..ca68369ebb 100644
--- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
+++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
@@ -1,6 +1,5 @@
-
+
-
Debug
iPhoneSimulator
@@ -48,5 +47,4 @@
-
\ No newline at end of file
diff --git a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs b/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs
new file mode 100644
index 0000000000..98e630abd2
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs
@@ -0,0 +1,154 @@
+// 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 Microsoft.EntityFrameworkCore.Internal;
+using NUnit.Framework;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+
+namespace osu.Game.Tests.Beatmaps
+{
+ [TestFixture]
+ public class EditorBeatmapTest
+ {
+ ///
+ /// Tests that the addition event is correctly invoked after a hitobject is added.
+ ///
+ [Test]
+ public void TestHitObjectAddEvent()
+ {
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+
+ HitObject addedObject = null;
+ editorBeatmap.HitObjectAdded += h => addedObject = h;
+
+ var hitCircle = new HitCircle();
+
+ editorBeatmap.Add(hitCircle);
+ Assert.That(addedObject, Is.EqualTo(hitCircle));
+ }
+
+ ///
+ /// Tests that the removal event is correctly invoked after a hitobject is removed.
+ ///
+ [Test]
+ public void HitObjectRemoveEvent()
+ {
+ var hitCircle = new HitCircle();
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+
+ HitObject removedObject = null;
+ editorBeatmap.HitObjectRemoved += h => removedObject = h;
+
+ editorBeatmap.Remove(hitCircle);
+ Assert.That(removedObject, Is.EqualTo(hitCircle));
+ }
+
+ ///
+ /// Tests that the changed event is correctly invoked after the start time of a hitobject is changed.
+ /// This tests for hitobjects which were already present before the editor beatmap was constructed.
+ ///
+ [Test]
+ public void TestInitialHitObjectStartTimeChangeEvent()
+ {
+ var hitCircle = new HitCircle();
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+
+ HitObject changedObject = null;
+ editorBeatmap.StartTimeChanged += h => changedObject = h;
+
+ hitCircle.StartTime = 1000;
+ Assert.That(changedObject, Is.EqualTo(hitCircle));
+ }
+
+ ///
+ /// Tests that the changed event is correctly invoked after the start time of a hitobject is changed.
+ /// This tests for hitobjects which were added to an existing editor beatmap.
+ ///
+ [Test]
+ public void TestAddedHitObjectStartTimeChangeEvent()
+ {
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+
+ HitObject changedObject = null;
+ editorBeatmap.StartTimeChanged += h => changedObject = h;
+
+ var hitCircle = new HitCircle();
+
+ editorBeatmap.Add(hitCircle);
+ Assert.That(changedObject, Is.Null);
+
+ hitCircle.StartTime = 1000;
+ Assert.That(changedObject, Is.EqualTo(hitCircle));
+ }
+
+ ///
+ /// Tests that the channged event is not invoked after a hitobject is removed from the beatmap/
+ ///
+ [Test]
+ public void TestRemovedHitObjectStartTimeChangeEvent()
+ {
+ var hitCircle = new HitCircle();
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+
+ HitObject changedObject = null;
+ editorBeatmap.StartTimeChanged += h => changedObject = h;
+
+ editorBeatmap.Remove(hitCircle);
+ Assert.That(changedObject, Is.Null);
+
+ hitCircle.StartTime = 1000;
+ Assert.That(changedObject, Is.Null);
+ }
+
+ ///
+ /// Tests that an added hitobject is correctly inserted to preserve the sorting order of the beatmap.
+ ///
+ [Test]
+ public void TestAddHitObjectInMiddle()
+ {
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap
+ {
+ HitObjects =
+ {
+ new HitCircle(),
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 2000 },
+ }
+ });
+
+ var hitCircle = new HitCircle { StartTime = 1000 };
+ editorBeatmap.Add(hitCircle);
+ Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
+ Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(3));
+ }
+
+ ///
+ /// Tests that the beatmap remains correctly sorted after the start time of a hitobject is changed.
+ ///
+ [Test]
+ public void TestResortWhenStartTimeChanged()
+ {
+ var hitCircle = new HitCircle { StartTime = 1000 };
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap
+ {
+ HitObjects =
+ {
+ new HitCircle(),
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 1000 },
+ hitCircle,
+ new HitCircle { StartTime = 2000 },
+ }
+ });
+
+ hitCircle.StartTime = 0;
+ Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
+ Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(1));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 535320530d..2ecc516919 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Timing;
+using osu.Game.IO;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -30,13 +31,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
public void TestDecodeBeatmapVersion()
{
using (var resStream = TestResources.OpenResource("beatmap-version.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var decoder = Decoder.GetDecoder(stream);
-
- stream.BaseStream.Position = 0;
- stream.DiscardBufferedData();
-
var working = new TestWorkingBeatmap(decoder.Decode(stream));
Assert.AreEqual(6, working.BeatmapInfo.BeatmapVersion);
@@ -51,7 +48,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var beatmapInfo = beatmap.BeatmapInfo;
@@ -75,7 +72,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var beatmapInfo = decoder.Decode(stream).BeatmapInfo;
@@ -101,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var beatmapInfo = beatmap.BeatmapInfo;
@@ -126,7 +123,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var difficulty = decoder.Decode(stream).BeatmapInfo.BaseDifficulty;
@@ -145,7 +142,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var metadata = beatmap.Metadata;
@@ -164,15 +161,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var controlPoints = beatmap.ControlPointInfo;
Assert.AreEqual(4, controlPoints.TimingPoints.Count);
- Assert.AreEqual(42, controlPoints.DifficultyPoints.Count);
- Assert.AreEqual(42, controlPoints.SamplePoints.Count);
- Assert.AreEqual(42, controlPoints.EffectPoints.Count);
+ Assert.AreEqual(5, controlPoints.DifficultyPoints.Count);
+ Assert.AreEqual(34, controlPoints.SamplePoints.Count);
+ Assert.AreEqual(8, controlPoints.EffectPoints.Count);
var timingPoint = controlPoints.TimingPointAt(0);
Assert.AreEqual(956, timingPoint.Time);
@@ -194,7 +191,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
difficultyPoint = controlPoints.DifficultyPointAt(48428);
- Assert.AreEqual(48428, difficultyPoint.Time);
+ Assert.AreEqual(0, difficultyPoint.Time);
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
difficultyPoint = controlPoints.DifficultyPointAt(116999);
@@ -227,7 +224,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsFalse(effectPoint.OmitFirstBarLine);
effectPoint = controlPoints.EffectPointAt(119637);
- Assert.AreEqual(119637, effectPoint.Time);
+ Assert.AreEqual(95901, effectPoint.Time);
Assert.IsFalse(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
}
@@ -239,7 +236,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("overlapping-control-points.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var controlPoints = decoder.Decode(stream).ControlPointInfo;
@@ -265,13 +262,28 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestTimingPointResetsSpeedMultiplier()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var controlPoints = decoder.Decode(stream).ControlPointInfo;
+
+ Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1));
+ }
+ }
+
[Test]
public void TestDecodeBeatmapColours()
{
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var comboColors = decoder.Decode(stream).ComboColours;
@@ -297,7 +309,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("hitobject-combo-offset.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
@@ -320,7 +332,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("hitobject-combo-offset.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
@@ -343,7 +355,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@@ -365,13 +377,30 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeControlPointDifficultyChange()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var controlPointInfo = decoder.Decode(stream).ControlPointInfo;
+
+ Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1));
+ Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10));
+ Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d));
+ Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5));
+ }
+ }
+
[Test]
public void TestDecodeControlPointCustomSampleBank()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("controlpoint-custom-samplebank.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@@ -393,7 +422,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("hitobject-custom-samplebank.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@@ -411,7 +440,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("hitobject-file-samples.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@@ -431,7 +460,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("slider-samples.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@@ -475,7 +504,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("hitobject-no-addition-bank.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@@ -489,10 +518,132 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var badResStream = TestResources.OpenResource("invalid-events.osu"))
- using (var badStream = new StreamReader(badResStream))
+ using (var badStream = new LineBufferedReader(badResStream))
{
Assert.DoesNotThrow(() => decoder.Decode(badStream));
}
}
+
+ [Test]
+ public void TestFallbackDecoderForCorruptedHeader()
+ {
+ Decoder decoder = null;
+ Beatmap beatmap = null;
+
+ using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder(stream));
+ Assert.IsInstanceOf(decoder);
+ Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
+ Assert.IsNotNull(beatmap);
+ Assert.AreEqual("Beatmap with corrupted header", beatmap.Metadata.Title);
+ Assert.AreEqual("Evil Hacker", beatmap.Metadata.AuthorString);
+ }
+ }
+
+ [Test]
+ public void TestFallbackDecoderForMissingHeader()
+ {
+ Decoder decoder = null;
+ Beatmap beatmap = null;
+
+ using (var resStream = TestResources.OpenResource("missing-header.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder(stream));
+ Assert.IsInstanceOf(decoder);
+ Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
+ Assert.IsNotNull(beatmap);
+ Assert.AreEqual("Beatmap with no header", beatmap.Metadata.Title);
+ Assert.AreEqual("Incredibly Evil Hacker", beatmap.Metadata.AuthorString);
+ }
+ }
+
+ [Test]
+ public void TestDecodeFileWithEmptyLinesAtStart()
+ {
+ Decoder decoder = null;
+ Beatmap beatmap = null;
+
+ using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder(stream));
+ Assert.IsInstanceOf(decoder);
+ Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
+ Assert.IsNotNull(beatmap);
+ Assert.AreEqual("Empty lines at start", beatmap.Metadata.Title);
+ Assert.AreEqual("Edge Case Hunter", beatmap.Metadata.AuthorString);
+ }
+ }
+
+ [Test]
+ public void TestDecodeFileWithEmptyLinesAndNoHeader()
+ {
+ Decoder decoder = null;
+ Beatmap beatmap = null;
+
+ using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder(stream));
+ Assert.IsInstanceOf(decoder);
+ Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
+ Assert.IsNotNull(beatmap);
+ Assert.AreEqual("The dog ate the file header", beatmap.Metadata.Title);
+ Assert.AreEqual("Why does this keep happening", beatmap.Metadata.AuthorString);
+ }
+ }
+
+ [Test]
+ public void TestDecodeFileWithContentImmediatelyAfterHeader()
+ {
+ Decoder decoder = null;
+ Beatmap beatmap = null;
+
+ using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder(stream));
+ Assert.IsInstanceOf(decoder);
+ Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
+ Assert.IsNotNull(beatmap);
+ Assert.AreEqual("No empty line delimiting header from contents", beatmap.Metadata.Title);
+ Assert.AreEqual("Edge Case Hunter", beatmap.Metadata.AuthorString);
+ }
+ }
+
+ [Test]
+ public void TestDecodeEmptyFile()
+ {
+ using (var resStream = new MemoryStream())
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ Assert.Throws(() => Decoder.GetDecoder(stream));
+ }
+ }
+
+ [Test]
+ public void TestAllowFallbackDecoderOverwrite()
+ {
+ Decoder decoder = null;
+
+ using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder(stream));
+ Assert.IsInstanceOf(decoder);
+ }
+
+ Assert.DoesNotThrow(LegacyDifficultyCalculatorBeatmapDecoder.Register);
+
+ using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder(stream));
+ Assert.IsInstanceOf(decoder);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs
index b4d219456c..335a6aeeb0 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using System.IO;
using NUnit.Framework;
using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats
@@ -18,7 +18,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LineLoggingDecoder(14);
using (var resStream = TestResources.OpenResource("comments.osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
decoder.Decode(stream);
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
index 953763c95d..66d53d7e7b 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
@@ -1,12 +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 System.IO;
using System.Linq;
using NUnit.Framework;
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
using osu.Game.Storyboards;
using osu.Game.Tests.Resources;
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("Himeringo - Yotsuya-san ni Yoroshiku (RLC) [Winber1's Extreme].osu"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
@@ -94,7 +94,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("variable-with-suffix.osb"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
index 4859abbb8e..63346b8c9d 100644
--- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -148,13 +149,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
private Beatmap decode(string filename, out Beatmap jsonDecoded)
{
using (var stream = TestResources.OpenResource(filename))
- using (var sr = new StreamReader(stream))
+ using (var sr = new LineBufferedReader(stream))
{
var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr);
using (var ms = new MemoryStream())
using (var sw = new StreamWriter(ms))
- using (var sr2 = new StreamReader(ms))
+ using (var sr2 = new LineBufferedReader(ms))
{
sw.Write(legacyDecoded.Serialize());
sw.Flush();
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index 385ab4064d..4766411cbd 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportWhenClosed()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenClosed"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenClosed)))
{
try
{
@@ -46,7 +46,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDelete()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDelete"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDelete)))
{
try
{
@@ -67,7 +67,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenImport()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImport)))
{
try
{
@@ -94,7 +94,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportCorruptThenImport()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportCorruptThenImport)))
{
try
{
@@ -136,7 +136,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestRollbackOnFailure()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestRollbackOnFailure"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestRollbackOnFailure)))
{
try
{
@@ -171,7 +171,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var breakTemp = TestResources.GetTestBeatmapForImport();
- MemoryStream brokenOsu = new MemoryStream(new byte[] { 1, 3, 3, 7 });
+ MemoryStream brokenOsu = new MemoryStream();
MemoryStream brokenOsz = new MemoryStream(File.ReadAllBytes(breakTemp));
File.Delete(breakTemp);
@@ -213,7 +213,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenImportDifferentHash()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImportDifferentHash"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportDifferentHash)))
{
try
{
@@ -244,7 +244,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImport()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDeleteThenImport)))
{
try
{
@@ -272,7 +272,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(TestImportThenDeleteThenImportWithOnlineIDMismatch)}-{set}"))
{
try
{
@@ -306,7 +306,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportWithDuplicateBeatmapIDs()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDuplicateBeatmapID"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateBeatmapIDs)))
{
try
{
@@ -392,7 +392,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWhenFileOpen()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenFileOpen"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenFileOpen)))
{
try
{
@@ -411,10 +411,52 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
+ [Test]
+ public async Task TestImportWithDuplicateHashes()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure)))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.AddEntry("duplicate.osu", Directory.GetFiles(extractedFolder, "*.osu").First());
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ await osu.Dependencies.Get().Import(temp);
+
+ ensureLoaded(osu);
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
[Test]
public async Task TestImportNestedStructure()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportNestedStructure"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure)))
{
try
{
@@ -456,9 +498,63 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
- public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null)
+ [Test]
+ public async Task TestImportWithIgnoredDirectoryInArchive()
{
- var temp = path ?? TestResources.GetTestBeatmapForImport();
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithIgnoredDirectoryInArchive)))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ string dataFolder = Path.Combine(extractedFolder, "actual_data");
+ string resourceForkFolder = Path.Combine(extractedFolder, "__MACOSX");
+ string resourceForkFilePath = Path.Combine(resourceForkFolder, ".extracted");
+
+ Directory.CreateDirectory(dataFolder);
+ Directory.CreateDirectory(resourceForkFolder);
+
+ using (var resourceForkFile = File.CreateText(resourceForkFilePath))
+ {
+ resourceForkFile.WriteLine("adding content so that it's not empty");
+ }
+
+ try
+ {
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(dataFolder);
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ var imported = await osu.Dependencies.Get().Import(temp);
+
+ ensureLoaded(osu);
+
+ Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored");
+ Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder");
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false)
+ {
+ var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
var manager = osu.Dependencies.Get();
diff --git a/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs
new file mode 100644
index 0000000000..25517ad615
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs
@@ -0,0 +1,125 @@
+// 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.IO;
+using System.Text;
+using NUnit.Framework;
+using osu.Game.IO;
+
+namespace osu.Game.Tests.Beatmaps.IO
+{
+ [TestFixture]
+ public class LineBufferedReaderTest
+ {
+ [Test]
+ public void TestReadLineByLine()
+ {
+ const string contents = "line 1\rline 2\nline 3";
+
+ using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
+ using (var bufferedReader = new LineBufferedReader(stream))
+ {
+ Assert.AreEqual("line 1", bufferedReader.ReadLine());
+ Assert.AreEqual("line 2", bufferedReader.ReadLine());
+ Assert.AreEqual("line 3", bufferedReader.ReadLine());
+ Assert.IsNull(bufferedReader.ReadLine());
+ }
+ }
+
+ [Test]
+ public void TestPeekLineOnce()
+ {
+ const string contents = "line 1\r\npeek this\nline 3";
+
+ using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
+ using (var bufferedReader = new LineBufferedReader(stream))
+ {
+ Assert.AreEqual("line 1", bufferedReader.ReadLine());
+ Assert.AreEqual("peek this", bufferedReader.PeekLine());
+ Assert.AreEqual("peek this", bufferedReader.ReadLine());
+ Assert.AreEqual("line 3", bufferedReader.ReadLine());
+ Assert.IsNull(bufferedReader.ReadLine());
+ }
+ }
+
+ [Test]
+ public void TestPeekLineMultipleTimes()
+ {
+ const string contents = "peek this once\nline 2\rpeek this a lot";
+
+ using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
+ using (var bufferedReader = new LineBufferedReader(stream))
+ {
+ Assert.AreEqual("peek this once", bufferedReader.PeekLine());
+ Assert.AreEqual("peek this once", bufferedReader.ReadLine());
+ Assert.AreEqual("line 2", bufferedReader.ReadLine());
+ Assert.AreEqual("peek this a lot", bufferedReader.PeekLine());
+ Assert.AreEqual("peek this a lot", bufferedReader.PeekLine());
+ Assert.AreEqual("peek this a lot", bufferedReader.PeekLine());
+ Assert.AreEqual("peek this a lot", bufferedReader.ReadLine());
+ Assert.IsNull(bufferedReader.ReadLine());
+ }
+ }
+
+ [Test]
+ public void TestPeekLineAtEndOfStream()
+ {
+ const string contents = "first line\r\nsecond line";
+
+ using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
+ using (var bufferedReader = new LineBufferedReader(stream))
+ {
+ Assert.AreEqual("first line", bufferedReader.ReadLine());
+ Assert.AreEqual("second line", bufferedReader.ReadLine());
+ Assert.IsNull(bufferedReader.PeekLine());
+ Assert.IsNull(bufferedReader.ReadLine());
+ Assert.IsNull(bufferedReader.PeekLine());
+ }
+ }
+
+ [Test]
+ public void TestPeekReadLineOnEmptyStream()
+ {
+ using (var stream = new MemoryStream())
+ using (var bufferedReader = new LineBufferedReader(stream))
+ {
+ Assert.IsNull(bufferedReader.PeekLine());
+ Assert.IsNull(bufferedReader.ReadLine());
+ Assert.IsNull(bufferedReader.ReadLine());
+ Assert.IsNull(bufferedReader.PeekLine());
+ }
+ }
+
+ [Test]
+ public void TestReadToEndNoPeeks()
+ {
+ const string contents = "first line\r\nsecond line";
+
+ using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
+ using (var bufferedReader = new LineBufferedReader(stream))
+ {
+ Assert.AreEqual(contents, bufferedReader.ReadToEnd());
+ }
+ }
+
+ [Test]
+ public void TestReadToEndAfterReadsAndPeeks()
+ {
+ const string contents = "this line is gone\rthis one shouldn't be\r\nthese ones\ndefinitely not";
+
+ using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
+ using (var bufferedReader = new LineBufferedReader(stream))
+ {
+ Assert.AreEqual("this line is gone", bufferedReader.ReadLine());
+ Assert.AreEqual("this one shouldn't be", bufferedReader.PeekLine());
+
+ var endingLines = bufferedReader.ReadToEnd().Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+ Assert.AreEqual(3, endingLines.Length);
+ Assert.AreEqual("this one shouldn't be", endingLines[0]);
+ Assert.AreEqual("these ones", endingLines[1]);
+ Assert.AreEqual("definitely not", endingLines[2]);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs
index 37e0565df0..022b2c1a59 100644
--- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Tests.Resources;
using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
using osu.Game.IO.Archives;
namespace osu.Game.Tests.Beatmaps.IO
@@ -50,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.IO
Beatmap beatmap;
- using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
+ using (var stream = new LineBufferedReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
beatmap = Decoder.GetDecoder(stream).Decode(stream);
var meta = beatmap.Metadata;
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index 9b4a90e9a9..fbb0416c45 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -273,6 +273,96 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(21, result.Links[0].Length);
}
+ [Test]
+ public void TestMarkdownFormatLinkWithInlineTitle()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [this link format](https://osu.ppy.sh \"osu!\") before..." });
+
+ Assert.AreEqual("I haven't seen this link format before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(16, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkWithInlineTitleAndEscapedQuotes()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [this link format](https://osu.ppy.sh \"inner quote \\\" just to confuse \") before..." });
+
+ Assert.AreEqual("I haven't seen this link format before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(16, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkWithUrlInTextAndInlineTitle()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [https://osu.ppy.sh](https://osu.ppy.sh \"https://osu.ppy.sh\") before..." });
+
+ Assert.AreEqual("I haven't seen https://osu.ppy.sh before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(18, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkWithUrlAndTextInTitle()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [oh no, text here! https://osu.ppy.sh](https://osu.ppy.sh) before..." });
+
+ Assert.AreEqual("I haven't seen oh no, text here! https://osu.ppy.sh before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(36, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkWithMisleadingUrlInText()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [https://google.com](https://osu.ppy.sh) before..." });
+
+ Assert.AreEqual("I haven't seen https://google.com before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(18, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkThatContractsIntoLargerLink()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "super broken https://[osu.ppy](https://reddit.com).sh/" });
+
+ Assert.AreEqual("super broken https://osu.ppy.sh/", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://reddit.com", result.Links[0].Url);
+ Assert.AreEqual(21, result.Links[0].Index);
+ Assert.AreEqual(7, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkDirectlyNextToRawLink()
+ {
+ // the raw link has a port at the end of it, so that the raw link regex terminates at the port and doesn't consume display text from the formatted one
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "https://localhost:8080[https://osu.ppy.sh](https://osu.ppy.sh) should be two links" });
+
+ Assert.AreEqual("https://localhost:8080https://osu.ppy.sh should be two links", result.DisplayContent);
+ Assert.AreEqual(2, result.Links.Count);
+
+ Assert.AreEqual("https://localhost:8080", result.Links[0].Url);
+ Assert.AreEqual(0, result.Links[0].Index);
+ Assert.AreEqual(22, result.Links[0].Length);
+
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[1].Url);
+ Assert.AreEqual(22, result.Links[1].Index);
+ Assert.AreEqual(18, result.Links[1].Length);
+ }
+
[Test]
public void TestChannelLink()
{
diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
new file mode 100644
index 0000000000..fe3cc375ea
--- /dev/null
+++ b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -0,0 +1,194 @@
+// 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.Testing;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Edit;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Editor
+{
+ [HeadlessTest]
+ public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene
+ {
+ private TestHitObjectComposer composer;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Child = composer = new TestHitObjectComposer();
+
+ BeatDivisor.Value = 1;
+
+ composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
+ composer.EditorBeatmap.ControlPointInfo.Clear();
+
+ composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 });
+ composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
+ });
+
+ [TestCase(1)]
+ [TestCase(2)]
+ public void TestSliderMultiplier(float multiplier)
+ {
+ AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier);
+
+ assertSnapDistance(100 * multiplier);
+ }
+
+ [TestCase(1)]
+ [TestCase(2)]
+ public void TestSpeedMultiplier(float multiplier)
+ {
+ AddStep($"set multiplier = {multiplier}", () =>
+ {
+ composer.EditorBeatmap.ControlPointInfo.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = multiplier });
+ });
+
+ assertSnapDistance(100 * multiplier);
+ }
+
+ [TestCase(1)]
+ [TestCase(2)]
+ public void TestBeatDivisor(int divisor)
+ {
+ AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor);
+
+ assertSnapDistance(100f / divisor);
+ }
+
+ [Test]
+ public void TestConvertDurationToDistance()
+ {
+ assertDurationToDistance(500, 50);
+ assertDurationToDistance(1000, 100);
+
+ AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
+
+ assertDurationToDistance(500, 100);
+ assertDurationToDistance(1000, 200);
+
+ AddStep("set beat length = 500", () =>
+ {
+ composer.EditorBeatmap.ControlPointInfo.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ });
+
+ assertDurationToDistance(500, 200);
+ assertDurationToDistance(1000, 400);
+ }
+
+ [Test]
+ public void TestConvertDistanceToDuration()
+ {
+ assertDistanceToDuration(50, 500);
+ assertDistanceToDuration(100, 1000);
+
+ AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
+
+ assertDistanceToDuration(100, 500);
+ assertDistanceToDuration(200, 1000);
+
+ AddStep("set beat length = 500", () =>
+ {
+ composer.EditorBeatmap.ControlPointInfo.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ });
+
+ assertDistanceToDuration(200, 500);
+ assertDistanceToDuration(400, 1000);
+ }
+
+ [Test]
+ public void TestGetSnappedDurationFromDistance()
+ {
+ assertSnappedDuration(50, 0);
+ assertSnappedDuration(100, 1000);
+ assertSnappedDuration(150, 1000);
+ assertSnappedDuration(200, 2000);
+ assertSnappedDuration(250, 2000);
+
+ AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
+
+ assertSnappedDuration(50, 0);
+ assertSnappedDuration(100, 0);
+ assertSnappedDuration(150, 0);
+ assertSnappedDuration(200, 1000);
+ assertSnappedDuration(250, 1000);
+
+ AddStep("set beat length = 500", () =>
+ {
+ composer.EditorBeatmap.ControlPointInfo.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ });
+
+ assertSnappedDuration(50, 0);
+ assertSnappedDuration(100, 0);
+ assertSnappedDuration(150, 0);
+ assertSnappedDuration(200, 500);
+ assertSnappedDuration(250, 500);
+ assertSnappedDuration(400, 1000);
+ }
+
+ [Test]
+ public void GetSnappedDistanceFromDistance()
+ {
+ assertSnappedDistance(50, 0);
+ assertSnappedDistance(100, 100);
+ assertSnappedDistance(150, 100);
+ assertSnappedDistance(200, 200);
+ assertSnappedDistance(250, 200);
+
+ AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
+
+ assertSnappedDistance(50, 0);
+ assertSnappedDistance(100, 0);
+ assertSnappedDistance(150, 0);
+ assertSnappedDistance(200, 200);
+ assertSnappedDistance(250, 200);
+
+ AddStep("set beat length = 500", () =>
+ {
+ composer.EditorBeatmap.ControlPointInfo.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ });
+
+ assertSnappedDistance(50, 0);
+ assertSnappedDistance(100, 0);
+ assertSnappedDistance(150, 0);
+ assertSnappedDistance(200, 200);
+ assertSnappedDistance(250, 200);
+ assertSnappedDistance(400, 400);
+ }
+
+ private void assertSnapDistance(float expectedDistance)
+ => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(0) == expectedDistance);
+
+ private void assertDurationToDistance(double duration, float expectedDistance)
+ => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(0, duration) == expectedDistance);
+
+ private void assertDistanceToDuration(float distance, double expectedDuration)
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(0, distance) == expectedDuration);
+
+ private void assertSnappedDuration(float distance, double expectedDuration)
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(0, distance) == expectedDuration);
+
+ private void assertSnappedDistance(float distance, float expectedDistance)
+ => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(0, distance) == expectedDistance);
+
+ private class TestHitObjectComposer : OsuHitObjectComposer
+ {
+ public new EditorBeatmap EditorBeatmap => base.EditorBeatmap;
+
+ public TestHitObjectComposer()
+ : base(new OsuRuleset())
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
new file mode 100644
index 0000000000..6d7159a825
--- /dev/null
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
@@ -0,0 +1,143 @@
+// 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.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
+using osu.Game.Audio;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneHitObjectAccentColour : OsuTestScene
+ {
+ private Container skinContainer;
+
+ [SetUp]
+ public void Setup() => Schedule(() => Child = skinContainer = new SkinProvidingContainer(new TestSkin()));
+
+ [Test]
+ public void TestChangeComboIndexBeforeLoad()
+ {
+ TestDrawableHitObject hitObject = null;
+
+ AddStep("set combo and add hitobject", () =>
+ {
+ hitObject = new TestDrawableHitObject();
+ hitObject.HitObject.ComboIndex = 1;
+
+ skinContainer.Add(hitObject);
+ });
+
+ AddAssert("combo colour is green", () => hitObject.AccentColour.Value == Color4.Green);
+ }
+
+ [Test]
+ public void TestChangeComboIndexDuringLoad()
+ {
+ TestDrawableHitObject hitObject = null;
+
+ AddStep("add hitobject and set combo", () =>
+ {
+ skinContainer.Add(hitObject = new TestDrawableHitObject());
+ hitObject.HitObject.ComboIndex = 1;
+ });
+
+ AddAssert("combo colour is green", () => hitObject.AccentColour.Value == Color4.Green);
+ }
+
+ [Test]
+ public void TestChangeComboIndexAfterLoad()
+ {
+ TestDrawableHitObject hitObject = null;
+
+ AddStep("add hitobject", () => skinContainer.Add(hitObject = new TestDrawableHitObject()));
+ AddAssert("combo colour is red", () => hitObject.AccentColour.Value == Color4.Red);
+
+ AddStep("change combo", () => hitObject.HitObject.ComboIndex = 1);
+ AddAssert("combo colour is green", () => hitObject.AccentColour.Value == Color4.Green);
+ }
+
+ private class TestDrawableHitObject : DrawableHitObject
+ {
+ public TestDrawableHitObject()
+ : base(new TestHitObjectWithCombo())
+ {
+ }
+ }
+
+ private class TestHitObjectWithCombo : HitObject, IHasComboInformation
+ {
+ public bool NewCombo { get; } = false;
+ public int ComboOffset { get; } = 0;
+
+ public Bindable IndexInCurrentComboBindable { get; } = new Bindable();
+
+ 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;
+ }
+
+ public Bindable LastInComboBindable { get; } = new Bindable();
+
+ public bool LastInCombo
+ {
+ get => LastInComboBindable.Value;
+ set => LastInComboBindable.Value = value;
+ }
+ }
+
+ private class TestSkin : ISkin
+ {
+ public readonly List ComboColours = new List
+ {
+ Color4.Red,
+ Color4.Green
+ };
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
+
+ public Texture GetTexture(string componentName) => throw new NotImplementedException();
+
+ public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+
+ public IBindable GetConfig(TLookup lookup)
+ {
+ switch (lookup)
+ {
+ case GlobalSkinConfiguration global:
+ switch (global)
+ {
+ case GlobalSkinConfiguration.ComboColours:
+ return SkinUtils.As(new Bindable>(ComboColours));
+ }
+
+ break;
+ }
+
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
new file mode 100644
index 0000000000..42a3b4cf43
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
@@ -0,0 +1,48 @@
+// 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;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [TestFixture]
+ public class BeatmapSetInfoEqualityTest
+ {
+ [Test]
+ public void TestOnlineWithOnline()
+ {
+ var ourInfo = new BeatmapSetInfo { OnlineBeatmapSetID = 123 };
+ var otherInfo = new BeatmapSetInfo { OnlineBeatmapSetID = 123 };
+
+ Assert.AreEqual(ourInfo, otherInfo);
+ }
+
+ [Test]
+ public void TestDatabasedWithDatabased()
+ {
+ var ourInfo = new BeatmapSetInfo { ID = 123 };
+ var otherInfo = new BeatmapSetInfo { ID = 123 };
+
+ Assert.AreEqual(ourInfo, otherInfo);
+ }
+
+ [Test]
+ public void TestDatabasedWithOnline()
+ {
+ var ourInfo = new BeatmapSetInfo { ID = 123, OnlineBeatmapSetID = 12 };
+ var otherInfo = new BeatmapSetInfo { OnlineBeatmapSetID = 12 };
+
+ Assert.AreEqual(ourInfo, otherInfo);
+ }
+
+ [Test]
+ public void TestCheckNullID()
+ {
+ var ourInfo = new BeatmapSetInfo { Status = BeatmapSetOnlineStatus.Loved };
+ var otherInfo = new BeatmapSetInfo { Status = BeatmapSetOnlineStatus.Approved };
+
+ Assert.AreNotEqual(ourInfo, otherInfo);
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
new file mode 100644
index 0000000000..a51b90851c
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -0,0 +1,227 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Beatmaps.ControlPoints;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [TestFixture]
+ public class ControlPointInfoTest
+ {
+ [Test]
+ public void TestAdd()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new TimingControlPoint());
+ cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
+ }
+
+ [Test]
+ public void TestAddRedundantTiming()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point.
+ cpi.Add(1000, new TimingControlPoint()); // is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddRedundantDifficulty()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new DifficultyControlPoint()); // is redundant
+ cpi.Add(1000, new DifficultyControlPoint()); // is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
+
+ cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddRedundantSample()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new SampleControlPoint()); // is redundant
+ cpi.Add(1000, new SampleControlPoint()); // is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
+
+ cpi.Add(1000, new SampleControlPoint { SampleVolume = 50 }); // is not redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddRedundantEffect()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new EffectControlPoint()); // is redundant
+ cpi.Add(1000, new EffectControlPoint()); // is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
+
+ cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+ var group2 = cpi.GroupAt(1000, true);
+
+ Assert.That(group, Is.EqualTo(group2));
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestGroupAtLookupOnly()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(5000, true);
+ Assert.That(group, Is.Not.Null);
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.GroupAt(1000), Is.Null);
+ Assert.That(cpi.GroupAt(5000), Is.Not.Null);
+ }
+
+ [Test]
+ public void TestAddRemoveGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+
+ cpi.RemoveGroup(group);
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void TestAddControlPointToGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+
+ // usually redundant, but adding to group forces it to be added
+ group.Add(new DifficultyControlPoint());
+
+ Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddDuplicateControlPointToGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+
+ group.Add(new DifficultyControlPoint());
+ group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 });
+
+ Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2));
+ }
+
+ [Test]
+ public void TestRemoveControlPointFromGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+
+ var difficultyPoint = new DifficultyControlPoint();
+
+ group.Add(difficultyPoint);
+ group.Remove(difficultyPoint);
+
+ Assert.That(group.ControlPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void TestOrdering()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new TimingControlPoint());
+ cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
+ cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
+ cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
+ cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
+ cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
+
+ Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(8));
+
+ Assert.That(cpi.Groups, Is.Ordered.Ascending.By(nameof(ControlPointGroup.Time)));
+
+ Assert.That(cpi.AllControlPoints, Is.Ordered.Ascending.By(nameof(ControlPoint.Time)));
+ Assert.That(cpi.TimingPoints, Is.Ordered.Ascending.By(nameof(ControlPoint.Time)));
+ }
+
+ [Test]
+ public void TestClear()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new TimingControlPoint());
+ cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
+ cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
+ cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
+ cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
+ cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
+
+ cpi.Clear();
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0));
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
new file mode 100644
index 0000000000..30686cb947
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
@@ -0,0 +1,201 @@
+// 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.Rulesets;
+using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Carousel;
+
+namespace osu.Game.Tests.NonVisual.Filtering
+{
+ [TestFixture]
+ public class FilterMatchingTest
+ {
+ private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
+ {
+ Ruleset = new RulesetInfo { ID = 5 },
+ StarDifficulty = 4.0d,
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ ApproachRate = 5.0f,
+ DrainRate = 3.0f,
+ CircleSize = 2.0f,
+ },
+ Metadata = new BeatmapMetadata
+ {
+ Artist = "The Artist",
+ ArtistUnicode = "check unicode too",
+ Title = "Title goes here",
+ TitleUnicode = "Title goes here",
+ AuthorString = "The Author",
+ Source = "unit tests",
+ Tags = "look for tags too",
+ },
+ Version = "version as well",
+ Length = 2500,
+ BPM = 160,
+ BeatDivisor = 12,
+ Status = BeatmapSetOnlineStatus.Loved
+ };
+
+ [Test]
+ public void TestCriteriaMatchingNoRuleset()
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria();
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.IsFalse(carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ public void TestCriteriaMatchingSpecificRuleset()
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.IsTrue(carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ public void TestCriteriaMatchingConvertedBeatmaps()
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 },
+ AllowConvertedBeatmaps = true
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.IsFalse(carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestCriteriaMatchingRangeMin(bool inclusive)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 },
+ AllowConvertedBeatmaps = true,
+ ApproachRate = new FilterCriteria.OptionalRange
+ {
+ IsLowerInclusive = inclusive,
+ Min = 5.0f
+ }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(!inclusive, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestCriteriaMatchingRangeMax(bool inclusive)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 },
+ AllowConvertedBeatmaps = true,
+ BPM = new FilterCriteria.OptionalRange
+ {
+ IsUpperInclusive = inclusive,
+ Max = 160d
+ }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(!inclusive, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase("artist", false)]
+ [TestCase("artist title author", false)]
+ [TestCase("an artist", true)]
+ [TestCase("tags too", false)]
+ [TestCase("version", false)]
+ [TestCase("an auteur", true)]
+ public void TestCriteriaMatchingTerms(string terms, bool filtered)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 },
+ AllowConvertedBeatmaps = true,
+ SearchText = terms
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(filtered, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase("", false)]
+ [TestCase("The", false)]
+ [TestCase("THE", false)]
+ [TestCase("author", false)]
+ [TestCase("the author", false)]
+ [TestCase("the author AND then something else", true)]
+ [TestCase("unknown", true)]
+ public void TestCriteriaMatchingCreator(string creatorName, bool filtered)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = creatorName }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(filtered, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase("", false)]
+ [TestCase("The", false)]
+ [TestCase("THE", false)]
+ [TestCase("artist", false)]
+ [TestCase("the artist", false)]
+ [TestCase("the artist AND then something else", true)]
+ [TestCase("unicode too", false)]
+ [TestCase("unknown", true)]
+ public void TestCriteriaMatchingArtist(string artistName, bool filtered)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(filtered, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase("", false)]
+ [TestCase("artist", false)]
+ [TestCase("unknown", true)]
+ public void TestCriteriaMatchingArtistWithNullUnicodeName(string artistName, bool filtered)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ exampleBeatmapInfo.Metadata.ArtistUnicode = null;
+
+ var criteria = new FilterCriteria
+ {
+ Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(filtered, carouselItem.Filtered.Value);
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
new file mode 100644
index 0000000000..9869ddde41
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -0,0 +1,184 @@
+// 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;
+using osu.Game.Screens.Select;
+
+namespace osu.Game.Tests.NonVisual.Filtering
+{
+ [TestFixture]
+ public class FilterQueryParserTest
+ {
+ [Test]
+ public void TestApplyQueriesBareWords()
+ {
+ const string query = "looking for a beatmap";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("looking for a beatmap", filterCriteria.SearchText);
+ Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
+ }
+
+ /*
+ * The following tests have been written a bit strangely (they don't check exact
+ * bound equality with what the filter says).
+ * This is to account for floating-point arithmetic issues.
+ * For example, specifying a bpm<140 filter would previously match beatmaps with BPM
+ * of 139.99999, which would be displayed in the UI as 140.
+ * Due to this the tests check the last tick inside the range and the first tick
+ * outside of the range.
+ */
+
+ [Test]
+ public void TestApplyStarQueries()
+ {
+ const string query = "stars<4 easy";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.IsNotNull(filterCriteria.StarDifficulty.Max);
+ Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d);
+ Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d);
+ Assert.IsNull(filterCriteria.StarDifficulty.Min);
+ }
+
+ [Test]
+ public void TestApplyApproachRateQueries()
+ {
+ const string query = "ar>=9 difficult";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("difficult", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.IsNotNull(filterCriteria.ApproachRate.Min);
+ Assert.Greater(filterCriteria.ApproachRate.Min, 8.9f);
+ Assert.Less(filterCriteria.ApproachRate.Min, 9.0f);
+ Assert.IsNull(filterCriteria.ApproachRate.Max);
+ }
+
+ [Test]
+ public void TestApplyDrainRateQueries()
+ {
+ const string query = "dr>2 quite specific dr<:6";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(2, filterCriteria.SearchTerms.Length);
+ Assert.Greater(filterCriteria.DrainRate.Min, 2.0f);
+ Assert.Less(filterCriteria.DrainRate.Min, 2.1f);
+ Assert.Greater(filterCriteria.DrainRate.Max, 6.0f);
+ Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
+ }
+
+ [Test]
+ public void TestApplyBPMQueries()
+ {
+ const string query = "bpm>:200 gotta go fast";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("gotta go fast", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
+ Assert.IsNotNull(filterCriteria.BPM.Min);
+ Assert.Greater(filterCriteria.BPM.Min, 199.99d);
+ Assert.Less(filterCriteria.BPM.Min, 200.00d);
+ Assert.IsNull(filterCriteria.BPM.Max);
+ }
+
+ private static object[] lengthQueryExamples =
+ {
+ new object[] { "6ms", TimeSpan.FromMilliseconds(6), TimeSpan.FromMilliseconds(1) },
+ new object[] { "23s", TimeSpan.FromSeconds(23), TimeSpan.FromSeconds(1) },
+ new object[] { "9m", TimeSpan.FromMinutes(9), TimeSpan.FromMinutes(1) },
+ new object[] { "0.25h", TimeSpan.FromHours(0.25), TimeSpan.FromHours(1) },
+ new object[] { "70", TimeSpan.FromSeconds(70), TimeSpan.FromSeconds(1) },
+ };
+
+ [Test]
+ [TestCaseSource(nameof(lengthQueryExamples))]
+ public void TestApplyLengthQueries(string lengthQuery, TimeSpan expectedLength, TimeSpan scale)
+ {
+ string query = $"length={lengthQuery} time";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("time", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual(expectedLength.TotalMilliseconds - scale.TotalMilliseconds / 2.0, filterCriteria.Length.Min);
+ Assert.AreEqual(expectedLength.TotalMilliseconds + scale.TotalMilliseconds / 2.0, filterCriteria.Length.Max);
+ }
+
+ [Test]
+ public void TestApplyDivisorQueries()
+ {
+ const string query = "that's a time signature alright! divisor:12";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("that's a time signature alright!", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual(12, filterCriteria.BeatDivisor.Min);
+ Assert.IsTrue(filterCriteria.BeatDivisor.IsLowerInclusive);
+ Assert.AreEqual(12, filterCriteria.BeatDivisor.Max);
+ Assert.IsTrue(filterCriteria.BeatDivisor.IsUpperInclusive);
+ }
+
+ [Test]
+ public void TestApplyStatusQueries()
+ {
+ const string query = "I want the pp status=ranked";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
+ Assert.IsTrue(filterCriteria.OnlineStatus.IsLowerInclusive);
+ Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
+ Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive);
+ }
+
+ [Test]
+ public void TestApplyCreatorQueries()
+ {
+ const string query = "beatmap specifically by creator=my_fav";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("beatmap specifically by", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm);
+ }
+
+ [Test]
+ public void TestApplyArtistQueries()
+ {
+ const string query = "find me songs by artist=singer please";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
+ }
+
+ [Test]
+ public void TestApplyArtistQueriesWithSpaces()
+ {
+ const string query = "really like artist=\"name with space\" yes";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
+ }
+
+ [Test]
+ public void TestApplyArtistQueriesOneDoubleQuote()
+ {
+ const string query = "weird artist=double\"quote";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("weird", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm);
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs
index 18cbd4e7c5..7df7df22ea 100644
--- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs
+++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs
@@ -225,8 +225,10 @@ namespace osu.Game.Tests.NonVisual
private void fastForwardToPoint(double destination)
{
for (int i = 0; i < 1000; i++)
+ {
if (handler.SetFrameFromTime(destination) == null)
return;
+ }
throw new TimeoutException("Seek was never fulfilled");
}
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 9cb85a63bf..66084a3204 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -11,13 +11,13 @@ namespace osu.Game.Tests.Resources
{
public static Stream OpenResource(string name) => new DllResourceStore("osu.Game.Tests.dll").GetStream($"Resources/{name}");
- public static Stream GetTestBeatmapStream() => new DllResourceStore("osu.Game.Resources.dll").GetStream("Beatmaps/241526 Soleily - Renatus.osz");
+ public static Stream GetTestBeatmapStream(bool virtualTrack = false) => new DllResourceStore("osu.Game.Resources.dll").GetStream($"Beatmaps/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz");
- public static string GetTestBeatmapForImport()
+ public static string GetTestBeatmapForImport(bool virtualTrack = false)
{
var temp = Path.GetTempFileName() + ".osz";
- using (var stream = GetTestBeatmapStream())
+ using (var stream = GetTestBeatmapStream(virtualTrack))
using (var newFile = File.Create(temp))
stream.CopyTo(newFile);
diff --git a/osu.Game.Tests/Resources/controlpoint-difficulty-multiplier.osu b/osu.Game.Tests/Resources/controlpoint-difficulty-multiplier.osu
new file mode 100644
index 0000000000..5f06fc33c8
--- /dev/null
+++ b/osu.Game.Tests/Resources/controlpoint-difficulty-multiplier.osu
@@ -0,0 +1,8 @@
+osu file format v7
+
+[TimingPoints]
+0,100,4,2,0,100,1,0
+12,500,4,2,0,100,1,0
+1000,-10,4,2,0,100,0,0
+2000,-54,4,2,0,100,0,0
+3000,-200,4,2,0,100,0,0
diff --git a/osu.Game.Tests/Resources/corrupted-header.osu b/osu.Game.Tests/Resources/corrupted-header.osu
new file mode 100644
index 0000000000..92701a4a7d
--- /dev/null
+++ b/osu.Game.Tests/Resources/corrupted-header.osu
@@ -0,0 +1,5 @@
+ow computerosu file format v14
+
+[Metadata]
+Title: Beatmap with corrupted header
+Creator: Evil Hacker
diff --git a/osu.Game.Tests/Resources/empty-line-instead-of-header.osu b/osu.Game.Tests/Resources/empty-line-instead-of-header.osu
new file mode 100644
index 0000000000..91ecf8d84a
--- /dev/null
+++ b/osu.Game.Tests/Resources/empty-line-instead-of-header.osu
@@ -0,0 +1,5 @@
+
+
+[Metadata]
+Title: The dog ate the file header
+Creator: Why does this keep happening
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/empty-lines-at-start.osu b/osu.Game.Tests/Resources/empty-lines-at-start.osu
new file mode 100644
index 0000000000..cb3b1761a2
--- /dev/null
+++ b/osu.Game.Tests/Resources/empty-lines-at-start.osu
@@ -0,0 +1,8 @@
+
+
+
+osu file format v14
+
+[Metadata]
+Title: Empty lines at start
+Creator: Edge Case Hunter
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/missing-header.osu b/osu.Game.Tests/Resources/missing-header.osu
new file mode 100644
index 0000000000..95fac0d79b
--- /dev/null
+++ b/osu.Game.Tests/Resources/missing-header.osu
@@ -0,0 +1,4 @@
+[Metadata]
+
+Title: Beatmap with no header
+Creator: Incredibly Evil Hacker
diff --git a/osu.Game.Tests/Resources/no-empty-line-after-header.osu b/osu.Game.Tests/Resources/no-empty-line-after-header.osu
new file mode 100644
index 0000000000..9db2b7c01c
--- /dev/null
+++ b/osu.Game.Tests/Resources/no-empty-line-after-header.osu
@@ -0,0 +1,4 @@
+osu file format v14
+[Metadata]
+Title: No empty line delimiting header from contents
+Creator: Edge Case Hunter
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/timingpoint-speedmultiplier-reset.osu b/osu.Game.Tests/Resources/timingpoint-speedmultiplier-reset.osu
new file mode 100644
index 0000000000..4512903c68
--- /dev/null
+++ b/osu.Game.Tests/Resources/timingpoint-speedmultiplier-reset.osu
@@ -0,0 +1,5 @@
+osu file format v14
+
+[TimingPoints]
+0,-200,4,1,0,100,0,0
+2000,100,1,1,0,100,1,0
diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
index 8bd846518b..085f502517 100644
--- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
+++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
@@ -2,8 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using System.IO;
using NUnit.Framework;
+using osu.Game.IO;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using osuTK.Graphics;
@@ -20,12 +20,14 @@ namespace osu.Game.Tests.Skins
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource(hasColours ? "skin.ini" : "skin-empty.ini"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var comboColors = decoder.Decode(stream).ComboColours;
List expectedColors;
+
if (hasColours)
+ {
expectedColors = new List
{
new Color4(142, 199, 255, 255),
@@ -33,6 +35,7 @@ namespace osu.Game.Tests.Skins
new Color4(128, 255, 255, 255),
new Color4(100, 100, 100, 100),
};
+ }
else
expectedColors = new DefaultSkin().Configuration.ComboColours;
@@ -48,7 +51,7 @@ namespace osu.Game.Tests.Skins
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("skin.ini"))
- using (var stream = new StreamReader(resStream))
+ using (var stream = new LineBufferedReader(resStream))
{
var config = decoder.Decode(stream);
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index bbcc4140a9..578030748b 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
@@ -17,6 +18,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Skins
{
[TestFixture]
+ [HeadlessTest]
public class TestSceneSkinConfigurationLookup : OsuTestScene
{
private LegacySkin source1;
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs
index 3061a3a542..f858174ff2 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs
@@ -285,6 +285,12 @@ namespace osu.Game.Tests.Visual.Background
});
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ rulesets?.Dispose();
+ }
+
private class DummySongSelect : PlaySongSelect
{
protected override BackgroundScreen CreateBackground()
diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
index df6740421b..d76905dab8 100644
--- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
+++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
@@ -3,16 +3,20 @@
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
+using static osu.Game.Tests.Visual.Components.TestScenePreviewTrackManager.TestPreviewTrackManager;
namespace osu.Game.Tests.Visual.Components
{
public class TestScenePreviewTrackManager : OsuTestScene, IPreviewTrackOwner
{
- private readonly PreviewTrackManager trackManager = new TestPreviewTrackManager();
+ private readonly TestPreviewTrackManager trackManager = new TestPreviewTrackManager();
+
+ private AudioManager audio;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@@ -23,8 +27,10 @@ namespace osu.Game.Tests.Visual.Components
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(AudioManager audio)
{
+ this.audio = audio;
+
Add(trackManager);
}
@@ -34,6 +40,7 @@ namespace osu.Game.Tests.Visual.Components
PreviewTrack track = null;
AddStep("get track", () => track = getOwnedTrack());
+ AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start", () => track.Start());
AddAssert("started", () => track.IsRunning);
AddStep("stop", () => track.Stop());
@@ -52,10 +59,15 @@ namespace osu.Game.Tests.Visual.Components
track2 = getOwnedTrack();
});
+ AddUntilStep("wait loaded", () => track1.IsLoaded && track2.IsLoaded);
+
AddStep("start track 1", () => track1.Start());
AddStep("start track 2", () => track2.Start());
AddAssert("track 1 stopped", () => !track1.IsRunning);
AddAssert("track 2 started", () => track2.IsRunning);
+ AddStep("start track 1", () => track1.Start());
+ AddAssert("track 2 stopped", () => !track2.IsRunning);
+ AddAssert("track 1 started", () => track1.IsRunning);
}
[Test]
@@ -64,6 +76,7 @@ namespace osu.Game.Tests.Visual.Components
PreviewTrack track = null;
AddStep("get track", () => track = getOwnedTrack());
+ AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start", () => track.Start());
AddStep("stop by owner", () => trackManager.StopAnyPlaying(this));
AddAssert("stopped", () => !track.IsRunning);
@@ -76,6 +89,7 @@ namespace osu.Game.Tests.Visual.Components
PreviewTrack track = null;
AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
+ AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start", () => track.Start());
AddStep("attempt stop", () => trackManager.StopAnyPlaying(this));
AddAssert("not stopped", () => track.IsRunning);
@@ -83,22 +97,100 @@ namespace osu.Game.Tests.Visual.Components
AddAssert("stopped", () => !track.IsRunning);
}
- private PreviewTrack getTrack() => trackManager.Get(null);
+ [Test]
+ public void TestNonPresentTrack()
+ {
+ TestPreviewTrack track = null;
- private PreviewTrack getOwnedTrack()
+ AddStep("get non-present track", () =>
+ {
+ Add(new TestTrackOwner(track = getTrack()));
+ track.Alpha = 0;
+ });
+ AddUntilStep("wait loaded", () => track.IsLoaded);
+ AddStep("start", () => track.Start());
+ AddStep("seek to end", () => track.Track.Seek(track.Track.Length));
+ AddAssert("track stopped", () => !track.IsRunning);
+ }
+
+ ///
+ /// Ensures that changes correctly.
+ ///
+ [Test]
+ public void TestCurrentTrackChanges()
+ {
+ PreviewTrack track = null;
+ TestTrackOwner owner = null;
+
+ AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
+ AddUntilStep("wait loaded", () => track.IsLoaded);
+ AddStep("start track", () => track.Start());
+ AddAssert("current is track", () => trackManager.CurrentTrack == track);
+ AddStep("pause manager updates", () => trackManager.AllowUpdate = false);
+ AddStep("stop any playing", () => trackManager.StopAnyPlaying(owner));
+ AddAssert("current not changed", () => trackManager.CurrentTrack == track);
+ AddStep("resume manager updates", () => trackManager.AllowUpdate = true);
+ AddAssert("current is null", () => trackManager.CurrentTrack == null);
+ }
+
+ ///
+ /// Ensures that mutes game-wide audio tracks correctly.
+ ///
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestEnsureMutingCorrectly(bool stopAnyPlaying)
+ {
+ PreviewTrack track = null;
+ TestTrackOwner owner = null;
+
+ AddStep("ensure volume not zero", () =>
+ {
+ if (audio.Volume.Value == 0)
+ audio.Volume.Value = 1;
+
+ if (audio.VolumeTrack.Value == 0)
+ audio.VolumeTrack.Value = 1;
+ });
+
+ AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0);
+
+ AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
+ AddUntilStep("wait loaded", () => track.IsLoaded);
+ AddStep("start track", () => track.Start());
+ AddAssert("game is muted", () => audio.Tracks.AggregateVolume.Value == 0);
+
+ if (stopAnyPlaying)
+ AddStep("stop any playing", () => trackManager.StopAnyPlaying(owner));
+ else
+ AddStep("stop track", () => track.Stop());
+
+ AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0);
+ }
+
+ private TestPreviewTrack getTrack() => (TestPreviewTrack)trackManager.Get(null);
+
+ private TestPreviewTrack getOwnedTrack()
{
var track = getTrack();
- Add(track);
+ LoadComponentAsync(track, Add);
return track;
}
private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner
{
+ private readonly PreviewTrack track;
+
public TestTrackOwner(PreviewTrack track)
{
- AddInternal(track);
+ this.track = track;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LoadComponentAsync(track, AddInternal);
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@@ -109,14 +201,28 @@ namespace osu.Game.Tests.Visual.Components
}
}
- private class TestPreviewTrackManager : PreviewTrackManager
+ public class TestPreviewTrackManager : PreviewTrackManager
{
+ public bool AllowUpdate = true;
+
+ public new PreviewTrack CurrentTrack => base.CurrentTrack;
+
protected override TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TestPreviewTrack(beatmapSetInfo, trackStore);
- protected class TestPreviewTrack : TrackManagerPreviewTrack
+ public override bool UpdateSubTree()
+ {
+ if (!AllowUpdate)
+ return true;
+
+ return base.UpdateSubTree();
+ }
+
+ public class TestPreviewTrack : TrackManagerPreviewTrack
{
private readonly ITrackStore trackManager;
+ public new Track Track => base.Track;
+
public TestPreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackManager)
: base(beatmapSetInfo, trackManager)
{
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs b/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
similarity index 72%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs
rename to osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
index 608df1965e..9f16e1d781 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Osu;
@@ -11,10 +9,8 @@ using osu.Game.Screens.Edit.Compose;
namespace osu.Game.Tests.Visual.Editor
{
[TestFixture]
- public class TestSceneEditorCompose : EditorClockTestScene
+ public class TestSceneComposeScreen : EditorClockTestScene
{
- public override IReadOnlyList RequiredTypes => new[] { typeof(ComposeScreen) };
-
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
new file mode 100644
index 0000000000..e4c987923c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
@@ -0,0 +1,171 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual.Editor
+{
+ public class TestSceneDistanceSnapGrid : EditorClockTestScene
+ {
+ private const double beat_length = 100;
+ private static readonly Vector2 grid_position = new Vector2(512, 384);
+
+ [Cached(typeof(IEditorBeatmap))]
+ private readonly EditorBeatmap editorBeatmap;
+
+ [Cached(typeof(IDistanceSnapProvider))]
+ private readonly SnapProvider snapProvider = new SnapProvider();
+
+ public TestSceneDistanceSnapGrid()
+ {
+ editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray
+ },
+ new TestDistanceSnapGrid(new HitObject(), grid_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 TestLimitedDistance()
+ {
+ AddStep("create limited grid", () =>
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray
+ },
+ new TestDistanceSnapGrid(new HitObject(), grid_position, new HitObject { StartTime = 100 })
+ };
+ });
+ }
+
+ private class TestDistanceSnapGrid : DistanceSnapGrid
+ {
+ public new float DistanceSpacing => base.DistanceSpacing;
+
+ public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
+ : base(hitObject, nextHitObject, centrePosition)
+ {
+ }
+
+ protected override void CreateContent(Vector2 centrePosition)
+ {
+ AddInternal(new Circle
+ {
+ Origin = Anchor.Centre,
+ Size = new Vector2(5),
+ Position = centrePosition
+ });
+
+ int beatIndex = 0;
+
+ for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
+ {
+ AddInternal(new Circle
+ {
+ Origin = Anchor.Centre,
+ Size = new Vector2(5, 10),
+ Position = new Vector2(s, centrePosition.Y),
+ Colour = GetColourForBeatIndex(beatIndex)
+ });
+ }
+
+ beatIndex = 0;
+
+ for (float s = centrePosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
+ {
+ AddInternal(new Circle
+ {
+ Origin = Anchor.Centre,
+ Size = new Vector2(5, 10),
+ Position = new Vector2(s, centrePosition.Y),
+ Colour = GetColourForBeatIndex(beatIndex)
+ });
+ }
+
+ beatIndex = 0;
+
+ for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
+ {
+ AddInternal(new Circle
+ {
+ Origin = Anchor.Centre,
+ Size = new Vector2(10, 5),
+ Position = new Vector2(centrePosition.X, s),
+ Colour = GetColourForBeatIndex(beatIndex)
+ });
+ }
+
+ beatIndex = 0;
+
+ for (float s = centrePosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
+ {
+ AddInternal(new Circle
+ {
+ Origin = Anchor.Centre,
+ Size = new Vector2(10, 5),
+ Position = new Vector2(centrePosition.X, s),
+ Colour = GetColourForBeatIndex(beatIndex)
+ });
+ }
+ }
+
+ public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition)
+ => (Vector2.Zero, 0);
+ }
+
+ private class SnapProvider : IDistanceSnapProvider
+ {
+ public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time);
+
+ public float GetBeatSnapDistanceAt(double referenceTime) => 10;
+
+ 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.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
index a8c2362910..6e5b3b93e9 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
@@ -10,9 +10,9 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
using osuTK.Graphics;
@@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Editor
}
}
- private class StartStopButton : Button
+ private class StartStopButton : OsuButton
{
private IAdjustableClock adjustableClock;
private bool started;
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
index b997d6aaeb..3118e0cabe 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
@@ -28,18 +28,7 @@ namespace osu.Game.Tests.Visual.Editor
{
var testBeatmap = new Beatmap
{
- ControlPointInfo = new ControlPointInfo
- {
- TimingPoints =
- {
- new TimingControlPoint { Time = 0, BeatLength = 200 },
- new TimingControlPoint { Time = 100, BeatLength = 400 },
- new TimingControlPoint { Time = 175, BeatLength = 800 },
- new TimingControlPoint { Time = 350, BeatLength = 200 },
- new TimingControlPoint { Time = 450, BeatLength = 100 },
- new TimingControlPoint { Time = 500, BeatLength = 307.69230769230802 }
- }
- },
+ ControlPointInfo = new ControlPointInfo(),
HitObjects =
{
new HitCircle { StartTime = 0 },
@@ -47,6 +36,13 @@ namespace osu.Game.Tests.Visual.Editor
}
};
+ testBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 200 });
+ testBeatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 400 });
+ testBeatmap.ControlPointInfo.Add(175, new TimingControlPoint { BeatLength = 800 });
+ testBeatmap.ControlPointInfo.Add(350, new TimingControlPoint { BeatLength = 200 });
+ testBeatmap.ControlPointInfo.Add(450, new TimingControlPoint { BeatLength = 100 });
+ testBeatmap.ControlPointInfo.Add(500, new TimingControlPoint { BeatLength = 307.69230769230802 });
+
Beatmap.Value = CreateWorkingBeatmap(testBeatmap);
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
index 0ea73fb3de..b7c7028b52 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
@@ -22,7 +22,7 @@ using osuTK;
namespace osu.Game.Tests.Visual.Editor
{
[TestFixture]
- public class TestSceneHitObjectComposer : OsuTestScene
+ public class TestSceneHitObjectComposer : EditorClockTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
new file mode 100644
index 0000000000..121853d8d0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
@@ -0,0 +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 System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit.Timing;
+
+namespace osu.Game.Tests.Visual.Editor
+{
+ [TestFixture]
+ public class TestSceneTimingScreen : EditorClockTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(ControlPointTable),
+ typeof(ControlPointSettings),
+ typeof(Section<>),
+ typeof(TimingSection),
+ typeof(EffectSection),
+ typeof(SampleSection),
+ typeof(DifficultySection),
+ typeof(RowAttribute)
+ };
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ Child = new TimingScreen();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
index 60ace8ea69..684e79b3f5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -46,7 +47,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRelativeBeatLengthScaleSingleTimingPoint()
{
- var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range / 2 });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 });
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -60,10 +62,10 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant()
{
- var beatmap = createBeatmap(
- new TimingControlPoint { BeatLength = time_range / 2 },
- new TimingControlPoint { Time = 12000, BeatLength = time_range },
- new TimingControlPoint { Time = 100000, BeatLength = time_range });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 });
+ beatmap.ControlPointInfo.Add(12000, new TimingControlPoint { BeatLength = time_range });
+ beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = time_range });
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -74,9 +76,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRelativeBeatLengthScaleFromSecondTimingPoint()
{
- var beatmap = createBeatmap(
- new TimingControlPoint { BeatLength = time_range },
- new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
+ beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -96,9 +98,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestNonRelativeScale()
{
- var beatmap = createBeatmap(
- new TimingControlPoint { BeatLength = time_range },
- new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
+ beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
createTest(beatmap);
@@ -115,6 +117,34 @@ namespace osu.Game.Tests.Visual.Gameplay
assertPosition(4, 1f);
}
+ [Test]
+ public void TestSliderMultiplierDoesNotAffectRelativeBeatLength()
+ {
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
+ beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
+
+ createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
+ AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 5000);
+
+ for (int i = 0; i < 5; i++)
+ assertPosition(i, i / 5f);
+ }
+
+ [Test]
+ public void TestSliderMultiplierAffectsNonRelativeBeatLength()
+ {
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
+ beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
+
+ createTest(beatmap);
+ AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 2000);
+
+ assertPosition(0, 0);
+ assertPosition(1, 1);
+ }
+
private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}",
() => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY));
@@ -127,14 +157,11 @@ namespace osu.Game.Tests.Visual.Gameplay
/// Creates an , containing 10 hitobjects and user-provided timing points.
/// The hitobjects are spaced milliseconds apart.
///
- /// The timing points to add to the beatmap.
/// The .
- private IBeatmap createBeatmap(params TimingControlPoint[] timingControlPoints)
+ private IBeatmap createBeatmap()
{
var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
- beatmap.ControlPointInfo.TimingPoints.AddRange(timingControlPoints);
-
for (int i = 0; i < 10; i++)
beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range });
@@ -193,6 +220,8 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
+ public new Bindable TimeRange => base.TimeRange;
+
public TestDrawableScrollingRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 50583e43c4..6e8975f11b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -69,6 +69,24 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmClockRunning(true);
}
+ [Test]
+ public void TestPauseWithResumeOverlay()
+ {
+ AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
+ AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1);
+
+ pauseAndConfirm();
+
+ resume();
+ confirmClockRunning(false);
+ confirmPauseOverlayShown(false);
+
+ pauseAndConfirm();
+
+ AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
+ confirmPaused();
+ }
+
[Test]
public void TestResumeWithResumeOverlaySkipped()
{
@@ -127,14 +145,47 @@ namespace osu.Game.Tests.Visual.Gameplay
exitAndConfirm();
}
+ [Test]
+ public void TestExitFromFailedGameplay()
+ {
+ AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddStep("exit", () => Player.Exit());
+
+ confirmExited();
+ }
+
+ [Test]
+ public void TestQuickRetryFromFailedGameplay()
+ {
+ AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddStep("quick retry", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke());
+
+ confirmExited();
+ }
+
+ [Test]
+ public void TestQuickExitFromFailedGameplay()
+ {
+ AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke());
+
+ confirmExited();
+ }
+
[Test]
public void TestExitFromGameplay()
{
AddStep("exit", () => Player.Exit());
- confirmPaused();
+ confirmExited();
+ }
- exitAndConfirm();
+ [Test]
+ public void TestQuickExitFromGameplay()
+ {
+ AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke());
+
+ confirmExited();
}
[Test]
@@ -186,6 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player not exited", () => Player.IsCurrentScreen());
AddStep("exit", () => Player.Exit());
confirmExited();
+ confirmNoTrackAdjustments();
}
private void confirmPaused()
@@ -207,6 +259,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player exited", () => !Player.IsCurrentScreen());
}
+ private void confirmNoTrackAdjustments()
+ {
+ AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
+ }
+
private void restart() => AddStep("restart", () => Player.Restart());
private void pause() => AddStep("pause", () => Player.Pause());
private void resume() => AddStep("resume", () => Player.Resume());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index ab519360ac..74ae641bfe 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -7,10 +7,16 @@ using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.MathUtils;
using osu.Framework.Screens;
+using osu.Game.Configuration;
+using osu.Game.Graphics.Containers;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
@@ -18,25 +24,49 @@ using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestScenePlayerLoader : ManualInputManagerTestScene
{
private TestPlayerLoader loader;
- private OsuScreenStack stack;
+ private TestPlayerLoaderContainer container;
+ private TestPlayer player;
- [SetUp]
- public void Setup() => Schedule(() =>
+ [Resolved]
+ private AudioManager audioManager { get; set; }
+
+ [Resolved]
+ private SessionStatics sessionStatics { get; set; }
+
+ ///
+ /// Sets the input manager child to a new test player loader container instance.
+ ///
+ /// If the test player should behave like the production one.
+ /// An action to run before player load but after bindable leases are returned.
+ /// An action to run after container load.
+ public void ResetPlayer(bool interactive, Action beforeLoadAction = null, Action afterLoadAction = null)
{
- InputManager.Child = stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both };
+ audioManager.Volume.SetDefault();
+
+ InputManager.Clear();
+
+ beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
- });
+
+ InputManager.Child = container = new TestPlayerLoaderContainer(
+ loader = new TestPlayerLoader(() =>
+ {
+ afterLoadAction?.Invoke();
+ return player = new TestPlayer(interactive, interactive);
+ }));
+ }
[Test]
public void TestBlockLoadViaMouseMovement()
{
- AddStep("load dummy beatmap", () => stack.Push(loader = new TestPlayerLoader(() => new TestPlayer(false, false))));
+ AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddRepeatStep("move mouse", () => InputManager.MoveMouseTo(loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft) * RNG.NextSingle()), 20);
AddAssert("loader still active", () => loader.IsCurrentScreen());
@@ -46,16 +76,17 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestLoadContinuation()
{
- Player player = null;
SlowLoadPlayer slowPlayer = null;
- AddStep("load dummy beatmap", () => stack.Push(loader = new TestPlayerLoader(() => player = new TestPlayer(false, false))));
+ AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen());
AddStep("load slow dummy beatmap", () =>
{
- stack.Push(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
+ InputManager.Child = container = new TestPlayerLoaderContainer(
+ loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
+
Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000);
});
@@ -65,16 +96,11 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestModReinstantiation()
{
- TestPlayer player = null;
TestMod gameMod = null;
TestMod playerMod1 = null;
TestMod playerMod2 = null;
- AddStep("load player", () =>
- {
- Mods.Value = new[] { gameMod = new TestMod() };
- stack.Push(loader = new TestPlayerLoader(() => player = new TestPlayer()));
- });
+ AddStep("load player", () => { ResetPlayer(true, () => Mods.Value = new[] { gameMod = new TestMod() }); });
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
@@ -97,6 +123,75 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("player mods applied", () => playerMod2.Applied);
}
+ [Test]
+ public void TestMutedNotificationMasterVolume() => addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, null, () => audioManager.Volume.IsDefault);
+
+ [Test]
+ public void TestMutedNotificationTrackVolume() => addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, null, () => audioManager.VolumeTrack.IsDefault);
+
+ [Test]
+ public void TestMutedNotificationMuteButton() => addVolumeSteps("mute button", null, () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
+
+ ///
+ /// Created for avoiding copy pasting code for the same steps.
+ ///
+ /// What part of the volume system is checked
+ /// The action to be invoked to set the volume before loading
+ /// The action to be invoked to set the volume after loading
+ /// The function to be invoked and checked
+ private void addVolumeSteps(string volumeName, Action beforeLoad, Action afterLoad, Func assert)
+ {
+ AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).Value = false);
+
+ AddStep("load player", () => ResetPlayer(false, beforeLoad, afterLoad));
+ AddUntilStep("wait for player", () => player.IsLoaded);
+
+ AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1);
+ AddStep("click notification", () =>
+ {
+ var scrollContainer = (OsuScrollContainer)container.NotificationOverlay.Children.Last();
+ var flowContainer = scrollContainer.Children.OfType>().First();
+ var notification = flowContainer.First();
+
+ InputManager.MoveMouseTo(notification);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("check " + volumeName, assert);
+ }
+
+ private class TestPlayerLoaderContainer : Container
+ {
+ [Cached]
+ public readonly NotificationOverlay NotificationOverlay;
+
+ [Cached]
+ public readonly VolumeOverlay VolumeOverlay;
+
+ public TestPlayerLoaderContainer(IScreen screen)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new OsuScreenStack(screen)
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ NotificationOverlay = new NotificationOverlay
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ },
+ VolumeOverlay = new VolumeOverlay
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ }
+ };
+ }
+ }
+
private class TestPlayerLoader : PlayerLoader
{
public new VisualSettings VisualSettings => base.VisualSettings;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index 0dfcda122f..7b22fedbd5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -7,11 +7,10 @@ using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
-using osu.Game.Screens.Play;
using osu.Game.Users;
-using osuTK;
using System;
using System.Collections.Generic;
+using osu.Game.Screens.Ranking.Pages;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -42,7 +41,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = new Vector2(80, 40),
};
});
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs
index 944480243d..cdfb3beb19 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
@@ -20,6 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
+ State = { Value = Visibility.Visible }
});
Add(container = new ExampleContainer());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs
index f3c8f89db7..7790126db5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs
@@ -3,11 +3,16 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Pages;
@@ -22,11 +27,13 @@ namespace osu.Game.Tests.Visual.Gameplay
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(ScoreInfo),
typeof(Results),
typeof(ResultsPage),
typeof(ScoreResultsPage),
- typeof(LocalLeaderboardPage)
+ typeof(RetryButton),
+ typeof(ReplayDownloadButton),
+ typeof(LocalLeaderboardPage),
+ typeof(TestPlayer)
};
[BackgroundDependencyLoader]
@@ -42,26 +49,82 @@ namespace osu.Game.Tests.Visual.Gameplay
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
+ }
- LoadScreen(new SoloResults(new ScoreInfo
+ private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo
+ {
+ TotalScore = 2845370,
+ Accuracy = 0.98,
+ MaxCombo = 123,
+ Rank = ScoreRank.A,
+ Date = DateTimeOffset.Now,
+ Statistics = new Dictionary
{
- TotalScore = 2845370,
- Accuracy = 0.98,
- MaxCombo = 123,
- Rank = ScoreRank.A,
- Date = DateTimeOffset.Now,
- Statistics = new Dictionary
+ { HitResult.Great, 50 },
+ { HitResult.Good, 20 },
+ { HitResult.Meh, 50 },
+ { HitResult.Miss, 1 }
+ },
+ User = new User
+ {
+ Username = "peppy",
+ }
+ });
+
+ [Test]
+ public void ResultsWithoutPlayer()
+ {
+ TestSoloResults screen = null;
+
+ AddStep("load results", () => Child = new OsuScreenStack(screen = createResultsScreen())
+ {
+ RelativeSizeAxes = Axes.Both
+ });
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
+ }
+
+ [Test]
+ public void ResultsWithPlayer()
+ {
+ TestSoloResults screen = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddAssert("retry overlay present", () => screen.RetryOverlay != null);
+ }
+
+ private class TestResultsContainer : Container
+ {
+ [Cached(typeof(Player))]
+ private readonly Player player = new TestPlayer();
+
+ public TestResultsContainer(IScreen screen)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = new OsuScreenStack(screen)
{
- { HitResult.Great, 50 },
- { HitResult.Good, 20 },
- { HitResult.Meh, 50 },
- { HitResult.Miss, 1 }
- },
- User = new User
- {
- Username = "peppy",
- }
- }));
+ RelativeSizeAxes = Axes.Both,
+ };
+ }
+ }
+
+ private class TestSoloResults : SoloResults
+ {
+ public HotkeyRetryOverlay RetryOverlay;
+
+ public TestSoloResults(ScoreInfo score)
+ : base(score)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ RetryOverlay = InternalChildren.OfType().SingleOrDefault();
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index b3d4820737..8beb107269 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -326,7 +326,11 @@ namespace osu.Game.Tests.Visual.Gameplay
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
- public event Action SourceChanged;
+ public event Action SourceChanged
+ {
+ add { }
+ remove { }
+ }
}
private class TestSkinComponent : ISkinComponent
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
index 000832b784..61fed3013e 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
@@ -33,23 +33,15 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
public void TestInstantLoad()
{
- bool logoVisible = false;
+ // visual only, very impossible to test this using asserts.
- AddStep("begin loading", () =>
+ AddStep("load immediately", () =>
{
loader = new TestLoader();
loader.AllowLoad.Set();
LoadScreen(loader);
});
-
- AddUntilStep("loaded", () =>
- {
- logoVisible = loader.Logo?.Alpha > 0;
- return loader.Logo != null && loader.ScreenLoaded;
- });
-
- AddAssert("logo was not visible", () => !logoVisible);
}
[Test]
@@ -58,7 +50,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("begin loading", () => LoadScreen(loader = new TestLoader()));
AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0);
AddStep("finish loading", () => loader.AllowLoad.Set());
- AddAssert("loaded", () => loader.Logo != null && loader.ScreenLoaded);
+ AddUntilStep("loaded", () => loader.Logo != null && loader.ScreenLoaded);
AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Menus/TestSceneScreenNavigation.cs
new file mode 100644
index 0000000000..471f67b7b6
--- /dev/null
+++ b/osu.Game.Tests/Visual/Menus/TestSceneScreenNavigation.cs
@@ -0,0 +1,244 @@
+// 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.Track;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Mods;
+using osu.Game.Screens;
+using osu.Game.Screens.Menu;
+using osu.Game.Screens.Play;
+using osu.Game.Screens.Select;
+using osu.Game.Tests.Beatmaps.IO;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+using IntroSequence = osu.Game.Configuration.IntroSequence;
+
+namespace osu.Game.Tests.Visual.Menus
+{
+ public class TestSceneScreenNavigation : ManualInputManagerTestScene
+ {
+ private const float click_padding = 25;
+
+ private GameHost host;
+ private TestOsuGame game;
+
+ private Vector2 backButtonPosition => game.ToScreenSpace(new Vector2(click_padding, game.LayoutRectangle.Bottom - click_padding));
+
+ private Vector2 optionsButtonPosition => game.ToScreenSpace(new Vector2(click_padding, click_padding));
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host)
+ {
+ this.host = host;
+
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ };
+ }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("Create new game instance", () =>
+ {
+ if (game != null)
+ {
+ Remove(game);
+ game.Dispose();
+ }
+
+ game = new TestOsuGame(LocalStorage, API);
+ game.SetHost(host);
+
+ // todo: this can be removed once we can run audio trakcs without a device present
+ // see https://github.com/ppy/osu/issues/1302
+ game.LocalConfig.Set(OsuSetting.IntroSequence, IntroSequence.Circles);
+
+ Add(game);
+ });
+ AddUntilStep("Wait for load", () => game.IsLoaded);
+ AddUntilStep("Wait for intro", () => game.ScreenStack.CurrentScreen is IntroScreen);
+ confirmAtMainMenu();
+ }
+
+ [Test]
+ public void TestExitSongSelectWithEscape()
+ {
+ TestSongSelect songSelect = null;
+
+ pushAndConfirm(() => songSelect = new TestSongSelect());
+ AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
+ AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
+ pushEscape();
+ AddAssert("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
+ exitViaEscapeAndConfirm();
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestSongContinuesAfterExitPlayer(bool withUserPause)
+ {
+ Player player = null;
+
+ WorkingBeatmap beatmap() => game.Beatmap.Value;
+ Track track() => beatmap().Track;
+
+ pushAndConfirm(() => new TestSongSelect());
+
+ AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Wait());
+
+ AddUntilStep("wait for selected", () => !game.Beatmap.IsDefault);
+
+ if (withUserPause)
+ AddStep("pause", () => game.Dependencies.Get().Stop());
+
+ AddStep("press enter", () => pressAndRelease(Key.Enter));
+
+ AddUntilStep("wait for player", () => (player = game.ScreenStack.CurrentScreen as Player) != null);
+ AddUntilStep("wait for fail", () => player.HasFailed);
+
+ AddUntilStep("wait for track stop", () => !track().IsRunning);
+ AddAssert("Ensure time before preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime);
+
+ pushEscape();
+
+ AddUntilStep("wait for track playing", () => track().IsRunning);
+ AddAssert("Ensure time wasn't reset to preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime);
+ }
+
+ [Test]
+ public void TestExitSongSelectWithClick()
+ {
+ TestSongSelect songSelect = null;
+
+ pushAndConfirm(() => songSelect = new TestSongSelect());
+ AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
+ AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
+ AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition));
+
+ // BackButton handles hover using its child button, so this checks whether or not any of BackButton's children are hovered.
+ AddUntilStep("Back button is hovered", () => InputManager.HoveredDrawables.Any(d => d.Parent == game.BackButton));
+
+ AddStep("Click back button", () => InputManager.Click(MouseButton.Left));
+ AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
+ exitViaBackButtonAndConfirm();
+ }
+
+ [Test]
+ public void TestExitMultiWithEscape()
+ {
+ pushAndConfirm(() => new Screens.Multi.Multiplayer());
+ exitViaEscapeAndConfirm();
+ }
+
+ [Test]
+ public void TestExitMultiWithBackButton()
+ {
+ pushAndConfirm(() => new Screens.Multi.Multiplayer());
+ exitViaBackButtonAndConfirm();
+ }
+
+ [Test]
+ public void TestOpenOptionsAndExitWithEscape()
+ {
+ AddUntilStep("Wait for options to load", () => game.Settings.IsLoaded);
+ AddStep("Enter menu", () => pressAndRelease(Key.Enter));
+ AddStep("Move mouse to options overlay", () => InputManager.MoveMouseTo(optionsButtonPosition));
+ AddStep("Click options overlay", () => InputManager.Click(MouseButton.Left));
+ AddAssert("Options overlay was opened", () => game.Settings.State.Value == Visibility.Visible);
+ AddStep("Hide options overlay using escape", () => pressAndRelease(Key.Escape));
+ AddAssert("Options overlay was closed", () => game.Settings.State.Value == Visibility.Hidden);
+ }
+
+ private void pushAndConfirm(Func newScreen)
+ {
+ Screen screen = null;
+ AddStep("Push new screen", () => game.ScreenStack.Push(screen = newScreen()));
+ AddUntilStep("Wait for new screen", () => game.ScreenStack.CurrentScreen == screen && screen.IsLoaded);
+ }
+
+ private void pushEscape() =>
+ AddStep("Press escape", () => pressAndRelease(Key.Escape));
+
+ private void exitViaEscapeAndConfirm()
+ {
+ pushEscape();
+ confirmAtMainMenu();
+ }
+
+ private void exitViaBackButtonAndConfirm()
+ {
+ AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition));
+ AddStep("Click back button", () => InputManager.Click(MouseButton.Left));
+ confirmAtMainMenu();
+ }
+
+ private void confirmAtMainMenu() => AddUntilStep("Wait for main menu", () => game.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded);
+
+ private void pressAndRelease(Key key)
+ {
+ InputManager.PressKey(key);
+ InputManager.ReleaseKey(key);
+ }
+
+ private class TestOsuGame : OsuGame
+ {
+ public new ScreenStack ScreenStack => base.ScreenStack;
+
+ public new BackButton BackButton => base.BackButton;
+
+ public new SettingsPanel Settings => base.Settings;
+
+ public new OsuConfigManager LocalConfig => base.LocalConfig;
+
+ public new Bindable Beatmap => base.Beatmap;
+
+ protected override Loader CreateLoader() => new TestLoader();
+
+ public TestOsuGame(Storage storage, IAPIProvider api)
+ {
+ Storage = storage;
+ API = api;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ API.Login("Rhythm Champion", "osu!");
+ }
+ }
+
+ private class TestSongSelect : PlaySongSelect
+ {
+ public ModSelectOverlay ModSelectOverlay => ModSelect;
+ }
+
+ private class TestLoader : Loader
+ {
+ protected override ShaderPrecompiler CreateShaderPrecompiler() => new TestShaderPrecompiler();
+
+ private class TestShaderPrecompiler : ShaderPrecompiler
+ {
+ protected override bool AllLoaded => true;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
new file mode 100644
index 0000000000..1f8df438fb
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
@@ -0,0 +1,102 @@
+// 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.Graphics.UserInterface;
+using osu.Game.Beatmaps;
+using osu.Game.Overlays.BeatmapSet;
+using osu.Game.Rulesets;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneBeatmapRulesetSelector : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(BeatmapRulesetSelector),
+ typeof(BeatmapRulesetTabItem),
+ };
+
+ private readonly TestRulesetSelector selector;
+
+ public TestSceneBeatmapRulesetSelector()
+ {
+ Add(selector = new TestRulesetSelector());
+ }
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ [Test]
+ public void TestMultipleRulesetsBeatmapSet()
+ {
+ var enabledRulesets = rulesets.AvailableRulesets.Skip(1).Take(2);
+
+ AddStep("load multiple rulesets beatmapset", () =>
+ {
+ selector.BeatmapSet = new BeatmapSetInfo
+ {
+ Beatmaps = enabledRulesets.Select(r => new BeatmapInfo { Ruleset = r }).ToList()
+ };
+ });
+
+ var tabItems = selector.TabContainer.TabItems;
+ AddAssert("other rulesets disabled", () => tabItems.Except(tabItems.Where(t => enabledRulesets.Any(r => r.Equals(t.Value)))).All(t => !t.Enabled.Value));
+ AddAssert("left-most ruleset selected", () => tabItems.First(t => t.Enabled.Value).Active.Value);
+ }
+
+ [Test]
+ public void TestSingleRulesetBeatmapSet()
+ {
+ var enabledRuleset = rulesets.AvailableRulesets.Last();
+
+ AddStep("load single ruleset beatmapset", () =>
+ {
+ selector.BeatmapSet = new BeatmapSetInfo
+ {
+ Beatmaps = new List
+ {
+ new BeatmapInfo
+ {
+ Ruleset = enabledRuleset
+ }
+ }
+ };
+ });
+
+ AddAssert("single ruleset selected", () => selector.SelectedTab.Value.Equals(enabledRuleset));
+ }
+
+ [Test]
+ public void TestEmptyBeatmapSet()
+ {
+ AddStep("load empty beatmapset", () => selector.BeatmapSet = new BeatmapSetInfo
+ {
+ Beatmaps = new List()
+ });
+
+ AddAssert("no ruleset selected", () => selector.SelectedTab == null);
+ AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
+ }
+
+ [Test]
+ public void TestNullBeatmapSet()
+ {
+ AddStep("load null beatmapset", () => selector.BeatmapSet = null);
+
+ AddAssert("no ruleset selected", () => selector.SelectedTab == null);
+ AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
+ }
+
+ private class TestRulesetSelector : BeatmapRulesetSelector
+ {
+ public new TabItem SelectedTab => base.SelectedTab;
+
+ public new TabFillFlowContainer TabContainer => base.TabContainer;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 9f03d947b9..286971bc90 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -40,24 +40,19 @@ namespace osu.Game.Tests.Visual.Online
typeof(PreviewButton),
typeof(SuccessRate),
typeof(BeatmapAvailability),
+ typeof(BeatmapRulesetSelector),
+ typeof(BeatmapRulesetTabItem),
};
protected override bool UseOnlineAPI => true;
- private RulesetInfo taikoRuleset;
- private RulesetInfo maniaRuleset;
-
public TestSceneBeatmapSetOverlay()
{
Add(overlay = new TestBeatmapSetOverlay());
}
- [BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
- {
- taikoRuleset = rulesets.GetRuleset(1);
- maniaRuleset = rulesets.GetRuleset(3);
- }
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
[Test]
public void TestLoading()
@@ -111,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 9.99,
Version = @"TEST",
Length = 456000,
- Ruleset = maniaRuleset,
+ Ruleset = rulesets.GetRuleset(3),
BaseDifficulty = new BeatmapDifficulty
{
CircleSize = 1,
@@ -189,7 +184,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 5.67,
Version = @"ANOTHER TEST",
Length = 123000,
- Ruleset = taikoRuleset,
+ Ruleset = rulesets.GetRuleset(1),
BaseDifficulty = new BeatmapDifficulty
{
CircleSize = 9,
@@ -217,6 +212,54 @@ namespace osu.Game.Tests.Visual.Online
downloadAssert(false);
}
+ [Test]
+ public void TestMultipleRulesets()
+ {
+ AddStep("show multiple rulesets beatmap", () =>
+ {
+ var beatmaps = new List();
+
+ foreach (var ruleset in rulesets.AvailableRulesets.Skip(1))
+ {
+ beatmaps.Add(new BeatmapInfo
+ {
+ Version = ruleset.Name,
+ Ruleset = ruleset,
+ BaseDifficulty = new BeatmapDifficulty(),
+ OnlineInfo = new BeatmapOnlineInfo(),
+ Metrics = new BeatmapMetrics
+ {
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ },
+ });
+ }
+
+ overlay.ShowBeatmapSet(new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = @"multiple rulesets beatmap",
+ Artist = @"none",
+ Author = new User
+ {
+ Username = "BanchoBot",
+ Id = 3,
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers(),
+ },
+ Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
+ Beatmaps = beatmaps
+ });
+ });
+
+ AddAssert("shown beatmaps of current ruleset", () => overlay.Header.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value)));
+ AddAssert("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
+ }
+
[Test]
public void TestHide()
{
@@ -281,12 +324,12 @@ namespace osu.Game.Tests.Visual.Online
private void downloadAssert(bool shown)
{
- AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.DownloadButtonsVisible == shown);
+ AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.DownloadButtonsVisible == shown);
}
private class TestBeatmapSetOverlay : BeatmapSetOverlay
{
- public bool DownloadButtonsVisible => Header.DownloadButtonsVisible;
+ public new Header Header => base.Header;
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
new file mode 100644
index 0000000000..86bd0ddd11
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
@@ -0,0 +1,59 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Online.API.Requests;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Overlays.Comments;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [TestFixture]
+ public class TestSceneCommentsContainer : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(CommentsContainer),
+ typeof(CommentsHeader),
+ typeof(DrawableComment),
+ typeof(HeaderButton),
+ typeof(SortTabControl),
+ typeof(ShowChildrenButton),
+ typeof(DeletedChildrenPlaceholder),
+ typeof(VotePill)
+ };
+
+ protected override bool UseOnlineAPI => true;
+
+ public TestSceneCommentsContainer()
+ {
+ BasicScrollContainer scrollFlow;
+
+ Add(scrollFlow = new BasicScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ });
+
+ AddStep("Big Black comments", () =>
+ {
+ scrollFlow.Clear();
+ scrollFlow.Add(new CommentsContainer(CommentableType.Beatmapset, 41823));
+ });
+
+ AddStep("Airman comments", () =>
+ {
+ scrollFlow.Clear();
+ scrollFlow.Add(new CommentsContainer(CommentableType.Beatmapset, 24313));
+ });
+
+ AddStep("lazer build comments", () =>
+ {
+ scrollFlow.Clear();
+ scrollFlow.Add(new CommentsContainer(CommentableType.Build, 4772));
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs
new file mode 100644
index 0000000000..bc3e0eff1a
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs
@@ -0,0 +1,39 @@
+// 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.Bindables;
+using osu.Game.Overlays.Comments;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [TestFixture]
+ public class TestSceneCommentsHeader : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(CommentsHeader),
+ typeof(HeaderButton),
+ typeof(SortTabControl),
+ };
+
+ private readonly Bindable sort = new Bindable();
+ private readonly BindableBool showDeleted = new BindableBool();
+
+ public TestSceneCommentsHeader()
+ {
+ Add(new CommentsHeader
+ {
+ Sort = { BindTarget = sort },
+ ShowDeleted = { BindTarget = showDeleted }
+ });
+
+ AddStep("Trigger ShowDeleted", () => showDeleted.Value = !showDeleted.Value);
+ AddStep("Select old", () => sort.Value = CommentsSortCriteria.Old);
+ AddStep("Select new", () => sort.Value = CommentsSortCriteria.New);
+ AddStep("Select top", () => sort.Value = CommentsSortCriteria.Top);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs b/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs
new file mode 100644
index 0000000000..8e2ee4e28d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs
@@ -0,0 +1,54 @@
+// 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.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Overlays.BeatmapSet.Buttons;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneFavouriteButton : OsuTestScene
+ {
+ private FavouriteButton favourite;
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create button", () => Child = favourite = new FavouriteButton
+ {
+ RelativeSizeAxes = Axes.None,
+ Size = new Vector2(50),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ }
+
+ [Test]
+ public void TestLoggedOutIn()
+ {
+ AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new BeatmapSetInfo { OnlineBeatmapSetID = 88 });
+ AddStep("log out", () => API.Logout());
+ checkEnabled(false);
+ AddStep("log in", () => API.Login("test", "test"));
+ checkEnabled(true);
+ }
+
+ [Test]
+ public void TestBeatmapChange()
+ {
+ AddStep("log in", () => API.Login("test", "test"));
+ AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new BeatmapSetInfo { OnlineBeatmapSetID = 88 });
+ checkEnabled(true);
+ AddStep("set invalid beatmap", () => favourite.BeatmapSet.Value = new BeatmapSetInfo());
+ checkEnabled(false);
+ }
+
+ private void checkEnabled(bool expected)
+ {
+ AddAssert("is " + (expected ? "enabled" : "disabled"), () => favourite.Enabled.Value == expected);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
new file mode 100644
index 0000000000..546f6ac182
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.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 osu.Game.Overlays;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneNewsOverlay : OsuTestScene
+ {
+ private NewsOverlay news;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Add(news = new NewsOverlay());
+ AddStep(@"Show", news.Show);
+ AddStep(@"Hide", news.Hide);
+
+ AddStep(@"Show front page", () => news.ShowFrontPage());
+ AddStep(@"Custom article", () => news.Current.Value = "Test Article 101");
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
index bccb263600..b9fbbfef6b 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
@@ -1,10 +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.Overlays.Profile.Sections;
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Framework.Allocation;
+using osu.Game.Graphics;
namespace osu.Game.Tests.Visual.Online
{
@@ -17,11 +19,11 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneShowMoreButton()
{
- ShowMoreButton button = null;
+ TestButton button = null;
int fireCount = 0;
- Add(button = new ShowMoreButton
+ Add(button = new TestButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -51,5 +53,16 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("action fired twice", () => fireCount == 2);
AddAssert("is in loading state", () => button.IsLoading);
}
+
+ private class TestButton : ShowMoreButton
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colors)
+ {
+ IdleColour = colors.YellowDark;
+ HoverColour = colors.Yellow;
+ ChevronIconColour = colors.Red;
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
index 91006bc0d9..28b5693ef4 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
@@ -6,6 +6,11 @@ using osu.Framework.Graphics;
using osu.Game.Online.Chat;
using osu.Game.Users;
using osuTK;
+using System;
+using System.Linq;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Containers;
+using osu.Game.Overlays.Chat;
namespace osu.Game.Tests.Visual.Online
{
@@ -32,17 +37,23 @@ namespace osu.Game.Tests.Visual.Online
Id = 4,
};
+ private readonly User longUsernameUser = new User
+ {
+ Username = "Very Long Long Username",
+ Id = 5,
+ };
+
[Cached]
private ChannelManager channelManager = new ChannelManager();
- private readonly StandAloneChatDisplay chatDisplay;
- private readonly StandAloneChatDisplay chatDisplay2;
+ private readonly TestStandAloneChatDisplay chatDisplay;
+ private readonly TestStandAloneChatDisplay chatDisplay2;
public TestSceneStandAloneChatDisplay()
{
Add(channelManager);
- Add(chatDisplay = new StandAloneChatDisplay
+ Add(chatDisplay = new TestStandAloneChatDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -50,7 +61,7 @@ namespace osu.Game.Tests.Visual.Online
Size = new Vector2(400, 80)
});
- Add(chatDisplay2 = new StandAloneChatDisplay(true)
+ Add(chatDisplay2 = new TestStandAloneChatDisplay(true)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@@ -99,6 +110,66 @@ namespace osu.Game.Tests.Visual.Online
Sender = admin,
Content = "Okay okay, calm down guys. Let's do this!"
}));
+
+ AddStep("message from long username", () => testChannel.AddNewMessages(new Message(sequence++)
+ {
+ Sender = longUsernameUser,
+ Content = "Hi guys, my new username is lit!"
+ }));
+
+ AddStep("message with new date", () => testChannel.AddNewMessages(new Message(sequence++)
+ {
+ Sender = longUsernameUser,
+ Content = "Message from the future!",
+ Timestamp = DateTimeOffset.Now
+ }));
+
+ AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom);
+
+ const int messages_per_call = 10;
+ AddRepeatStep("add many messages", () =>
+ {
+ for (int i = 0; i < messages_per_call; i++)
+ {
+ testChannel.AddNewMessages(new Message(sequence++)
+ {
+ Sender = longUsernameUser,
+ Content = "Many messages! " + Guid.NewGuid(),
+ Timestamp = DateTimeOffset.Now
+ });
+ }
+ }, Channel.MAX_HISTORY / messages_per_call + 5);
+
+ AddAssert("Ensure no adjacent day separators", () =>
+ {
+ var indices = chatDisplay.FillFlow.OfType().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
+
+ foreach (var i in indices)
+ {
+ if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator)
+ return false;
+ }
+
+ return true;
+ });
+
+ AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom);
+ }
+
+ private class TestStandAloneChatDisplay : StandAloneChatDisplay
+ {
+ public TestStandAloneChatDisplay(bool textbox = false)
+ : base(textbox)
+ {
+ }
+
+ protected DrawableChannel DrawableChannel => InternalChildren.OfType().First();
+
+ protected OsuScrollContainer ScrollContainer => (OsuScrollContainer)((Container)DrawableChannel.Child).Child;
+
+ public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child;
+
+ public bool ScrolledToBottom => ScrollContainer.IsScrolledToEnd(1);
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs b/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs
new file mode 100644
index 0000000000..4702d24125
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Bindables;
+using osu.Game.Overlays.Comments;
+using osu.Framework.MathUtils;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneTotalCommentsCounter : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(TotalCommentsCounter),
+ };
+
+ public TestSceneTotalCommentsCounter()
+ {
+ var count = new BindableInt();
+
+ Add(new TotalCommentsCounter
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Current = { BindTarget = count }
+ });
+
+ AddStep(@"Set 100", () => count.Value = 100);
+ AddStep(@"Set 0", () => count.Value = 0);
+ AddStep(@"Set random", () => count.Value = RNG.Next(0, int.MaxValue));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index 93e6607ac5..98da63508b 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -107,6 +107,15 @@ namespace osu.Game.Tests.Visual.Online
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
}, api.IsLoggedIn));
+ AddStep("Show bancho", () => profile.ShowUser(new User
+ {
+ Username = @"BanchoBot",
+ Id = 3,
+ IsBot = true,
+ Country = new Country { FullName = @"Saint Helena", FlagName = @"SH" },
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg"
+ }, api.IsLoggedIn));
+
AddStep("Hide", profile.Hide);
AddStep("Show without reload", profile.Show);
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
new file mode 100644
index 0000000000..8197cf72de
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
@@ -0,0 +1,76 @@
+// 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.Overlays.Comments;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [TestFixture]
+ public class TestSceneVotePill : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(VotePill)
+ };
+
+ private VotePill votePill;
+
+ [Test]
+ public void TestUserCommentPill()
+ {
+ AddStep("Log in", logIn);
+ AddStep("User comment", () => addVotePill(getUserComment()));
+ AddStep("Click", () => votePill.Click());
+ AddAssert("Not loading", () => !votePill.IsLoading);
+ }
+
+ [Test]
+ public void TestRandomCommentPill()
+ {
+ AddStep("Log in", logIn);
+ AddStep("Random comment", () => addVotePill(getRandomComment()));
+ AddStep("Click", () => votePill.Click());
+ AddAssert("Loading", () => votePill.IsLoading);
+ }
+
+ [Test]
+ public void TestOfflineRandomCommentPill()
+ {
+ AddStep("Log out", API.Logout);
+ AddStep("Random comment", () => addVotePill(getRandomComment()));
+ AddStep("Click", () => votePill.Click());
+ AddAssert("Not loading", () => !votePill.IsLoading);
+ }
+
+ private void logIn() => API.Login("localUser", "password");
+
+ private Comment getUserComment() => new Comment
+ {
+ IsVoted = false,
+ UserId = API.LocalUser.Value.Id,
+ VotesCount = 10,
+ };
+
+ private Comment getRandomComment() => new Comment
+ {
+ IsVoted = false,
+ UserId = 4444,
+ VotesCount = 2,
+ };
+
+ private void addVotePill(Comment comment)
+ {
+ Clear();
+ Add(votePill = new VotePill(comment)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 6669ec7da3..132b104afb 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -10,6 +10,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
@@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private readonly Stack selectedSets = new Stack();
private readonly HashSet eagerSelectedIDs = new HashSet();
- private BeatmapInfo currentSelection;
+ private BeatmapInfo currentSelection => carousel.SelectedBeatmap;
private const int set_count = 5;
@@ -51,50 +52,433 @@ namespace osu.Game.Tests.Visual.SongSelect
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
-
- Add(carousel = new TestBeatmapCarousel
- {
- RelativeSizeAxes = Axes.Both,
- });
-
- List beatmapSets = new List();
-
- for (int i = 1; i <= set_count; i++)
- beatmapSets.Add(createTestBeatmapSet(i));
-
- carousel.SelectionChanged = s => currentSelection = s;
-
- loadBeatmaps(beatmapSets);
-
- testTraversal();
- testFiltering();
- testRandom();
- testAddRemove();
- testSorting();
-
- testRemoveAll();
- testEmptyTraversal();
- testHiding();
- testSelectingFilteredRuleset();
- testCarouselRootIsRandom();
}
- private void loadBeatmaps(List beatmapSets)
+ ///
+ /// Test keyboard traversal
+ ///
+ [Test]
+ public void TestTraversal()
{
+ loadBeatmaps();
+
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(1, 1);
+
+ advanceSelection(direction: 1, diff: true);
+ waitForSelection(1, 2);
+
+ advanceSelection(direction: -1, diff: false);
+ waitForSelection(set_count, 1);
+
+ advanceSelection(direction: -1, diff: true);
+ waitForSelection(set_count - 1, 3);
+
+ advanceSelection(diff: false);
+ advanceSelection(diff: false);
+ waitForSelection(1, 2);
+
+ advanceSelection(direction: -1, diff: true);
+ advanceSelection(direction: -1, diff: true);
+ waitForSelection(set_count, 3);
+ }
+
+ ///
+ /// Test filtering
+ ///
+ [Test]
+ public void TestFiltering()
+ {
+ loadBeatmaps();
+
+ // basic filtering
+
+ setSelected(1, 1);
+
+ AddStep("Filter", () => carousel.Filter(new FilterCriteria { SearchText = "set #3!" }, false));
+ checkVisibleItemCount(diff: false, count: 1);
+ checkVisibleItemCount(diff: true, count: 3);
+ waitForSelection(3, 1);
+
+ advanceSelection(diff: true, count: 4);
+ waitForSelection(3, 2);
+
+ AddStep("Un-filter (debounce)", () => carousel.Filter(new FilterCriteria()));
+ AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
+ checkVisibleItemCount(diff: false, count: set_count);
+ checkVisibleItemCount(diff: true, count: 3);
+
+ // test filtering some difficulties (and keeping current beatmap set selected).
+
+ setSelected(1, 2);
+ AddStep("Filter some difficulties", () => carousel.Filter(new FilterCriteria { SearchText = "Normal" }, false));
+ waitForSelection(1, 1);
+
+ AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
+ waitForSelection(1, 1);
+
+ AddStep("Filter all", () => carousel.Filter(new FilterCriteria { SearchText = "Dingo" }, false));
+
+ checkVisibleItemCount(false, 0);
+ checkVisibleItemCount(true, 0);
+ AddAssert("Selection is null", () => currentSelection == null);
+
+ advanceSelection(true);
+ AddAssert("Selection is null", () => currentSelection == null);
+
+ advanceSelection(false);
+ AddAssert("Selection is null", () => currentSelection == null);
+
+ AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
+
+ AddAssert("Selection is non-null", () => currentSelection != null);
+
+ setSelected(1, 3);
+ }
+
+ [Test]
+ public void TestFilterRange()
+ {
+ loadBeatmaps();
+
+ // buffer the selection
+ setSelected(3, 2);
+
+ setSelected(1, 3);
+
+ AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria
+ {
+ SearchText = "#3",
+ StarDifficulty = new FilterCriteria.OptionalRange
+ {
+ Min = 2,
+ Max = 5.5,
+ IsLowerInclusive = true
+ }
+ }, false));
+
+ // should reselect the buffered selection.
+ waitForSelection(3, 2);
+ }
+
+ ///
+ /// Test random non-repeating algorithm
+ ///
+ [Test]
+ public void TestRandom()
+ {
+ loadBeatmaps();
+
+ setSelected(1, 1);
+
+ nextRandom();
+ ensureRandomDidntRepeat();
+ nextRandom();
+ ensureRandomDidntRepeat();
+ nextRandom();
+ ensureRandomDidntRepeat();
+
+ prevRandom();
+ ensureRandomFetchSuccess();
+ prevRandom();
+ ensureRandomFetchSuccess();
+
+ nextRandom();
+ ensureRandomDidntRepeat();
+ nextRandom();
+ ensureRandomDidntRepeat();
+
+ nextRandom();
+ AddAssert("ensure repeat", () => selectedSets.Contains(carousel.SelectedBeatmapSet));
+
+ AddStep("Add set with 100 difficulties", () => carousel.UpdateBeatmapSet(createTestBeatmapSetWithManyDifficulties(set_count + 1)));
+ AddStep("Filter Extra", () => carousel.Filter(new FilterCriteria { SearchText = "Extra 10" }, false));
+ checkInvisibleDifficultiesUnselectable();
+ checkInvisibleDifficultiesUnselectable();
+ checkInvisibleDifficultiesUnselectable();
+ checkInvisibleDifficultiesUnselectable();
+ checkInvisibleDifficultiesUnselectable();
+ AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
+ }
+
+ ///
+ /// Test adding and removing beatmap sets
+ ///
+ [Test]
+ public void TestAddRemove()
+ {
+ loadBeatmaps();
+
+ AddStep("Add new set", () => carousel.UpdateBeatmapSet(createTestBeatmapSet(set_count + 1)));
+ AddStep("Add new set", () => carousel.UpdateBeatmapSet(createTestBeatmapSet(set_count + 2)));
+
+ checkVisibleItemCount(false, set_count + 2);
+
+ AddStep("Remove set", () => carousel.RemoveBeatmapSet(createTestBeatmapSet(set_count + 2)));
+
+ checkVisibleItemCount(false, set_count + 1);
+
+ setSelected(set_count + 1, 1);
+
+ AddStep("Remove set", () => carousel.RemoveBeatmapSet(createTestBeatmapSet(set_count + 1)));
+
+ checkVisibleItemCount(false, set_count);
+
+ waitForSelection(set_count);
+ }
+
+ ///
+ /// Test sorting
+ ///
+ [Test]
+ public void TestSorting()
+ {
+ loadBeatmaps();
+
+ AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
+ AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
+ AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
+ AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
+ }
+
+ [Test]
+ public void TestSortingStability()
+ {
+ var sets = new List();
+
+ for (int i = 0; i < 20; i++)
+ {
+ var set = createTestBeatmapSet(i);
+ set.Metadata.Artist = "same artist";
+ set.Metadata.Title = "same title";
+ sets.Add(set);
+ }
+
+ loadBeatmaps(sets);
+
+ AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
+ AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b));
+
+ AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
+ AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b));
+ }
+
+ [Test]
+ public void TestSortingWithFiltered()
+ {
+ List sets = new List();
+
+ for (int i = 0; i < 3; i++)
+ {
+ var set = createTestBeatmapSet(i);
+ set.Beatmaps[0].StarDifficulty = 3 - i;
+ set.Beatmaps[2].StarDifficulty = 6 + i;
+ sets.Add(set);
+ }
+
+ loadBeatmaps(sets);
+
+ AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
+ AddAssert("Check first set at end", () => carousel.BeatmapSets.First() == sets.Last());
+ AddAssert("Check last set at start", () => carousel.BeatmapSets.Last() == sets.First());
+
+ AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
+ AddAssert("Check first set at start", () => carousel.BeatmapSets.First() == sets.First());
+ AddAssert("Check last set at end", () => carousel.BeatmapSets.Last() == sets.Last());
+ }
+
+ [Test]
+ public void TestRemoveAll()
+ {
+ loadBeatmaps();
+
+ setSelected(2, 1);
+ AddAssert("Selection is non-null", () => currentSelection != null);
+
+ AddStep("Remove selected", () => carousel.RemoveBeatmapSet(carousel.SelectedBeatmapSet));
+ waitForSelection(2);
+
+ AddStep("Remove first", () => carousel.RemoveBeatmapSet(carousel.BeatmapSets.First()));
+ AddStep("Remove first", () => carousel.RemoveBeatmapSet(carousel.BeatmapSets.First()));
+ waitForSelection(1);
+
+ AddUntilStep("Remove all", () =>
+ {
+ if (!carousel.BeatmapSets.Any()) return true;
+
+ carousel.RemoveBeatmapSet(carousel.BeatmapSets.Last());
+ return false;
+ });
+
+ checkNoSelection();
+ }
+
+ [Test]
+ public void TestEmptyTraversal()
+ {
+ loadBeatmaps(new List());
+
+ advanceSelection(direction: 1, diff: false);
+ checkNoSelection();
+
+ advanceSelection(direction: 1, diff: true);
+ checkNoSelection();
+
+ advanceSelection(direction: -1, diff: false);
+ checkNoSelection();
+
+ advanceSelection(direction: -1, diff: true);
+ checkNoSelection();
+ }
+
+ [Test]
+ public void TestHiding()
+ {
+ BeatmapSetInfo hidingSet = null;
+ List hiddenList = new List();
+
+ AddStep("create hidden set", () =>
+ {
+ hidingSet = createTestBeatmapSet(1);
+ hidingSet.Beatmaps[1].Hidden = true;
+
+ hiddenList.Clear();
+ hiddenList.Add(hidingSet);
+ });
+
+ loadBeatmaps(hiddenList);
+
+ setSelected(1, 1);
+
+ checkVisibleItemCount(true, 2);
+ advanceSelection(true);
+ waitForSelection(1, 3);
+
+ setHidden(3);
+ waitForSelection(1, 1);
+
+ setHidden(2, false);
+ advanceSelection(true);
+ waitForSelection(1, 2);
+
+ setHidden(1);
+ waitForSelection(1, 2);
+
+ setHidden(2);
+ checkNoSelection();
+
+ void setHidden(int diff, bool hidden = true)
+ {
+ AddStep((hidden ? "" : "un") + $"hide diff {diff}", () =>
+ {
+ hidingSet.Beatmaps[diff - 1].Hidden = hidden;
+ carousel.UpdateBeatmapSet(hidingSet);
+ });
+ }
+ }
+
+ [Test]
+ public void TestSelectingFilteredRuleset()
+ {
+ BeatmapSetInfo testMixed = null;
+
+ createCarousel();
+
+ AddStep("add mixed ruleset beatmapset", () =>
+ {
+ testMixed = createTestBeatmapSet(set_count + 1);
+
+ for (int i = 0; i <= 2; i++)
+ {
+ testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
+ testMixed.Beatmaps[i].RulesetID = i;
+ }
+
+ carousel.UpdateBeatmapSet(testMixed);
+ });
+ AddStep("filter to ruleset 0", () =>
+ carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false));
+ AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false));
+ AddAssert("unfiltered beatmap selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0]));
+
+ AddStep("remove mixed set", () =>
+ {
+ carousel.RemoveBeatmapSet(testMixed);
+ testMixed = null;
+ });
+ var testSingle = createTestBeatmapSet(set_count + 2);
+ testSingle.Beatmaps.ForEach(b =>
+ {
+ b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
+ b.RulesetID = b.Ruleset.ID ?? 1;
+ });
+ AddStep("add single ruleset beatmapset", () => carousel.UpdateBeatmapSet(testSingle));
+ AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testSingle.Beatmaps[0], false));
+ checkNoSelection();
+ AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle));
+ }
+
+ [Test]
+ public void TestCarouselRootIsRandom()
+ {
+ List manySets = new List();
+
+ for (int i = 1; i <= 50; i++)
+ manySets.Add(createTestBeatmapSet(i));
+
+ loadBeatmaps(manySets);
+
+ advanceSelection(direction: 1, diff: false);
+ checkNonmatchingFilter();
+ checkNonmatchingFilter();
+ checkNonmatchingFilter();
+ checkNonmatchingFilter();
+ checkNonmatchingFilter();
+ AddAssert("Selection was random", () => eagerSelectedIDs.Count > 1);
+ }
+
+ private void loadBeatmaps(List beatmapSets = null)
+ {
+ createCarousel();
+
+ if (beatmapSets == null)
+ {
+ beatmapSets = new List();
+
+ for (int i = 1; i <= set_count; i++)
+ beatmapSets.Add(createTestBeatmapSet(i));
+ }
+
bool changed = false;
AddStep($"Load {beatmapSets.Count} Beatmaps", () =>
{
+ carousel.Filter(new FilterCriteria());
carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets;
});
+
AddUntilStep("Wait for load", () => changed);
}
+ private void createCarousel(Container target = null)
+ {
+ AddStep("Create carousel", () =>
+ {
+ selectedSets.Clear();
+ eagerSelectedIDs.Clear();
+
+ (target ?? this).Child = carousel = new TestBeatmapCarousel
+ {
+ RelativeSizeAxes = Axes.Both,
+ };
+ });
+ }
+
private void ensureRandomFetchSuccess() =>
AddAssert("ensure prev random fetch worked", () => selectedSets.Peek() == carousel.SelectedBeatmapSet);
- private void checkSelected(int set, int? diff = null) =>
- AddAssert($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () =>
+ private void waitForSelection(int set, int? diff = null) =>
+ AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () =>
{
if (diff != null)
return carousel.SelectedBeatmap == carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Skip(diff.Value - 1).First();
@@ -109,8 +493,10 @@ namespace osu.Game.Tests.Visual.SongSelect
private void advanceSelection(bool diff, int direction = 1, int count = 1)
{
if (count == 1)
+ {
AddStep($"select {(direction > 0 ? "next" : "prev")} {(diff ? "diff" : "set")}", () =>
carousel.SelectNext(direction, !diff));
+ }
else
{
AddRepeatStep($"select {(direction > 0 ? "next" : "prev")} {(diff ? "diff" : "set")}", () =>
@@ -170,275 +556,6 @@ namespace osu.Game.Tests.Visual.SongSelect
});
}
- ///
- /// Test keyboard traversal
- ///
- private void testTraversal()
- {
- advanceSelection(direction: 1, diff: false);
- checkSelected(1, 1);
-
- advanceSelection(direction: 1, diff: true);
- checkSelected(1, 2);
-
- advanceSelection(direction: -1, diff: false);
- checkSelected(set_count, 1);
-
- advanceSelection(direction: -1, diff: true);
- checkSelected(set_count - 1, 3);
-
- advanceSelection(diff: false);
- advanceSelection(diff: false);
- checkSelected(1, 2);
-
- advanceSelection(direction: -1, diff: true);
- advanceSelection(direction: -1, diff: true);
- checkSelected(set_count, 3);
- }
-
- ///
- /// Test filtering
- ///
- private void testFiltering()
- {
- // basic filtering
-
- setSelected(1, 1);
-
- AddStep("Filter", () => carousel.Filter(new FilterCriteria { SearchText = "set #3!" }, false));
- checkVisibleItemCount(diff: false, count: 1);
- checkVisibleItemCount(diff: true, count: 3);
- checkSelected(3, 1);
-
- advanceSelection(diff: true, count: 4);
- checkSelected(3, 2);
-
- AddStep("Un-filter (debounce)", () => carousel.Filter(new FilterCriteria()));
- AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
- checkVisibleItemCount(diff: false, count: set_count);
- checkVisibleItemCount(diff: true, count: 3);
-
- // test filtering some difficulties (and keeping current beatmap set selected).
-
- setSelected(1, 2);
- AddStep("Filter some difficulties", () => carousel.Filter(new FilterCriteria { SearchText = "Normal" }, false));
- checkSelected(1, 1);
-
- AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
- checkSelected(1, 1);
-
- AddStep("Filter all", () => carousel.Filter(new FilterCriteria { SearchText = "Dingo" }, false));
-
- checkVisibleItemCount(false, 0);
- checkVisibleItemCount(true, 0);
- AddAssert("Selection is null", () => currentSelection == null);
-
- advanceSelection(true);
- AddAssert("Selection is null", () => currentSelection == null);
-
- advanceSelection(false);
- AddAssert("Selection is null", () => currentSelection == null);
-
- AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
-
- AddAssert("Selection is non-null", () => currentSelection != null);
- }
-
- ///
- /// Test random non-repeating algorithm
- ///
- private void testRandom()
- {
- setSelected(1, 1);
-
- nextRandom();
- ensureRandomDidntRepeat();
- nextRandom();
- ensureRandomDidntRepeat();
- nextRandom();
- ensureRandomDidntRepeat();
-
- prevRandom();
- ensureRandomFetchSuccess();
- prevRandom();
- ensureRandomFetchSuccess();
-
- nextRandom();
- ensureRandomDidntRepeat();
- nextRandom();
- ensureRandomDidntRepeat();
-
- nextRandom();
- AddAssert("ensure repeat", () => selectedSets.Contains(carousel.SelectedBeatmapSet));
-
- AddStep("Add set with 100 difficulties", () => carousel.UpdateBeatmapSet(createTestBeatmapSetWithManyDifficulties(set_count + 1)));
- AddStep("Filter Extra", () => carousel.Filter(new FilterCriteria { SearchText = "Extra 10" }, false));
- checkInvisibleDifficultiesUnselectable();
- checkInvisibleDifficultiesUnselectable();
- checkInvisibleDifficultiesUnselectable();
- checkInvisibleDifficultiesUnselectable();
- checkInvisibleDifficultiesUnselectable();
- AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
- }
-
- ///
- /// Test adding and removing beatmap sets
- ///
- private void testAddRemove()
- {
- AddStep("Add new set", () => carousel.UpdateBeatmapSet(createTestBeatmapSet(set_count + 1)));
- AddStep("Add new set", () => carousel.UpdateBeatmapSet(createTestBeatmapSet(set_count + 2)));
-
- checkVisibleItemCount(false, set_count + 2);
-
- AddStep("Remove set", () => carousel.RemoveBeatmapSet(createTestBeatmapSet(set_count + 2)));
-
- checkVisibleItemCount(false, set_count + 1);
-
- setSelected(set_count + 1, 1);
-
- AddStep("Remove set", () => carousel.RemoveBeatmapSet(createTestBeatmapSet(set_count + 1)));
-
- checkVisibleItemCount(false, set_count);
-
- checkSelected(set_count);
- }
-
- ///
- /// Test sorting
- ///
- private void testSorting()
- {
- AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
- AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
- AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
- AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
- }
-
- private void testRemoveAll()
- {
- setSelected(2, 1);
- AddAssert("Selection is non-null", () => currentSelection != null);
-
- AddStep("Remove selected", () => carousel.RemoveBeatmapSet(carousel.SelectedBeatmapSet));
- checkSelected(2);
-
- AddStep("Remove first", () => carousel.RemoveBeatmapSet(carousel.BeatmapSets.First()));
- AddStep("Remove first", () => carousel.RemoveBeatmapSet(carousel.BeatmapSets.First()));
- checkSelected(1);
-
- AddUntilStep("Remove all", () =>
- {
- if (!carousel.BeatmapSets.Any()) return true;
-
- carousel.RemoveBeatmapSet(carousel.BeatmapSets.Last());
- return false;
- });
-
- checkNoSelection();
- }
-
- private void testEmptyTraversal()
- {
- advanceSelection(direction: 1, diff: false);
- checkNoSelection();
-
- advanceSelection(direction: 1, diff: true);
- checkNoSelection();
-
- advanceSelection(direction: -1, diff: false);
- checkNoSelection();
-
- advanceSelection(direction: -1, diff: true);
- checkNoSelection();
- }
-
- private void testHiding()
- {
- var hidingSet = createTestBeatmapSet(1);
- hidingSet.Beatmaps[1].Hidden = true;
- AddStep("Add set with diff 2 hidden", () => carousel.UpdateBeatmapSet(hidingSet));
- setSelected(1, 1);
-
- checkVisibleItemCount(true, 2);
- advanceSelection(true);
- checkSelected(1, 3);
-
- setHidden(3);
- checkSelected(1, 1);
-
- setHidden(2, false);
- advanceSelection(true);
- checkSelected(1, 2);
-
- setHidden(1);
- checkSelected(1, 2);
-
- setHidden(2);
- checkNoSelection();
-
- void setHidden(int diff, bool hidden = true)
- {
- AddStep((hidden ? "" : "un") + $"hide diff {diff}", () =>
- {
- hidingSet.Beatmaps[diff - 1].Hidden = hidden;
- carousel.UpdateBeatmapSet(hidingSet);
- });
- }
- }
-
- private void testSelectingFilteredRuleset()
- {
- var testMixed = createTestBeatmapSet(set_count + 1);
- AddStep("add mixed ruleset beatmapset", () =>
- {
- for (int i = 0; i <= 2; i++)
- {
- testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
- testMixed.Beatmaps[i].RulesetID = i;
- }
-
- carousel.UpdateBeatmapSet(testMixed);
- });
- AddStep("filter to ruleset 0", () =>
- carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false));
- AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false));
- AddAssert("unfiltered beatmap selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0]));
-
- AddStep("remove mixed set", () =>
- {
- carousel.RemoveBeatmapSet(testMixed);
- testMixed = null;
- });
- var testSingle = createTestBeatmapSet(set_count + 2);
- testSingle.Beatmaps.ForEach(b =>
- {
- b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
- b.RulesetID = b.Ruleset.ID ?? 1;
- });
- AddStep("add single ruleset beatmapset", () => carousel.UpdateBeatmapSet(testSingle));
- AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testSingle.Beatmaps[0], false));
- checkNoSelection();
- AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle));
- }
-
- private void testCarouselRootIsRandom()
- {
- List beatmapSets = new List();
-
- for (int i = 1; i <= 50; i++)
- beatmapSets.Add(createTestBeatmapSet(i));
-
- loadBeatmaps(beatmapSets);
- advanceSelection(direction: 1, diff: false);
- checkNonmatchingFilter();
- checkNonmatchingFilter();
- checkNonmatchingFilter();
- checkNonmatchingFilter();
- checkNonmatchingFilter();
- AddAssert("Selection was random", () => eagerSelectedIDs.Count > 1);
- }
-
private BeatmapSetInfo createTestBeatmapSet(int id)
{
return new BeatmapSetInfo
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
index 932e114580..f49d7a14a6 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
@@ -75,7 +75,6 @@ namespace osu.Game.Tests.Visual.SongSelect
testBeatmapLabels(instance);
- // TODO: adjust cases once more info is shown for other gamemodes
switch (instance)
{
case OsuRuleset _:
@@ -99,8 +98,6 @@ namespace osu.Game.Tests.Visual.SongSelect
break;
}
}
-
- testNullBeatmap();
}
private void testBeatmapLabels(Ruleset ruleset)
@@ -117,7 +114,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check info labels count", () => infoWedge.Info.InfoLabelContainer.Children.Count == expectedCount);
}
- private void testNullBeatmap()
+ [Test]
+ public void TestNullBeatmap()
{
selectBeatmap(null);
AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text));
@@ -127,6 +125,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any());
}
+ [Test]
+ public void TestTruncation()
+ {
+ selectBeatmap(createLongMetadata());
+ }
+
private void selectBeatmap([CanBeNull] IBeatmap b)
{
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null;
@@ -166,6 +170,25 @@ namespace osu.Game.Tests.Visual.SongSelect
};
}
+ private IBeatmap createLongMetadata()
+ {
+ return new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ AuthorString = "WWWWWWWWWWWWWWW",
+ Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
+ Source = "Verrrrry long Source",
+ Title = "Verrrrry long Title"
+ },
+ Version = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
+ Status = BeatmapSetOnlineStatus.Graveyard,
+ },
+ };
+ }
+
private class TestBeatmapInfoWedge : BeatmapInfoWedge
{
public new BufferedWedgeInfo Info => base.Info;
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs
index ecdc484887..f55c099d83 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs
@@ -18,8 +18,8 @@ namespace osu.Game.Tests.Visual.SongSelect
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, Color4.Purple, null, Key.Number1);
overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, Color4.Purple, null, Key.Number2);
- overlay.AddButton(@"Edit", @"Beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null, Key.Number3);
- overlay.AddButton(@"Delete", @"Beatmap", FontAwesome.Solid.Trash, Color4.Pink, null, Key.Number4, float.MaxValue);
+ overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, Color4.Pink, null, Key.Number3);
+ overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null, Key.Number4);
Add(overlay);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 263eada07c..a4b8d1a24a 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -16,6 +16,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@@ -34,6 +35,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private RulesetStore rulesets;
+ private MusicController music;
+
private WorkingBeatmap defaultBeatmap;
public override IReadOnlyList RequiredTypes => new[]
@@ -54,23 +57,6 @@ namespace osu.Game.Tests.Visual.SongSelect
typeof(DrawableCarouselBeatmapSet),
};
- private class TestSongSelect : PlaySongSelect
- {
- public Action StartRequested;
-
- public new Bindable Ruleset => base.Ruleset;
-
- public WorkingBeatmap CurrentBeatmap => Beatmap.Value;
- public WorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap;
- public new BeatmapCarousel Carousel => base.Carousel;
-
- protected override bool OnStart()
- {
- StartRequested?.Invoke();
- return base.OnStart();
- }
- }
-
private TestSongSelect songSelect;
[BackgroundDependencyLoader]
@@ -79,6 +65,11 @@ namespace osu.Game.Tests.Visual.SongSelect
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, defaultBeatmap = Beatmap.Default));
+ Dependencies.Cache(music = new MusicController());
+
+ // required to get bindables attached
+ Add(music);
+
Beatmap.SetDefault();
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
@@ -93,6 +84,70 @@ namespace osu.Game.Tests.Visual.SongSelect
manager?.Delete(manager.GetAllUsableBeatmapSets());
});
+ [Test]
+ public void TestSingleFilterOnEnter()
+ {
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ createSongSelect();
+
+ AddAssert("filter count is 1", () => songSelect.FilterCount == 1);
+ }
+
+ [Test]
+ public void TestAudioResuming()
+ {
+ createSongSelect();
+
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ checkMusicPlaying(true);
+ AddStep("select first", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.First()));
+ checkMusicPlaying(true);
+
+ AddStep("manual pause", () => music.TogglePause());
+ checkMusicPlaying(false);
+ AddStep("select next difficulty", () => songSelect.Carousel.SelectNext(skipDifficulties: false));
+ checkMusicPlaying(false);
+
+ AddStep("select next set", () => songSelect.Carousel.SelectNext());
+ checkMusicPlaying(true);
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestAudioRemainsCorrectOnRulesetChange(bool rulesetsInSameBeatmap)
+ {
+ createSongSelect();
+
+ // start with non-osu! to avoid convert confusion
+ changeRuleset(1);
+
+ if (rulesetsInSameBeatmap)
+ {
+ AddStep("import multi-ruleset map", () =>
+ {
+ var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray();
+ manager.Import(createTestBeatmapSet(0, usableRulesets)).Wait();
+ });
+ }
+ else
+ {
+ addRulesetImportStep(1);
+ addRulesetImportStep(0);
+ }
+
+ checkMusicPlaying(true);
+
+ AddStep("manual pause", () => music.TogglePause());
+ checkMusicPlaying(false);
+
+ changeRuleset(0);
+ checkMusicPlaying(!rulesetsInSameBeatmap);
+ }
+
[Test]
public void TestDummy()
{
@@ -128,12 +183,10 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
- [Ignore("needs fixing")]
public void TestImportUnderDifferentRuleset()
{
createSongSelect();
- changeRuleset(2);
- addRulesetImportStep(0);
+ addRulesetImportStep(2);
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null);
}
@@ -183,6 +236,22 @@ namespace osu.Game.Tests.Visual.SongSelect
void onRulesetChange(ValueChangedEvent e) => rulesetChangeIndex = actionIndex++;
}
+ [Test]
+ public void TestModsRetainedBetweenSongSelect()
+ {
+ AddAssert("empty mods", () => !Mods.Value.Any());
+
+ createSongSelect();
+
+ addRulesetImportStep(0);
+
+ changeMods(new OsuModHardRock());
+
+ createSongSelect();
+
+ AddAssert("mods retained", () => Mods.Value.Any());
+ }
+
[Test]
public void TestStartAfterUnMatchingFilterDoesNotStart()
{
@@ -224,6 +293,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private static int importId;
private int getImportId() => ++importId;
+ private void checkMusicPlaying(bool playing) =>
+ AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing);
+
private void changeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.Acronym))}", () => Mods.Value = mods);
private void changeRuleset(int id) => AddStep($"change ruleset to {id}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == id));
@@ -289,5 +361,36 @@ namespace osu.Game.Tests.Visual.SongSelect
DateAdded = DateTimeOffset.UtcNow,
};
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ rulesets?.Dispose();
+ }
+
+ private class TestSongSelect : PlaySongSelect
+ {
+ public Action StartRequested;
+
+ public new Bindable Ruleset => base.Ruleset;
+
+ public WorkingBeatmap CurrentBeatmap => Beatmap.Value;
+ public WorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap;
+ public new BeatmapCarousel Carousel => base.Carousel;
+
+ protected override bool OnStart()
+ {
+ StartRequested?.Invoke();
+ return base.OnStart();
+ }
+
+ public int FilterCount;
+
+ protected override void ApplyFilterToCarousel(FilterCriteria criteria)
+ {
+ FilterCount++;
+ base.ApplyFilterToCarousel(criteria);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
index fcc3a3596f..e495b2a95a 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual
private IReadOnlyList requiredGameDependencies => new[]
{
typeof(OsuGame),
- typeof(RavenLogger),
+ typeof(SentryLogger),
typeof(OsuLogo),
typeof(IdleTracker),
typeof(OnScreenDisplay),
@@ -109,16 +109,20 @@ namespace osu.Game.Tests.Visual
AddAssert("check OsuGame DI members", () =>
{
foreach (var type in requiredGameDependencies)
+ {
if (game.Dependencies.Get(type) == null)
throw new Exception($"{type} has not been cached");
+ }
return true;
});
AddAssert("check OsuGameBase DI members", () =>
{
foreach (var type in requiredGameBaseDependencies)
+ {
if (gameBase.Dependencies.Get(type) == null)
throw new Exception($"{type} has not been cached");
+ }
return true;
});
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs
index 38a9af05d8..b7d7053dcd 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs
@@ -22,6 +22,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestSceneBackButton()
{
BackButton button;
+ BackButton.Receptor receptor = new BackButton.Receptor();
Child = new Container
{
@@ -31,12 +32,13 @@ namespace osu.Game.Tests.Visual.UserInterface
Masking = true,
Children = new Drawable[]
{
+ receptor,
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
- button = new BackButton
+ button = new BackButton(receptor)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
index d84ffa0d93..ed44d82bce 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
@@ -10,7 +11,6 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Lists;
using osu.Framework.Timing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
@@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.UserInterface
};
}
- private SortedList timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints;
+ private List timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList();
private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs
similarity index 81%
rename from osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs
rename to osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs
index 73e0191adb..8179f92ffc 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs
@@ -6,12 +6,12 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
-using osu.Game.Screens.Edit.Setup.Components.LabelledComponents;
+using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
- public class TestSceneLabelledComponent : OsuTestScene
+ public class TestSceneLabelledDrawable : OsuTestScene
{
[TestCase(false)]
[TestCase(true)]
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
- LabelledComponent component;
+ LabelledDrawable component;
Child = new Container
{
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
- Child = component = padded ? (LabelledComponent)new PaddedLabelledComponent() : new NonPaddedLabelledComponent(),
+ Child = component = padded ? (LabelledDrawable)new PaddedLabelledDrawable() : new NonPaddedLabelledDrawable(),
};
component.Label = "a sample component";
@@ -41,9 +41,9 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
- private class PaddedLabelledComponent : LabelledComponent
+ private class PaddedLabelledDrawable : LabelledDrawable
{
- public PaddedLabelledComponent()
+ public PaddedLabelledDrawable()
: base(true)
{
}
@@ -57,9 +57,9 @@ namespace osu.Game.Tests.Visual.UserInterface
};
}
- private class NonPaddedLabelledComponent : LabelledComponent
+ private class NonPaddedLabelledDrawable : LabelledDrawable
{
- public NonPaddedLabelledComponent()
+ public NonPaddedLabelledDrawable()
: base(false)
{
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs
new file mode 100644
index 0000000000..6ca4d9fa4c
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.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 System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterfaceV2;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneLabelledSwitchButton : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(LabelledSwitchButton),
+ typeof(SwitchButton)
+ };
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestSwitchButton(bool hasDescription) => createSwitchButton(hasDescription);
+
+ private void createSwitchButton(bool hasDescription = false)
+ {
+ AddStep("create component", () =>
+ {
+ LabelledSwitchButton component;
+
+ Child = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 500,
+ AutoSizeAxes = Axes.Y,
+ Child = component = new LabelledSwitchButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+
+ component.Label = "a sample component";
+ component.Description = hasDescription ? "this text describes the component" : string.Empty;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs
index 395905a30d..8208b55952 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs
@@ -7,7 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Screens.Edit.Setup.Components.LabelledComponents;
+using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -19,6 +19,36 @@ namespace osu.Game.Tests.Visual.UserInterface
typeof(LabelledTextBox),
};
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestTextBox(bool hasDescription) => createTextBox(hasDescription);
+
+ private void createTextBox(bool hasDescription = false)
+ {
+ AddStep("create component", () =>
+ {
+ LabelledTextBox component;
+
+ Child = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 500,
+ AutoSizeAxes = Axes.Y,
+ Child = component = new LabelledTextBox
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Label = "Testing text",
+ PlaceholderText = "This is definitely working as intended",
+ }
+ };
+
+ component.Label = "a sample component";
+ component.Description = hasDescription ? "this text describes the component" : string.Empty;
+ });
+ }
+
[BackgroundDependencyLoader]
private void load()
{
@@ -32,7 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- LabelText = "Testing text",
+ Label = "Testing text",
PlaceholderText = "This is definitely working as intended",
}
};
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs
new file mode 100644
index 0000000000..2ada5b927b
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs
@@ -0,0 +1,145 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneStatefulMenuItem : ManualInputManagerTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(OsuMenu),
+ typeof(StatefulMenuItem),
+ typeof(TernaryStateMenuItem),
+ typeof(DrawableStatefulMenuItem),
+ };
+
+ [Test]
+ public void TestTernaryMenuItem()
+ {
+ OsuMenu menu = null;
+
+ Bindable state = new Bindable(TernaryState.Indeterminate);
+
+ AddStep("create menu", () =>
+ {
+ state.Value = TernaryState.Indeterminate;
+
+ Child = menu = new OsuMenu(Direction.Vertical, true)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Items = new[]
+ {
+ new TernaryStateMenuItem("First"),
+ new TernaryStateMenuItem("Second") { State = { BindTarget = state } },
+ new TernaryStateMenuItem("Third") { State = { Value = TernaryState.True } },
+ }
+ };
+ });
+
+ checkState(TernaryState.Indeterminate);
+
+ click();
+ checkState(TernaryState.True);
+
+ click();
+ checkState(TernaryState.False);
+
+ click();
+ checkState(TernaryState.True);
+
+ click();
+ checkState(TernaryState.False);
+
+ AddStep("change state via bindable", () => state.Value = TernaryState.True);
+
+ void click() =>
+ AddStep("click", () =>
+ {
+ InputManager.MoveMouseTo(menu.ScreenSpaceDrawQuad.Centre);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ void checkState(TernaryState expected)
+ => AddAssert($"state is {expected}", () => state.Value == expected);
+ }
+
+ [Test]
+ public void TestCustomState()
+ {
+ AddStep("create menu", () =>
+ {
+ Child = new OsuMenu(Direction.Vertical, true)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Items = new[]
+ {
+ new TestMenuItem("First", MenuItemType.Standard, getNextState),
+ new TestMenuItem("Second", MenuItemType.Standard, getNextState) { State = { Value = TestStates.State2 } },
+ new TestMenuItem("Third", MenuItemType.Standard, getNextState) { State = { Value = TestStates.State3 } },
+ }
+ };
+ });
+ }
+
+ private TestStates getNextState(TestStates state)
+ {
+ switch (state)
+ {
+ case TestStates.State1:
+ return TestStates.State2;
+
+ case TestStates.State2:
+ return TestStates.State3;
+
+ case TestStates.State3:
+ return TestStates.State1;
+ }
+
+ return TestStates.State1;
+ }
+
+ private class TestMenuItem : StatefulMenuItem
+ {
+ public TestMenuItem(string text, MenuItemType type, Func changeStateFunc)
+ : base(text, changeStateFunc, type)
+ {
+ }
+
+ public override IconUsage? GetIconForState(TestStates state)
+ {
+ switch (state)
+ {
+ case TestStates.State1:
+ return FontAwesome.Solid.DiceOne;
+
+ case TestStates.State2:
+ return FontAwesome.Solid.DiceTwo;
+
+ case TestStates.State3:
+ return FontAwesome.Solid.DiceThree;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(state), state, null);
+ }
+ }
+ }
+
+ private enum TestStates
+ {
+ State1,
+ State2,
+ State3
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs
new file mode 100644
index 0000000000..4a104b4a41
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterfaceV2;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneSwitchButton : ManualInputManagerTestScene
+ {
+ private SwitchButton switchButton;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Child = switchButton = new SwitchButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
+ });
+
+ [Test]
+ public void TestChangeThroughInput()
+ {
+ AddStep("move to switch button", () => InputManager.MoveMouseTo(switchButton));
+ AddStep("click on", () => InputManager.Click(MouseButton.Left));
+ AddStep("click off", () => InputManager.Click(MouseButton.Left));
+ }
+
+ [Test]
+ public void TestChangeThroughBindable()
+ {
+ BindableBool bindable = null;
+
+ AddStep("bind bindable", () => switchButton.Current.BindTo(bindable = new BindableBool()));
+ AddStep("toggle bindable", () => bindable.Toggle());
+ AddStep("toggle bindable", () => bindable.Toggle());
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs
new file mode 100644
index 0000000000..2abda56a28
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneToggleMenuItem : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(OsuMenu),
+ typeof(ToggleMenuItem),
+ typeof(DrawableStatefulMenuItem)
+ };
+
+ public TestSceneToggleMenuItem()
+ {
+ Add(new OsuMenu(Direction.Vertical, true)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Items = new[]
+ {
+ new ToggleMenuItem("First"),
+ new ToggleMenuItem("Second") { State = { Value = true } }
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneWaveContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs
similarity index 97%
rename from osu.Game.Tests/Visual/Editor/TestSceneWaveContainer.cs
rename to osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs
index de19727251..5b130b9224 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneWaveContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs
@@ -12,7 +12,7 @@ using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneWaveContainer : OsuTestScene
diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs
index db9576b5fa..0d16a78f75 100644
--- a/osu.Game.Tests/WaveformTestBeatmap.cs
+++ b/osu.Game.Tests/WaveformTestBeatmap.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Video;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Tests.Resources;
@@ -56,7 +57,7 @@ namespace osu.Game.Tests
private Beatmap createTestBeatmap()
{
using (var beatmapStream = getBeatmapStream())
- using (var beatmapReader = new StreamReader(beatmapStream))
+ using (var beatmapReader = new LineBufferedReader(beatmapStream))
return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader);
}
}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 75e6354612..c5998c9cfc 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,14 +3,14 @@
-
+
WinExe
- netcoreapp2.2
+ netcoreapp3.0
diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs
index 41d32d9448..9905e17824 100644
--- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs
+++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs
@@ -102,7 +102,8 @@ namespace osu.Game.Tournament.Tests.Components
Content = "Okay okay, calm down guys. Let's do this!"
}));
- AddStep("multiple messages", () => testChannel.AddNewMessages(new Message(nextMessageId())
+ AddStep("multiple messages", () => testChannel.AddNewMessages(
+ new Message(nextMessageId())
{
Sender = admin,
Content = "I spam you!"
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs
new file mode 100644
index 0000000000..650b4c5412
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.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 osu.Framework.Allocation;
+using osu.Game.Tournament.Screens;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneSetupScreen : TournamentTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(new SetupScreen());
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs
index 3d340e393c..e36b594ff2 100644
--- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs
@@ -3,7 +3,6 @@
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.TeamIntro;
@@ -13,7 +12,7 @@ namespace osu.Game.Tournament.Tests.Screens
public class TestSceneTeamIntroScreen : LadderTestScene
{
[Cached]
- private readonly Bindable currentMatch = new Bindable();
+ private readonly LadderInfo ladder = new LadderInfo();
[BackgroundDependencyLoader]
private void load()
@@ -22,7 +21,7 @@ namespace osu.Game.Tournament.Tests.Screens
match.Team1.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "USA");
match.Team2.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "JPN");
match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals");
- currentMatch.Value = match;
+ ladder.CurrentMatch.Value = match;
Add(new TeamIntroScreen
{
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs
index 6f5e17a36e..5cb35a506f 100644
--- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs
@@ -3,7 +3,6 @@
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.TeamWin;
@@ -13,7 +12,7 @@ namespace osu.Game.Tournament.Tests.Screens
public class TestSceneTeamWinScreen : LadderTestScene
{
[Cached]
- private readonly Bindable currentMatch = new Bindable();
+ private readonly LadderInfo ladder = new LadderInfo();
[BackgroundDependencyLoader]
private void load()
@@ -22,7 +21,7 @@ namespace osu.Game.Tournament.Tests.Screens
match.Team1.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "USA");
match.Team2.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "JPN");
match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals");
- currentMatch.Value = match;
+ ladder.CurrentMatch.Value = match;
Add(new TeamWinScreen
{
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 491cf54686..d58a724c27 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,13 +5,13 @@
-
+
WinExe
- netcoreapp2.2
+ netcoreapp3.0
diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index f6c1be0e36..0908814537 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -131,6 +131,7 @@ namespace osu.Game.Tournament.Components
});
if (!string.IsNullOrEmpty(mods))
+ {
AddInternal(new Sprite
{
Texture = textures.Get($"mods/{mods}"),
@@ -139,6 +140,7 @@ namespace osu.Game.Tournament.Components
Margin = new MarginPadding(20),
Scale = new Vector2(0.5f)
});
+ }
}
private void matchChanged(ValueChangedEvent match)
diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs
index 4f4660f645..206689ca1a 100644
--- a/osu.Game.Tournament/Components/TourneyVideo.cs
+++ b/osu.Game.Tournament/Components/TourneyVideo.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Video;
+using osu.Framework.Timing;
using osu.Game.Graphics;
namespace osu.Game.Tournament.Components
@@ -15,6 +16,8 @@ namespace osu.Game.Tournament.Components
{
private readonly VideoSprite video;
+ private readonly ManualClock manualClock;
+
public TourneyVideo(Stream stream)
{
if (stream == null)
@@ -26,11 +29,14 @@ namespace osu.Game.Tournament.Components
};
}
else
+ {
InternalChild = video = new VideoSprite(stream)
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
+ Clock = new FramedClock(manualClock = new ManualClock())
};
+ }
}
public bool Loop
@@ -41,5 +47,17 @@ namespace osu.Game.Tournament.Components
video.Loop = value;
}
}
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (manualClock != null && Clock.ElapsedFrameTime < 100)
+ {
+ // we want to avoid seeking as much as possible, because we care about performance, not sync.
+ // to avoid seeking completely, we only increment out local clock when in an updating state.
+ manualClock.CurrentTime += Clock.ElapsedFrameTime;
+ }
+ }
}
}
diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs
index 4fd858bd12..47f2bed77a 100644
--- a/osu.Game.Tournament/IPC/FileBasedIPC.cs
+++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs
@@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Platform.Windows;
+using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Online.API;
@@ -26,103 +27,122 @@ namespace osu.Game.Tournament.IPC
[Resolved]
protected RulesetStore Rulesets { get; private set; }
+ [Resolved]
+ private GameHost host { get; set; }
+
+ [Resolved]
+ private LadderInfo ladder { get; set; }
+
private int lastBeatmapId;
+ private ScheduledDelegate scheduled;
+
+ public Storage Storage { get; private set; }
[BackgroundDependencyLoader]
- private void load(LadderInfo ladder, GameHost host)
+ private void load()
{
- StableStorage stable;
+ LocateStableStorage();
+ }
+
+ public Storage LocateStableStorage()
+ {
+ scheduled?.Cancel();
+
+ Storage = null;
try
{
- stable = new StableStorage(host as DesktopGameHost);
+ Storage = new StableStorage(host as DesktopGameHost);
+
+ const string file_ipc_filename = "ipc.txt";
+ const string file_ipc_state_filename = "ipc-state.txt";
+ const string file_ipc_scores_filename = "ipc-scores.txt";
+ const string file_ipc_channel_filename = "ipc-channel.txt";
+
+ if (Storage.Exists(file_ipc_filename))
+ {
+ scheduled = Scheduler.AddDelayed(delegate
+ {
+ try
+ {
+ using (var stream = Storage.GetStream(file_ipc_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ var beatmapId = int.Parse(sr.ReadLine());
+ var mods = int.Parse(sr.ReadLine());
+
+ if (lastBeatmapId != beatmapId)
+ {
+ lastBeatmapId = beatmapId;
+
+ var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.BeatmapInfo != null);
+
+ if (existing != null)
+ Beatmap.Value = existing.BeatmapInfo;
+ else
+ {
+ var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId });
+ req.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets);
+ API.Queue(req);
+ }
+ }
+
+ Mods.Value = (LegacyMods)mods;
+ }
+ }
+ catch
+ {
+ // file might be in use.
+ }
+
+ try
+ {
+ using (var stream = Storage.GetStream(file_ipc_channel_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ ChatChannel.Value = sr.ReadLine();
+ }
+ }
+ catch (Exception)
+ {
+ // file might be in use.
+ }
+
+ try
+ {
+ using (var stream = Storage.GetStream(file_ipc_state_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine());
+ }
+ }
+ catch (Exception)
+ {
+ // file might be in use.
+ }
+
+ try
+ {
+ using (var stream = Storage.GetStream(file_ipc_scores_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ Score1.Value = int.Parse(sr.ReadLine());
+ Score2.Value = int.Parse(sr.ReadLine());
+ }
+ }
+ catch (Exception)
+ {
+ // file might be in use.
+ }
+ }, 250, true);
+ }
}
catch (Exception e)
{
Logger.Error(e, "Stable installation could not be found; disabling file based IPC");
- return;
}
- const string file_ipc_filename = "ipc.txt";
- const string file_ipc_state_filename = "ipc-state.txt";
- const string file_ipc_scores_filename = "ipc-scores.txt";
- const string file_ipc_channel_filename = "ipc-channel.txt";
-
- if (stable.Exists(file_ipc_filename))
- Scheduler.AddDelayed(delegate
- {
- try
- {
- using (var stream = stable.GetStream(file_ipc_filename))
- using (var sr = new StreamReader(stream))
- {
- var beatmapId = int.Parse(sr.ReadLine());
- var mods = int.Parse(sr.ReadLine());
-
- if (lastBeatmapId != beatmapId)
- {
- lastBeatmapId = beatmapId;
-
- var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.BeatmapInfo != null);
-
- if (existing != null)
- Beatmap.Value = existing.BeatmapInfo;
- else
- {
- var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId });
- req.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets);
- API.Queue(req);
- }
- }
-
- Mods.Value = (LegacyMods)mods;
- }
- }
- catch
- {
- // file might be in use.
- }
-
- try
- {
- using (var stream = stable.GetStream(file_ipc_channel_filename))
- using (var sr = new StreamReader(stream))
- {
- ChatChannel.Value = sr.ReadLine();
- }
- }
- catch (Exception)
- {
- // file might be in use.
- }
-
- try
- {
- using (var stream = stable.GetStream(file_ipc_state_filename))
- using (var sr = new StreamReader(stream))
- {
- State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine());
- }
- }
- catch (Exception)
- {
- // file might be in use.
- }
-
- try
- {
- using (var stream = stable.GetStream(file_ipc_scores_filename))
- using (var sr = new StreamReader(stream))
- {
- Score1.Value = int.Parse(sr.ReadLine());
- Score2.Value = int.Parse(sr.ReadLine());
- }
- }
- catch (Exception)
- {
- // file might be in use.
- }
- }, 250, true);
+ return Storage;
}
///
diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs
index 547c4eab08..5db0b01547 100644
--- a/osu.Game.Tournament/Models/LadderInfo.cs
+++ b/osu.Game.Tournament/Models/LadderInfo.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Framework.Bindables;
+using osu.Game.Rulesets;
namespace osu.Game.Tournament.Models
{
@@ -14,6 +15,8 @@ namespace osu.Game.Tournament.Models
[Serializable]
public class LadderInfo
{
+ public Bindable Ruleset = new Bindable();
+
public BindableList Matches = new BindableList();
public BindableList Rounds = new BindableList();
public BindableList Teams = new BindableList();
diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
index 3a14b6d9c2..5efa0a1e69 100644
--- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
+++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
@@ -15,7 +15,6 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.Drawings.Components;
@@ -24,7 +23,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Drawings
{
- public class DrawingsScreen : CompositeDrawable
+ public class DrawingsScreen : TournamentScreen
{
private const string results_filename = "drawings_results.txt";
@@ -128,21 +127,21 @@ namespace osu.Game.Tournament.Screens.Drawings
// Control panel container
new ControlPanel
{
- new OsuButton
+ new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Begin random",
Action = teamsContainer.StartScrolling,
},
- new OsuButton
+ new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Stop random",
Action = teamsContainer.StopScrolling,
},
- new OsuButton
+ new TourneyButton
{
RelativeSizeAxes = Axes.X,
@@ -150,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Drawings
Action = reloadTeams
},
new ControlPanel.Spacer(),
- new OsuButton
+ new TourneyButton
{
RelativeSizeAxes = Axes.X,
@@ -195,7 +194,7 @@ namespace osu.Game.Tournament.Screens.Drawings
}
}
- writeOp = writeOp?.ContinueWith(t => { writeAction(); }) ?? Task.Run((Action)writeAction);
+ writeOp = writeOp?.ContinueWith(t => { writeAction(); }) ?? Task.Run(writeAction);
}
private void reloadTeams()
diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
index b036350879..2c515edda7 100644
--- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
@@ -266,12 +266,14 @@ namespace osu.Game.Tournament.Screens.Editors
drawableContainer.Clear();
if (Model.BeatmapInfo != null)
+ {
drawableContainer.Child = new TournamentBeatmapPanel(Model.BeatmapInfo, Model.Mods)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 300
};
+ }
}
}
}
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index a4479f3cfd..11c2732d62 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -11,9 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models;
@@ -25,14 +23,14 @@ namespace osu.Game.Tournament.Screens.Editors
public class TeamEditorScreen : TournamentEditorScreen
{
[Resolved]
- private Framework.Game game { get; set; }
+ private TournamentGameBase game { get; set; }
protected override BindableList Storage => LadderInfo.Teams;
[BackgroundDependencyLoader]
private void load()
{
- ControlPanel.Add(new OsuButton
+ ControlPanel.Add(new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Add all countries",
@@ -199,6 +197,9 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
protected IAPIProvider API { get; private set; }
+ [Resolved]
+ private TournamentGameBase game { get; set; }
+
private readonly Bindable userId = new Bindable();
private readonly Container drawableContainer;
@@ -281,25 +282,7 @@ namespace osu.Game.Tournament.Screens.Editors
return;
}
- var req = new GetUserRequest(user.Id);
-
- req.Success += res =>
- {
- // TODO: this should be done in a better way.
- user.Username = res.Username;
- user.Country = res.Country;
- user.Cover = res.Cover;
-
- updatePanel();
- };
-
- req.Failure += _ =>
- {
- user.Id = 1;
- updatePanel();
- };
-
- API.Queue(req);
+ game.PopulateUser(user, updatePanel, updatePanel);
}, true);
}
diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
index 50d3207345..32cf6bbcc8 100644
--- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Components;
using osuTK;
@@ -56,7 +55,7 @@ namespace osu.Game.Tournament.Screens.Editors
{
Children = new Drawable[]
{
- new OsuButton
+ new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Add new",
diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
index b9a74bfe16..6a3095d42d 100644
--- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
@@ -103,13 +103,13 @@ namespace osu.Game.Tournament.Screens.Gameplay
{
Children = new Drawable[]
{
- warmupButton = new OsuButton
+ warmupButton = new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Toggle warmup",
Action = () => warmup.Toggle()
},
- new OsuButton
+ new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Toggle chat",
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs
index f613ce5f46..724612ebce 100644
--- a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs
+++ b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
@@ -32,7 +33,7 @@ namespace osu.Game.Tournament.Screens.Ladder
protected override bool OnScroll(ScrollEvent e)
{
- var newScale = MathHelper.Clamp(scale + e.ScrollDelta.Y / 15 * scale, min_scale, max_scale);
+ var newScale = Math.Clamp(scale + e.ScrollDelta.Y / 15 * scale, min_scale, max_scale);
this.MoveTo(target = target - e.MousePosition * (newScale - scale), 2000, Easing.OutQuint);
this.ScaleTo(scale = newScale, 2000, Easing.OutQuint);
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
index 83a41a662f..66e68a0f37 100644
--- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
+++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
@@ -81,8 +81,10 @@ namespace osu.Game.Tournament.Screens.Ladder
LadderInfo.Matches.ItemsRemoved += matches =>
{
foreach (var p in matches)
- foreach (var d in MatchesContainer.Where(d => d.Match == p))
- d.Expire();
+ {
+ foreach (var d in MatchesContainer.Where(d => d.Match == p))
+ d.Expire();
+ }
layout.Invalidate();
};
diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
index d32c0d6156..7a5fc2cd06 100644
--- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
+++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
@@ -60,32 +60,32 @@ namespace osu.Game.Tournament.Screens.MapPool
{
Text = "Current Mode"
},
- buttonRedBan = new OsuButton
+ buttonRedBan = new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Red Ban",
Action = () => setMode(TeamColour.Red, ChoiceType.Ban)
},
- buttonBlueBan = new OsuButton
+ buttonBlueBan = new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Blue Ban",
Action = () => setMode(TeamColour.Blue, ChoiceType.Ban)
},
- buttonRedPick = new OsuButton
+ buttonRedPick = new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Red Pick",
Action = () => setMode(TeamColour.Red, ChoiceType.Pick)
},
- buttonBluePick = new OsuButton
+ buttonBluePick = new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Blue Pick",
Action = () => setMode(TeamColour.Blue, ChoiceType.Pick)
},
new ControlPanel.Spacer(),
- new OsuButton
+ new TourneyButton
{
RelativeSizeAxes = Axes.X,
Text = "Reset",
@@ -196,7 +196,7 @@ namespace osu.Game.Tournament.Screens.MapPool
setNextMode();
- if (pickType == ChoiceType.Pick)
+ if (pickType == ChoiceType.Pick && currentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick))
{
scheduledChange?.Cancel();
scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000);
diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs
new file mode 100644
index 0000000000..8e1481d87c
--- /dev/null
+++ b/osu.Game.Tournament/Screens/SetupScreen.cs
@@ -0,0 +1,174 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Online.API;
+using osu.Game.Overlays;
+using osu.Game.Rulesets;
+using osu.Game.Tournament.IPC;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Screens
+{
+ public class SetupScreen : TournamentScreen, IProvideVideo
+ {
+ private FillFlowContainer fillFlow;
+
+ private LoginOverlay loginOverlay;
+
+ [Resolved]
+ private MatchIPCInfo ipc { get; set; }
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = fillFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding(10),
+ Spacing = new Vector2(10),
+ };
+
+ api.LocalUser.BindValueChanged(_ => Schedule(reload));
+ reload();
+ }
+
+ private void reload()
+ {
+ var fileBasedIpc = ipc as FileBasedIPC;
+
+ fillFlow.Children = new Drawable[]
+ {
+ new ActionableInfo
+ {
+ Label = "Current IPC source",
+ ButtonText = "Refresh",
+ Action = () =>
+ {
+ fileBasedIpc?.LocateStableStorage();
+ reload();
+ },
+ Value = fileBasedIpc?.Storage?.GetFullPath(string.Empty) ?? "Not found",
+ Failing = fileBasedIpc?.Storage == null,
+ Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation, and that it is registered as the default osu! install."
+ },
+ new ActionableInfo
+ {
+ Label = "Current User",
+ ButtonText = "Change Login",
+ Action = () =>
+ {
+ api.Logout();
+
+ if (loginOverlay == null)
+ {
+ AddInternal(loginOverlay = new LoginOverlay
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ });
+ }
+
+ loginOverlay.State.Value = Visibility.Visible;
+ },
+ Value = api?.LocalUser.Value.Username,
+ Failing = api?.IsLoggedIn != true,
+ Description = "In order to access the API and display metadata, a login is required."
+ },
+ new LabelledDropdown
+ {
+ Label = "Ruleset",
+ Description = "Decides what stats are displayed and which ranks are retrieved for players",
+ Items = rulesets.AvailableRulesets,
+ Current = LadderInfo.Ruleset,
+ },
+ };
+ }
+
+ public class LabelledDropdown : LabelledComponent, T>
+ {
+ public LabelledDropdown()
+ : base(true)
+ {
+ }
+
+ public IEnumerable Items
+ {
+ get => Component.Items;
+ set => Component.Items = value;
+ }
+
+ protected override OsuDropdown CreateComponent() => new OsuDropdown
+ {
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ };
+ }
+
+ private class ActionableInfo : LabelledDrawable
+ {
+ private OsuButton button;
+
+ public ActionableInfo()
+ : base(true)
+ {
+ }
+
+ public string ButtonText
+ {
+ set => button.Text = value;
+ }
+
+ public string Value
+ {
+ set => valueText.Text = value;
+ }
+
+ public bool Failing
+ {
+ set => valueText.Colour = value ? Color4.Red : Color4.White;
+ }
+
+ public Action Action;
+
+ private OsuSpriteText valueText;
+
+ protected override Drawable CreateComponent() => new Container
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Children = new Drawable[]
+ {
+ valueText = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ button = new TriangleButton
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Size = new Vector2(100, 30),
+ Action = () => Action?.Invoke()
+ },
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
index c901a5c7ef..47c923ff30 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
@@ -164,6 +164,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
if (team != null)
{
foreach (var p in team.Players)
+ {
players.Add(new OsuSpriteText
{
Text = p.Username,
@@ -172,6 +173,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft,
Origin = left ? Anchor.CentreRight : Anchor.CentreLeft,
});
+ }
}
}
diff --git a/osu.Game.Tournament/Screens/TournamentScreen.cs b/osu.Game.Tournament/Screens/TournamentScreen.cs
index 9d58ca2240..0b5b3e728b 100644
--- a/osu.Game.Tournament/Screens/TournamentScreen.cs
+++ b/osu.Game.Tournament/Screens/TournamentScreen.cs
@@ -10,6 +10,8 @@ namespace osu.Game.Tournament.Screens
{
public abstract class TournamentScreen : CompositeDrawable
{
+ public const double FADE_DELAY = 200;
+
[Resolved]
protected LadderInfo LadderInfo { get; private set; }
@@ -18,14 +20,8 @@ namespace osu.Game.Tournament.Screens
RelativeSizeAxes = Axes.Both;
}
- public override void Hide()
- {
- this.FadeOut(200);
- }
+ public override void Hide() => this.FadeOut(FADE_DELAY);
- public override void Show()
- {
- this.FadeIn(200);
- }
+ public override void Show() => this.FadeIn(FADE_DELAY);
}
}
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index dbfa70704b..f2a158971b 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Drawing;
using System.IO;
using System.Linq;
@@ -18,15 +19,16 @@ using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models;
+using osu.Game.Users;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tournament
{
+ [Cached(typeof(TournamentGameBase))]
public abstract class TournamentGameBase : OsuGameBase
{
private const string bracket_filename = "bracket.json";
@@ -76,7 +78,7 @@ namespace osu.Game.Tournament
AddRange(new[]
{
- new OsuButton
+ new TourneyButton
{
Text = "Save Changes",
Width = 140,
@@ -127,6 +129,8 @@ namespace osu.Game.Tournament
ladder = new LadderInfo();
}
+ Ruleset.BindTo(ladder.Ruleset);
+
dependencies.Cache(ladder);
bool addedInfo = false;
@@ -165,15 +169,17 @@ namespace osu.Game.Tournament
// link matches to rounds
foreach (var round in ladder.Rounds)
- foreach (var id in round.Matches)
{
- var found = ladder.Matches.FirstOrDefault(p => p.ID == id);
-
- if (found != null)
+ foreach (var id in round.Matches)
{
- found.Round.Value = round;
- if (round.StartDate.Value > found.Date.Value)
- found.Date.Value = round.StartDate.Value;
+ var found = ladder.Matches.FirstOrDefault(p => p.ID == id);
+
+ if (found != null)
+ {
+ found.Round.Value = round;
+ if (round.StartDate.Value > found.Date.Value)
+ found.Date.Value = round.StartDate.Value;
+ }
}
}
@@ -193,15 +199,13 @@ namespace osu.Game.Tournament
bool addedInfo = false;
foreach (var t in ladder.Teams)
- foreach (var p in t.Players)
- if (string.IsNullOrEmpty(p.Username))
+ {
+ foreach (var p in t.Players)
{
- var req = new GetUserRequest(p.Id);
- req.Perform(API);
- p.Username = req.Result.Username;
-
+ PopulateUser(p);
addedInfo = true;
}
+ }
return addedInfo;
}
@@ -214,19 +218,47 @@ namespace osu.Game.Tournament
bool addedInfo = false;
foreach (var r in ladder.Rounds)
- foreach (var b in r.Beatmaps)
- if (b.BeatmapInfo == null)
+ {
+ foreach (var b in r.Beatmaps)
{
- var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
- req.Perform(API);
- b.BeatmapInfo = req.Result?.ToBeatmap(RulesetStore);
+ if (b.BeatmapInfo == null && b.ID > 0)
+ {
+ var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
+ req.Perform(API);
+ b.BeatmapInfo = req.Result?.ToBeatmap(RulesetStore);
- addedInfo = true;
+ addedInfo = true;
+ }
}
+ }
return addedInfo;
}
+ public void PopulateUser(User user, Action success = null, Action failure = null)
+ {
+ var req = new GetUserRequest(user.Id, Ruleset.Value);
+
+ req.Success += res =>
+ {
+ user.Username = res.Username;
+ user.Statistics = res.Statistics;
+ user.Username = res.Username;
+ user.Country = res.Country;
+ user.Cover = res.Cover;
+
+ success?.Invoke();
+ };
+
+ req.Failure += _ =>
+ {
+ user.Id = 1;
+ failure?.Invoke();
+ };
+
+ API.Queue(req);
+ }
+
protected override void LoadComplete()
{
MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display
diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs
index 4c255be463..de3d685c31 100644
--- a/osu.Game.Tournament/TournamentSceneManager.cs
+++ b/osu.Game.Tournament/TournamentSceneManager.cs
@@ -8,7 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
-using osu.Game.Graphics.UserInterface;
+using osu.Framework.Threading;
+using osu.Game.Graphics;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens;
@@ -36,6 +37,7 @@ namespace osu.Game.Tournament
private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay();
private Container chatContainer;
+ private FillFlowContainer buttons;
public TournamentSceneManager()
{
@@ -69,6 +71,7 @@ namespace osu.Game.Tournament
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
+ new SetupScreen(),
new ScheduleScreen(),
new LadderScreen(),
new LadderEditorScreen(),
@@ -100,66 +103,136 @@ namespace osu.Game.Tournament
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
- new FillFlowContainer
+ buttons = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
+ Spacing = new Vector2(2),
+ Padding = new MarginPadding(2),
Children = new Drawable[]
{
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "Team Editor", Action = () => SetScreen(typeof(TeamEditorScreen)) },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "Rounds Editor", Action = () => SetScreen(typeof(RoundEditorScreen)) },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "Bracket Editor", Action = () => SetScreen(typeof(LadderEditorScreen)) },
- new Container { RelativeSizeAxes = Axes.X, Height = 50 },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "Drawings", Action = () => SetScreen(typeof(DrawingsScreen)) },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "Showcase", Action = () => SetScreen(typeof(ShowcaseScreen)) },
- new Container { RelativeSizeAxes = Axes.X, Height = 50 },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "Schedule", Action = () => SetScreen(typeof(ScheduleScreen)) },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "Bracket", Action = () => SetScreen(typeof(LadderScreen)) },
- new Container { RelativeSizeAxes = Axes.X, Height = 50 },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "TeamIntro", Action = () => SetScreen(typeof(TeamIntroScreen)) },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "MapPool", Action = () => SetScreen(typeof(MapPoolScreen)) },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "Gameplay", Action = () => SetScreen(typeof(GameplayScreen)) },
- new Container { RelativeSizeAxes = Axes.X, Height = 50 },
- new OsuButton { RelativeSizeAxes = Axes.X, Text = "Win", Action = () => SetScreen(typeof(TeamWinScreen)) },
+ new ScreenButton(typeof(SetupScreen)) { Text = "Setup", RequestSelection = SetScreen },
+ new Separator(),
+ new ScreenButton(typeof(TeamEditorScreen)) { Text = "Team Editor", RequestSelection = SetScreen },
+ new ScreenButton(typeof(RoundEditorScreen)) { Text = "Rounds Editor", RequestSelection = SetScreen },
+ new ScreenButton(typeof(LadderEditorScreen)) { Text = "Bracket Editor", RequestSelection = SetScreen },
+ new Separator(),
+ new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen },
+ new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen },
+ new Separator(),
+ new ScreenButton(typeof(TeamIntroScreen)) { Text = "TeamIntro", RequestSelection = SetScreen },
+ new Separator(),
+ new ScreenButton(typeof(MapPoolScreen)) { Text = "MapPool", RequestSelection = SetScreen },
+ new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen },
+ new Separator(),
+ new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen },
+ new Separator(),
+ new ScreenButton(typeof(DrawingsScreen)) { Text = "Drawings", RequestSelection = SetScreen },
+ new ScreenButton(typeof(ShowcaseScreen)) { Text = "Showcase", RequestSelection = SetScreen },
}
},
},
},
};
- SetScreen(typeof(ScheduleScreen));
+ foreach (var drawable in screens)
+ drawable.Hide();
+
+ SetScreen(typeof(SetupScreen));
}
+ private float depth;
+
+ private Drawable currentScreen;
+ private ScheduledDelegate scheduledHide;
+
public void SetScreen(Type screenType)
{
- var screen = screens.FirstOrDefault(s => s.GetType() == screenType);
- if (screen == null) return;
+ var target = screens.FirstOrDefault(s => s.GetType() == screenType);
- foreach (var s in screens.Children)
+ if (target == null || currentScreen == target) return;
+
+ if (scheduledHide?.Completed == false)
{
- if (s == screen)
- {
- s.Show();
- if (s is IProvideVideo)
- video.FadeOut(200);
- else
- video.Show();
- }
- else
- s.Hide();
+ scheduledHide.RunTask();
+ scheduledHide.Cancel(); // see https://github.com/ppy/osu-framework/issues/2967
+ scheduledHide = null;
}
- switch (screen)
+ var lastScreen = currentScreen;
+ currentScreen = target;
+
+ if (currentScreen is IProvideVideo)
+ {
+ video.FadeOut(200);
+
+ // delay the hide to avoid a double-fade transition.
+ scheduledHide = Scheduler.AddDelayed(() => lastScreen?.Hide(), TournamentScreen.FADE_DELAY);
+ }
+ else
+ {
+ lastScreen?.Hide();
+ video.Show();
+ }
+
+ screens.ChangeChildDepth(currentScreen, depth--);
+ currentScreen.Show();
+
+ switch (currentScreen)
{
case GameplayScreen _:
case MapPoolScreen _:
- chatContainer.FadeIn(100);
+ chatContainer.FadeIn(TournamentScreen.FADE_DELAY);
break;
default:
- chatContainer.FadeOut(100);
+ chatContainer.FadeOut(TournamentScreen.FADE_DELAY);
break;
}
+
+ foreach (var s in buttons.OfType())
+ s.IsSelected = screenType == s.Type;
+ }
+
+ private class Separator : CompositeDrawable
+ {
+ public Separator()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 20;
+ }
+ }
+
+ private class ScreenButton : TourneyButton
+ {
+ public readonly Type Type;
+
+ public ScreenButton(Type type)
+ {
+ Type = type;
+ BackgroundColour = OsuColour.Gray(0.2f);
+ Action = () => RequestSelection(type);
+
+ RelativeSizeAxes = Axes.X;
+ }
+
+ private bool isSelected;
+
+ public Action RequestSelection;
+
+ public bool IsSelected
+ {
+ get => isSelected;
+ set
+ {
+ if (value == isSelected)
+ return;
+
+ isSelected = value;
+ BackgroundColour = isSelected ? Color4.SkyBlue : OsuColour.Gray(0.2f);
+ SpriteText.Colour = isSelected ? Color4.Black : Color4.White;
+ }
+ }
}
}
}
diff --git a/osu.Game.Tournament/TourneyButton.cs b/osu.Game.Tournament/TourneyButton.cs
new file mode 100644
index 0000000000..12872d3197
--- /dev/null
+++ b/osu.Game.Tournament/TourneyButton.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.Graphics.UserInterface;
+
+namespace osu.Game.Tournament
+{
+ public class TourneyButton : OsuButton
+ {
+ public TourneyButton()
+ : base(null)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj
index 4790fcbcde..8e881fdd9c 100644
--- a/osu.Game.Tournament/osu.Game.Tournament.csproj
+++ b/osu.Game.Tournament/osu.Game.Tournament.csproj
@@ -1,9 +1,7 @@