diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 58c24181d3..b51ecb4f7e 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -31,6 +31,12 @@
"commands": [
"CodeFileSanity"
]
+ },
+ "ppy.localisationanalyser.tools": {
+ "version": "2021.524.0",
+ "commands": [
+ "localisation"
+ ]
}
}
}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index a5f7795882..f4d7e08d08 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,14 +10,6 @@ trim_trailing_whitespace = true
#Roslyn naming styles
-#PascalCase for public and protected members
-dotnet_naming_style.pascalcase.capitalization = pascal_case
-dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
-dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
-dotnet_naming_rule.public_members_pascalcase.severity = error
-dotnet_naming_rule.public_members_pascalcase.symbols = public_members
-dotnet_naming_rule.public_members_pascalcase.style = pascalcase
-
#camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case
@@ -194,4 +186,7 @@ dotnet_diagnostic.IDE0068.severity = none
dotnet_diagnostic.IDE0069.severity = none
#Disable operator overloads requiring alternate named methods
-dotnet_diagnostic.CA2225.severity = none
\ No newline at end of file
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
index 0b80ce44dd..7026179259 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md
@@ -1,7 +1,18 @@
---
name: Bug Report
-about: Issues regarding encountered bugs.
+about: Report a bug or crash to desktop
---
+
+
+
+
**Describe the bug:**
**Screenshots or videos showing encountered issue:**
@@ -9,8 +20,11 @@ about: Issues regarding encountered bugs.
**osu!lazer version:**
**Logs:**
+
diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
deleted file mode 100644
index ada8de73c0..0000000000
--- a/.github/ISSUE_TEMPLATE/02-crash-issues.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-name: Crash Report
-about: Issues regarding crashes or permanent freezes.
----
-**Describe the crash:**
-
-**Screenshots or videos showing encountered issue:**
-
-**osu!lazer version:**
-
-**Logs:**
-
-
-**Computer Specifications:**
diff --git a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md b/.github/ISSUE_TEMPLATE/03-feature-request-issues.md
deleted file mode 100644
index 54c4ff94e5..0000000000
--- a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Feature Request
-about: Features you would like to see in the game!
----
-**Describe the new feature:**
-
-**Proposal designs of the feature:**
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 69baeee60c..c62231e8e0 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,12 @@
blank_issues_enabled: false
contact_links:
+ - name: Suggestions or feature request
+ url: https://github.com/ppy/osu/discussions/categories/ideas
+ about: Got something you think should change or be added? Search for or start a new discussion!
+ - name: Help
+ url: https://github.com/ppy/osu/discussions/categories/q-a
+ about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues
- about: For issues regarding osu!stable (not osu!lazer), open them here.
+ about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..9e9af23b27
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,46 @@
+version: 2
+updates:
+- package-ecosystem: nuget
+ directory: "/"
+ schedule:
+ interval: monthly
+ time: "17:00"
+ open-pull-requests-limit: 99
+ ignore:
+ - dependency-name: Microsoft.EntityFrameworkCore.Design
+ versions:
+ - "> 2.2.6"
+ - dependency-name: Microsoft.EntityFrameworkCore.Sqlite
+ versions:
+ - "> 2.2.6"
+ - dependency-name: Microsoft.EntityFrameworkCore.Sqlite.Core
+ versions:
+ - "> 2.2.6"
+ - dependency-name: Microsoft.Extensions.DependencyInjection
+ versions:
+ - ">= 5.a, < 6"
+ - dependency-name: NUnit3TestAdapter
+ versions:
+ - ">= 3.16.a, < 3.17"
+ - dependency-name: Microsoft.NET.Test.Sdk
+ versions:
+ - 16.9.1
+ - dependency-name: Microsoft.Extensions.DependencyInjection
+ versions:
+ - 3.1.11
+ - 3.1.12
+ - dependency-name: Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson
+ versions:
+ - 3.1.11
+ - dependency-name: Microsoft.NETCore.Targets
+ versions:
+ - 5.0.0
+ - dependency-name: Microsoft.AspNetCore.SignalR.Protocols.MessagePack
+ versions:
+ - 5.0.2
+ - dependency-name: NUnit
+ versions:
+ - 3.13.1
+ - dependency-name: Microsoft.AspNetCore.SignalR.Client
+ versions:
+ - 3.1.11
diff --git a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml
index 27ba142e96..7b08163ceb 100644
--- a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml
+++ b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml
deleted file mode 100644
index 680312ad27..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml
deleted file mode 100644
index 9ece926b34..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6c327f01b3..e14be20642 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -24,7 +24,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
* the in-game logs, which are located at:
* `%AppData%/osu/logs` (on Windows),
* `~/.local/share/osu/logs` (on Linux and macOS),
- * `Android/Data/sh.ppy.osulazer/logs` (on Android),
+ * `Android/data/sh.ppy.osulazer/files/logs` (on Android),
* on iOS they can be obtained by connecting your device to your desktop and [copying the `logs` directory from the app's own document storage using iTunes](https://support.apple.com/en-us/HT201301#copy-to-computer),
* your system specifications (including the operating system and platform you are playing on),
* a reproduction scenario (list of steps you have performed leading up to the occurrence of the bug),
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 47839608c9..46c50dbfa2 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -7,3 +7,4 @@ M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
+M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead.
\ No newline at end of file
diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset
index d497365f87..6a99e230d1 100644
--- a/CodeAnalysis/osu.ruleset
+++ b/CodeAnalysis/osu.ruleset
@@ -30,7 +30,7 @@
-
+
diff --git a/Directory.Build.props b/Directory.Build.props
index 2e1873a9ed..53ad973e47 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,7 +18,7 @@
-
+
$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset
diff --git a/README.md b/README.md
index e09b4d86a5..3054f19e79 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commo
This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
-**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passses come at the end of development, preceeded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
+**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passes come at the end of development, preceded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:
@@ -41,7 +41,7 @@ If your platform is not listed above, there is still a chance you can manually b
## Developing a custom ruleset
-osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu-templates).
+osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
@@ -97,7 +97,7 @@ Before committing your code, please run a code formatter. This can be achieved b
We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
-JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`, which is [only supported on Windows](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
+JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`. Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## Contributing
diff --git a/Templates/Directory.Build.props b/Templates/Directory.Build.props
new file mode 100644
index 0000000000..0e470106e8
--- /dev/null
+++ b/Templates/Directory.Build.props
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Templates/README.md b/Templates/README.md
new file mode 100644
index 0000000000..cf25a89273
--- /dev/null
+++ b/Templates/README.md
@@ -0,0 +1,21 @@
+# Templates
+
+Templates for use when creating osu! dependent projects. Create a fully-testable (and ready for git) custom ruleset in just two lines.
+
+## Usage
+
+```bash
+# install (or update) templates package.
+# this only needs to be done once
+dotnet new -i ppy.osu.Game.Templates
+
+# create an empty freeform ruleset
+dotnet new ruleset -n MyCoolRuleset
+# create an empty scrolling ruleset (which provides the basics for a scrolling ←↑→↓ ruleset)
+dotnet new ruleset-scrolling -n MyCoolRuleset
+
+# ..or start with a working sample freeform game
+dotnet new ruleset-example -n MyCoolWorkingRuleset
+# ..or a working sample scrolling game
+dotnet new ruleset-scrolling-example -n MyCoolWorkingRuleset
+```
diff --git a/Templates/Rulesets/ruleset-empty/.editorconfig b/Templates/Rulesets/ruleset-empty/.editorconfig
new file mode 100644
index 0000000000..f3badda9b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/.editorconfig
@@ -0,0 +1,200 @@
+# EditorConfig is awesome: http://editorconfig.org
+root = true
+
+[*.cs]
+end_of_line = crlf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+#Roslyn naming styles
+
+#PascalCase for public and protected members
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.public_members_pascalcase.severity = error
+dotnet_naming_rule.public_members_pascalcase.symbols = public_members
+dotnet_naming_rule.public_members_pascalcase.style = pascalcase
+
+#camelCase for private members
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_symbols.private_members.applicable_accessibilities = private
+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
+
+dotnet_naming_symbols.local_function.applicable_kinds = local_function
+dotnet_naming_rule.local_function_camelcase.severity = warning
+dotnet_naming_rule.local_function_camelcase.symbols = local_function
+dotnet_naming_rule.local_function_camelcase.style = camelcase
+
+#all_lower for private and local constants/static readonlys
+dotnet_naming_style.all_lower.capitalization = all_lower
+dotnet_naming_style.all_lower.word_separator = _
+
+dotnet_naming_symbols.private_constants.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants.required_modifiers = const
+dotnet_naming_symbols.private_constants.applicable_kinds = field
+dotnet_naming_rule.private_const_all_lower.severity = warning
+dotnet_naming_rule.private_const_all_lower.symbols = private_constants
+dotnet_naming_rule.private_const_all_lower.style = all_lower
+
+dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly.applicable_kinds = field
+dotnet_naming_rule.private_static_readonly_all_lower.severity = warning
+dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly
+dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+dotnet_naming_rule.local_const_all_lower.severity = warning
+dotnet_naming_rule.local_const_all_lower.symbols = local_constants
+dotnet_naming_rule.local_const_all_lower.style = all_lower
+
+#ALL_UPPER for non private constants/static readonlys
+dotnet_naming_style.all_upper.capitalization = all_upper
+dotnet_naming_style.all_upper.word_separator = _
+
+dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_constants.required_modifiers = const
+dotnet_naming_symbols.public_constants.applicable_kinds = field
+dotnet_naming_rule.public_const_all_upper.severity = warning
+dotnet_naming_rule.public_const_all_upper.symbols = public_constants
+dotnet_naming_rule.public_const_all_upper.style = all_upper
+
+dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.public_static_readonly.applicable_kinds = field
+dotnet_naming_rule.public_static_readonly_all_upper.severity = warning
+dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly
+dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper
+
+#Roslyn formating options
+
+#Formatting - indentation options
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+#Formatting - new line options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_open_brace = all
+#csharp_new_line_before_members_in_anonymous_types = true
+#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing
+csharp_new_line_between_query_expression_clauses = true
+
+#Formatting - organize using options
+dotnet_sort_system_directives_first = true
+
+#Formatting - spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+
+#Formatting - wrapping options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#Roslyn language styles
+
+#Style - this. qualification
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+#Style - type names
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+csharp_style_var_when_type_is_apparent = true:none
+csharp_style_var_for_built_in_types = true:none
+csharp_style_var_elsewhere = true:silent
+
+#Style - modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning
+
+#Style - parentheses
+# Skipped because roslyn cannot separate +-*/ with << >>
+
+#Style - expression bodies
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
+
+#Style - expression preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:warning
+
+#Style - null/type checks
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:warning
+
+#Style - unused
+dotnet_style_readonly_field = true:silent
+dotnet_code_quality_unused_parameters = non_public:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_unused_value_assignment_preference = discard_variable:warning
+
+#Style - variable declaration
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+
+#Style - other C# 7.x features
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+
+#Style - C# 8 features
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:silent
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_switch_expression = false:none
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Private method is unused
+dotnet_diagnostic.IDE0051.severity = silent
+#Private member is unused
+dotnet_diagnostic.IDE0052.severity = silent
+
+#Rules for disposable
+dotnet_diagnostic.IDE0067.severity = none
+dotnet_diagnostic.IDE0068.severity = none
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/.gitignore b/Templates/Rulesets/ruleset-empty/.gitignore
new file mode 100644
index 0000000000..940794e60f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/.gitignore
@@ -0,0 +1,288 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
diff --git a/Templates/Rulesets/ruleset-empty/.template.config/template.json b/Templates/Rulesets/ruleset-empty/.template.config/template.json
new file mode 100644
index 0000000000..6bfe2e19dc
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/.template.config/template.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "ppy Pty Ltd",
+ "classifications": [
+ "Console"
+ ],
+ "name": "osu! ruleset",
+ "identity": "ppy.osu.Game.Templates.Rulesets",
+ "shortName": "ruleset",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "sourceName": "EmptyFreeform",
+ "preferNameDirectory": true
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json
new file mode 100644
index 0000000000..fd03878699
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "VisualTests (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Debug)",
+ "env": {},
+ "console": "internalConsole"
+ },
+ {
+ "name": "VisualTests (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Release)",
+ "env": {},
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/tasks.json
new file mode 100644
index 0000000000..509df6a510
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/tasks.json
@@ -0,0 +1,47 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.EmptyFreeformRuleset.Tests.csproj",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.EmptyFreeformRuleset.Tests.csproj",
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "restore"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
new file mode 100644
index 0000000000..9c512a01ea
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
@@ -0,0 +1,32 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Tests
+{
+ public class TestSceneOsuGame : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuGameBase gameBase)
+ {
+ OsuGame game = new OsuGame();
+ game.SetHost(host);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ },
+ game
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuPlayer.cs
new file mode 100644
index 0000000000..0f2ddf82a5
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuPlayer.cs
@@ -0,0 +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 NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Tests
+{
+ [TestFixture]
+ public class TestSceneOsuPlayer : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new EmptyFreeformRuleset();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs
new file mode 100644
index 0000000000..4f810ce17f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.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 System;
+using osu.Framework;
+using osu.Framework.Platform;
+using osu.Game.Tests;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Tests
+{
+ public static class VisualTestRunner
+ {
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ {
+ host.Run(new OsuTestBrowser());
+ return 0;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
new file mode 100644
index 0000000000..5eb5efa54c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+ osu.Game.Rulesets.EmptyFreeform.Tests.VisualTestRunner
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ WinExe
+ net5.0
+ osu.Game.Rulesets.EmptyFreeform.Tests
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln
new file mode 100644
index 0000000000..706df08472
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln
@@ -0,0 +1,96 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29123.88
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.EmptyFreeform", "osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyFreeform.Tests", "osu.Game.Rulesets.EmptyFreeform.Tests\osu.Game.Rulesets.EmptyFreeform.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ VisualTests|Any CPU = VisualTests|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ 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
+ 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/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings
new file mode 100644
index 0000000000..aa8f8739c1
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings
@@ -0,0 +1,934 @@
+
+ True
+ True
+ True
+ True
+ ExplicitlyExcluded
+ ExplicitlyExcluded
+ SOLUTION
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ True
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ 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
+ HINT
+ WARNING
+ HINT
+ SUGGESTION
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ ERROR
+ WARNING
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ 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
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+
+ True
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ 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)
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ Explicit
+ ExpressionBody
+ BlockBody
+ True
+ NEXT_LINE
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ NEXT_LINE
+ MULTILINE
+ True
+ True
+ True
+ True
+ 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
+ HTML
+ HUD
+ ID
+ IL
+ IOS
+ IP
+ IPC
+ JIT
+ LTRB
+ MD5
+ NS
+ OS
+ PM
+ RGB
+ RNG
+ SHA
+ SRGB
+ TK
+ SS
+ PP
+ GMT
+ QAT
+ BNG
+ UI
+ False
+ 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
+ TestFolder
+ 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
+ True
+ True
+ True
+ True
+ True
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Beatmaps/EmptyFreeformBeatmapConverter.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Beatmaps/EmptyFreeformBeatmapConverter.cs
new file mode 100644
index 0000000000..a441438d80
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Beatmaps/EmptyFreeformBeatmapConverter.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.Collections.Generic;
+using System.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.EmptyFreeform.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Beatmaps
+{
+ public class EmptyFreeformBeatmapConverter : BeatmapConverter
+ {
+ public EmptyFreeformBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
+ {
+ }
+
+ // todo: Check for conversion types that should be supported (ie. Beatmap.HitObjects.Any(h => h is IHasXPosition))
+ // https://github.com/ppy/osu/tree/master/osu.Game/Rulesets/Objects/Types
+ public override bool CanConvert() => true;
+
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
+ {
+ yield return new EmptyFreeformHitObject
+ {
+ Samples = original.Samples,
+ StartTime = original.StartTime,
+ Position = (original as IHasPosition)?.Position ?? Vector2.Zero,
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
new file mode 100644
index 0000000000..59a68245a6
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.EmptyFreeform
+{
+ public class EmptyFreeformDifficultyCalculator : DifficultyCalculator
+ {
+ public EmptyFreeformDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ : base(ruleset, beatmap)
+ {
+ }
+
+ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+ {
+ return new DifficultyAttributes(mods, skills, 0);
+ }
+
+ protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
+
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformInputManager.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformInputManager.cs
new file mode 100644
index 0000000000..b292a28c0d
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformInputManager.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 System.ComponentModel;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.EmptyFreeform
+{
+ public class EmptyFreeformInputManager : RulesetInputManager
+ {
+ public EmptyFreeformInputManager(RulesetInfo ruleset)
+ : base(ruleset, 0, SimultaneousBindingMode.Unique)
+ {
+ }
+ }
+
+ public enum EmptyFreeformAction
+ {
+ [Description("Button 1")]
+ Button1,
+
+ [Description("Button 2")]
+ Button2,
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs
new file mode 100644
index 0000000000..96675e3e99
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.EmptyFreeform.Beatmaps;
+using osu.Game.Rulesets.EmptyFreeform.Mods;
+using osu.Game.Rulesets.EmptyFreeform.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.EmptyFreeform
+{
+ public class EmptyFreeformRuleset : Ruleset
+ {
+ public override string Description => "a very emptyfreeformruleset ruleset";
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) =>
+ new DrawableEmptyFreeformRuleset(this, beatmap, mods);
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) =>
+ new EmptyFreeformBeatmapConverter(beatmap, this);
+
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) =>
+ new EmptyFreeformDifficultyCalculator(this, beatmap);
+
+ public override IEnumerable GetModsFor(ModType type)
+ {
+ switch (type)
+ {
+ case ModType.Automation:
+ return new[] { new EmptyFreeformModAutoplay() };
+
+ default:
+ return new Mod[] { null };
+ }
+ }
+
+ public override string ShortName => "emptyfreeformruleset";
+
+ public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[]
+ {
+ new KeyBinding(InputKey.Z, EmptyFreeformAction.Button1),
+ new KeyBinding(InputKey.X, EmptyFreeformAction.Button2),
+ };
+
+ public override Drawable CreateIcon() => new Icon(ShortName[0]);
+
+ public class Icon : CompositeDrawable
+ {
+ public Icon(char c)
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Circle
+ {
+ Size = new Vector2(20),
+ Colour = Color4.White,
+ },
+ new SpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = c.ToString(),
+ Font = OsuFont.Default.With(size: 18)
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs
new file mode 100644
index 0000000000..d5c1e9bd15
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.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 System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.EmptyFreeform.Objects;
+using osu.Game.Rulesets.EmptyFreeform.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Mods
+{
+ public class EmptyFreeformModAutoplay : ModAutoplay
+ {
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
+ {
+ ScoreInfo = new ScoreInfo
+ {
+ User = new User { Username = "sample" },
+ },
+ Replay = new EmptyFreeformAutoGenerator(beatmap).Generate(),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs
new file mode 100644
index 0000000000..0f38e9fdf8
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Objects.Drawables
+{
+ public class DrawableEmptyFreeformHitObject : DrawableHitObject
+ {
+ public DrawableEmptyFreeformHitObject(EmptyFreeformHitObject hitObject)
+ : base(hitObject)
+ {
+ Size = new Vector2(40);
+ Origin = Anchor.Centre;
+
+ Position = hitObject.Position;
+
+ // todo: add visuals.
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ if (timeOffset >= 0)
+ // todo: implement judgement logic
+ ApplyResult(r => r.Type = HitResult.Perfect);
+ }
+
+ protected override void UpdateHitStateTransforms(ArmedState state)
+ {
+ const double duration = 1000;
+
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.FadeOut(duration, Easing.OutQuint).Expire();
+ break;
+
+ case ArmedState.Miss:
+ this.FadeColour(Color4.Red, duration);
+ this.FadeOut(duration, Easing.InQuint).Expire();
+ break;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs
new file mode 100644
index 0000000000..9cd18d2d9f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs
@@ -0,0 +1,20 @@
+// 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.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Objects
+{
+ public class EmptyFreeformHitObject : HitObject, IHasPosition
+ {
+ public override Judgement CreateJudgement() => new Judgement();
+
+ public Vector2 Position { get; set; }
+
+ public float X => Position.X;
+ public float Y => Position.Y;
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs
new file mode 100644
index 0000000000..62f394d1ce
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.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 osu.Game.Beatmaps;
+using osu.Game.Rulesets.EmptyFreeform.Objects;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Replays
+{
+ public class EmptyFreeformAutoGenerator : AutoGenerator
+ {
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public EmptyFreeformAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ protected override void GenerateFrames()
+ {
+ Frames.Add(new EmptyFreeformReplayFrame());
+
+ foreach (EmptyFreeformHitObject hitObject in Beatmap.HitObjects)
+ {
+ Frames.Add(new EmptyFreeformReplayFrame
+ {
+ Time = hitObject.StartTime,
+ Position = hitObject.Position,
+ // todo: add required inputs and extra frames.
+ });
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs
new file mode 100644
index 0000000000..cc4483de31
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.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.Collections.Generic;
+using System.Linq;
+using osu.Framework.Input.StateChanges;
+using osu.Framework.Utils;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Replays
+{
+ public class EmptyFreeformFramedReplayInputHandler : FramedReplayInputHandler
+ {
+ public EmptyFreeformFramedReplayInputHandler(Replay replay)
+ : base(replay)
+ {
+ }
+
+ protected override bool IsImportant(EmptyFreeformReplayFrame frame) => frame.Actions.Any();
+
+ public override void CollectPendingInputs(List inputs)
+ {
+ var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
+
+ inputs.Add(new MousePositionAbsoluteInput
+ {
+ Position = GamefieldToScreenSpace(position),
+ });
+ inputs.Add(new ReplayState
+ {
+ PressedActions = CurrentFrame?.Actions ?? new List(),
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs
new file mode 100644
index 0000000000..c84101ca70
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Replays;
+using osuTK;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Replays
+{
+ public class EmptyFreeformReplayFrame : ReplayFrame
+ {
+ public List Actions = new List();
+ public Vector2 Position;
+
+ public EmptyFreeformReplayFrame(EmptyFreeformAction? button = null)
+ {
+ if (button.HasValue)
+ Actions.Add(button.Value);
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/DrawableEmptyFreeformRuleset.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/DrawableEmptyFreeformRuleset.cs
new file mode 100644
index 0000000000..290f35f516
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/DrawableEmptyFreeformRuleset.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.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Input;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Handlers;
+using osu.Game.Replays;
+using osu.Game.Rulesets.EmptyFreeform.Objects;
+using osu.Game.Rulesets.EmptyFreeform.Objects.Drawables;
+using osu.Game.Rulesets.EmptyFreeform.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.EmptyFreeform.UI
+{
+ [Cached]
+ public class DrawableEmptyFreeformRuleset : DrawableRuleset
+ {
+ public DrawableEmptyFreeformRuleset(EmptyFreeformRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ }
+
+ protected override Playfield CreatePlayfield() => new EmptyFreeformPlayfield();
+
+ protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new EmptyFreeformFramedReplayInputHandler(replay);
+
+ public override DrawableHitObject CreateDrawableRepresentation(EmptyFreeformHitObject h) => new DrawableEmptyFreeformHitObject(h);
+
+ protected override PassThroughInputManager CreateInputManager() => new EmptyFreeformInputManager(Ruleset?.RulesetInfo);
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/EmptyFreeformPlayfield.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/EmptyFreeformPlayfield.cs
new file mode 100644
index 0000000000..9df5935c45
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/EmptyFreeformPlayfield.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.EmptyFreeform.UI
+{
+ [Cached]
+ public class EmptyFreeformPlayfield : Playfield
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ HitObjectContainer,
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
new file mode 100644
index 0000000000..cfe2bd1cb2
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.1
+ osu.Game.Rulesets.Sample
+ Library
+ AnyCPU
+ osu.Game.Rulesets.EmptyFreeform
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/.editorconfig b/Templates/Rulesets/ruleset-example/.editorconfig
new file mode 100644
index 0000000000..f3badda9b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/.editorconfig
@@ -0,0 +1,200 @@
+# EditorConfig is awesome: http://editorconfig.org
+root = true
+
+[*.cs]
+end_of_line = crlf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+#Roslyn naming styles
+
+#PascalCase for public and protected members
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.public_members_pascalcase.severity = error
+dotnet_naming_rule.public_members_pascalcase.symbols = public_members
+dotnet_naming_rule.public_members_pascalcase.style = pascalcase
+
+#camelCase for private members
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_symbols.private_members.applicable_accessibilities = private
+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
+
+dotnet_naming_symbols.local_function.applicable_kinds = local_function
+dotnet_naming_rule.local_function_camelcase.severity = warning
+dotnet_naming_rule.local_function_camelcase.symbols = local_function
+dotnet_naming_rule.local_function_camelcase.style = camelcase
+
+#all_lower for private and local constants/static readonlys
+dotnet_naming_style.all_lower.capitalization = all_lower
+dotnet_naming_style.all_lower.word_separator = _
+
+dotnet_naming_symbols.private_constants.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants.required_modifiers = const
+dotnet_naming_symbols.private_constants.applicable_kinds = field
+dotnet_naming_rule.private_const_all_lower.severity = warning
+dotnet_naming_rule.private_const_all_lower.symbols = private_constants
+dotnet_naming_rule.private_const_all_lower.style = all_lower
+
+dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly.applicable_kinds = field
+dotnet_naming_rule.private_static_readonly_all_lower.severity = warning
+dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly
+dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+dotnet_naming_rule.local_const_all_lower.severity = warning
+dotnet_naming_rule.local_const_all_lower.symbols = local_constants
+dotnet_naming_rule.local_const_all_lower.style = all_lower
+
+#ALL_UPPER for non private constants/static readonlys
+dotnet_naming_style.all_upper.capitalization = all_upper
+dotnet_naming_style.all_upper.word_separator = _
+
+dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_constants.required_modifiers = const
+dotnet_naming_symbols.public_constants.applicable_kinds = field
+dotnet_naming_rule.public_const_all_upper.severity = warning
+dotnet_naming_rule.public_const_all_upper.symbols = public_constants
+dotnet_naming_rule.public_const_all_upper.style = all_upper
+
+dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.public_static_readonly.applicable_kinds = field
+dotnet_naming_rule.public_static_readonly_all_upper.severity = warning
+dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly
+dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper
+
+#Roslyn formating options
+
+#Formatting - indentation options
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+#Formatting - new line options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_open_brace = all
+#csharp_new_line_before_members_in_anonymous_types = true
+#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing
+csharp_new_line_between_query_expression_clauses = true
+
+#Formatting - organize using options
+dotnet_sort_system_directives_first = true
+
+#Formatting - spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+
+#Formatting - wrapping options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#Roslyn language styles
+
+#Style - this. qualification
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+#Style - type names
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+csharp_style_var_when_type_is_apparent = true:none
+csharp_style_var_for_built_in_types = true:none
+csharp_style_var_elsewhere = true:silent
+
+#Style - modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning
+
+#Style - parentheses
+# Skipped because roslyn cannot separate +-*/ with << >>
+
+#Style - expression bodies
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
+
+#Style - expression preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:warning
+
+#Style - null/type checks
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:warning
+
+#Style - unused
+dotnet_style_readonly_field = true:silent
+dotnet_code_quality_unused_parameters = non_public:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_unused_value_assignment_preference = discard_variable:warning
+
+#Style - variable declaration
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+
+#Style - other C# 7.x features
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+
+#Style - C# 8 features
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:silent
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_switch_expression = false:none
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Private method is unused
+dotnet_diagnostic.IDE0051.severity = silent
+#Private member is unused
+dotnet_diagnostic.IDE0052.severity = silent
+
+#Rules for disposable
+dotnet_diagnostic.IDE0067.severity = none
+dotnet_diagnostic.IDE0068.severity = none
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/.gitignore b/Templates/Rulesets/ruleset-example/.gitignore
new file mode 100644
index 0000000000..940794e60f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/.gitignore
@@ -0,0 +1,288 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
diff --git a/Templates/Rulesets/ruleset-example/.template.config/template.json b/Templates/Rulesets/ruleset-example/.template.config/template.json
new file mode 100644
index 0000000000..5d2f6f1ebd
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/.template.config/template.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "ppy Pty Ltd",
+ "classifications": [
+ "Console"
+ ],
+ "name": "osu! ruleset (pippidon example)",
+ "identity": "ppy.osu.Game.Templates.Rulesets.Pippidon",
+ "shortName": "ruleset-example",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "sourceName": "Pippidon",
+ "preferNameDirectory": true
+}
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
new file mode 100644
index 0000000000..bd9db14259
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "VisualTests (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Debug)",
+ "env": {},
+ "console": "internalConsole"
+ },
+ {
+ "name": "VisualTests (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Release)",
+ "env": {},
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json
new file mode 100644
index 0000000000..0ee07c1036
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json
@@ -0,0 +1,47 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "restore"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
new file mode 100644
index 0000000000..270d906b01
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
@@ -0,0 +1,32 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ public class TestSceneOsuGame : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuGameBase gameBase)
+ {
+ OsuGame game = new OsuGame();
+ game.SetHost(host);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ },
+ game
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs
new file mode 100644
index 0000000000..f00528900c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs
@@ -0,0 +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 NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ [TestFixture]
+ public class TestSceneOsuPlayer : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new PippidonRuleset();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
new file mode 100644
index 0000000000..fd6bd9b714
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.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 System;
+using osu.Framework;
+using osu.Framework.Platform;
+using osu.Game.Tests;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ public static class VisualTestRunner
+ {
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ {
+ host.Run(new OsuTestBrowser());
+ return 0;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
new file mode 100644
index 0000000000..d7c116411a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+ osu.Game.Rulesets.Pippidon.Tests.VisualTestRunner
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ WinExe
+ net5.0
+ osu.Game.Rulesets.Pippidon.Tests
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln
new file mode 100644
index 0000000000..bccffcd7ff
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln
@@ -0,0 +1,96 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29123.88
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Pippidon", "osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon.Tests", "osu.Game.Rulesets.Pippidon.Tests\osu.Game.Rulesets.Pippidon.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ VisualTests|Any CPU = VisualTests|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ 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
+ 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/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings
new file mode 100644
index 0000000000..aa8f8739c1
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings
@@ -0,0 +1,934 @@
+
+ True
+ True
+ True
+ True
+ ExplicitlyExcluded
+ ExplicitlyExcluded
+ SOLUTION
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ True
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ 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
+ HINT
+ WARNING
+ HINT
+ SUGGESTION
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ ERROR
+ WARNING
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ 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
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+
+ True
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ 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)
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ Explicit
+ ExpressionBody
+ BlockBody
+ True
+ NEXT_LINE
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ NEXT_LINE
+ MULTILINE
+ True
+ True
+ True
+ True
+ 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
+ HTML
+ HUD
+ ID
+ IL
+ IOS
+ IP
+ IPC
+ JIT
+ LTRB
+ MD5
+ NS
+ OS
+ PM
+ RGB
+ RNG
+ SHA
+ SRGB
+ TK
+ SS
+ PP
+ GMT
+ QAT
+ BNG
+ UI
+ False
+ 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
+ TestFolder
+ 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
+ True
+ True
+ True
+ True
+ True
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
new file mode 100644
index 0000000000..a2a4784603
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.Beatmaps
+{
+ public class PippidonBeatmapConverter : BeatmapConverter
+ {
+ public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
+ {
+ }
+
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition);
+
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
+ {
+ yield return new PippidonHitObject
+ {
+ Samples = original.Samples,
+ StartTime = original.StartTime,
+ Position = (original as IHasPosition)?.Position ?? Vector2.Zero,
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
new file mode 100644
index 0000000000..8ea334c99c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.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 System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.Replays;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Rulesets.Pippidon.Mods
+{
+ public class PippidonModAutoplay : ModAutoplay
+ {
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
+ {
+ ScoreInfo = new ScoreInfo
+ {
+ User = new User { Username = "sample" },
+ },
+ Replay = new PippidonAutoGenerator(beatmap).Generate(),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
new file mode 100644
index 0000000000..399d6adda2
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
@@ -0,0 +1,78 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
+{
+ public class DrawablePippidonHitObject : DrawableHitObject
+ {
+ private const double time_preempt = 600;
+ private const double time_fadein = 400;
+
+ public override bool HandlePositionalInput => true;
+
+ public DrawablePippidonHitObject(PippidonHitObject hitObject)
+ : base(hitObject)
+ {
+ Size = new Vector2(80);
+
+ Origin = Anchor.Centre;
+ Position = hitObject.Position;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ AddInternal(new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ Texture = textures.Get("coin"),
+ });
+ }
+
+ public override IEnumerable GetSamples() => new[]
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK)
+ };
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ if (timeOffset >= 0)
+ ApplyResult(r => r.Type = IsHovered ? HitResult.Perfect : HitResult.Miss);
+ }
+
+ protected override double InitialLifetimeOffset => time_preempt;
+
+ protected override void UpdateInitialTransforms() => this.FadeInFromZero(time_fadein);
+
+ protected override void UpdateHitStateTransforms(ArmedState state)
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(5, 1500, Easing.OutQuint).FadeOut(1500, Easing.OutQuint).Expire();
+ break;
+
+ case ArmedState.Miss:
+ const double duration = 1000;
+
+ this.ScaleTo(0.8f, duration, Easing.OutQuint);
+ this.MoveToOffset(new Vector2(0, 10), duration, Easing.In);
+ this.FadeColour(Color4.Red.Opacity(0.5f), duration / 2, Easing.OutQuint).Then().FadeOut(duration / 2, Easing.InQuint).Expire();
+ break;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs
new file mode 100644
index 0000000000..0c22554e82
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs
@@ -0,0 +1,20 @@
+// 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.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.Objects
+{
+ public class PippidonHitObject : HitObject, IHasPosition
+ {
+ public override Judgement CreateJudgement() => new Judgement();
+
+ public Vector2 Position { get; set; }
+
+ public float X => Position.X;
+ public float Y => Position.Y;
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
new file mode 100644
index 0000000000..f6340f6c25
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonDifficultyCalculator : DifficultyCalculator
+ {
+ public PippidonDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ : base(ruleset, beatmap)
+ {
+ }
+
+ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+ {
+ return new DifficultyAttributes(mods, skills, 0);
+ }
+
+ protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
+
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs
new file mode 100644
index 0000000000..aa7fa3188b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.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 System.ComponentModel;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonInputManager : RulesetInputManager
+ {
+ public PippidonInputManager(RulesetInfo ruleset)
+ : base(ruleset, 0, SimultaneousBindingMode.Unique)
+ {
+ }
+ }
+
+ public enum PippidonAction
+ {
+ [Description("Button 1")]
+ Button1,
+
+ [Description("Button 2")]
+ Button2,
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs
new file mode 100644
index 0000000000..89fed791cd
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs
@@ -0,0 +1,57 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Pippidon.Beatmaps;
+using osu.Game.Rulesets.Pippidon.Mods;
+using osu.Game.Rulesets.Pippidon.UI;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonRuleset : Ruleset
+ {
+ public override string Description => "gather the osu!coins";
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) =>
+ new DrawablePippidonRuleset(this, beatmap, mods);
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) =>
+ new PippidonBeatmapConverter(beatmap, this);
+
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) =>
+ new PippidonDifficultyCalculator(this, beatmap);
+
+ public override IEnumerable GetModsFor(ModType type)
+ {
+ switch (type)
+ {
+ case ModType.Automation:
+ return new[] { new PippidonModAutoplay() };
+
+ default:
+ return new Mod[] { null };
+ }
+ }
+
+ public override string ShortName => "pippidon";
+
+ public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[]
+ {
+ new KeyBinding(InputKey.Z, PippidonAction.Button1),
+ new KeyBinding(InputKey.X, PippidonAction.Button2),
+ };
+
+ public override Drawable CreateIcon() => new Sprite
+ {
+ Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
new file mode 100644
index 0000000000..612288257d
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonAutoGenerator : AutoGenerator
+ {
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public PippidonAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ protected override void GenerateFrames()
+ {
+ Frames.Add(new PippidonReplayFrame());
+
+ foreach (PippidonHitObject hitObject in Beatmap.HitObjects)
+ {
+ Frames.Add(new PippidonReplayFrame
+ {
+ Time = hitObject.StartTime,
+ Position = hitObject.Position,
+ });
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs
new file mode 100644
index 0000000000..e005346e1e
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Input.StateChanges;
+using osu.Framework.Utils;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonFramedReplayInputHandler : FramedReplayInputHandler
+ {
+ public PippidonFramedReplayInputHandler(Replay replay)
+ : base(replay)
+ {
+ }
+
+ protected override bool IsImportant(PippidonReplayFrame frame) => true;
+
+ public override void CollectPendingInputs(List inputs)
+ {
+ var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
+
+ inputs.Add(new MousePositionAbsoluteInput
+ {
+ Position = GamefieldToScreenSpace(position)
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs
new file mode 100644
index 0000000000..949ca160be
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Replays;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonReplayFrame : ReplayFrame
+ {
+ public Vector2 Position;
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3
new file mode 100644
index 0000000000..90b13d1f73
Binary files /dev/null and b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 differ
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png
new file mode 100644
index 0000000000..e79d2528ec
Binary files /dev/null and b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png differ
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png
new file mode 100644
index 0000000000..3cd89c6ce6
Binary files /dev/null and b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png differ
diff --git a/osu.Game/Rulesets/Replays/IAutoGenerator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs
similarity index 55%
rename from osu.Game/Rulesets/Replays/IAutoGenerator.cs
rename to Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs
index b1905e2b6f..1c4fe698c2 100644
--- a/osu.Game/Rulesets/Replays/IAutoGenerator.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs
@@ -1,12 +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 osu.Game.Replays;
+using osu.Game.Rulesets.Scoring;
-namespace osu.Game.Rulesets.Replays
+namespace osu.Game.Rulesets.Pippidon.Scoring
{
- public interface IAutoGenerator
+ public class PippidonScoreProcessor : ScoreProcessor
{
- Replay Generate();
}
}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs
new file mode 100644
index 0000000000..d923963bef
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Input;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Handlers;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.Objects.Drawables;
+using osu.Game.Rulesets.Pippidon.Replays;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ [Cached]
+ public class DrawablePippidonRuleset : DrawableRuleset
+ {
+ public DrawablePippidonRuleset(PippidonRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ }
+
+ public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new PippidonPlayfieldAdjustmentContainer();
+
+ protected override Playfield CreatePlayfield() => new PippidonPlayfield();
+
+ protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new PippidonFramedReplayInputHandler(replay);
+
+ public override DrawableHitObject CreateDrawableRepresentation(PippidonHitObject h) => new DrawablePippidonHitObject(h);
+
+ protected override PassThroughInputManager CreateInputManager() => new PippidonInputManager(Ruleset?.RulesetInfo);
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonCursorContainer.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonCursorContainer.cs
new file mode 100644
index 0000000000..9de3f4ba14
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonCursorContainer.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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ public class PippidonCursorContainer : GameplayCursorContainer
+ {
+ private Sprite cursorSprite;
+ private Texture cursorTexture;
+
+ protected override Drawable CreateCursor() => cursorSprite = new Sprite
+ {
+ Scale = new Vector2(0.5f),
+ Origin = Anchor.Centre,
+ Texture = cursorTexture,
+ };
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ cursorTexture = textures.Get("character");
+
+ if (cursorSprite != null)
+ cursorSprite.Texture = cursorTexture;
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
new file mode 100644
index 0000000000..b5a97c5ea3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ [Cached]
+ public class PippidonPlayfield : Playfield
+ {
+ protected override GameplayCursorContainer CreateCursor() => new PippidonCursorContainer();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ HitObjectContainer,
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfieldAdjustmentContainer.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfieldAdjustmentContainer.cs
new file mode 100644
index 0000000000..9236683827
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfieldAdjustmentContainer.cs
@@ -0,0 +1,20 @@
+// 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.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ public class PippidonPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
+ {
+ public PippidonPlayfieldAdjustmentContainer()
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Size = new Vector2(0.8f);
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
new file mode 100644
index 0000000000..61b859f45b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.1
+ osu.Game.Rulesets.Sample
+ Library
+ AnyCPU
+ osu.Game.Rulesets.Pippidon
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig
new file mode 100644
index 0000000000..f3badda9b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig
@@ -0,0 +1,200 @@
+# EditorConfig is awesome: http://editorconfig.org
+root = true
+
+[*.cs]
+end_of_line = crlf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+#Roslyn naming styles
+
+#PascalCase for public and protected members
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.public_members_pascalcase.severity = error
+dotnet_naming_rule.public_members_pascalcase.symbols = public_members
+dotnet_naming_rule.public_members_pascalcase.style = pascalcase
+
+#camelCase for private members
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_symbols.private_members.applicable_accessibilities = private
+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
+
+dotnet_naming_symbols.local_function.applicable_kinds = local_function
+dotnet_naming_rule.local_function_camelcase.severity = warning
+dotnet_naming_rule.local_function_camelcase.symbols = local_function
+dotnet_naming_rule.local_function_camelcase.style = camelcase
+
+#all_lower for private and local constants/static readonlys
+dotnet_naming_style.all_lower.capitalization = all_lower
+dotnet_naming_style.all_lower.word_separator = _
+
+dotnet_naming_symbols.private_constants.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants.required_modifiers = const
+dotnet_naming_symbols.private_constants.applicable_kinds = field
+dotnet_naming_rule.private_const_all_lower.severity = warning
+dotnet_naming_rule.private_const_all_lower.symbols = private_constants
+dotnet_naming_rule.private_const_all_lower.style = all_lower
+
+dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly.applicable_kinds = field
+dotnet_naming_rule.private_static_readonly_all_lower.severity = warning
+dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly
+dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+dotnet_naming_rule.local_const_all_lower.severity = warning
+dotnet_naming_rule.local_const_all_lower.symbols = local_constants
+dotnet_naming_rule.local_const_all_lower.style = all_lower
+
+#ALL_UPPER for non private constants/static readonlys
+dotnet_naming_style.all_upper.capitalization = all_upper
+dotnet_naming_style.all_upper.word_separator = _
+
+dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_constants.required_modifiers = const
+dotnet_naming_symbols.public_constants.applicable_kinds = field
+dotnet_naming_rule.public_const_all_upper.severity = warning
+dotnet_naming_rule.public_const_all_upper.symbols = public_constants
+dotnet_naming_rule.public_const_all_upper.style = all_upper
+
+dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.public_static_readonly.applicable_kinds = field
+dotnet_naming_rule.public_static_readonly_all_upper.severity = warning
+dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly
+dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper
+
+#Roslyn formating options
+
+#Formatting - indentation options
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+#Formatting - new line options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_open_brace = all
+#csharp_new_line_before_members_in_anonymous_types = true
+#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing
+csharp_new_line_between_query_expression_clauses = true
+
+#Formatting - organize using options
+dotnet_sort_system_directives_first = true
+
+#Formatting - spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+
+#Formatting - wrapping options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#Roslyn language styles
+
+#Style - this. qualification
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+#Style - type names
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+csharp_style_var_when_type_is_apparent = true:none
+csharp_style_var_for_built_in_types = true:none
+csharp_style_var_elsewhere = true:silent
+
+#Style - modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning
+
+#Style - parentheses
+# Skipped because roslyn cannot separate +-*/ with << >>
+
+#Style - expression bodies
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
+
+#Style - expression preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:warning
+
+#Style - null/type checks
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:warning
+
+#Style - unused
+dotnet_style_readonly_field = true:silent
+dotnet_code_quality_unused_parameters = non_public:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_unused_value_assignment_preference = discard_variable:warning
+
+#Style - variable declaration
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+
+#Style - other C# 7.x features
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+
+#Style - C# 8 features
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:silent
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_switch_expression = false:none
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Private method is unused
+dotnet_diagnostic.IDE0051.severity = silent
+#Private member is unused
+dotnet_diagnostic.IDE0052.severity = silent
+
+#Rules for disposable
+dotnet_diagnostic.IDE0067.severity = none
+dotnet_diagnostic.IDE0068.severity = none
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.gitignore b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore
new file mode 100644
index 0000000000..940794e60f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore
@@ -0,0 +1,288 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.template.config/template.json b/Templates/Rulesets/ruleset-scrolling-empty/.template.config/template.json
new file mode 100644
index 0000000000..3eb99a1f9d
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/.template.config/template.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "ppy Pty Ltd",
+ "classifications": [
+ "Console"
+ ],
+ "name": "osu! ruleset (scrolling)",
+ "identity": "ppy.osu.Game.Templates.Rulesets.Scrolling",
+ "shortName": "ruleset-scrolling",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "sourceName": "EmptyScrolling",
+ "preferNameDirectory": true
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json
new file mode 100644
index 0000000000..24e4873ed6
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "VisualTests (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Debug)",
+ "env": {},
+ "console": "internalConsole"
+ },
+ {
+ "name": "VisualTests (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Release)",
+ "env": {},
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/tasks.json
new file mode 100644
index 0000000000..00d0dc7d9b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/tasks.json
@@ -0,0 +1,47 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "restore"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
new file mode 100644
index 0000000000..aed6abb6bf
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
@@ -0,0 +1,32 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Tests
+{
+ public class TestSceneOsuGame : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuGameBase gameBase)
+ {
+ OsuGame game = new OsuGame();
+ game.SetHost(host);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ },
+ game
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuPlayer.cs
new file mode 100644
index 0000000000..9460576196
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuPlayer.cs
@@ -0,0 +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 NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Tests
+{
+ [TestFixture]
+ public class TestSceneOsuPlayer : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new EmptyScrollingRuleset();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs
new file mode 100644
index 0000000000..65cfb2bff4
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.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 System;
+using osu.Framework;
+using osu.Framework.Platform;
+using osu.Game.Tests;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Tests
+{
+ public static class VisualTestRunner
+ {
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ {
+ host.Run(new OsuTestBrowser());
+ return 0;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
new file mode 100644
index 0000000000..89b551286b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+ osu.Game.Rulesets.EmptyScrolling.Tests.VisualTestRunner
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ WinExe
+ net5.0
+ osu.Game.Rulesets.EmptyScrolling.Tests
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln
new file mode 100644
index 0000000000..97361e1a7b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln
@@ -0,0 +1,96 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29123.88
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.EmptyScrolling", "osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyScrolling.Tests", "osu.Game.Rulesets.EmptyScrolling.Tests\osu.Game.Rulesets.EmptyScrolling.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ VisualTests|Any CPU = VisualTests|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ 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
+ 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/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings
new file mode 100644
index 0000000000..aa8f8739c1
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings
@@ -0,0 +1,934 @@
+
+ True
+ True
+ True
+ True
+ ExplicitlyExcluded
+ ExplicitlyExcluded
+ SOLUTION
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ True
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ 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
+ HINT
+ WARNING
+ HINT
+ SUGGESTION
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ ERROR
+ WARNING
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ 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
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+
+ True
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ 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)
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ Explicit
+ ExpressionBody
+ BlockBody
+ True
+ NEXT_LINE
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ NEXT_LINE
+ MULTILINE
+ True
+ True
+ True
+ True
+ 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
+ HTML
+ HUD
+ ID
+ IL
+ IOS
+ IP
+ IPC
+ JIT
+ LTRB
+ MD5
+ NS
+ OS
+ PM
+ RGB
+ RNG
+ SHA
+ SRGB
+ TK
+ SS
+ PP
+ GMT
+ QAT
+ BNG
+ UI
+ False
+ 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
+ TestFolder
+ 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
+ True
+ True
+ True
+ True
+ True
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Beatmaps/EmptyScrollingBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Beatmaps/EmptyScrollingBeatmapConverter.cs
new file mode 100644
index 0000000000..02fb9a9dd5
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Beatmaps/EmptyScrollingBeatmapConverter.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.EmptyScrolling.Objects;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Beatmaps
+{
+ public class EmptyScrollingBeatmapConverter : BeatmapConverter
+ {
+ public EmptyScrollingBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
+ {
+ }
+
+ // todo: Check for conversion types that should be supported (ie. Beatmap.HitObjects.Any(h => h is IHasXPosition))
+ // https://github.com/ppy/osu/tree/master/osu.Game/Rulesets/Objects/Types
+ public override bool CanConvert() => true;
+
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
+ {
+ yield return new EmptyScrollingHitObject
+ {
+ Samples = original.Samples,
+ StartTime = original.StartTime,
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
new file mode 100644
index 0000000000..7f29c4e712
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.EmptyScrolling
+{
+ public class EmptyScrollingDifficultyCalculator : DifficultyCalculator
+ {
+ public EmptyScrollingDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ : base(ruleset, beatmap)
+ {
+ }
+
+ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+ {
+ return new DifficultyAttributes(mods, skills, 0);
+ }
+
+ protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
+
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingInputManager.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingInputManager.cs
new file mode 100644
index 0000000000..632e04f301
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingInputManager.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 System.ComponentModel;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.EmptyScrolling
+{
+ public class EmptyScrollingInputManager : RulesetInputManager
+ {
+ public EmptyScrollingInputManager(RulesetInfo ruleset)
+ : base(ruleset, 0, SimultaneousBindingMode.Unique)
+ {
+ }
+ }
+
+ public enum EmptyScrollingAction
+ {
+ [Description("Button 1")]
+ Button1,
+
+ [Description("Button 2")]
+ Button2,
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs
new file mode 100644
index 0000000000..c1d4de52b7
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs
@@ -0,0 +1,57 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.EmptyScrolling.Beatmaps;
+using osu.Game.Rulesets.EmptyScrolling.Mods;
+using osu.Game.Rulesets.EmptyScrolling.UI;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.EmptyScrolling
+{
+ public class EmptyScrollingRuleset : Ruleset
+ {
+ public override string Description => "a very emptyscrolling ruleset";
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableEmptyScrollingRuleset(this, beatmap, mods);
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new EmptyScrollingBeatmapConverter(beatmap, this);
+
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new EmptyScrollingDifficultyCalculator(this, beatmap);
+
+ public override IEnumerable GetModsFor(ModType type)
+ {
+ switch (type)
+ {
+ case ModType.Automation:
+ return new[] { new EmptyScrollingModAutoplay() };
+
+ default:
+ return new Mod[] { null };
+ }
+ }
+
+ public override string ShortName => "emptyscrolling";
+
+ public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[]
+ {
+ new KeyBinding(InputKey.Z, EmptyScrollingAction.Button1),
+ new KeyBinding(InputKey.X, EmptyScrollingAction.Button2),
+ };
+
+ public override Drawable CreateIcon() => new SpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = ShortName[0].ToString(),
+ Font = OsuFont.Default.With(size: 18),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs
new file mode 100644
index 0000000000..6dad1ff43b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.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.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.EmptyScrolling.Objects;
+using osu.Game.Rulesets.EmptyScrolling.Replays;
+using osu.Game.Scoring;
+using osu.Game.Users;
+using System.Collections.Generic;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Mods
+{
+ public class EmptyScrollingModAutoplay : ModAutoplay
+ {
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
+ {
+ ScoreInfo = new ScoreInfo
+ {
+ User = new User { Username = "sample" },
+ },
+ Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs
new file mode 100644
index 0000000000..b5ff0cde7c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.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 osu.Framework.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Objects.Drawables
+{
+ public class DrawableEmptyScrollingHitObject : DrawableHitObject
+ {
+ public DrawableEmptyScrollingHitObject(EmptyScrollingHitObject hitObject)
+ : base(hitObject)
+ {
+ Size = new Vector2(40);
+ Origin = Anchor.Centre;
+
+ // todo: add visuals.
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ if (timeOffset >= 0)
+ // todo: implement judgement logic
+ ApplyResult(r => r.Type = HitResult.Perfect);
+ }
+
+ protected override void UpdateHitStateTransforms(ArmedState state)
+ {
+ const double duration = 1000;
+
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.FadeOut(duration, Easing.OutQuint).Expire();
+ break;
+
+ case ArmedState.Miss:
+
+ this.FadeColour(Color4.Red, duration);
+ this.FadeOut(duration, Easing.InQuint).Expire();
+ break;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/EmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/EmptyScrollingHitObject.cs
new file mode 100644
index 0000000000..9b469be496
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/EmptyScrollingHitObject.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Objects
+{
+ public class EmptyScrollingHitObject : HitObject
+ {
+ public override Judgement CreateJudgement() => new Judgement();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs
new file mode 100644
index 0000000000..1058f756f3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.EmptyScrolling.Objects;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Replays
+{
+ public class EmptyScrollingAutoGenerator : AutoGenerator
+ {
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public EmptyScrollingAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ protected override void GenerateFrames()
+ {
+ Frames.Add(new EmptyScrollingReplayFrame());
+
+ foreach (EmptyScrollingHitObject hitObject in Beatmap.HitObjects)
+ {
+ Frames.Add(new EmptyScrollingReplayFrame
+ {
+ Time = hitObject.StartTime
+ // todo: add required inputs and extra frames.
+ });
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs
new file mode 100644
index 0000000000..4b998cfca3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Input.StateChanges;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Replays
+{
+ public class EmptyScrollingFramedReplayInputHandler : FramedReplayInputHandler
+ {
+ public EmptyScrollingFramedReplayInputHandler(Replay replay)
+ : base(replay)
+ {
+ }
+
+ protected override bool IsImportant(EmptyScrollingReplayFrame frame) => frame.Actions.Any();
+
+ public override void CollectPendingInputs(List inputs)
+ {
+ inputs.Add(new ReplayState
+ {
+ PressedActions = CurrentFrame?.Actions ?? new List(),
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs
new file mode 100644
index 0000000000..2f19cffd2a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Replays
+{
+ public class EmptyScrollingReplayFrame : ReplayFrame
+ {
+ public List Actions = new List();
+
+ public EmptyScrollingReplayFrame(EmptyScrollingAction? button = null)
+ {
+ if (button.HasValue)
+ Actions.Add(button.Value);
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/DrawableEmptyScrollingRuleset.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/DrawableEmptyScrollingRuleset.cs
new file mode 100644
index 0000000000..620a4abc51
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/DrawableEmptyScrollingRuleset.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Input;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Handlers;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.EmptyScrolling.Objects;
+using osu.Game.Rulesets.EmptyScrolling.Objects.Drawables;
+using osu.Game.Rulesets.EmptyScrolling.Replays;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.EmptyScrolling.UI
+{
+ [Cached]
+ public class DrawableEmptyScrollingRuleset : DrawableScrollingRuleset
+ {
+ public DrawableEmptyScrollingRuleset(EmptyScrollingRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ Direction.Value = ScrollingDirection.Left;
+ TimeRange.Value = 6000;
+ }
+
+ protected override Playfield CreatePlayfield() => new EmptyScrollingPlayfield();
+
+ protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new EmptyScrollingFramedReplayInputHandler(replay);
+
+ public override DrawableHitObject CreateDrawableRepresentation(EmptyScrollingHitObject h) => new DrawableEmptyScrollingHitObject(h);
+
+ protected override PassThroughInputManager CreateInputManager() => new EmptyScrollingInputManager(Ruleset?.RulesetInfo);
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/EmptyScrollingPlayfield.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/EmptyScrollingPlayfield.cs
new file mode 100644
index 0000000000..56620e44b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/EmptyScrollingPlayfield.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.EmptyScrolling.UI
+{
+ [Cached]
+ public class EmptyScrollingPlayfield : ScrollingPlayfield
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ HitObjectContainer,
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
new file mode 100644
index 0000000000..9dce3c9a0a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.1
+ osu.Game.Rulesets.Sample
+ Library
+ AnyCPU
+ osu.Game.Rulesets.EmptyScrolling
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig
new file mode 100644
index 0000000000..f3badda9b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig
@@ -0,0 +1,200 @@
+# EditorConfig is awesome: http://editorconfig.org
+root = true
+
+[*.cs]
+end_of_line = crlf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+#Roslyn naming styles
+
+#PascalCase for public and protected members
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.public_members_pascalcase.severity = error
+dotnet_naming_rule.public_members_pascalcase.symbols = public_members
+dotnet_naming_rule.public_members_pascalcase.style = pascalcase
+
+#camelCase for private members
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_symbols.private_members.applicable_accessibilities = private
+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
+
+dotnet_naming_symbols.local_function.applicable_kinds = local_function
+dotnet_naming_rule.local_function_camelcase.severity = warning
+dotnet_naming_rule.local_function_camelcase.symbols = local_function
+dotnet_naming_rule.local_function_camelcase.style = camelcase
+
+#all_lower for private and local constants/static readonlys
+dotnet_naming_style.all_lower.capitalization = all_lower
+dotnet_naming_style.all_lower.word_separator = _
+
+dotnet_naming_symbols.private_constants.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants.required_modifiers = const
+dotnet_naming_symbols.private_constants.applicable_kinds = field
+dotnet_naming_rule.private_const_all_lower.severity = warning
+dotnet_naming_rule.private_const_all_lower.symbols = private_constants
+dotnet_naming_rule.private_const_all_lower.style = all_lower
+
+dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly.applicable_kinds = field
+dotnet_naming_rule.private_static_readonly_all_lower.severity = warning
+dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly
+dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+dotnet_naming_rule.local_const_all_lower.severity = warning
+dotnet_naming_rule.local_const_all_lower.symbols = local_constants
+dotnet_naming_rule.local_const_all_lower.style = all_lower
+
+#ALL_UPPER for non private constants/static readonlys
+dotnet_naming_style.all_upper.capitalization = all_upper
+dotnet_naming_style.all_upper.word_separator = _
+
+dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_constants.required_modifiers = const
+dotnet_naming_symbols.public_constants.applicable_kinds = field
+dotnet_naming_rule.public_const_all_upper.severity = warning
+dotnet_naming_rule.public_const_all_upper.symbols = public_constants
+dotnet_naming_rule.public_const_all_upper.style = all_upper
+
+dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.public_static_readonly.applicable_kinds = field
+dotnet_naming_rule.public_static_readonly_all_upper.severity = warning
+dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly
+dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper
+
+#Roslyn formating options
+
+#Formatting - indentation options
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+#Formatting - new line options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_open_brace = all
+#csharp_new_line_before_members_in_anonymous_types = true
+#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing
+csharp_new_line_between_query_expression_clauses = true
+
+#Formatting - organize using options
+dotnet_sort_system_directives_first = true
+
+#Formatting - spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+
+#Formatting - wrapping options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#Roslyn language styles
+
+#Style - this. qualification
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+#Style - type names
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+csharp_style_var_when_type_is_apparent = true:none
+csharp_style_var_for_built_in_types = true:none
+csharp_style_var_elsewhere = true:silent
+
+#Style - modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning
+
+#Style - parentheses
+# Skipped because roslyn cannot separate +-*/ with << >>
+
+#Style - expression bodies
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
+
+#Style - expression preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:warning
+
+#Style - null/type checks
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:warning
+
+#Style - unused
+dotnet_style_readonly_field = true:silent
+dotnet_code_quality_unused_parameters = non_public:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_unused_value_assignment_preference = discard_variable:warning
+
+#Style - variable declaration
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+
+#Style - other C# 7.x features
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+
+#Style - C# 8 features
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:silent
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_switch_expression = false:none
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Private method is unused
+dotnet_diagnostic.IDE0051.severity = silent
+#Private member is unused
+dotnet_diagnostic.IDE0052.severity = silent
+
+#Rules for disposable
+dotnet_diagnostic.IDE0067.severity = none
+dotnet_diagnostic.IDE0068.severity = none
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/.gitignore b/Templates/Rulesets/ruleset-scrolling-example/.gitignore
new file mode 100644
index 0000000000..940794e60f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/.gitignore
@@ -0,0 +1,288 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
diff --git a/Templates/Rulesets/ruleset-scrolling-example/.template.config/template.json b/Templates/Rulesets/ruleset-scrolling-example/.template.config/template.json
new file mode 100644
index 0000000000..a1c097f1c8
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/.template.config/template.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "ppy Pty Ltd",
+ "classifications": [
+ "Console"
+ ],
+ "name": "osu! ruleset (scrolling pippidon example)",
+ "identity": "ppy.osu.Game.Templates.Rulesets.Scrolling.Pippidon",
+ "shortName": "ruleset-scrolling-example",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "sourceName": "Pippidon",
+ "preferNameDirectory": true
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
new file mode 100644
index 0000000000..bd9db14259
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "VisualTests (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Debug)",
+ "env": {},
+ "console": "internalConsole"
+ },
+ {
+ "name": "VisualTests (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Release)",
+ "env": {},
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json
new file mode 100644
index 0000000000..0ee07c1036
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json
@@ -0,0 +1,47 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "restore"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
new file mode 100644
index 0000000000..270d906b01
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
@@ -0,0 +1,32 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ public class TestSceneOsuGame : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuGameBase gameBase)
+ {
+ OsuGame game = new OsuGame();
+ game.SetHost(host);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ },
+ game
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs
new file mode 100644
index 0000000000..f00528900c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs
@@ -0,0 +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 NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ [TestFixture]
+ public class TestSceneOsuPlayer : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new PippidonRuleset();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
new file mode 100644
index 0000000000..fd6bd9b714
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.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 System;
+using osu.Framework;
+using osu.Framework.Platform;
+using osu.Game.Tests;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ public static class VisualTestRunner
+ {
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ {
+ host.Run(new OsuTestBrowser());
+ return 0;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
new file mode 100644
index 0000000000..d7c116411a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+ osu.Game.Rulesets.Pippidon.Tests.VisualTestRunner
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ WinExe
+ net5.0
+ osu.Game.Rulesets.Pippidon.Tests
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln
new file mode 100644
index 0000000000..bccffcd7ff
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln
@@ -0,0 +1,96 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29123.88
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Pippidon", "osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon.Tests", "osu.Game.Rulesets.Pippidon.Tests\osu.Game.Rulesets.Pippidon.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ VisualTests|Any CPU = VisualTests|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ 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
+ 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/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings
new file mode 100644
index 0000000000..aa8f8739c1
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings
@@ -0,0 +1,934 @@
+
+ True
+ True
+ True
+ True
+ ExplicitlyExcluded
+ ExplicitlyExcluded
+ SOLUTION
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ True
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ 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
+ HINT
+ WARNING
+ HINT
+ SUGGESTION
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ ERROR
+ WARNING
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ 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
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+
+ True
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ 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)
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ Explicit
+ ExpressionBody
+ BlockBody
+ True
+ NEXT_LINE
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ NEXT_LINE
+ MULTILINE
+ True
+ True
+ True
+ True
+ 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
+ HTML
+ HUD
+ ID
+ IL
+ IOS
+ IP
+ IPC
+ JIT
+ LTRB
+ MD5
+ NS
+ OS
+ PM
+ RGB
+ RNG
+ SHA
+ SRGB
+ TK
+ SS
+ PP
+ GMT
+ QAT
+ BNG
+ UI
+ False
+ 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
+ TestFolder
+ 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
+ True
+ True
+ True
+ True
+ True
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
new file mode 100644
index 0000000000..8f0b31ef1b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.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 System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.Beatmaps
+{
+ public class PippidonBeatmapConverter : BeatmapConverter
+ {
+ private readonly float minPosition;
+ private readonly float maxPosition;
+
+ public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
+ {
+ minPosition = beatmap.HitObjects.Min(getUsablePosition);
+ maxPosition = beatmap.HitObjects.Max(getUsablePosition);
+ }
+
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition);
+
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
+ {
+ yield return new PippidonHitObject
+ {
+ Samples = original.Samples,
+ StartTime = original.StartTime,
+ Lane = getLane(original)
+ };
+ }
+
+ private int getLane(HitObject hitObject) => (int)MathHelper.Clamp(
+ (getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1);
+
+ private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X;
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
new file mode 100644
index 0000000000..8ea334c99c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.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 System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.Replays;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Rulesets.Pippidon.Mods
+{
+ public class PippidonModAutoplay : ModAutoplay
+ {
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
+ {
+ ScoreInfo = new ScoreInfo
+ {
+ User = new User { Username = "sample" },
+ },
+ Replay = new PippidonAutoGenerator(beatmap).Generate(),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
new file mode 100644
index 0000000000..e458cacef9
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
@@ -0,0 +1,75 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Pippidon.UI;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
+{
+ public class DrawablePippidonHitObject : DrawableHitObject
+ {
+ private BindableNumber currentLane;
+
+ public DrawablePippidonHitObject(PippidonHitObject hitObject)
+ : base(hitObject)
+ {
+ Size = new Vector2(40);
+
+ Origin = Anchor.Centre;
+ Y = hitObject.Lane * PippidonPlayfield.LANE_HEIGHT;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(PippidonPlayfield playfield, TextureStore textures)
+ {
+ AddInternal(new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ Texture = textures.Get("coin"),
+ });
+
+ currentLane = playfield.CurrentLane.GetBoundCopy();
+ }
+
+ public override IEnumerable GetSamples() => new[]
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK)
+ };
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ if (timeOffset >= 0)
+ ApplyResult(r => r.Type = currentLane.Value == HitObject.Lane ? HitResult.Perfect : HitResult.Miss);
+ }
+
+ protected override void UpdateHitStateTransforms(ArmedState state)
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(5, 1500, Easing.OutQuint).FadeOut(1500, Easing.OutQuint).Expire();
+ break;
+
+ case ArmedState.Miss:
+
+ const double duration = 1000;
+
+ this.ScaleTo(0.8f, duration, Easing.OutQuint);
+ this.MoveToOffset(new Vector2(0, 10), duration, Easing.In);
+ this.FadeColour(Color4.Red, duration / 2, Easing.OutQuint).Then().FadeOut(duration / 2, Easing.InQuint).Expire();
+ break;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs
new file mode 100644
index 0000000000..9dd135479f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs
@@ -0,0 +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 osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Pippidon.Objects
+{
+ public class PippidonHitObject : HitObject
+ {
+ ///
+ /// Range = [-1,1]
+ ///
+ public int Lane;
+
+ public override Judgement CreateJudgement() => new Judgement();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
new file mode 100644
index 0000000000..f6340f6c25
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonDifficultyCalculator : DifficultyCalculator
+ {
+ public PippidonDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ : base(ruleset, beatmap)
+ {
+ }
+
+ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+ {
+ return new DifficultyAttributes(mods, skills, 0);
+ }
+
+ protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
+
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs
new file mode 100644
index 0000000000..c9e6e6faaa
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.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 System.ComponentModel;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonInputManager : RulesetInputManager
+ {
+ public PippidonInputManager(RulesetInfo ruleset)
+ : base(ruleset, 0, SimultaneousBindingMode.Unique)
+ {
+ }
+ }
+
+ public enum PippidonAction
+ {
+ [Description("Move up")]
+ MoveUp,
+
+ [Description("Move down")]
+ MoveDown,
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs
new file mode 100644
index 0000000000..ede00f1510
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Pippidon.Beatmaps;
+using osu.Game.Rulesets.Pippidon.Mods;
+using osu.Game.Rulesets.Pippidon.UI;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonRuleset : Ruleset
+ {
+ public override string Description => "gather the osu!coins";
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawablePippidonRuleset(this, beatmap, mods);
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new PippidonBeatmapConverter(beatmap, this);
+
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new PippidonDifficultyCalculator(this, beatmap);
+
+ public override IEnumerable GetModsFor(ModType type)
+ {
+ switch (type)
+ {
+ case ModType.Automation:
+ return new[] { new PippidonModAutoplay() };
+
+ default:
+ return new Mod[] { null };
+ }
+ }
+
+ public override string ShortName => "pippidon";
+
+ public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[]
+ {
+ new KeyBinding(InputKey.W, PippidonAction.MoveUp),
+ new KeyBinding(InputKey.S, PippidonAction.MoveDown),
+ };
+
+ public override Drawable CreateIcon() => new Sprite
+ {
+ Margin = new MarginPadding { Top = 3 },
+ Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
new file mode 100644
index 0000000000..724026273d
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
@@ -0,0 +1,60 @@
+// 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.Game.Beatmaps;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.UI;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonAutoGenerator : AutoGenerator
+ {
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public PippidonAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ protected override void GenerateFrames()
+ {
+ int currentLane = 0;
+
+ Frames.Add(new PippidonReplayFrame());
+
+ foreach (PippidonHitObject hitObject in Beatmap.HitObjects)
+ {
+ if (currentLane == hitObject.Lane)
+ continue;
+
+ int totalTravel = Math.Abs(hitObject.Lane - currentLane);
+ var direction = hitObject.Lane > currentLane ? PippidonAction.MoveDown : PippidonAction.MoveUp;
+
+ double time = hitObject.StartTime - 5;
+
+ if (totalTravel == PippidonPlayfield.LANE_COUNT - 1)
+ addFrame(time, direction == PippidonAction.MoveDown ? PippidonAction.MoveUp : PippidonAction.MoveDown);
+ else
+ {
+ time -= totalTravel * KEY_UP_DELAY;
+
+ for (int i = 0; i < totalTravel; i++)
+ {
+ addFrame(time, direction);
+ time += KEY_UP_DELAY;
+ }
+ }
+
+ currentLane = hitObject.Lane;
+ }
+ }
+
+ private void addFrame(double time, PippidonAction direction)
+ {
+ Frames.Add(new PippidonReplayFrame(direction) { Time = time });
+ Frames.Add(new PippidonReplayFrame { Time = time + KEY_UP_DELAY }); //Release the keys as well
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs
new file mode 100644
index 0000000000..7652357b4d
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Input.StateChanges;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonFramedReplayInputHandler : FramedReplayInputHandler
+ {
+ public PippidonFramedReplayInputHandler(Replay replay)
+ : base(replay)
+ {
+ }
+
+ protected override bool IsImportant(PippidonReplayFrame frame) => frame.Actions.Any();
+
+ public override void CollectPendingInputs(List inputs)
+ {
+ inputs.Add(new ReplayState
+ {
+ PressedActions = CurrentFrame?.Actions ?? new List(),
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs
new file mode 100644
index 0000000000..468ac9c725
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonReplayFrame : ReplayFrame
+ {
+ public List Actions = new List();
+
+ public PippidonReplayFrame(PippidonAction? button = null)
+ {
+ if (button.HasValue)
+ Actions.Add(button.Value);
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3
new file mode 100644
index 0000000000..90b13d1f73
Binary files /dev/null and b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 differ
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png
new file mode 100644
index 0000000000..e79d2528ec
Binary files /dev/null and b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png differ
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png
new file mode 100644
index 0000000000..3cd89c6ce6
Binary files /dev/null and b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png differ
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs
new file mode 100644
index 0000000000..9a73dd7790
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Input;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Handlers;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.Objects.Drawables;
+using osu.Game.Rulesets.Pippidon.Replays;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ [Cached]
+ public class DrawablePippidonRuleset : DrawableScrollingRuleset
+ {
+ public DrawablePippidonRuleset(PippidonRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ Direction.Value = ScrollingDirection.Left;
+ TimeRange.Value = 6000;
+ }
+
+ protected override Playfield CreatePlayfield() => new PippidonPlayfield();
+
+ protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new PippidonFramedReplayInputHandler(replay);
+
+ public override DrawableHitObject CreateDrawableRepresentation(PippidonHitObject h) => new DrawablePippidonHitObject(h);
+
+ protected override PassThroughInputManager CreateInputManager() => new PippidonInputManager(Ruleset?.RulesetInfo);
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs
new file mode 100644
index 0000000000..dd0a20f1b4
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs
@@ -0,0 +1,87 @@
+// 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.Audio.Track;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ public class PippidonCharacter : BeatSyncedContainer, IKeyBindingHandler
+ {
+ public readonly BindableInt LanePosition = new BindableInt
+ {
+ Value = 0,
+ MinValue = 0,
+ MaxValue = PippidonPlayfield.LANE_COUNT - 1,
+ };
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ Size = new Vector2(PippidonPlayfield.LANE_HEIGHT);
+
+ Child = new Sprite
+ {
+ FillMode = FillMode.Fit,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(1.2f),
+ RelativeSizeAxes = Axes.Both,
+ Texture = textures.Get("character")
+ };
+
+ LanePosition.BindValueChanged(e => { this.MoveToY(e.NewValue * PippidonPlayfield.LANE_HEIGHT); });
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
+ {
+ if (effectPoint.KiaiMode)
+ {
+ bool direction = beatIndex % 2 == 1;
+ double duration = timingPoint.BeatLength / 2;
+
+ Child.RotateTo(direction ? 10 : -10, duration * 2, Easing.InOutSine);
+
+ Child.Animate(i => i.MoveToY(-10, duration, Easing.Out))
+ .Then(i => i.MoveToY(0, duration, Easing.In));
+ }
+ else
+ {
+ Child.ClearTransforms();
+ Child.RotateTo(0, 500, Easing.Out);
+ Child.MoveTo(Vector2.Zero, 500, Easing.Out);
+ }
+ }
+
+ public bool OnPressed(PippidonAction action)
+ {
+ switch (action)
+ {
+ case PippidonAction.MoveUp:
+ changeLane(-1);
+ return true;
+
+ case PippidonAction.MoveDown:
+ changeLane(1);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ public void OnReleased(PippidonAction action)
+ {
+ }
+
+ private void changeLane(int change) => LanePosition.Value = (LanePosition.Value + change + PippidonPlayfield.LANE_COUNT) % PippidonPlayfield.LANE_COUNT;
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
new file mode 100644
index 0000000000..0e50030162
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
@@ -0,0 +1,128 @@
+// 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.Audio.Track;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ [Cached]
+ public class PippidonPlayfield : ScrollingPlayfield
+ {
+ public const float LANE_HEIGHT = 70;
+
+ public const int LANE_COUNT = 6;
+
+ public BindableInt CurrentLane => pippidon.LanePosition;
+
+ private PippidonCharacter pippidon;
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ new LaneContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding
+ {
+ Left = 200,
+ Top = LANE_HEIGHT / 2,
+ Bottom = LANE_HEIGHT / 2
+ },
+ Children = new Drawable[]
+ {
+ HitObjectContainer,
+ pippidon = new PippidonCharacter
+ {
+ Origin = Anchor.Centre,
+ },
+ }
+ },
+ },
+ });
+ }
+
+ private class LaneContainer : BeatSyncedContainer
+ {
+ private OsuColour colours;
+ private FillFlowContainer fill;
+
+ private readonly Container content = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ };
+
+ protected override Container Content => content;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ this.colours = colours;
+
+ InternalChildren = new Drawable[]
+ {
+ fill = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Colour = colours.BlueLight,
+ Direction = FillDirection.Vertical,
+ },
+ content,
+ };
+
+ for (int i = 0; i < LANE_COUNT; i++)
+ {
+ fill.Add(new Lane
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = LANE_HEIGHT,
+ });
+ }
+ }
+
+ private class Lane : CompositeDrawable
+ {
+ public Lane()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.White,
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Height = 0.95f,
+ },
+ };
+ }
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
+ {
+ if (effectPoint.KiaiMode)
+ fill.FlashColour(colours.PinkLight, 800, Easing.In);
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
new file mode 100644
index 0000000000..61b859f45b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.1
+ osu.Game.Rulesets.Sample
+ Library
+ AnyCPU
+ osu.Game.Rulesets.Pippidon
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj
new file mode 100644
index 0000000000..31a24a301f
--- /dev/null
+++ b/Templates/osu.Game.Templates.csproj
@@ -0,0 +1,24 @@
+
+
+ Template
+ ppy.osu.Game.Templates
+ osu! templates
+ ppy Pty Ltd
+ https://github.com/ppy/osu/blob/master/LICENCE
+ https://github.com/ppy/osu/blob/master/Templates
+ https://github.com/ppy/osu
+ Automated release.
+ Copyright (c) 2021 ppy Pty Ltd
+ Templates to use when creating a ruleset for consumption in osu!.
+ dotnet-new;templates;osu
+ netstandard2.1
+ true
+ false
+ content
+
+
+
+
+
+
+
diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml
index 737e5c43ab..adf98848bc 100644
--- a/appveyor_deploy.yml
+++ b/appveyor_deploy.yml
@@ -16,6 +16,8 @@ environment:
job_depends_on: osu-game
- job_name: mania-ruleset
job_depends_on: osu-game
+ - job_name: templates
+ job_depends_on: osu-game
nuget:
project_feed: true
@@ -59,6 +61,22 @@ for:
- cmd: dotnet remove osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
+ -
+ matrix:
+ only:
+ - job_name: templates
+ build_script:
+ - cmd: dotnet remove Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game\osu.Game.csproj
+ - cmd: dotnet remove Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
+ - cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game\osu.Game.csproj
+ - cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
+
+ - cmd: dotnet add Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
+ - cmd: dotnet add Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
+ - cmd: dotnet add Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
+ - cmd: dotnet add Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
+
+ - cmd: dotnet pack Templates\osu.Game.Templates.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
artifacts:
- path: '**\*.nupkg'
diff --git a/osu.Android.props b/osu.Android.props
index bfdc8f6b3c..e95c7e6619 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index ad929bbac3..cffcea22c2 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -17,7 +17,7 @@ using osu.Game.Database;
namespace osu.Android
{
- [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)]
+ [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })]
@@ -100,15 +100,15 @@ namespace osu.Android
// copy to an arbitrary-access memory stream to be able to proceed with the import.
var copy = new MemoryStream();
using (var stream = ContentResolver.OpenInputStream(uri))
- await stream.CopyToAsync(copy);
+ await stream.CopyToAsync(copy).ConfigureAwait(false);
lock (tasks)
{
tasks.Add(new ImportTask(copy, filename));
}
- }));
+ })).ConfigureAwait(false);
- await game.Import(tasks.ToArray());
+ await game.Import(tasks.ToArray()).ConfigureAwait(false);
}, TaskCreationOptions.LongRunning);
}
}
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 21d6336b2c..050bf2b787 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -7,6 +7,8 @@ using Android.OS;
using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
+using osu.Game.Utils;
+using Xamarin.Essentials;
namespace osu.Android
{
@@ -72,5 +74,14 @@ namespace osu.Android
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
+
+ protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
+
+ private class AndroidBatteryInfo : BatteryInfo
+ {
+ public override double ChargeLevel => Battery.ChargeLevel;
+
+ public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery;
+ }
}
}
diff --git a/osu.Android/Properties/AndroidManifest.xml b/osu.Android/Properties/AndroidManifest.xml
index 770eaf2222..e717bab310 100644
--- a/osu.Android/Properties/AndroidManifest.xml
+++ b/osu.Android/Properties/AndroidManifest.xml
@@ -6,5 +6,6 @@
+
\ No newline at end of file
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index a2638e95c8..582c856a47 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -20,6 +20,11 @@
d8
r8
+
+ None
+ cjk;mideast;other;rare;west
+ true
+
@@ -53,5 +58,13 @@
+
+
+ 5.0.0
+
+
+
+
+
\ No newline at end of file
diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf
index d2c14d321a..503e5935f5 100644
--- a/osu.Desktop.slnf
+++ b/osu.Desktop.slnf
@@ -15,7 +15,16 @@
"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"
+ "osu.Game\\osu.Game.csproj",
+
+ "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
+ "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj",
+ "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
+ "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj"
]
}
-}
\ No newline at end of file
+}
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 5909b82c8f..4de1e84fbf 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -2,12 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Microsoft.Win32;
+using osu.Desktop.Security;
using osu.Desktop.Overlays;
using osu.Framework.Platform;
using osu.Game;
@@ -18,6 +20,7 @@ using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
using osu.Desktop.Windows;
+using osu.Framework.Threading;
using osu.Game.IO;
namespace osu.Desktop
@@ -54,7 +57,7 @@ namespace osu.Desktop
private string getStableInstallPath()
{
- static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
+ static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")) || File.Exists(Path.Combine(p, "osu!.cfg"));
string stableInstallPath;
@@ -111,6 +114,8 @@ namespace osu.Desktop
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
+
+ LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
}
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)
@@ -136,33 +141,47 @@ namespace osu.Desktop
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
- switch (host.Window)
- {
- // Legacy osuTK DesktopGameWindow
- case OsuTKDesktopWindow desktopGameWindow:
- desktopGameWindow.CursorState |= CursorState.Hidden;
- desktopGameWindow.SetIconFromStream(iconStream);
- desktopGameWindow.Title = Name;
- desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
- break;
+ var desktopWindow = (SDL2DesktopWindow)host.Window;
- // SDL2 DesktopWindow
- case SDL2DesktopWindow desktopWindow:
- desktopWindow.CursorState |= CursorState.Hidden;
- desktopWindow.SetIconFromStream(iconStream);
- desktopWindow.Title = Name;
- desktopWindow.DragDrop += f => fileDrop(new[] { f });
- break;
- }
+ desktopWindow.CursorState |= CursorState.Hidden;
+ desktopWindow.SetIconFromStream(iconStream);
+ desktopWindow.Title = Name;
+ desktopWindow.DragDrop += f => fileDrop(new[] { f });
}
+ private readonly List importableFiles = new List();
+ private ScheduledDelegate importSchedule;
+
private void fileDrop(string[] filePaths)
{
- var firstExtension = Path.GetExtension(filePaths.First());
+ lock (importableFiles)
+ {
+ var firstExtension = Path.GetExtension(filePaths.First());
- if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
+ if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
- Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning);
+ importableFiles.AddRange(filePaths);
+
+ Logger.Log($"Adding {filePaths.Length} files for import");
+
+ // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
+ // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
+ importSchedule?.Cancel();
+ importSchedule = Scheduler.AddDelayed(handlePendingImports, 100);
+ }
+ }
+
+ private void handlePendingImports()
+ {
+ lock (importableFiles)
+ {
+ Logger.Log($"Handling batch import of {importableFiles.Count} files");
+
+ var paths = importableFiles.ToArray();
+ importableFiles.Clear();
+
+ Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
+ }
}
}
}
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 6ca7079654..5fb09c0cef 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -22,9 +22,8 @@ namespace osu.Desktop
{
// Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory;
- bool useOsuTK = args.Contains("--tk");
- using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useOsuTK: useOsuTK))
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
{
host.ExceptionThrown += handleException;
@@ -70,7 +69,6 @@ namespace osu.Desktop
/// Allow a maximum of one unhandled exception, per second of execution.
///
///
- ///
private static bool handleException(Exception arg)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
new file mode 100644
index 0000000000..01458b4c37
--- /dev/null
+++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
@@ -0,0 +1,83 @@
+// 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.Security.Principal;
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Notifications;
+
+namespace osu.Desktop.Security
+{
+ ///
+ /// Checks if the game is running with elevated privileges (as admin in Windows, root in Unix) and displays a warning notification if so.
+ ///
+ public class ElevatedPrivilegesChecker : Component
+ {
+ [Resolved]
+ private NotificationOverlay notifications { get; set; }
+
+ private bool elevated;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ elevated = checkElevated();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (elevated)
+ notifications.Post(new ElevatedPrivilegesNotification());
+ }
+
+ private bool checkElevated()
+ {
+ try
+ {
+ switch (RuntimeInfo.OS)
+ {
+ case RuntimeInfo.Platform.Windows:
+ if (!OperatingSystem.IsWindows()) return false;
+
+ var windowsIdentity = WindowsIdentity.GetCurrent();
+ var windowsPrincipal = new WindowsPrincipal(windowsIdentity);
+
+ return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
+
+ case RuntimeInfo.Platform.macOS:
+ case RuntimeInfo.Platform.Linux:
+ return Mono.Unix.Native.Syscall.geteuid() == 0;
+ }
+ }
+ catch
+ {
+ }
+
+ return false;
+ }
+
+ private class ElevatedPrivilegesNotification : SimpleNotification
+ {
+ public override bool IsImportant => true;
+
+ public ElevatedPrivilegesNotification()
+ {
+ Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, NotificationOverlay notificationOverlay)
+ {
+ Icon = FontAwesome.Solid.ShieldAlt;
+ IconBackgound.Colour = colours.YellowDark;
+ }
+ }
+ }
+}
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 71f9fafe57..47cd39dc5a 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -42,7 +42,7 @@ namespace osu.Desktop.Updater
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
}
- protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
+ protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
@@ -51,9 +51,9 @@ namespace osu.Desktop.Updater
try
{
- updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
+ updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false);
- var info = await updateManager.CheckForUpdate(!useDeltaPatching);
+ var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
if (info.ReleasesToApply.Count == 0)
{
@@ -79,12 +79,12 @@ namespace osu.Desktop.Updater
try
{
- await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f);
+ await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.Progress = 0;
notification.Text = @"Installing update...";
- await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
+ await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.State = ProgressNotificationState.Completed;
updatePending = true;
@@ -97,7 +97,7 @@ namespace osu.Desktop.Updater
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
- await checkForUpdateAsync(false, notification);
+ await checkForUpdateAsync(false, notification).ConfigureAwait(false);
scheduleRecheck = false;
}
else
@@ -116,7 +116,7 @@ namespace osu.Desktop.Updater
if (scheduleRecheck)
{
// check again in 30 minutes.
- Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
+ Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
}
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 3e0f0cb7f6..ad5c323e9b 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -25,6 +25,7 @@
+
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index ea43d9a54c..7a74563b2b 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
index 88b420ffad..94fdba4a3e 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
@@ -14,6 +14,11 @@
Properties\AndroidManifest.xml
armeabi-v7a;x86;arm64-v8a
+
+ None
+ cjk;mideast;other;rare;west
+ true
+
@@ -35,5 +40,10 @@
osu.Game
+
+
+ 5.0.0
+
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index f4ee3f5a42..5580358f89 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(5.0565038923984691d, "diffcalc-test")]
+ [TestCase(5.169743871843191d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime());
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
index f15da29993..1248409b2a 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[BackgroundDependencyLoader]
private void load()
{
- LocalConfig.Set(OsuSetting.IncreaseFirstObjectVisibility, false);
+ LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false);
}
[Test]
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
index 64695153b5..b7cd6737b1 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
@@ -1,8 +1,16 @@
// 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.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Skinning;
using osu.Game.Tests.Visual;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -10,5 +18,22 @@ namespace osu.Game.Rulesets.Catch.Tests
public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
+ [Test]
+ public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin)
+ {
+ if (withModifiedSkin)
+ {
+ AddStep("change component scale", () => Player.ChildrenOfType().First().Scale = new Vector2(2f));
+ AddStep("update target", () => Player.ChildrenOfType().ForEach(LegacySkin.UpdateDrawableTarget));
+ AddStep("exit player", () => Player.Exit());
+ CreateTest(null);
+ }
+
+ AddAssert("legacy HUD combo counter hidden", () =>
+ {
+ return Player.ChildrenOfType().All(c => c.ChildrenOfType().Single().Alpha == 0f);
+ });
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs
new file mode 100644
index 0000000000..a10371b0f7
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs
@@ -0,0 +1,53 @@
+// 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.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneCatchReplay : TestSceneCatchPlayer
+ {
+ protected override bool Autoplay => true;
+
+ private const int object_count = 10;
+
+ [Test]
+ public void TestReplayCatcherPositionIsFramePerfect()
+ {
+ AddUntilStep("caught all fruits", () => Player.ScoreProcessor.Combo.Value == object_count);
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ var beatmap = new Beatmap
+ {
+ BeatmapInfo =
+ {
+ Ruleset = ruleset,
+ }
+ };
+
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
+
+ for (int i = 0; i < object_count / 2; i++)
+ {
+ beatmap.HitObjects.Add(new Fruit
+ {
+ StartTime = (i + 1) * 1000,
+ X = 0
+ });
+ beatmap.HitObjects.Add(new Fruit
+ {
+ StartTime = (i + 1) * 1000 + 1,
+ X = CatchPlayfield.WIDTH
+ });
+ }
+
+ return beatmap;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index e8bb57cdf3..517027a9fc 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
@@ -20,6 +21,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -170,16 +172,25 @@ namespace osu.Game.Rulesets.Catch.Tests
}
[Test]
- public void TestCatcherStacking()
+ public void TestCatcherRandomStacking()
+ {
+ AddStep("catch more fruits", () => attemptCatch(() => new Fruit
+ {
+ X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(Vector2.One)
+ }, 50));
+ }
+
+ [Test]
+ public void TestCatcherStackingSameCaughtPosition()
{
AddStep("catch fruit", () => attemptCatch(new Fruit()));
checkPlate(1);
- AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
+ AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
checkPlate(10);
AddAssert("caught objects are stacked", () =>
- catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
- catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
- catcher.CaughtObjects.Any(obj => obj.Y < -20));
+ catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
+ catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
+ catcher.CaughtObjects.Any(obj => obj.Y < -25));
}
[Test]
@@ -189,11 +200,11 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
AddAssert("tiny droplet is exploded", () => catcher.CaughtObjects.Count() == 1 && droppedObjectContainer.Count == 1);
AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
- AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
+ AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
AddStep("explode", () => catcher.Explode());
AddAssert("fruits are exploded", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
- AddStep("catch fruits", () => attemptCatch(new Fruit(), 10));
+ AddStep("catch fruits", () => attemptCatch(() => new Fruit(), 10));
AddStep("drop", () => catcher.Drop());
AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
}
@@ -202,7 +213,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public void TestHitLightingColour()
{
var fruitColour = SkinConfiguration.DefaultComboColours[1];
- AddStep("enable hit lighting", () => config.Set(OsuSetting.HitLighting, true));
+ AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour", () =>
catcher.ChildrenOfType().First()?.ObjectColour == fruitColour);
@@ -211,7 +222,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestHitLightingDisabled()
{
- AddStep("disable hit lighting", () => config.Set(OsuSetting.HitLighting, false));
+ AddStep("disable hit lighting", () => config.SetValue(OsuSetting.HitLighting, false));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("no hit lighting", () => !catcher.ChildrenOfType().Any());
}
@@ -222,10 +233,15 @@ namespace osu.Game.Rulesets.Catch.Tests
private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
- private void attemptCatch(CatchHitObject hitObject, int count = 1)
+ private void attemptCatch(CatchHitObject hitObject)
+ {
+ attemptCatch(() => hitObject, 1);
+ }
+
+ private void attemptCatch(Func hitObject, int count)
{
for (var i = 0; i < count; i++)
- attemptCatch(hitObject, out _, out _);
+ attemptCatch(hitObject(), out _, out _);
}
private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result)
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index 1cbfa6338e..4af5098451 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -8,6 +8,8 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Threading;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
@@ -31,12 +33,32 @@ namespace osu.Game.Rulesets.Catch.Tests
private float circleSize;
+ private ScheduledDelegate addManyFruit;
+
+ private BeatmapDifficulty beatmapDifficulty;
+
public TestSceneCatcherArea()
{
AddSliderStep("circle size", 0, 8, 5, createCatcher);
AddToggleStep("hyper dash", t => this.ChildrenOfType().ForEach(area => area.ToggleHyperDash(t)));
- AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
+ AddStep("catch many random fruit", () =>
+ {
+ int count = 50;
+
+ addManyFruit?.Cancel();
+ addManyFruit = Scheduler.AddDelayed(() =>
+ {
+ attemptCatch(new Fruit
+ {
+ X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty) * 0.6f,
+ });
+
+ if (count-- == 0)
+ addManyFruit?.Cancel();
+ }, 50, true);
+ });
AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true }));
AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit()));
AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true }));
@@ -45,10 +67,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void attemptCatch(Fruit fruit)
{
fruit.X = fruit.OriginalX + catcher.X;
- fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
- {
- CircleSize = circleSize
- });
+ fruit.ApplyDefaults(new ControlPointInfo(), beatmapDifficulty);
foreach (var area in this.ChildrenOfType())
{
@@ -71,7 +90,12 @@ namespace osu.Game.Rulesets.Catch.Tests
{
circleSize = size;
- SetContents(() =>
+ beatmapDifficulty = new BeatmapDifficulty
+ {
+ CircleSize = circleSize
+ };
+
+ SetContents(_ =>
{
var droppedObjectContainer = new Container
{
@@ -84,7 +108,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Children = new Drawable[]
{
droppedObjectContainer,
- new TestCatcherArea(droppedObjectContainer, new BeatmapDifficulty { CircleSize = size })
+ new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
index c7b322c8a0..064a84cb98 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
scoreProcessor = new ScoreProcessor();
- SetContents(() => new CatchComboDisplay
+ SetContents(_ => new CatchComboDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index 3a651605d3..943adbef52 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -19,22 +19,22 @@ namespace osu.Game.Rulesets.Catch.Tests
{
base.LoadComplete();
- AddStep("show pear", () => SetContents(() => createDrawableFruit(0)));
- AddStep("show grape", () => SetContents(() => createDrawableFruit(1)));
- AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2)));
- AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3)));
+ AddStep("show pear", () => SetContents(_ => createDrawableFruit(0)));
+ AddStep("show grape", () => SetContents(_ => createDrawableFruit(1)));
+ AddStep("show pineapple / apple", () => SetContents(_ => createDrawableFruit(2)));
+ AddStep("show raspberry / orange", () => SetContents(_ => createDrawableFruit(3)));
- AddStep("show banana", () => SetContents(createDrawableBanana));
+ AddStep("show banana", () => SetContents(_ => createDrawableBanana()));
- AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
- AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
+ AddStep("show droplet", () => SetContents(_ => createDrawableDroplet()));
+ AddStep("show tiny droplet", () => SetContents(_ => createDrawableTinyDroplet()));
- AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true)));
- AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true)));
- AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true)));
- AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true)));
+ AddStep("show hyperdash pear", () => SetContents(_ => createDrawableFruit(0, true)));
+ AddStep("show hyperdash grape", () => SetContents(_ => createDrawableFruit(1, true)));
+ AddStep("show hyperdash pineapple / apple", () => SetContents(_ => createDrawableFruit(2, true)));
+ AddStep("show hyperdash raspberry / orange", () => SetContents(_ => createDrawableFruit(3, true)));
- AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
+ AddStep("show hyperdash droplet", () => SetContents(_ => createDrawableDroplet(true)));
}
private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
index 125e0c674c..9446e864a1 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override void LoadComplete()
{
- AddStep("fruit changes visual and hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
+ AddStep("fruit changes visual and hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
{
IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
HyperDashBindable = { BindTarget = hyperDash },
}))));
- AddStep("droplet changes hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
+ AddStep("droplet changes hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
{
HyperDashBindable = { BindTarget = hyperDash },
}))));
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs
index eea83ef7c1..bc3daca16f 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs
@@ -32,28 +32,28 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(false, false)]
- public override void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
+ public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
- base.TestBeatmapComboColours(userHasCustomColours, useBeatmapSkin);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
+ ConfigureTest(useBeatmapSkin, true, userHasCustomColours);
AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours));
}
[TestCase(true)]
[TestCase(false)]
- public override void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
+ public void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
- base.TestBeatmapComboColoursOverride(useBeatmapSkin);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
+ ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
}
[TestCase(true)]
[TestCase(false)]
- public override void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
+ public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
- base.TestBeatmapComboColoursOverrideWithDefaultColours(useBeatmapSkin);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
+ ConfigureTest(useBeatmapSkin, false, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
}
@@ -61,10 +61,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false, true)]
[TestCase(true, false)]
[TestCase(false, false)]
- public override void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
+ public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false);
- base.TestBeatmapNoComboColours(useBeatmapSkin, useBeatmapColour);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, false));
+ ConfigureTest(useBeatmapSkin, useBeatmapColour, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
}
@@ -72,10 +72,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false, true)]
[TestCase(true, false)]
[TestCase(false, false)]
- public override void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
+ public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false);
- base.TestBeatmapNoComboColoursSkinOverride(useBeatmapSkin, useBeatmapColour);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, false));
+ ConfigureTest(useBeatmapSkin, useBeatmapColour, true);
AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
}
@@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false)]
public void TestBeatmapHyperDashColours(bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, true, true);
AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestBeatmapSkin.HYPER_DASH_COLOUR);
AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false)]
public void TestBeatmapHyperDashColoursOverride(bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestSkin.HYPER_DASH_COLOUR);
AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);
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 bf3aba5859..83d0744588 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,8 +2,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 0a817eca0d..23ce444560 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,6 +21,7 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
using osu.Game.Skinning;
@@ -50,40 +51,40 @@ namespace osu.Game.Rulesets.Catch
public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
- if (mods.HasFlag(LegacyMods.Nightcore))
+ if (mods.HasFlagFast(LegacyMods.Nightcore))
yield return new CatchModNightcore();
- else if (mods.HasFlag(LegacyMods.DoubleTime))
+ else if (mods.HasFlagFast(LegacyMods.DoubleTime))
yield return new CatchModDoubleTime();
- if (mods.HasFlag(LegacyMods.Perfect))
+ if (mods.HasFlagFast(LegacyMods.Perfect))
yield return new CatchModPerfect();
- else if (mods.HasFlag(LegacyMods.SuddenDeath))
+ else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
yield return new CatchModSuddenDeath();
- if (mods.HasFlag(LegacyMods.Cinema))
+ if (mods.HasFlagFast(LegacyMods.Cinema))
yield return new CatchModCinema();
- else if (mods.HasFlag(LegacyMods.Autoplay))
+ else if (mods.HasFlagFast(LegacyMods.Autoplay))
yield return new CatchModAutoplay();
- if (mods.HasFlag(LegacyMods.Easy))
+ if (mods.HasFlagFast(LegacyMods.Easy))
yield return new CatchModEasy();
- if (mods.HasFlag(LegacyMods.Flashlight))
+ if (mods.HasFlagFast(LegacyMods.Flashlight))
yield return new CatchModFlashlight();
- if (mods.HasFlag(LegacyMods.HalfTime))
+ if (mods.HasFlagFast(LegacyMods.HalfTime))
yield return new CatchModHalfTime();
- if (mods.HasFlag(LegacyMods.HardRock))
+ if (mods.HasFlagFast(LegacyMods.HardRock))
yield return new CatchModHardRock();
- if (mods.HasFlag(LegacyMods.Hidden))
+ if (mods.HasFlagFast(LegacyMods.Hidden))
yield return new CatchModHidden();
- if (mods.HasFlag(LegacyMods.NoFail))
+ if (mods.HasFlagFast(LegacyMods.NoFail))
yield return new CatchModNoFail();
- if (mods.HasFlag(LegacyMods.Relax))
+ if (mods.HasFlagFast(LegacyMods.Relax))
yield return new CatchModRelax();
}
@@ -113,6 +114,7 @@ namespace osu.Game.Rulesets.Catch
return new Mod[]
{
new CatchModDifficultyAdjust(),
+ new CatchModClassic(),
};
case ModType.Automation:
@@ -125,7 +127,8 @@ namespace osu.Game.Rulesets.Catch
case ModType.Fun:
return new Mod[]
{
- new MultiMod(new ModWindUp(), new ModWindDown())
+ new MultiMod(new ModWindUp(), new ModWindDown()),
+ new CatchModFloatingFruits()
};
default:
@@ -158,13 +161,13 @@ namespace osu.Game.Rulesets.Catch
switch (result)
{
case HitResult.LargeTickHit:
- return "large droplet";
+ return "Large droplet";
case HitResult.SmallTickHit:
- return "small droplet";
+ return "Small droplet";
case HitResult.LargeBonus:
- return "banana";
+ return "Banana";
}
return base.GetDisplayNameForHitResult(result);
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 10aae70722..f5cce47186 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
private const double star_scaling_factor = 0.153;
- protected override int SectionLength => 750;
-
private float halfCatcherWidth;
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 83db9216ed..849af75228 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
- public class Movement : Skill
+ public class Movement : StrainSkill
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
@@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
protected override double DecayWeight => 0.94;
+ protected override int SectionLength => 750;
+
protected readonly float HalfCatcherWidth;
private float? lastPlayerPosition;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModClassic.cs b/osu.Game.Rulesets.Catch/Mods/CatchModClassic.cs
new file mode 100644
index 0000000000..9624e84018
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModClassic.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.
+
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Catch.Mods
+{
+ public class CatchModClassic : ModClassic
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
new file mode 100644
index 0000000000..63203dd57c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Mods
+{
+ public class CatchModFloatingFruits : Mod, IApplicableToDrawableRuleset
+ {
+ public override string Name => "Floating Fruits";
+ public override string Acronym => "FF";
+ public override string Description => "The fruits are... floating?";
+ public override double ScoreMultiplier => 1;
+ public override IconUsage? Icon => FontAwesome.Solid.Cloud;
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ drawableRuleset.Anchor = Anchor.Centre;
+ drawableRuleset.Origin = Anchor.Centre;
+
+ drawableRuleset.Scale = new Vector2(1, -1);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
index 4b008d2734..7bad4c79cb 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
@@ -3,13 +3,16 @@
using System.Linq;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModHidden : ModHidden
+ public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset
{
public override string Description => @"Play with fading fruits.";
public override double ScoreMultiplier => 1.06;
@@ -17,10 +20,20 @@ namespace osu.Game.Rulesets.Catch.Mods
private const double fade_out_offset_multiplier = 0.6;
private const double fade_out_duration_multiplier = 0.44;
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset;
+ var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield;
+
+ catchPlayfield.CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
+ }
+
+ protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
+ {
+ }
+
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
- base.ApplyNormalVisibilityState(hitObject, state);
-
if (!(hitObject is DrawableCatchHitObject catchDrawable))
return;
@@ -43,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Mods
var offset = hitObject.TimePreempt * fade_out_offset_multiplier;
var duration = offset - hitObject.TimePreempt * fade_out_duration_multiplier;
- using (drawable.BeginAbsoluteSequence(hitObject.StartTime - offset, true))
+ using (drawable.BeginAbsoluteSequence(hitObject.StartTime - offset))
drawable.FadeOut(duration);
}
}
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 64ded8e94f..a81703119a 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -5,7 +5,6 @@ using System;
using System.Linq;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
-using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@@ -13,26 +12,19 @@ using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Catch.Replays
{
- internal class CatchAutoGenerator : AutoGenerator
+ internal class CatchAutoGenerator : AutoGenerator
{
- public const double RELEASE_DELAY = 20;
-
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
public CatchAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
- Replay = new Replay();
}
- protected Replay Replay;
-
- private CatchReplayFrame currentFrame;
-
- public override Replay Generate()
+ protected override void GenerateFrames()
{
if (Beatmap.HitObjects.Count == 0)
- return Replay;
+ return;
// todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED;
@@ -119,19 +111,11 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
}
-
- return Replay;
}
private void addFrame(double time, float? position = null, bool dashing = false)
{
- // todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
- if (Replay.Frames.Count == 0)
- Replay.Frames.Add(new CatchReplayFrame(time - 1, position, false, null));
-
- var last = currentFrame;
- currentFrame = new CatchReplayFrame(time, position, dashing, last);
- Replay.Frames.Add(currentFrame);
+ Frames.Add(new CatchReplayFrame(time, position, dashing, LastFrame));
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
index 99d899db80..137328b1c3 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using osu.Framework.Input.StateChanges;
using osu.Framework.Utils;
@@ -20,29 +19,14 @@ namespace osu.Game.Rulesets.Catch.Replays
protected override bool IsImportant(CatchReplayFrame frame) => frame.Actions.Any();
- protected float? Position
- {
- get
- {
- var frame = CurrentFrame;
-
- if (frame == null)
- return null;
-
- Debug.Assert(CurrentTime != null);
-
- return NextFrame != null ? Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time) : frame.Position;
- }
- }
-
public override void CollectPendingInputs(List inputs)
{
- if (!Position.HasValue) return;
+ var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
inputs.Add(new CatchReplayState
{
PressedActions = CurrentFrame?.Actions ?? new List(),
- CatcherX = Position.Value
+ CatcherX = position
});
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index 41fd0fe776..8c9e602cd4 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -1,11 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK.Graphics;
-using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
///
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
///
- private bool providesComboCounter => this.HasFont(GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score");
+ private bool providesComboCounter => this.HasFont(LegacyFont.Combo);
public CatchLegacySkinTransformer(ISkinSource source)
: base(source)
@@ -23,60 +24,68 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable GetDrawableComponent(ISkinComponent component)
{
- if (component is HUDSkinComponent hudComponent)
+ if (component is SkinnableTargetComponent targetComponent)
{
- switch (hudComponent.Component)
+ switch (targetComponent.Target)
{
- case HUDSkinComponents.ComboCounter:
- // catch may provide its own combo counter; hide the default.
- return providesComboCounter ? Drawable.Empty() : null;
+ case SkinnableTarget.MainHUDComponents:
+ var components = Source.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
+
+ if (providesComboCounter && components != null)
+ {
+ // catch may provide its own combo counter; hide the default.
+ // todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed.
+ foreach (var legacyComboCounter in components.OfType())
+ legacyComboCounter.HiddenByRulesetImplementation = false;
+ }
+
+ return components;
}
}
- if (!(component is CatchSkinComponent catchSkinComponent))
- return null;
-
- switch (catchSkinComponent.Component)
+ if (component is CatchSkinComponent catchSkinComponent)
{
- case CatchSkinComponents.Fruit:
- if (GetTexture("fruit-pear") != null)
- return new LegacyFruitPiece();
+ switch (catchSkinComponent.Component)
+ {
+ case CatchSkinComponents.Fruit:
+ if (GetTexture("fruit-pear") != null)
+ return new LegacyFruitPiece();
- break;
+ return null;
- case CatchSkinComponents.Banana:
- if (GetTexture("fruit-bananas") != null)
- return new LegacyBananaPiece();
+ case CatchSkinComponents.Banana:
+ if (GetTexture("fruit-bananas") != null)
+ return new LegacyBananaPiece();
- break;
+ return null;
- case CatchSkinComponents.Droplet:
- if (GetTexture("fruit-drop") != null)
- return new LegacyDropletPiece();
+ case CatchSkinComponents.Droplet:
+ if (GetTexture("fruit-drop") != null)
+ return new LegacyDropletPiece();
- break;
+ return null;
- case CatchSkinComponents.CatcherIdle:
- return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
- this.GetAnimation("fruit-ryuuta", true, true, true);
+ case CatchSkinComponents.CatcherIdle:
+ return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
+ this.GetAnimation("fruit-ryuuta", true, true, true);
- case CatchSkinComponents.CatcherFail:
- return this.GetAnimation("fruit-catcher-fail", true, true, true) ??
- this.GetAnimation("fruit-ryuuta", true, true, true);
+ case CatchSkinComponents.CatcherFail:
+ return this.GetAnimation("fruit-catcher-fail", true, true, true) ??
+ this.GetAnimation("fruit-ryuuta", true, true, true);
- case CatchSkinComponents.CatcherKiai:
- return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
- this.GetAnimation("fruit-ryuuta", true, true, true);
+ case CatchSkinComponents.CatcherKiai:
+ return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
+ this.GetAnimation("fruit-ryuuta", true, true, true);
- case CatchSkinComponents.CatchComboCounter:
+ case CatchSkinComponents.CatchComboCounter:
+ if (providesComboCounter)
+ return new LegacyCatchComboCounter(Source);
- if (providesComboCounter)
- return new LegacyCatchComboCounter(Source);
-
- break;
+ return null;
+ }
}
- return null;
+ return Source.GetDrawableComponent(component);
}
public override IBindable GetConfig(TLookup lookup)
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
index f797ae75c2..33c3867f5a 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
@@ -7,7 +7,6 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
-using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
@@ -22,9 +21,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public LegacyCatchComboCounter(ISkin skin)
{
- var fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score";
- var fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f;
-
AutoSizeAxes = Axes.Both;
Alpha = 0f;
@@ -34,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
InternalChildren = new Drawable[]
{
- explosion = new LegacyRollingCounter(skin, fontName, fontOverlap)
+ explosion = new LegacyRollingCounter(LegacyFont.Combo)
{
Alpha = 0.65f,
Blending = BlendingParameters.Additive,
@@ -42,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
Origin = Anchor.Centre,
Scale = new Vector2(1.5f),
},
- counter = new LegacyRollingCounter(skin, fontName, fontOverlap)
+ counter = new LegacyRollingCounter(LegacyFont.Combo)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 73420a9eda..0e1ef90737 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -51,8 +51,11 @@ namespace osu.Game.Rulesets.Catch.UI
{
droppedObjectContainer,
CatcherArea.MovableCatcher.CreateProxiedContent(),
- HitObjectContainer,
+ HitObjectContainer.CreateProxy(),
+ // This ordering (`CatcherArea` before `HitObjectContainer`) is important to
+ // make sure the up-to-date catcher position is used for the catcher catching logic of hit objects.
CatcherArea,
+ HitObjectContainer,
};
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index ed875e7002..0d6a577d1e 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -43,11 +43,26 @@ namespace osu.Game.Rulesets.Catch.UI
///
public bool HyperDashing => hyperDashModifier != 1;
+ ///
+ /// Whether fruit should appear on the plate.
+ ///
+ public bool CatchFruitOnPlate { get; set; } = true;
+
///
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
///
public const double BASE_SPEED = 1.0;
+ ///
+ /// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
+ ///
+ public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5;
+
+ ///
+ /// The amount by which caught fruit should be scaled down to fit on the plate.
+ ///
+ private const float caught_fruit_scale_adjust = 0.5f;
+
[NotNull]
private readonly Container trailsTarget;
@@ -197,13 +212,13 @@ namespace osu.Game.Rulesets.Catch.UI
/// Calculates the width of the area used for attempting catches in gameplay.
///
/// The scale of the catcher.
- internal static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
+ public static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
///
/// Calculates the width of the area used for attempting catches in gameplay.
///
/// The beatmap difficulty.
- internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
+ public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
///
/// Determine if this catcher can catch a in the current position.
@@ -235,9 +250,10 @@ namespace osu.Game.Rulesets.Catch.UI
if (result.IsHit)
{
- var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2);
+ var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X);
- placeCaughtObject(palpableObject, positionInStack);
+ if (CatchFruitOnPlate)
+ placeCaughtObject(palpableObject, positionInStack);
if (hitLighting.Value)
addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value);
@@ -378,16 +394,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
updateTrailVisibility();
- if (hyperDashing)
- {
- this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
- this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
- }
- else
- {
- this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
- this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
- }
+ this.FadeColour(hyperDashing ? hyperDashColour : Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
@@ -473,7 +480,7 @@ namespace osu.Game.Rulesets.Catch.UI
caughtObject.CopyStateFrom(drawableObject);
caughtObject.Anchor = Anchor.TopCentre;
caughtObject.Position = position;
- caughtObject.Scale /= 2;
+ caughtObject.Scale *= caught_fruit_scale_adjust;
caughtObjectContainer.Add(caughtObject);
@@ -483,19 +490,21 @@ namespace osu.Game.Rulesets.Catch.UI
private Vector2 computePositionInStack(Vector2 position, float displayRadius)
{
- const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2;
- const float allowance = 10;
+ // this is taken from osu-stable (lenience should be 10 * 10 at standard scale).
+ const float lenience_adjust = 10 / CatchHitObject.OBJECT_RADIUS;
- while (caughtObjectContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2)))
+ float adjustedRadius = displayRadius * lenience_adjust;
+ float checkDistance = MathF.Pow(adjustedRadius, 2);
+
+ // offset fruit vertically to better place "above" the plate.
+ position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET;
+
+ while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance))
{
- float diff = (displayRadius + radius_div_2) / allowance;
-
- position.X += (RNG.NextSingle() - 0.5f) * diff * 2;
- position.Y -= RNG.NextSingle() * diff;
+ position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius);
+ position.Y -= RNG.NextSingle(0, 5);
}
- position.X = Math.Clamp(position.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
-
return position;
}
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
index 0e557cb260..9674186039 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
@@ -14,6 +14,11 @@
Properties\AndroidManifest.xml
armeabi-v7a;x86;arm64-v8a
+
+ None
+ cjk;mideast;other;rare;west
+ true
+
@@ -35,5 +40,10 @@
osu.Game
+
+
+ 5.0.0
+
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
index 24f4c6858e..5e99264d7d 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
}
};
- AddBlueprint(new HoldNoteSelectionBlueprint(drawableObject));
+ AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableObject);
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
index aaf96c63a6..8474279b01 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
@@ -184,8 +184,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
- AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
- AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
+ AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
+ AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
}
private void setScrollStep(ScrollingDirection direction)
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs
index 36c34a8fb9..a162c5ec44 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs
@@ -15,7 +15,6 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
-using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
@@ -35,7 +34,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestPlaceBeforeCurrentTimeDownwards()
{
- AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
+ AddStep("move mouse before current time", () =>
+ {
+ var column = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
+ });
AddStep("click", () => InputManager.Click(MouseButton.Left));
@@ -45,7 +48,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestPlaceAfterCurrentTimeDownwards()
{
- AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("move mouse after current time", () =>
+ {
+ var column = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
+ });
AddStep("click", () => InputManager.Click(MouseButton.Left));
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
index 0e47a12a8e..9c3ad0b4ff 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
Child = drawableObject = new DrawableNote(note)
};
- AddBlueprint(new NoteSelectionBlueprint(drawableObject));
+ AddBlueprint(new NoteSelectionBlueprint(note), drawableObject);
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
index 09ca04be8a..6e6500a339 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(2.7646128945056723d, "diffcalc-test")]
+ [TestCase(2.7879104989252959d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new ManiaModDoubleTime());
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
index 96444fd316..b7d7af6b8c 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[SetUp]
public void SetUp() => Schedule(() =>
{
- SetContents(() => new FillFlowContainer
+ SetContents(_ => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
index bde323f187..106b2d188d 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new FillFlowContainer
+ SetContents(_ => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground())
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
{
RelativeSizeAxes = Axes.Both
}
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground())
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
{
RelativeSizeAxes = Axes.Both
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs
index 4392666cb7..215f8fb1d5 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new FillFlowContainer
+ SetContents(_ => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
index dcb25f21ba..75a5495078 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
if (hitWindows.IsHitResultAllowed(result))
{
- AddStep("Show " + result.GetDescription(), () => SetContents(() =>
+ AddStep("Show " + result.GetDescription(), () => SetContents(_ =>
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{
Type = result
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
index 4dc6700786..004793e1e5 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() =>
+ SetContents(_ =>
{
var pool = new DrawablePool(5);
hitExplosionPools.Add(pool);
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
index 7e80419944..7564bd84ad 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new FillFlowContainer
+ SetContents(_ => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 0), _ => new DefaultKeyArea())
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
},
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 1), _ => new DefaultKeyArea())
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
},
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
index 161eda650e..c7dc5fc8b5 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
new StageDefinition { Columns = 2 }
};
- SetContents(() => new ManiaPlayfield(stageDefinitions));
+ SetContents(_ => new ManiaPlayfield(stageDefinitions));
});
}
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
new StageDefinition { Columns = 2 }
};
- SetContents(() => new ManiaPlayfield(stageDefinitions));
+ SetContents(_ => new ManiaPlayfield(stageDefinitions));
});
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
index 37b97a444a..7804261906 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() =>
+ SetContents(_ =>
{
ManiaAction normalAction = ManiaAction.Key1;
ManiaAction specialAction = ManiaAction.Special1;
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
index a15fb392d6..410a43fc73 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
+ SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
_ => new DefaultStageBackground())
{
Anchor = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
index bceee1c599..27e97152bc 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null)
+ SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
index a5248c7712..cffec3dfd5 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Tests
///
/// The number of frames which are generated at the start of a replay regardless of hitobject content.
///
- private const int frame_offset = 1;
+ private const int frame_offset = 0;
[Test]
public void TestSingleNote()
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
+ Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
@@ -54,9 +54,9 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
+ Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+ Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
}
@@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
+ Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
@@ -96,10 +96,10 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
+ Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+ Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
@@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
+ Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time");
Assert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time");
@@ -146,11 +146,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
+ Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
+ Assert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
- Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
+ Assert.AreEqual(4000, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released");
@@ -168,12 +168,12 @@ namespace osu.Game.Rulesets.Mania.Tests
// | | |
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
- beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 - ManiaAutoGenerator.RELEASE_DELAY });
+ beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 });
var generated = new ManiaAutoGenerator(beatmap).Generate();
- Assert.IsTrue(generated.Frames.Count == frame_offset + 3, "Replay must have 3 generated frames");
+ Assert.AreEqual(generated.Frames.Count, frame_offset + 3, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time");
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time");
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
new file mode 100644
index 0000000000..4a6c59e297
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
@@ -0,0 +1,67 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneDrawableManiaHitObject : OsuTestScene
+ {
+ private readonly ManualClock clock = new ManualClock();
+
+ private Column column;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ Child = new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Y,
+ TimeRange = 2000,
+ Clock = new FramedClock(clock),
+ Child = column = new Column(0)
+ {
+ Action = { Value = ManiaAction.Key1 },
+ Height = 0.85f,
+ AccentColour = Color4.Gray
+ },
+ };
+ });
+
+ [Test]
+ public void TestHoldNoteHeadVisibility()
+ {
+ DrawableHoldNote note = null;
+ AddStep("Add hold note", () =>
+ {
+ var h = new HoldNote
+ {
+ StartTime = 0,
+ Duration = 1000
+ };
+ h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ column.Add(note = new DrawableHoldNote(h));
+ });
+ AddStep("Hold key", () =>
+ {
+ clock.CurrentTime = 0;
+ note.OnPressed(ManiaAction.Key1);
+ });
+ AddStep("progress time", () => clock.CurrentTime = 500);
+ AddAssert("head is visible", () => note.Head.Alpha == 1);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 596430f9e5..471dad87d5 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -286,17 +286,83 @@ namespace osu.Game.Rulesets.Mania.Tests
.All(j => j.Type.IsHit()));
}
+ [Test]
+ public void TestHitTailBeforeLastTick()
+ {
+ const int tick_rate = 8;
+ const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate;
+ const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1);
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = time_tail - time_head,
+ Column = 0,
+ }
+ },
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_last_tick - 5)
+ }, beatmap);
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertLastTickJudgement(HitResult.LargeTickMiss);
+ assertTailJudgement(HitResult.Ok);
+ }
+
+ [Test]
+ public void TestZeroLength()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = 1000,
+ Duration = 0,
+ Column = 0,
+ },
+ },
+ BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(beatmap.HitObjects[0].StartTime, ManiaAction.Key1),
+ new ManiaReplayFrame(beatmap.HitObjects[0].GetEndTime() + 1),
+ }, beatmap);
+
+ AddAssert("hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
+ .All(j => j.Type.IsHit()));
+ }
+
private void assertHeadJudgement(HitResult result)
- => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
+ => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result);
private void assertTailJudgement(HitResult result)
- => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result);
+ => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type == result);
private void assertNoteJudgement(HitResult result)
- => AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result);
+ => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type == result);
private void assertTickJudgement(HitResult result)
- => AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick
+ => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Any(j => j.Type == result));
+
+ private void assertLastTickJudgement(HitResult result)
+ => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type == result);
private ScoreAccessibleReplayPlayer currentPlayer;
@@ -345,13 +411,16 @@ namespace osu.Game.Rulesets.Mania.Tests
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
- AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
+
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor?.HasCompleted.Value == true);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+ public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
+
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs
index 0d726e1a50..ea57e51d1c 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public class TestSceneManiaHitObjectSamples : HitObjectSampleTest
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
- protected override IResourceStore Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples)));
+ protected override IResourceStore RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples)));
///
/// Tests that when a normal sample bank is used, the normal hitsound will be looked up.
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
index 6b8f5d5d9d..706268e478 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -97,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}
private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor)
- => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor);
+ => hitObject.Anchor.HasFlagFast(expectedAnchor) && hitObject.Origin.HasFlagFast(expectedAnchor);
private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor)
=> verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor));
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
new file mode 100644
index 0000000000..e14ad92842
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
@@ -0,0 +1,80 @@
+// 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.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Tests.Visual;
+using osu.Framework.Timing;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Configuration;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class TestSceneTimingBasedNoteColouring : OsuTestScene
+ {
+ [Resolved]
+ private RulesetConfigCache configCache { get; set; }
+
+ private readonly Bindable configTimingBasedNoteColouring = new Bindable();
+
+ protected override void LoadComplete()
+ {
+ const double beat_length = 500;
+
+ var ruleset = new ManiaRuleset();
+
+ var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
+ {
+ HitObjects =
+ {
+ new Note { StartTime = 0 },
+ new Note { StartTime = beat_length / 16 },
+ new Note { StartTime = beat_length / 12 },
+ new Note { StartTime = beat_length / 8 },
+ new Note { StartTime = beat_length / 6 },
+ new Note { StartTime = beat_length / 4 },
+ new Note { StartTime = beat_length / 3 },
+ new Note { StartTime = beat_length / 2 },
+ new Note { StartTime = beat_length }
+ },
+ ControlPointInfo = new ControlPointInfo(),
+ BeatmapInfo = { Ruleset = ruleset.RulesetInfo },
+ };
+
+ foreach (var note in beatmap.HitObjects)
+ {
+ note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ }
+
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint
+ {
+ BeatLength = beat_length
+ });
+
+ Child = new Container
+ {
+ Clock = new FramedClock(new ManualClock()),
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new[]
+ {
+ ruleset.CreateDrawableRulesetWith(beatmap)
+ }
+ };
+
+ var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
+ config.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
+
+ AddStep("Enable", () => configTimingBasedNoteColouring.Value = true);
+ AddStep("Disable", () => configTimingBasedNoteColouring.Value = false);
+ }
+ }
+}
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 fcc0cafefc..b2a0912d19 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,8 +2,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 7a0e3b2b76..26393c8edb 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
if (IsForCurrentRuleset)
{
- TargetColumns = (int)Math.Max(1, roundedCircleSize);
+ TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo);
if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{
@@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
originalTargetColumns = TargetColumns;
}
+ public static int GetColumnCountForNonConvert(BeatmapInfo beatmap)
+ {
+ var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize);
+ return (int)Math.Max(1, roundedCircleSize);
+ }
+
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 30d33de06e..26e5d381e2 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
@@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 6.5)
{
- if (convertType.HasFlag(PatternType.LowProbability))
+ if (convertType.HasFlagFast(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
@@ -149,7 +150,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 4)
{
- if (convertType.HasFlag(PatternType.LowProbability))
+ if (convertType.HasFlagFast(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
@@ -157,13 +158,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 2.5)
{
- if (convertType.HasFlag(PatternType.LowProbability))
+ if (convertType.HasFlagFast(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.3, 0, 0);
return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
}
- if (convertType.HasFlag(PatternType.LowProbability))
+ if (convertType.HasFlagFast(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.17, 0, 0);
return generateNRandomNotes(StartTime, 0.27, 0, 0);
@@ -221,7 +222,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
- if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
+ if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
int lastColumn = nextColumn;
@@ -373,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
- bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
+ bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes)
@@ -406,7 +407,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int endTime = startTime + SegmentDuration * SpanCount;
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
- if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
+ if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
for (int i = 0; i < columnRepeat; i++)
@@ -435,7 +436,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
- if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
+ if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
holdColumn = FindAvailableColumn(holdColumn, PreviousPattern);
// Create the hold note
@@ -481,7 +482,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Retrieves the sample info list at a point in time.
///
/// The time to retrieve the sample info list from.
- ///
private IList sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
///
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index bc4ab55767..54c37e9742 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
using osuTK;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -78,7 +79,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
else
convertType |= PatternType.LowProbability;
- if (!convertType.HasFlag(PatternType.KeepSingle))
+ if (!convertType.HasFlagFast(PatternType.KeepSingle))
{
if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8)
convertType |= PatternType.Mirror;
@@ -101,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0;
- if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any())
+ if (convertType.HasFlagFast(PatternType.Reverse) && PreviousPattern.HitObjects.Any())
{
// Generate a new pattern by copying the last hit objects in reverse-column order
for (int i = RandomStart; i < TotalColumns; i++)
@@ -113,11 +114,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern;
}
- if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
- // If we convert to 7K + 1, let's not overload the special key
- && (TotalColumns != 8 || lastColumn != 0)
- // Make sure the last column was not the centre column
- && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
+ if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
+ // If we convert to 7K + 1, let's not overload the special key
+ && (TotalColumns != 8 || lastColumn != 0)
+ // Make sure the last column was not the centre column
+ && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
{
// Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
int column = RandomStart + TotalColumns - lastColumn - 1;
@@ -126,7 +127,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern;
}
- if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any())
+ if (convertType.HasFlagFast(PatternType.ForceStack) && PreviousPattern.HitObjects.Any())
{
// Generate a new pattern by placing on the already filled columns
for (int i = RandomStart; i < TotalColumns; i++)
@@ -140,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (PreviousPattern.HitObjects.Count() == 1)
{
- if (convertType.HasFlag(PatternType.Stair))
+ if (convertType.HasFlagFast(PatternType.Stair))
{
// Generate a new pattern by placing on the next column, cycling back to the start if there is no "next"
int targetColumn = lastColumn + 1;
@@ -151,7 +152,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern;
}
- if (convertType.HasFlag(PatternType.ReverseStair))
+ if (convertType.HasFlagFast(PatternType.ReverseStair))
{
// Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous"
int targetColumn = lastColumn - 1;
@@ -163,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
}
- if (convertType.HasFlag(PatternType.KeepSingle))
+ if (convertType.HasFlagFast(PatternType.KeepSingle))
return generateRandomNotes(1);
- if (convertType.HasFlag(PatternType.Mirror))
+ if (convertType.HasFlagFast(PatternType.Mirror))
{
if (ConversionDifficulty > 6.5)
return generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
@@ -178,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 6.5)
{
- if (convertType.HasFlag(PatternType.LowProbability))
+ if (convertType.HasFlagFast(PatternType.LowProbability))
return generateRandomPattern(0.78, 0.42, 0, 0);
return generateRandomPattern(1, 0.62, 0, 0);
@@ -186,7 +187,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 4)
{
- if (convertType.HasFlag(PatternType.LowProbability))
+ if (convertType.HasFlagFast(PatternType.LowProbability))
return generateRandomPattern(0.35, 0.08, 0, 0);
return generateRandomPattern(0.52, 0.15, 0, 0);
@@ -194,7 +195,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 2)
{
- if (convertType.HasFlag(PatternType.LowProbability))
+ if (convertType.HasFlagFast(PatternType.LowProbability))
return generateRandomPattern(0.18, 0, 0, 0);
return generateRandomPattern(0.45, 0, 0, 0);
@@ -207,9 +208,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in p.HitObjects)
{
- if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
+ if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1)
StairType = PatternType.ReverseStair;
- if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
+ if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart)
StairType = PatternType.Stair;
}
@@ -229,7 +230,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
var pattern = new Pattern();
- bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack);
+ bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack);
if (!allowStacking)
noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects);
@@ -249,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int getNextColumn(int last)
{
- if (convertType.HasFlag(PatternType.Gathered))
+ if (convertType.HasFlagFast(PatternType.Gathered))
{
last++;
if (last == TotalColumns)
@@ -296,7 +297,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The containing the hit objects.
private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3)
{
- if (convertType.HasFlag(PatternType.ForceNotStack))
+ if (convertType.HasFlagFast(PatternType.ForceNotStack))
return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
var pattern = new Pattern();
diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
index 756f2b7b2f..ac8168dfc9 100644
--- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
@@ -20,8 +20,9 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
- Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
- Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
+ SetDefault(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
+ SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
+ SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
@@ -34,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaRulesetSetting
{
ScrollTime,
- ScrollDirection
+ ScrollDirection,
+ TimingBasedNoteColouring
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
index d6ea58ee78..2ba2ee6b4a 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -7,11 +7,10 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
- public class Strain : Skill
+ public class Strain : StrainSkill
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
@@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
- var endTime = maniaCurrent.BaseObject.GetEndTime();
+ var endTime = maniaCurrent.EndTime;
var column = maniaCurrent.BaseObject.Column;
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
@@ -46,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
for (int i = 0; i < holdEndTimes.Length; ++i)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
- if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
+ if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
@@ -73,8 +72,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
}
protected override double GetPeakStrain(double offset)
- => applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base)
- + applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base);
+ => applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base)
+ + applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base);
private double applyDecay(double value, double deltaTime, double decayBase)
=> value * Math.Pow(decayBase, deltaTime / 1000);
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs
similarity index 61%
rename from osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
rename to osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs
index 4e73883de0..6933571be8 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs
@@ -2,34 +2,35 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
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
+ public class HoldNoteNoteOverlay : CompositeDrawable
{
- protected new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
-
+ private readonly HoldNoteSelectionBlueprint holdNoteBlueprint;
private readonly HoldNotePosition position;
- public HoldNoteNoteSelectionBlueprint(DrawableHoldNote holdNote, HoldNotePosition position)
- : base(holdNote)
+ public HoldNoteNoteOverlay(HoldNoteSelectionBlueprint holdNoteBlueprint, HoldNotePosition position)
{
+ this.holdNoteBlueprint = holdNoteBlueprint;
this.position = position;
- InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
- Select();
+ InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
}
protected override void Update()
{
base.Update();
+ var drawableObject = holdNoteBlueprint.DrawableObject;
+
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
- if (DrawableObject.IsLoaded)
+ if (drawableObject.IsLoaded)
{
- DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)DrawableObject.Head : DrawableObject.Tail;
+ DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)drawableObject.Head : drawableObject.Tail;
Anchor = note.Anchor;
Origin = note.Origin;
@@ -38,8 +39,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
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 1f92929392..093a8da24f 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return;
base.OnMouseUp(e);
- EndPlacement(true);
+ EndPlacement(HitObject.Duration > 0);
}
private double originalStartTime;
@@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.UpdateTimeAndPosition(result);
- if (PlacementActive)
+ if (PlacementActive == PlacementState.Active)
{
if (result.Time is double endTime)
{
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index 1737c4d2e5..d04c5cd4aa 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -8,13 +8,14 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
+ public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
{
public new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private OsuColour colours { get; set; }
- public HoldNoteSelectionBlueprint(DrawableHoldNote hold)
+ public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold)
{
}
@@ -32,16 +33,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
InternalChildren = new Drawable[]
{
- new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.Start),
- new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.End),
+ new HoldNoteNoteOverlay(this, HoldNotePosition.Start),
+ new HoldNoteNoteOverlay(this, HoldNotePosition.End),
new Container
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 5e09054667..8f25668dd0 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.UpdateTimeAndPosition(result);
- if (!PlacementActive)
+ if (PlacementActive == PlacementState.Waiting)
Column = result.Playfield as Column;
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index 384f49d9b2..e744bd3c83 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -4,22 +4,23 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint
+ public abstract class ManiaSelectionBlueprint : HitObjectSelectionBlueprint
+ where T : ManiaHitObject
{
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
- protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
- : base(drawableObject)
+ protected ManiaSelectionBlueprint(T hitObject)
+ : base(hitObject)
{
RelativeSizeAxes = Axes.None;
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
index 2bff33c4cf..e2b6ee0048 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
@@ -3,13 +3,13 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public class NoteSelectionBlueprint : ManiaSelectionBlueprint
+ public class NoteSelectionBlueprint : ManiaSelectionBlueprint
{
- public NoteSelectionBlueprint(DrawableNote note)
+ public NoteSelectionBlueprint(Note note)
: base(note)
{
AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X });
diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs
similarity index 78%
rename from osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
rename to osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs
index 445df79f6f..b0af8c503b 100644
--- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
+++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs
@@ -12,16 +12,16 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Edit
{
- public class DrawableManiaEditRuleset : DrawableManiaRuleset
+ public class DrawableManiaEditorRuleset : DrawableManiaRuleset
{
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
- public DrawableManiaEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods)
+ public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
}
- protected override Playfield CreatePlayfield() => new ManiaEditPlayfield(Beatmap.Stages)
+ protected override Playfield CreatePlayfield() => new ManiaEditorPlayfield(Beatmap.Stages)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
index afc08dcc96..9d1f5429a1 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
@@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Edit
foreach (var line in grid.Objects.OfType())
availableLines.Push(line);
- grid.Clear(false);
+ grid.Clear();
}
if (selectionTimeRange == null)
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
index 2fa3f378ff..c5a109a6d1 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
@@ -3,8 +3,8 @@
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Mania.Edit
@@ -16,20 +16,20 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
- public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
+ public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
{
switch (hitObject)
{
- case DrawableNote note:
+ case Note note:
return new NoteSelectionBlueprint(note);
- case DrawableHoldNote holdNote:
+ case HoldNote holdNote:
return new HoldNoteSelectionBlueprint(holdNote);
}
- return base.CreateBlueprintFor(hitObject);
+ return base.CreateHitObjectBlueprintFor(hitObject);
}
- protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
+ protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditPlayfield.cs b/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs
similarity index 75%
rename from osu.Game.Rulesets.Mania/Edit/ManiaEditPlayfield.cs
rename to osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs
index a42f793a77..186d50716e 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaEditPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs
@@ -7,9 +7,9 @@ using System.Collections.Generic;
namespace osu.Game.Rulesets.Mania.Edit
{
- public class ManiaEditPlayfield : ManiaPlayfield
+ public class ManiaEditorPlayfield : ManiaPlayfield
{
- public ManiaEditPlayfield(List stages)
+ public ManiaEditorPlayfield(List stages)
: base(stages)
{
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index 324670c4b2..2baec95c94 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaHitObjectComposer : HitObjectComposer
{
- private DrawableManiaEditRuleset drawableRuleset;
+ private DrawableManiaEditorRuleset drawableRuleset;
private ManiaBeatSnapGrid beatSnapGrid;
private InputManager inputManager;
@@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
{
- drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods);
+ drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
// This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it
dependencies.CacheAs(drawableRuleset.ScrollingInfo);
@@ -119,5 +119,8 @@ namespace osu.Game.Rulesets.Mania.Edit
beatSnapGrid.SelectionTimeRange = null;
}
}
+
+ public override string ConvertSelectionToString()
+ => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 50629f41a9..dc858fb54f 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -5,14 +5,14 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Mania.Edit
{
- public class ManiaSelectionHandler : SelectionHandler
+ public class ManiaSelectionHandler : EditorSelectionHandler
{
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
@@ -20,21 +20,21 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved]
private HitObjectComposer composer { get; set; }
- public override bool HandleMovement(MoveSelectionEvent moveEvent)
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
- var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
- int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
+ var hitObjectBlueprint = (HitObjectSelectionBlueprint)moveEvent.Blueprint;
+ int lastColumn = ((ManiaHitObject)hitObjectBlueprint.Item).Column;
performColumnMovement(lastColumn, moveEvent);
return true;
}
- private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
+ private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
- var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.ScreenSpacePosition);
+ var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.Blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
if (currentColumn == null)
return;
@@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit
int minColumn = int.MaxValue;
int maxColumn = int.MinValue;
+ // find min/max in an initial pass before actually performing the movement.
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType())
{
if (obj.Column < minColumn)
@@ -55,8 +56,12 @@ namespace osu.Game.Rulesets.Mania.Edit
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
- foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType())
- obj.Column += columnDelta;
+ EditorBeatmap.PerformOnSelection(h =>
+ {
+ maniaPlayfield.Remove(h);
+ ((ManiaHitObject)h).Column += columnDelta;
+ maniaPlayfield.Add(h);
+ });
}
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
new file mode 100644
index 0000000000..d9a278ef29
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Filter;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Filter;
+
+namespace osu.Game.Rulesets.Mania
+{
+ public class ManiaFilterCriteria : IRulesetFilterCriteria
+ {
+ private FilterCriteria.OptionalRange keys;
+
+ public bool Matches(BeatmapInfo beatmap)
+ {
+ return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap)));
+ }
+
+ public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
+ {
+ switch (key)
+ {
+ case "key":
+ case "keys":
+ return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value);
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 4c729fef83..fbb9b3c466 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
@@ -21,6 +22,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Difficulty;
@@ -45,7 +47,7 @@ namespace osu.Game.Rulesets.Mania
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
- public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.2);
+ public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.5);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
@@ -59,76 +61,76 @@ namespace osu.Game.Rulesets.Mania
public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
- if (mods.HasFlag(LegacyMods.Nightcore))
+ if (mods.HasFlagFast(LegacyMods.Nightcore))
yield return new ManiaModNightcore();
- else if (mods.HasFlag(LegacyMods.DoubleTime))
+ else if (mods.HasFlagFast(LegacyMods.DoubleTime))
yield return new ManiaModDoubleTime();
- if (mods.HasFlag(LegacyMods.Perfect))
+ if (mods.HasFlagFast(LegacyMods.Perfect))
yield return new ManiaModPerfect();
- else if (mods.HasFlag(LegacyMods.SuddenDeath))
+ else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
yield return new ManiaModSuddenDeath();
- if (mods.HasFlag(LegacyMods.Cinema))
+ if (mods.HasFlagFast(LegacyMods.Cinema))
yield return new ManiaModCinema();
- else if (mods.HasFlag(LegacyMods.Autoplay))
+ else if (mods.HasFlagFast(LegacyMods.Autoplay))
yield return new ManiaModAutoplay();
- if (mods.HasFlag(LegacyMods.Easy))
+ if (mods.HasFlagFast(LegacyMods.Easy))
yield return new ManiaModEasy();
- if (mods.HasFlag(LegacyMods.FadeIn))
+ if (mods.HasFlagFast(LegacyMods.FadeIn))
yield return new ManiaModFadeIn();
- if (mods.HasFlag(LegacyMods.Flashlight))
+ if (mods.HasFlagFast(LegacyMods.Flashlight))
yield return new ManiaModFlashlight();
- if (mods.HasFlag(LegacyMods.HalfTime))
+ if (mods.HasFlagFast(LegacyMods.HalfTime))
yield return new ManiaModHalfTime();
- if (mods.HasFlag(LegacyMods.HardRock))
+ if (mods.HasFlagFast(LegacyMods.HardRock))
yield return new ManiaModHardRock();
- if (mods.HasFlag(LegacyMods.Hidden))
+ if (mods.HasFlagFast(LegacyMods.Hidden))
yield return new ManiaModHidden();
- if (mods.HasFlag(LegacyMods.Key1))
+ if (mods.HasFlagFast(LegacyMods.Key1))
yield return new ManiaModKey1();
- if (mods.HasFlag(LegacyMods.Key2))
+ if (mods.HasFlagFast(LegacyMods.Key2))
yield return new ManiaModKey2();
- if (mods.HasFlag(LegacyMods.Key3))
+ if (mods.HasFlagFast(LegacyMods.Key3))
yield return new ManiaModKey3();
- if (mods.HasFlag(LegacyMods.Key4))
+ if (mods.HasFlagFast(LegacyMods.Key4))
yield return new ManiaModKey4();
- if (mods.HasFlag(LegacyMods.Key5))
+ if (mods.HasFlagFast(LegacyMods.Key5))
yield return new ManiaModKey5();
- if (mods.HasFlag(LegacyMods.Key6))
+ if (mods.HasFlagFast(LegacyMods.Key6))
yield return new ManiaModKey6();
- if (mods.HasFlag(LegacyMods.Key7))
+ if (mods.HasFlagFast(LegacyMods.Key7))
yield return new ManiaModKey7();
- if (mods.HasFlag(LegacyMods.Key8))
+ if (mods.HasFlagFast(LegacyMods.Key8))
yield return new ManiaModKey8();
- if (mods.HasFlag(LegacyMods.Key9))
+ if (mods.HasFlagFast(LegacyMods.Key9))
yield return new ManiaModKey9();
- if (mods.HasFlag(LegacyMods.KeyCoop))
+ if (mods.HasFlagFast(LegacyMods.KeyCoop))
yield return new ManiaModDualStages();
- if (mods.HasFlag(LegacyMods.NoFail))
+ if (mods.HasFlagFast(LegacyMods.NoFail))
yield return new ManiaModNoFail();
- if (mods.HasFlag(LegacyMods.Random))
+ if (mods.HasFlagFast(LegacyMods.Random))
yield return new ManiaModRandom();
- if (mods.HasFlag(LegacyMods.Mirror))
+ if (mods.HasFlagFast(LegacyMods.Mirror))
yield return new ManiaModMirror();
}
@@ -237,6 +239,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModDualStages(),
new ManiaModMirror(),
new ManiaModDifficultyAdjust(),
+ new ManiaModClassic(),
new ManiaModInvert(),
new ManiaModConstantSpeed()
};
@@ -381,6 +384,11 @@ namespace osu.Game.Rulesets.Mania
}
}
};
+
+ public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
+ {
+ return new ManiaFilterCriteria();
+ }
}
public enum PlayfieldType
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index de77af8306..1c89d9cd00 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -37,6 +37,11 @@ namespace osu.Game.Rulesets.Mania
Current = config.GetBindable(ManiaRulesetSetting.ScrollTime),
KeyboardStep = 5
},
+ new SettingsCheckbox
+ {
+ LabelText = "Timing-based note colouring",
+ Current = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring),
+ }
};
}
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
index f078345fc1..9aebf51576 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
@@ -9,12 +9,6 @@ namespace osu.Game.Rulesets.Mania
{
public class ManiaSkinComponent : GameplaySkinComponent
{
- ///
- /// The intended index for this component.
- /// May be null if the component does not exist in a .
- ///
- public readonly int? TargetColumn;
-
///
/// The intended for this component.
/// May be null if the component is not a direct member of a .
@@ -25,12 +19,10 @@ namespace osu.Game.Rulesets.Mania
/// Creates a new .
///
/// The component.
- /// The intended index for this component. May be null if the component does not exist in a .
/// The intended for this component. May be null if the component is not a direct member of a .
- public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null)
+ public ManiaSkinComponent(ManiaSkinComponents component, StageDefinition? stageDefinition = null)
: base(component)
{
- TargetColumn = targetColumn;
StageDefinition = stageDefinition;
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs
new file mode 100644
index 0000000000..073dda9de8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.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.
+
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Mania.Mods
+{
+ public class ManiaModClassic : ModClassic
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
index cbdcd49c5b..f80c9e1f7c 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
@@ -1,18 +1,20 @@
// 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.Sprites;
-using osu.Game.Graphics;
+using System;
+using System.Linq;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModFadeIn : ManiaModHidden
+ public class ManiaModFadeIn : ManiaModPlayfieldCover
{
public override string Name => "Fade In";
public override string Acronym => "FI";
- public override IconUsage? Icon => OsuIcon.ModHidden;
public override string Description => @"Keys appear out of nowhere!";
+ public override double ScoreMultiplier => 1;
+
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
index 4bdb15526f..e3ac624a6e 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
@@ -3,43 +3,17 @@
using System;
using System.Linq;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModHidden : ModHidden, IApplicableToDrawableRuleset
+ public class ManiaModHidden : ManiaModPlayfieldCover
{
public override string Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
- ///
- /// The direction in which the cover should expand.
- ///
- protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
- public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
- {
- ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
-
- foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
- {
- HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
- Container hocParent = (Container)hoc.Parent;
-
- hocParent.Remove(hoc);
- hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
- {
- c.RelativeSizeAxes = Axes.Both;
- c.Direction = ExpandDirection;
- c.Coverage = 0.5f;
- }));
- }
- }
+ protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
index 485595cea9..12f379bddb 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Mirror";
public override string Acronym => "MR";
public override ModType Type => ModType.Conversion;
+ public override string Description => "Notes are flipped horizontally.";
public override double ScoreMultiplier => 1;
public override bool Ranked => true;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
new file mode 100644
index 0000000000..3c24e91d54
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
@@ -0,0 +1,52 @@
+// 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;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Mania.Mods
+{
+ public abstract class ManiaModPlayfieldCover : ModHidden, IApplicableToDrawableRuleset
+ {
+ public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
+
+ ///
+ /// The direction in which the cover should expand.
+ ///
+ protected abstract CoverExpandDirection ExpandDirection { get; }
+
+ public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
+
+ foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
+ {
+ HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
+ Container hocParent = (Container)hoc.Parent;
+
+ hocParent.Remove(hoc);
+ hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
+ {
+ c.RelativeSizeAxes = Axes.Both;
+ c.Direction = ExpandDirection;
+ c.Coverage = 0.5f;
+ }));
+ }
+ }
+
+ protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
+ {
+ }
+
+ protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 4f062753a6..d1310d42eb 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -2,6 +2,7 @@
// 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;
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -29,21 +31,21 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public DrawableHoldNoteHead Head => headContainer.Child;
public DrawableHoldNoteTail Tail => tailContainer.Child;
- private readonly Container headContainer;
- private readonly Container tailContainer;
- private readonly Container tickContainer;
+ private Container headContainer;
+ private Container tailContainer;
+ private Container tickContainer;
///
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
///
- private readonly Container sizingContainer;
+ private Container sizingContainer;
///
/// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of .
///
- private readonly Container maskingContainer;
+ private Container maskingContainer;
- private readonly SkinnableDrawable bodyPiece;
+ private SkinnableDrawable bodyPiece;
///
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
@@ -60,11 +62,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
private double? releaseTime;
+ public DrawableHoldNote()
+ : this(null)
+ {
+ }
+
public DrawableHoldNote(HoldNote hitObject)
: base(hitObject)
{
- RelativeSizeAxes = Axes.X;
+ }
+ [BackgroundDependencyLoader]
+ private void load()
+ {
Container maskedContents;
AddRangeInternal(new Drawable[]
@@ -86,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
headContainer = new Container { RelativeSizeAxes = Axes.Both }
}
},
- bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
+ bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece
{
RelativeSizeAxes = Axes.Both,
})
@@ -105,6 +115,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
});
}
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ sizingContainer.Size = Vector2.One;
+ HoldStartTime = null;
+ HoldBrokenTime = null;
+ releaseTime = null;
+ }
+
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
@@ -128,37 +148,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
- headContainer.Clear();
- tailContainer.Clear();
- tickContainer.Clear();
+ headContainer.Clear(false);
+ tailContainer.Clear(false);
+ tickContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
- case TailNote _:
- return new DrawableHoldNoteTail(this)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AccentColour = { BindTarget = AccentColour }
- };
+ case TailNote tail:
+ return new DrawableHoldNoteTail(tail);
- case Note _:
- return new DrawableHoldNoteHead(this)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AccentColour = { BindTarget = AccentColour }
- };
+ case HeadNote head:
+ return new DrawableHoldNoteHead(head);
case HoldNoteTick tick:
- return new DrawableHoldNoteTick(tick)
- {
- HoldStartTime = () => HoldStartTime,
- AccentColour = { BindTarget = AccentColour }
- };
+ return new DrawableHoldNoteTick(tick);
}
return base.CreateNestedHitObject(hitObject);
@@ -221,7 +227,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// As the note is being held, adjust the size of the sizing container. This has two effects:
// 1. The contained masking container will mask the body and ticks.
// 2. The head note will move along with the new "head position" in the container.
- if (Head.IsHit && releaseTime == null)
+ if (Head.IsHit && releaseTime == null && DrawHeight > 0)
{
// How far past the hit target this hold note is. Always a positive value.
float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y);
@@ -233,6 +239,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
if (Tail.AllJudged)
{
+ foreach (var tick in tickContainer)
+ {
+ if (!tick.Judged)
+ tick.MissForcefully();
+ }
+
ApplyResult(r => r.Type = r.Judgement.MaxResult);
endHold();
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index 75dcf0e55e..be600f0d47 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -1,6 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
///
@@ -10,11 +13,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead;
- public DrawableHoldNoteHead(DrawableHoldNote holdNote)
- : base(holdNote.HitObject.Head)
+ public DrawableHoldNoteHead()
+ : this(null)
{
}
+ public DrawableHoldNoteHead(HeadNote headNote)
+ : base(headNote)
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+ }
+
public void UpdateResult() => base.UpdateResult(true);
protected override void UpdateInitialTransforms()
@@ -25,6 +35,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
LifetimeEnd = LifetimeStart + 30000;
}
+ protected override void UpdateHitStateTransforms(ArmedState state)
+ {
+ // suppress the base call explicitly.
+ // the hold note head should never change its visual state on its own due to the "freezing" mechanic
+ // (when hit, it remains visible in place at the judgement line; when dropped, it will scroll past the line).
+ // it will be hidden along with its parenting hold note when required.
+ }
+
public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
public override void OnReleased(ManiaAction action)
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index 3a00933e4d..18aa3f66d4 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
+using osu.Framework.Graphics;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
@@ -20,12 +21,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
- private readonly DrawableHoldNote holdNote;
+ protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
- public DrawableHoldNoteTail(DrawableHoldNote holdNote)
- : base(holdNote.HitObject.Tail)
+ public DrawableHoldNoteTail()
+ : this(null)
{
- this.holdNote = holdNote;
+ }
+
+ public DrawableHoldNoteTail(TailNote tailNote)
+ : base(tailNote)
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
}
public void UpdateResult() => base.UpdateResult(true);
@@ -54,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r =>
{
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
- if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null))
+ if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null))
result = HitResult.Meh;
r.Type = result;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
index 98931dceed..f040dad135 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
@@ -2,7 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osuTK;
+using System.Diagnostics;
+using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -19,38 +20,48 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
/// References the time at which the user started holding the hold note.
///
- public Func HoldStartTime;
+ private Func holdStartTime;
+
+ private Container glowContainer;
+
+ public DrawableHoldNoteTick()
+ : this(null)
+ {
+ }
public DrawableHoldNoteTick(HoldNoteTick hitObject)
: base(hitObject)
{
- Container glowContainer;
-
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
RelativeSizeAxes = Axes.X;
- Size = new Vector2(1);
+ }
- AddRangeInternal(new[]
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(glowContainer = new CircularContainer
{
- glowContainer = new CircularContainer
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Children = new[]
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Children = new[]
+ new Box
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true
- }
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
}
}
});
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
AccentColour.BindValueChanged(colour =>
{
@@ -64,12 +75,29 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}, true);
}
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ Debug.Assert(ParentHitObject != null);
+
+ var holdNote = (DrawableHoldNote)ParentHitObject;
+ holdStartTime = () => holdNote.HoldStartTime;
+ }
+
+ protected override void OnFree()
+ {
+ base.OnFree();
+
+ holdStartTime = null;
+ }
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (Time.Current < HitObject.StartTime)
return;
- var startTime = HoldStartTime?.Invoke();
+ var startTime = holdStartTime?.Invoke();
if (startTime == null || startTime > HitObject.StartTime)
ApplyResult(r => r.Type = r.Judgement.MinResult);
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 1550faee50..380ab35339 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -6,6 +6,7 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI;
@@ -24,6 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
[Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; }
+ ///
+ /// Gets the samples that are played by this object during gameplay.
+ ///
+ public ISampleInfo[] GetGameplaySamples() => Samples.Samples;
+
protected override float SamplePlaybackPosition
{
get
@@ -44,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected DrawableManiaHitObject(ManiaHitObject hitObject)
: base(hitObject)
{
+ RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader(true)]
@@ -53,9 +60,31 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Action.BindTo(action);
Direction.BindTo(scrollingInfo.Direction);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
Direction.BindValueChanged(OnDirectionChanged, true);
}
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ if (ParentHitObject != null)
+ AccentColour.BindTo(ParentHitObject.AccentColour);
+ }
+
+ protected override void OnFree()
+ {
+ base.OnFree();
+
+ if (ParentHitObject != null)
+ AccentColour.UnbindFrom(ParentHitObject.AccentColour);
+ }
+
private double computedLifetimeStart;
public override double LifetimeStart
@@ -141,12 +170,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public abstract class DrawableManiaHitObject : DrawableManiaHitObject
where TObject : ManiaHitObject
{
- public new readonly TObject HitObject;
+ public new TObject HitObject => (TObject)base.HitObject;
protected DrawableManiaHitObject(TObject hitObject)
: base(hitObject)
{
- HitObject = hitObject;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index b512986ccb..33d872dfb6 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -2,13 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
using osu.Game.Skinning;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -17,23 +23,49 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public class DrawableNote : DrawableManiaHitObject, IKeyBindingHandler
{
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ [Resolved(canBeNull: true)]
+ private IBeatmap beatmap { get; set; }
+
+ private readonly Bindable configTimingBasedNoteColouring = new Bindable();
+
protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
- private readonly Drawable headPiece;
+ private Drawable headPiece;
+
+ public DrawableNote()
+ : this(null)
+ {
+ }
public DrawableNote(Note hitObject)
: base(hitObject)
{
- RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
+ }
- AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component, hitObject.Column), _ => new DefaultNotePiece())
+ [BackgroundDependencyLoader(true)]
+ private void load(ManiaRulesetConfigManager rulesetConfig)
+ {
+ rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
+
+ AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component), _ => new DefaultNotePiece())
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
});
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour());
+ StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true);
+ }
+
protected override void OnDirectionChanged(ValueChangedEvent e)
{
base.OnDirectionChanged(e);
@@ -73,5 +105,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public virtual void OnReleased(ManiaAction action)
{
}
+
+ private void updateSnapColour()
+ {
+ if (beatmap == null || HitObject == null) return;
+
+ int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime);
+
+ Colour = configTimingBasedNoteColouring.Value ? BindableBeatDivisor.GetColourFor(snapDivisor, colours) : Color4.White;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
new file mode 100644
index 0000000000..e69cc62aed
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
@@ -0,0 +1,9 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Mania.Objects
+{
+ public class HeadNote : Note
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index 6cc7ff92d3..43e876b7aa 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Objects
///
/// The head note of the hold.
///
- public Note Head { get; private set; }
+ public HeadNote Head { get; private set; }
///
/// The tail note of the hold.
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Objects
createTicks(cancellationToken);
- AddNested(Head = new Note
+ AddNested(Head = new HeadNote
{
StartTime = StartTime,
Column = Column,
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index 27bf50493d..6289744df1 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
-using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index 3ebbe5af8e..517b708691 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -3,14 +3,14 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Mania.Replays
{
- internal class ManiaAutoGenerator : AutoGenerator
+ internal class ManiaAutoGenerator : AutoGenerator
{
public const double RELEASE_DELAY = 20;
@@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Mania.Replays
public ManiaAutoGenerator(ManiaBeatmap beatmap)
: base(beatmap)
{
- Replay = new Replay();
-
columnActions = new ManiaAction[Beatmap.TotalColumns];
var normalAction = ManiaAction.Key1;
@@ -42,12 +40,10 @@ namespace osu.Game.Rulesets.Mania.Replays
}
}
- protected Replay Replay;
-
- public override Replay Generate()
+ protected override void GenerateFrames()
{
if (Beatmap.HitObjects.Count == 0)
- return Replay;
+ return;
var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time);
@@ -69,14 +65,8 @@ namespace osu.Game.Rulesets.Mania.Replays
}
}
- // todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
- if (Replay.Frames.Count == 0)
- Replay.Frames.Add(new ManiaReplayFrame(group.First().Time - 1));
-
- Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray()));
+ Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray()));
}
-
- return Replay;
}
private IEnumerable generateActionPoints()
@@ -85,20 +75,28 @@ namespace osu.Game.Rulesets.Mania.Replays
{
var currentObject = Beatmap.HitObjects[i];
var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button
-
- double endTime = currentObject.GetEndTime();
-
- bool canDelayKeyUp = nextObjectInColumn == null ||
- nextObjectInColumn.StartTime > endTime + RELEASE_DELAY;
-
- double calculatedDelay = canDelayKeyUp ? RELEASE_DELAY : (nextObjectInColumn.StartTime - endTime) * 0.9;
+ var releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn);
yield return new HitPoint { Time = currentObject.StartTime, Column = currentObject.Column };
- yield return new ReleasePoint { Time = endTime + calculatedDelay, Column = currentObject.Column };
+ yield return new ReleasePoint { Time = releaseTime, Column = currentObject.Column };
}
}
+ private double calculateReleaseTime(HitObject currentObject, HitObject nextObject)
+ {
+ double endTime = currentObject.GetEndTime();
+
+ if (currentObject is HoldNote)
+ // hold note releases must be timed exactly.
+ return endTime;
+
+ bool canDelayKeyUpFully = nextObject == null ||
+ nextObject.StartTime > endTime + RELEASE_DELAY;
+
+ return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.StartTime - endTime) * 0.9);
+ }
+
protected override HitObject GetNextObject(int currentIndex)
{
int desiredColumn = Beatmap.HitObjects[currentIndex].Column;
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 71cc0bdf1f..48b377c794 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -7,8 +7,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor
{
- protected override double DefaultAccuracyPortion => 0.95;
+ protected override double DefaultAccuracyPortion => 0.99;
- protected override double DefaultComboPortion => 0.05;
+ protected override double DefaultComboPortion => 0.01;
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
index 73aece1ed4..e4d466dca5 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion
{
+ public const double FADE_IN_DURATION = 80;
+
private readonly IBindable direction = new Bindable();
private Drawable explosion;
@@ -72,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
(explosion as IFramedAnimation)?.GotoFrame(0);
- explosion?.FadeInFromZero(80)
+ explosion?.FadeInFromZero(FADE_IN_DURATION)
.Then().FadeOut(120);
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
index 78ccb83a8c..10319a7d4d 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
@@ -101,8 +101,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
if (action == column.Action.Value)
{
- upSprite.FadeTo(1);
- downSprite.FadeTo(0);
+ upSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(1);
+ downSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(0);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index cbbbacfe19..261b8b1fad 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
break;
}
- return null;
+ return Source.GetDrawableComponent(component);
}
private Drawable getResult(HitResult result)
@@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return animation == null ? null : new LegacyManiaJudgementPiece(result, animation);
}
- public override Sample GetSample(ISampleInfo sampleInfo)
+ public override ISample GetSample(ISampleInfo sampleInfo)
{
// layered hit sounds never play in mania
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index d2a9b69b60..9b5893b268 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.UI
@@ -27,6 +28,12 @@ namespace osu.Game.Rulesets.Mania.UI
public const float COLUMN_WIDTH = 80;
public const float SPECIAL_COLUMN_WIDTH = 70;
+ ///
+ /// For hitsounds played by this (i.e. not as a result of hitting a hitobject),
+ /// a certain number of samples are allowed to be played concurrently so that it feels better when spam-pressing the key.
+ ///
+ private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY;
+
///
/// The index of this column as part of the whole playfield.
///
@@ -38,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
private readonly DrawablePool hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy;
+ private readonly Container hitSounds;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
@@ -48,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y;
Width = COLUMN_WIDTH;
- Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground())
+ Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
{
RelativeSizeAxes = Axes.Both
};
@@ -59,17 +67,36 @@ namespace osu.Game.Rulesets.Mania.UI
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea())
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
},
background,
+ hitSounds = new Container
+ {
+ Name = "Column samples pool",
+ RelativeSizeAxes = Axes.Both,
+ Children = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray()
+ },
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
+
+ RegisterPool(10, 50);
+ RegisterPool(10, 50);
+ RegisterPool(10, 50);
+ RegisterPool(10, 50);
+ RegisterPool(50, 250);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ NewResult += OnNewResult;
}
public ColumnType ColumnType { get; set; }
@@ -85,28 +112,14 @@ namespace osu.Game.Rulesets.Mania.UI
return dependencies;
}
- ///
- /// Adds a DrawableHitObject to this Playfield.
- ///
- /// The DrawableHitObject to add.
- public override void Add(DrawableHitObject hitObject)
+ protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject)
{
- hitObject.AccentColour.Value = AccentColour;
- hitObject.OnNewResult += OnNewResult;
+ base.OnNewDrawableHitObject(drawableHitObject);
- DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
+ DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)drawableHitObject;
+
+ maniaObject.AccentColour.Value = AccentColour;
maniaObject.CheckHittable = hitPolicy.IsHittable;
-
- base.Add(hitObject);
- }
-
- public override bool Remove(DrawableHitObject h)
- {
- if (!base.Remove(h))
- return false;
-
- h.OnNewResult -= OnNewResult;
- return true;
}
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
@@ -120,6 +133,8 @@ namespace osu.Game.Rulesets.Mania.UI
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
}
+ private int nextHitSoundIndex;
+
public bool OnPressed(ManiaAction action)
{
if (action != Action.Value)
@@ -131,7 +146,15 @@ namespace osu.Game.Rulesets.Mania.UI
HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
HitObjectContainer.Objects.LastOrDefault();
- nextObject?.PlaySamples();
+ if (nextObject is DrawableManiaHitObject maniaObject)
+ {
+ var hitSound = hitSounds[nextHitSoundIndex];
+
+ hitSound.Samples = maniaObject.GetGameplaySamples();
+ hitSound.Play();
+
+ nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds;
+ }
return true;
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
index b365ae45a9..f69d2aafdc 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
RelativeSizeAxes = Axes.Both,
Depth = 2,
},
- hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget())
+ hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
{
RelativeSizeAxes = Axes.X,
Depth = 1
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 4ee060e91e..e497646a13 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -18,7 +18,6 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -134,20 +133,7 @@ namespace osu.Game.Rulesets.Mania.UI
protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
- public override DrawableHitObject CreateDrawableRepresentation(ManiaHitObject h)
- {
- switch (h)
- {
- case HoldNote holdNote:
- return new DrawableHoldNote(holdNote);
-
- case Note note:
- return new DrawableNote(note);
-
- default:
- return null;
- }
- }
+ public override DrawableHitObject CreateDrawableRepresentation(ManiaHitObject h) => null;
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 271e432e8d..8830c440c0 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -9,6 +9,7 @@ using System.Linq;
using osu.Framework.Allocation;
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;
@@ -56,6 +57,10 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
+ public override void Add(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Add(hitObject);
+
+ public override bool Remove(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Remove(hitObject);
+
public override void Add(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Add(h);
public override bool Remove(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Remove(h);
diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
index 64b7d7d550..90d3c6c4c7 100644
--- a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.UI
[BackgroundDependencyLoader]
private void load()
{
- InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), _ => new DefaultHitExplosion())
+ InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), _ => new DefaultHitExplosion())
{
RelativeSizeAxes = Axes.Both
};
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index dc34bffab1..8c703e7a8a 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI.Components;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -132,33 +133,19 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
- public override void Add(DrawableHitObject h)
+ protected override void LoadComplete()
{
- var maniaObject = (ManiaHitObject)h.HitObject;
-
- int columnIndex = -1;
-
- maniaObject.ColumnBindable.BindValueChanged(_ =>
- {
- if (columnIndex != -1)
- Columns.ElementAt(columnIndex).Remove(h);
-
- columnIndex = maniaObject.Column - firstColumnIndex;
- Columns.ElementAt(columnIndex).Add(h);
- }, true);
-
- h.OnNewResult += OnNewResult;
+ base.LoadComplete();
+ NewResult += OnNewResult;
}
- public override bool Remove(DrawableHitObject h)
- {
- var maniaObject = (ManiaHitObject)h.HitObject;
- int columnIndex = maniaObject.Column - firstColumnIndex;
- Columns.ElementAt(columnIndex).Remove(h);
+ public override void Add(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Add(hitObject);
- h.OnNewResult -= OnNewResult;
- return true;
- }
+ public override bool Remove(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Remove(hitObject);
+
+ public override void Add(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Add(h);
+
+ public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline));
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
index dcf1573522..f4b673f10b 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
@@ -14,6 +14,11 @@
Properties\AndroidManifest.xml
armeabi-v7a;x86;arm64-v8a
+
+ None
+ cjk;mideast;other;rare;west
+ true
+
@@ -35,5 +40,10 @@
osu.Game
+
+
+ 5.0.0
+
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs
new file mode 100644
index 0000000000..a6873c6de9
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs
@@ -0,0 +1,250 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Edit.Checks;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
+{
+ [TestFixture]
+ public class CheckOffscreenObjectsTest
+ {
+ private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE * 0.5f;
+
+ private CheckOffscreenObjects check;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckOffscreenObjects();
+ }
+
+ [Test]
+ public void TestCircleInCenter()
+ {
+ assertOk(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = playfield_centre
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestCircleNearEdge()
+ {
+ assertOk(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(5, 5)
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestCircleNearEdgeStackedOffscreen()
+ {
+ assertOffscreenCircle(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(5, 5),
+ StackHeight = 5
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestCircleOffscreen()
+ {
+ assertOffscreenCircle(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(0, 0)
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestSliderInCenter()
+ {
+ assertOk(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = new Vector2(420, 240),
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(new Vector2(-100, 0))
+ }),
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestSliderNearEdge()
+ {
+ assertOk(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = playfield_centre,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
+ }),
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestSliderNearEdgeStackedOffscreen()
+ {
+ assertOffscreenSlider(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = playfield_centre,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
+ }),
+ StackHeight = 5
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestSliderOffscreenStart()
+ {
+ assertOffscreenSlider(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = new Vector2(0, 0),
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(playfield_centre)
+ }),
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestSliderOffscreenEnd()
+ {
+ assertOffscreenSlider(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = playfield_centre,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(-playfield_centre)
+ }),
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestSliderOffscreenPath()
+ {
+ assertOffscreenSlider(new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = playfield_centre,
+ Path = new SliderPath(new[]
+ {
+ // Circular arc shoots over the top of the screen.
+ new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve),
+ new PathControlPoint(new Vector2(-100, -200)),
+ new PathControlPoint(new Vector2(100, -200))
+ }),
+ }
+ }
+ });
+ }
+
+ private void assertOk(IBeatmap beatmap)
+ {
+ var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ Assert.That(check.Run(context), Is.Empty);
+ }
+
+ private void assertOffscreenCircle(IBeatmap beatmap)
+ {
+ var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
+ }
+
+ private void assertOffscreenSlider(IBeatmap beatmap)
+ {
+ var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs
index 66cd405195..315493318d 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
Add(drawableObject = new DrawableHitCircle(hitCircle));
- AddBlueprint(blueprint = new TestBlueprint(drawableObject));
+ AddBlueprint(blueprint = new TestBlueprint(hitCircle), drawableObject);
});
[Test]
@@ -63,8 +63,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public new HitCirclePiece CirclePiece => base.CirclePiece;
- public TestBlueprint(DrawableHitCircle drawableCircle)
- : base(drawableCircle)
+ public TestBlueprint(HitCircle circle)
+ : base(circle)
{
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs
new file mode 100644
index 0000000000..d0348c1b6b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ public class TestSceneOsuEditorSelectInvalidPath : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ [Test]
+ public void TestSelectDoesNotModify()
+ {
+ Slider slider = new Slider { StartTime = 0, Position = new Vector2(320, 40) };
+
+ PathControlPoint[] points =
+ {
+ new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
+ new PathControlPoint(new Vector2(-100, 0)),
+ new PathControlPoint(new Vector2(100, 20))
+ };
+
+ int preSelectVersion = -1;
+ AddStep("add slider", () =>
+ {
+ slider.Path = new SliderPath(points);
+ EditorBeatmap.Add(slider);
+ preSelectVersion = slider.Path.Version.Value;
+ });
+
+ AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
+
+ AddAssert("slider same path", () => slider.Path.Version.Value == preSelectVersion);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
index 738a21b17e..35b79aa8ac 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
@@ -4,9 +4,11 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
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.Tests.Visual;
@@ -14,7 +16,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
- public class TestScenePathControlPointVisualiser : OsuTestScene
+ public class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene
{
private Slider slider;
private PathControlPointVisualiser visualiser;
@@ -43,12 +45,145 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
}
+ [Test]
+ public void TestPerfectCurveTooManyPoints()
+ {
+ createVisualiser(true);
+
+ addControlPointStep(new Vector2(200), PathType.Bezier);
+ addControlPointStep(new Vector2(300));
+ addControlPointStep(new Vector2(500, 300));
+ addControlPointStep(new Vector2(700, 200));
+ addControlPointStep(new Vector2(500, 100));
+
+ // Must be both hovering and selecting the control point for the context menu to work.
+ moveMouseToControlPoint(1);
+ AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
+ addContextMenuItemStep("Perfect curve");
+
+ assertControlPointPathType(0, PathType.Bezier);
+ assertControlPointPathType(1, PathType.PerfectCurve);
+ assertControlPointPathType(3, PathType.Bezier);
+ }
+
+ [Test]
+ public void TestPerfectCurveLastThreePoints()
+ {
+ createVisualiser(true);
+
+ addControlPointStep(new Vector2(200), PathType.Bezier);
+ addControlPointStep(new Vector2(300));
+ addControlPointStep(new Vector2(500, 300));
+ addControlPointStep(new Vector2(700, 200));
+ addControlPointStep(new Vector2(500, 100));
+
+ moveMouseToControlPoint(2);
+ AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true);
+ addContextMenuItemStep("Perfect curve");
+
+ assertControlPointPathType(0, PathType.Bezier);
+ assertControlPointPathType(2, PathType.PerfectCurve);
+ assertControlPointPathType(4, null);
+ }
+
+ [Test]
+ public void TestPerfectCurveLastTwoPoints()
+ {
+ createVisualiser(true);
+
+ addControlPointStep(new Vector2(200), PathType.Bezier);
+ addControlPointStep(new Vector2(300));
+ addControlPointStep(new Vector2(500, 300));
+ addControlPointStep(new Vector2(700, 200));
+ addControlPointStep(new Vector2(500, 100));
+
+ moveMouseToControlPoint(3);
+ AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
+ addContextMenuItemStep("Perfect curve");
+
+ assertControlPointPathType(0, PathType.Bezier);
+ AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null);
+ }
+
+ [Test]
+ public void TestPerfectCurveTooManyPointsLinear()
+ {
+ createVisualiser(true);
+
+ addControlPointStep(new Vector2(200), PathType.Linear);
+ addControlPointStep(new Vector2(300));
+ addControlPointStep(new Vector2(500, 300));
+ addControlPointStep(new Vector2(700, 200));
+ addControlPointStep(new Vector2(500, 100));
+
+ // Must be both hovering and selecting the control point for the context menu to work.
+ moveMouseToControlPoint(1);
+ AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
+ addContextMenuItemStep("Perfect curve");
+
+ assertControlPointPathType(0, PathType.Linear);
+ assertControlPointPathType(1, PathType.PerfectCurve);
+ assertControlPointPathType(3, PathType.Linear);
+ }
+
+ [Test]
+ public void TestPerfectCurveChangeToBezier()
+ {
+ createVisualiser(true);
+
+ addControlPointStep(new Vector2(200), PathType.Bezier);
+ addControlPointStep(new Vector2(300), PathType.PerfectCurve);
+ addControlPointStep(new Vector2(500, 300));
+ addControlPointStep(new Vector2(700, 200), PathType.Bezier);
+ addControlPointStep(new Vector2(500, 100));
+
+ moveMouseToControlPoint(3);
+ AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
+ addContextMenuItemStep("Inherit");
+
+ assertControlPointPathType(0, PathType.Bezier);
+ assertControlPointPathType(1, PathType.Bezier);
+ assertControlPointPathType(3, null);
+ }
+
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
- private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position)));
+ private void addControlPointStep(Vector2 position) => addControlPointStep(position, null);
+
+ private void addControlPointStep(Vector2 position, PathType? type)
+ {
+ AddStep($"add {type} control point at {position}", () =>
+ {
+ slider.Path.ControlPoints.Add(new PathControlPoint(position, type));
+ });
+ }
+
+ private void moveMouseToControlPoint(int index)
+ {
+ AddStep($"move mouse to control point {index}", () =>
+ {
+ Vector2 position = slider.Path.ControlPoints[index].Position.Value;
+ InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
+ });
+ }
+
+ private void assertControlPointPathType(int controlPointIndex, PathType? type)
+ {
+ AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type.Value == type);
+ }
+
+ private void addContextMenuItemStep(string contextMenuText)
+ {
+ AddStep($"click context menu item \"{contextMenuText}\"", () =>
+ {
+ MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
+
+ item?.Action?.Value();
+ });
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs
new file mode 100644
index 0000000000..24b947c854
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs
@@ -0,0 +1,175 @@
+// 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.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+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.Editor
+{
+ public class TestSceneSliderControlPointPiece : SelectionBlueprintTestScene
+ {
+ private Slider slider;
+ private DrawableSlider drawableObject;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Clear();
+
+ slider = new Slider
+ {
+ Position = new Vector2(256, 192),
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
+ new PathControlPoint(new Vector2(150, 150)),
+ new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
+ new PathControlPoint(new Vector2(400, 0)),
+ new PathControlPoint(new Vector2(400, 150))
+ })
+ };
+
+ slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
+
+ Add(drawableObject = new DrawableSlider(slider));
+ AddBlueprint(new TestSliderBlueprint(slider), drawableObject);
+ });
+
+ [Test]
+ public void TestDragControlPoint()
+ {
+ moveMouseToControlPoint(1);
+ AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
+
+ addMovementStep(new Vector2(150, 50));
+ AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ assertControlPointPosition(1, new Vector2(150, 50));
+ assertControlPointType(0, PathType.PerfectCurve);
+ }
+
+ [Test]
+ public void TestDragControlPointAlmostLinearlyExterior()
+ {
+ moveMouseToControlPoint(1);
+ AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
+
+ addMovementStep(new Vector2(400, 0.01f));
+ AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ assertControlPointPosition(1, new Vector2(400, 0.01f));
+ assertControlPointType(0, PathType.Bezier);
+ }
+
+ [Test]
+ public void TestDragControlPointPathRecovery()
+ {
+ moveMouseToControlPoint(1);
+ AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
+
+ addMovementStep(new Vector2(400, 0.01f));
+ assertControlPointType(0, PathType.Bezier);
+
+ addMovementStep(new Vector2(150, 50));
+ AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ assertControlPointPosition(1, new Vector2(150, 50));
+ assertControlPointType(0, PathType.PerfectCurve);
+ }
+
+ [Test]
+ public void TestDragControlPointPathRecoveryOtherSegment()
+ {
+ moveMouseToControlPoint(4);
+ AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
+
+ addMovementStep(new Vector2(350, 0.01f));
+ assertControlPointType(2, PathType.Bezier);
+
+ addMovementStep(new Vector2(150, 150));
+ AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ assertControlPointPosition(4, new Vector2(150, 150));
+ assertControlPointType(2, PathType.PerfectCurve);
+ }
+
+ [Test]
+ public void TestDragControlPointPathAfterChangingType()
+ {
+ AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type.Value = PathType.Bezier);
+ AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10))));
+ AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type.Value = PathType.PerfectCurve);
+
+ moveMouseToControlPoint(4);
+ AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
+
+ assertControlPointType(3, PathType.PerfectCurve);
+
+ addMovementStep(new Vector2(350, 0.01f));
+ AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ assertControlPointPosition(4, new Vector2(350, 0.01f));
+ assertControlPointType(3, PathType.Bezier);
+ }
+
+ private void addMovementStep(Vector2 relativePosition)
+ {
+ AddStep($"move mouse to {relativePosition}", () =>
+ {
+ Vector2 position = slider.Position + relativePosition;
+ InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
+ });
+ }
+
+ private void moveMouseToControlPoint(int index)
+ {
+ AddStep($"move mouse to control point {index}", () =>
+ {
+ Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
+ InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
+ });
+ }
+
+ private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type.Value == type);
+
+ private void assertControlPointPosition(int index, Vector2 position) =>
+ AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position.Value, 1));
+
+ private class TestSliderBlueprint : SliderSelectionBlueprint
+ {
+ public new SliderBodyPiece BodyPiece => base.BodyPiece;
+ public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
+ public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
+ public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
+
+ public TestSliderBlueprint(Slider slider)
+ : base(slider)
+ {
+ }
+
+ protected override SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new TestSliderCircleOverlay(slider, position);
+ }
+
+ private class TestSliderCircleOverlay : SliderCircleOverlay
+ {
+ public new HitCirclePiece CirclePiece => base.CirclePiece;
+
+ public TestSliderCircleOverlay(Slider slider, SliderPosition position)
+ : base(slider, position)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs
new file mode 100644
index 0000000000..ce529f2a88
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs
@@ -0,0 +1,198 @@
+// 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.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ [TestFixture]
+ public class TestSceneSliderLengthValidity : TestSceneOsuEditor
+ {
+ private OsuPlayfield playfield;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+ AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First());
+ AddStep("seek to first timing point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time));
+ }
+
+ [Test]
+ public void TestDraggingStartingPointRemainsValid()
+ {
+ Slider slider = null;
+
+ AddStep("Add slider", () =>
+ {
+ slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
+
+ PathControlPoint[] points =
+ {
+ new PathControlPoint(new Vector2(0), PathType.Linear),
+ new PathControlPoint(new Vector2(100, 0)),
+ };
+
+ slider.Path = new SliderPath(points);
+ EditorBeatmap.Add(slider);
+ });
+
+ AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
+
+ moveMouse(new Vector2(300));
+ AddStep("select slider", () => InputManager.Click(MouseButton.Left));
+
+ double distanceBefore = 0;
+
+ AddStep("store distance", () => distanceBefore = slider.Path.Distance);
+
+ moveMouse(new Vector2(300, 300));
+
+ AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
+ moveMouse(new Vector2(350, 300));
+ moveMouse(new Vector2(400, 300));
+ AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
+ AddAssert("ensure slider still has valid length", () => slider.Path.Distance > 0);
+ }
+
+ [Test]
+ public void TestDraggingEndingPointRemainsValid()
+ {
+ Slider slider = null;
+
+ AddStep("Add slider", () =>
+ {
+ slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
+
+ PathControlPoint[] points =
+ {
+ new PathControlPoint(new Vector2(0), PathType.Linear),
+ new PathControlPoint(new Vector2(100, 0)),
+ };
+
+ slider.Path = new SliderPath(points);
+ EditorBeatmap.Add(slider);
+ });
+
+ AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
+
+ moveMouse(new Vector2(300));
+ AddStep("select slider", () => InputManager.Click(MouseButton.Left));
+
+ double distanceBefore = 0;
+
+ AddStep("store distance", () => distanceBefore = slider.Path.Distance);
+
+ moveMouse(new Vector2(400, 300));
+
+ AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
+ moveMouse(new Vector2(350, 300));
+ moveMouse(new Vector2(300, 300));
+ AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
+ AddAssert("ensure slider still has valid length", () => slider.Path.Distance > 0);
+ }
+
+ ///
+ /// If a control point is deleted which results in the slider becoming so short it can't exist,
+ /// for simplicity delete the slider rather than having it in an invalid state.
+ ///
+ /// Eventually we may need to change this, based on user feedback. I think it's likely enough of
+ /// an edge case that we won't get many complaints, though (and there's always the undo button).
+ ///
+ [Test]
+ public void TestDeletingPointCausesSliderDeletion()
+ {
+ AddStep("Add slider", () =>
+ {
+ Slider slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
+
+ PathControlPoint[] points =
+ {
+ new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
+ new PathControlPoint(new Vector2(100, 0)),
+ new PathControlPoint(new Vector2(0, 10))
+ };
+
+ slider.Path = new SliderPath(points);
+ EditorBeatmap.Add(slider);
+ });
+
+ AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
+
+ AddStep("select slider", () => InputManager.Click(MouseButton.Left));
+
+ moveMouse(new Vector2(400, 300));
+ AddStep("delete second point", () =>
+ {
+ InputManager.PressKey(Key.ShiftLeft);
+ InputManager.Click(MouseButton.Right);
+ InputManager.ReleaseKey(Key.ShiftLeft);
+ });
+
+ AddAssert("ensure object deleted", () => EditorBeatmap.HitObjects.Count == 0);
+ }
+
+ ///
+ /// If a scale operation is performed where a single slider is the only thing selected, the path's shape will change.
+ /// If the scale results in the path becoming too short, further mouse movement in the same direction will not change the shape.
+ ///
+ [Test]
+ public void TestScalingSliderTooSmallRemainsValid()
+ {
+ Slider slider = null;
+
+ AddStep("Add slider", () =>
+ {
+ slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300, 200) };
+
+ PathControlPoint[] points =
+ {
+ new PathControlPoint(new Vector2(0), PathType.Linear),
+ new PathControlPoint(new Vector2(0, 50)),
+ new PathControlPoint(new Vector2(0, 100))
+ };
+
+ slider.Path = new SliderPath(points);
+ EditorBeatmap.Add(slider);
+ });
+
+ AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
+
+ moveMouse(new Vector2(300));
+ AddStep("select slider", () => InputManager.Click(MouseButton.Left));
+
+ double distanceBefore = 0;
+
+ AddStep("store distance", () => distanceBefore = slider.Path.Distance);
+
+ AddStep("move mouse to handle", () => InputManager.MoveMouseTo(Editor.ChildrenOfType().Skip(1).First()));
+ AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
+ moveMouse(new Vector2(300, 300));
+ moveMouse(new Vector2(300, 250));
+ moveMouse(new Vector2(300, 200));
+ AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
+ AddAssert("ensure slider still has valid length", () => slider.Path.Distance > 0);
+ }
+
+ private void moveMouse(Vector2 pos) =>
+ AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
index 67a2e5a47c..8235e1bc79 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
@@ -41,9 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addClickStep(MouseButton.Left);
addClickStep(MouseButton.Right);
- assertPlaced(true);
- assertLength(0);
- assertControlPointType(0, PathType.Linear);
+ assertPlaced(false);
}
[Test]
@@ -276,6 +274,104 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointType(0, PathType.Linear);
}
+ [Test]
+ public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior()
+ {
+ Vector2 startPosition = new Vector2(200);
+
+ addMovementStep(startPosition);
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(startPosition + new Vector2(300, 0));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(startPosition + new Vector2(150, 0.1f));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(3);
+ assertControlPointType(0, PathType.Bezier);
+ }
+
+ [Test]
+ public void TestPlacePerfectCurveSegmentRecovery()
+ {
+ Vector2 startPosition = new Vector2(200);
+
+ addMovementStep(startPosition);
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(startPosition + new Vector2(300, 0));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(startPosition + new Vector2(150, 0.1f)); // Should convert to bezier
+ addMovementStep(startPosition + new Vector2(400.0f, 50.0f)); // Should convert back to perfect
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(3);
+ assertControlPointType(0, PathType.PerfectCurve);
+ }
+
+ [Test]
+ public void TestPlacePerfectCurveSegmentLarge()
+ {
+ Vector2 startPosition = new Vector2(400);
+
+ addMovementStep(startPosition);
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(startPosition + new Vector2(220, 220));
+ addClickStep(MouseButton.Left);
+
+ // Playfield dimensions are 640 x 480.
+ // So a 440 x 440 bounding box should be ok.
+ addMovementStep(startPosition + new Vector2(-220, 220));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(3);
+ assertControlPointType(0, PathType.PerfectCurve);
+ }
+
+ [Test]
+ public void TestPlacePerfectCurveSegmentTooLarge()
+ {
+ Vector2 startPosition = new Vector2(480, 200);
+
+ addMovementStep(startPosition);
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(startPosition + new Vector2(400, 400));
+ addClickStep(MouseButton.Left);
+
+ // Playfield dimensions are 640 x 480.
+ // So an 800 * 800 bounding box area should not be ok.
+ addMovementStep(startPosition + new Vector2(-400, 400));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(3);
+ assertControlPointType(0, PathType.Bezier);
+ }
+
+ [Test]
+ public void TestPlacePerfectCurveSegmentCompleteArc()
+ {
+ addMovementStep(new Vector2(400));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(600, 400));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400, 410));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(3);
+ assertControlPointType(0, PathType.PerfectCurve);
+ }
+
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
private void addClickStep(MouseButton button)
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs
index f6e1be693b..0d828a79c8 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
Add(drawableObject = new DrawableSlider(slider));
- AddBlueprint(blueprint = new TestSliderBlueprint(drawableObject));
+ AddBlueprint(blueprint = new TestSliderBlueprint(slider), drawableObject);
});
[Test]
@@ -174,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("body positioned correctly", () => blueprint.BodyPiece.Position == slider.StackedPosition);
AddAssert("head positioned correctly",
- () => Precision.AlmostEquals(blueprint.HeadBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.HeadCircle.ScreenSpaceDrawQuad.Centre));
+ () => Precision.AlmostEquals(blueprint.HeadOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.HeadCircle.ScreenSpaceDrawQuad.Centre));
AddAssert("tail positioned correctly",
- () => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
+ () => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
}
private void moveMouseToControlPoint(int index)
@@ -195,23 +195,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
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 TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
+ public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
- public TestSliderBlueprint(DrawableSlider slider)
+ public TestSliderBlueprint(Slider slider)
: base(slider)
{
}
- protected override SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new TestSliderCircleBlueprint(slider, position);
+ protected override SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new TestSliderCircleOverlay(slider, position);
}
- private class TestSliderCircleBlueprint : SliderCircleSelectionBlueprint
+ private class TestSliderCircleOverlay : SliderCircleOverlay
{
public new HitCirclePiece CirclePiece => base.CirclePiece;
- public TestSliderCircleBlueprint(DrawableSlider slider, SliderPosition position)
+ public TestSliderCircleOverlay(Slider slider, SliderPosition position)
: base(slider, position)
{
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs
index 4248f68a60..5007841805 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Child = drawableSpinner = new DrawableSpinner(spinner)
});
- AddBlueprint(new SpinnerSelectionBlueprint(drawableSpinner) { Size = new Vector2(0.5f) });
+ AddBlueprint(new SpinnerSelectionBlueprint(spinner) { Size = new Vector2(0.5f) }, drawableSpinner);
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs
new file mode 100644
index 0000000000..e29a67c770
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs
@@ -0,0 +1,72 @@
+// 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.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ [TestFixture]
+ public class TestSliderScaling : TestSceneOsuEditor
+ {
+ private OsuPlayfield playfield;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+ AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First());
+ AddStep("seek to first timing point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time));
+ }
+
+ [Test]
+ public void TestScalingLinearSlider()
+ {
+ Slider slider = null;
+
+ AddStep("Add slider", () =>
+ {
+ slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
+
+ PathControlPoint[] points =
+ {
+ new PathControlPoint(new Vector2(0), PathType.Linear),
+ new PathControlPoint(new Vector2(100, 0)),
+ };
+
+ slider.Path = new SliderPath(points);
+ EditorBeatmap.Add(slider);
+ });
+
+ AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
+
+ moveMouse(new Vector2(300));
+ AddStep("select slider", () => InputManager.Click(MouseButton.Left));
+
+ double distanceBefore = 0;
+
+ AddStep("store distance", () => distanceBefore = slider.Path.Distance);
+
+ AddStep("move mouse to handle", () => InputManager.MoveMouseTo(Editor.ChildrenOfType().Skip(1).First()));
+ AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
+ moveMouse(new Vector2(300, 300));
+ AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
+ }
+
+ private void moveMouse(Vector2 pos) =>
+ AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 856b6554b9..0ba775e5c7 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private void runSpmTest(Mod mod)
{
- SpinnerSpmCounter spmCounter = null;
+ SpinnerSpmCalculator spmCalculator = null;
CreateModTest(new ModTestData
{
@@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
});
- AddUntilStep("fetch SPM counter", () =>
+ AddUntilStep("fetch SPM calculator", () =>
{
- spmCounter = this.ChildrenOfType().SingleOrDefault();
- return spmCounter != null;
+ spmCalculator = this.ChildrenOfType().SingleOrDefault();
+ return spmCalculator != null;
});
- AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
+ AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
index 7df5ca0f7c..24e69703a6 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
@@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Beatmap = singleSpinnerBeatmap,
PassCondition = () =>
{
- var counter = Player.ChildrenOfType().SingleOrDefault();
- return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
+ var counter = Player.ChildrenOfType().SingleOrDefault();
+ return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
}
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
index 7d32895083..5f44e1b6b6 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
@@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase("repeat-slider")]
[TestCase("uneven-repeat-slider")]
[TestCase("old-stacking")]
+ [TestCase("multi-segment-slider")]
public void Test(string name) => base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index a365ea10d4..afd94f4570 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.9311451172608853d, "diffcalc-test")]
- [TestCase(1.0736587013228804d, "zero-length-sliders")]
+ [TestCase(6.9311451172574934d, "diffcalc-test")]
+ [TestCase(1.0736586907780401d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(8.6228371119393064d, "diffcalc-test")]
- [TestCase(1.2864585434597433d, "zero-length-sliders")]
+ [TestCase(8.7212283220412345d, "diffcalc-test")]
+ [TestCase(1.3212137158641493d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
index cad98185ce..233aaf2ed9 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Tests
get
{
if (content == null)
- base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
+ base.Content.Add(content = new OsuInputManager(new OsuRuleset().RulesetInfo));
return content;
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor.png
new file mode 100755
index 0000000000..fe305468fe
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursortrail.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursortrail.png
new file mode 100755
index 0000000000..f3327dc92f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursortrail.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png
new file mode 100644
index 0000000000..8304617d8c
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png
new file mode 100644
index 0000000000..c3b85eb873
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png
new file mode 100644
index 0000000000..7f65eb7ca7
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png
new file mode 100644
index 0000000000..82bec3babe
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png
new file mode 100644
index 0000000000..5e38c75a9d
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png
new file mode 100644
index 0000000000..a562d9f2ac
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png
new file mode 100644
index 0000000000..b4cf81f26e
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png
new file mode 100644
index 0000000000..a23f5379b2
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png
new file mode 100644
index 0000000000..430b18509d
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png
new file mode 100644
index 0000000000..add1202c31
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png
new file mode 100644
index 0000000000..f68d32957f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png
new file mode 100644
index 0000000000..80c39b8745
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png
new file mode 100644
index 0000000000..fc750abc7e
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png
new file mode 100644
index 0000000000..779773f8bd
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
index 5369de24e9..89bcd68343 100644
--- a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
+++ b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
@@ -1,2 +1,6 @@
[General]
-Version: 1.0
\ No newline at end of file
+Version: 1.0
+
+[Fonts]
+HitCircleOverlap: 3
+ScoreOverlap: 3
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png
new file mode 100644
index 0000000000..73753554f7
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index e2d9f144c0..0ba97fac54 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
RelativeSizeAxes = Axes.Both;
}
- public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
+ public Drawable GetDrawableComponent(ISkinComponent component) => null;
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
@@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
return null;
}
- public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public ISample GetSample(ISampleInfo sampleInfo) => null;
- public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
+ public IBindable GetConfig(TLookup lookup) => null;
public event Action SourceChanged
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index e4158d8f07..7821ae9cf0 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestHitLightingDisabled()
{
- AddStep("hit lighting disabled", () => config.Set(OsuSetting.HitLighting, false));
+ AddStep("hit lighting disabled", () => config.SetValue(OsuSetting.HitLighting, false));
showResult(HitResult.Great);
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestHitLightingEnabled()
{
- AddStep("hit lighting enabled", () => config.Set(OsuSetting.HitLighting, true));
+ AddStep("hit lighting enabled", () => config.SetValue(OsuSetting.HitLighting, true));
showResult(HitResult.Great);
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
int poolIndex = 0;
- SetContents(() =>
+ SetContents(_ =>
{
DrawablePool pool;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 461779b185..a95159ce4c 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -4,13 +4,22 @@
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input;
using osu.Framework.Testing.Input;
using osu.Framework.Utils;
+using osu.Game.Audio;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@@ -21,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached]
private GameplayBeatmap gameplayBeatmap;
- private ClickingCursorContainer lastContainer;
+ private OsuCursorContainer lastContainer;
[Resolved]
private OsuConfigManager config { get; set; }
@@ -46,14 +55,12 @@ namespace osu.Game.Rulesets.Osu.Tests
AddSliderStep("circle size", 0f, 10f, 0f, val =>
{
- config.Set(OsuSetting.AutoCursorSize, true);
+ config.SetValue(OsuSetting.AutoCursorSize, true);
gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
- Scheduler.AddOnce(recreate);
+ Scheduler.AddOnce(() => loadContent(false));
});
- AddStep("test cursor container", recreate);
-
- void recreate() => SetContents(() => new OsuInputManager(new OsuRuleset().RulesetInfo) { Child = new OsuCursorContainer() });
+ AddStep("test cursor container", () => loadContent(false));
}
[TestCase(1, 1)]
@@ -64,36 +71,64 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(10, 1.5f)]
public void TestSizing(int circleSize, float userScale)
{
- AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
+ AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
- AddStep("turn on autosizing", () => config.Set(OsuSetting.AutoCursorSize, true));
+ AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
- AddStep("load content", loadContent);
+ AddStep("load content", () => loadContent());
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
- AddStep("set user scale to 1", () => config.Set(OsuSetting.GameplayCursorSize, 1f));
+ AddStep("set user scale to 1", () => config.SetValue(OsuSetting.GameplayCursorSize, 1f));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize));
- AddStep("turn off autosizing", () => config.Set(OsuSetting.AutoCursorSize, false));
+ AddStep("turn off autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, false));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1);
- AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
+ AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
}
- private void loadContent()
+ [Test]
+ public void TestTopLeftOrigin()
{
- SetContents(() => new MovingCursorInputManager
+ AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
+ }
+
+ private void loadContent(bool automated = true, Func skinProvider = null)
+ {
+ SetContents(_ =>
{
- Child = lastContainer = new ClickingCursorContainer
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- }
+ var inputManager = automated ? (InputManager)new MovingCursorInputManager() : new OsuInputManager(new OsuRuleset().RulesetInfo);
+ var skinContainer = skinProvider?.Invoke() ?? new SkinProvidingContainer(null);
+
+ lastContainer = automated ? new ClickingCursorContainer() : new OsuCursorContainer();
+
+ return inputManager.WithChild(skinContainer.WithChild(lastContainer));
});
}
+ private class TopLeftCursorSkin : ISkin
+ {
+ public Drawable GetDrawableComponent(ISkinComponent component) => null;
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
+ public ISample GetSample(ISampleInfo sampleInfo) => null;
+
+ public IBindable GetConfig(TLookup lookup)
+ {
+ switch (lookup)
+ {
+ case OsuSkinConfiguration osuLookup:
+ if (osuLookup == OsuSkinConfiguration.CursorCentre)
+ return SkinUtils.As(new BindableBool(false));
+
+ break;
+ }
+
+ return null;
+ }
+ }
+
private class ClickingCursorContainer : OsuCursorContainer
{
private bool pressed;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index 1278a0ff2d..58e46b6687 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -23,18 +23,18 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestVariousHitCircles()
{
- AddStep("Miss Big Single", () => SetContents(() => testSingle(2)));
- AddStep("Miss Medium Single", () => SetContents(() => testSingle(5)));
- AddStep("Miss Small Single", () => SetContents(() => testSingle(7)));
- AddStep("Hit Big Single", () => SetContents(() => testSingle(2, true)));
- AddStep("Hit Medium Single", () => SetContents(() => testSingle(5, true)));
- AddStep("Hit Small Single", () => SetContents(() => testSingle(7, true)));
- AddStep("Miss Big Stream", () => SetContents(() => testStream(2)));
- AddStep("Miss Medium Stream", () => SetContents(() => testStream(5)));
- AddStep("Miss Small Stream", () => SetContents(() => testStream(7)));
- AddStep("Hit Big Stream", () => SetContents(() => testStream(2, true)));
- AddStep("Hit Medium Stream", () => SetContents(() => testStream(5, true)));
- AddStep("Hit Small Stream", () => SetContents(() => testStream(7, true)));
+ AddStep("Miss Big Single", () => SetContents(_ => testSingle(2)));
+ AddStep("Miss Medium Single", () => SetContents(_ => testSingle(5)));
+ AddStep("Miss Small Single", () => SetContents(_ => testSingle(7)));
+ AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
+ AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
+ AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
+ AddStep("Miss Big Stream", () => SetContents(_ => testStream(2)));
+ AddStep("Miss Medium Stream", () => SetContents(_ => testStream(5)));
+ AddStep("Miss Small Stream", () => SetContents(_ => testStream(7)));
+ AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
+ AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
+ AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
}
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs
index 5fc1082743..8b3fead366 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
Position = new Vector2(128, 128),
ComboIndex = 1,
- }), null));
+ })));
}
private HitCircle prepareObject(HitCircle circle)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
index 0649989dc0..1fdcd73dde 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- Child = new SkinProvidingContainer(new DefaultSkin())
+ Child = new SkinProvidingContainer(new DefaultSkin(null))
{
RelativeSizeAxes = Axes.Both,
Child = drawableHitCircle = new DrawableHitCircle(hitCircle)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs
index c26419b0e8..56307861f1 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs
@@ -30,28 +30,28 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(false, false)]
- public override void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
+ public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
{
- TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, true);
- base.TestBeatmapComboColours(userHasCustomColours, useBeatmapSkin);
+ PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true));
+ ConfigureTest(useBeatmapSkin, true, userHasCustomColours);
AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours));
}
[TestCase(true)]
[TestCase(false)]
- public override void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
+ public void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
{
- TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, true);
- base.TestBeatmapComboColoursOverride(useBeatmapSkin);
+ PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true));
+ ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
}
[TestCase(true)]
[TestCase(false)]
- public override void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
+ public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
{
- TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, true);
- base.TestBeatmapComboColoursOverrideWithDefaultColours(useBeatmapSkin);
+ PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true));
+ ConfigureTest(useBeatmapSkin, false, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
}
@@ -59,10 +59,10 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(false, true)]
[TestCase(true, false)]
[TestCase(false, false)]
- public override void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
+ public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
{
- TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, false);
- base.TestBeatmapNoComboColours(useBeatmapSkin, useBeatmapColour);
+ PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, false));
+ ConfigureTest(useBeatmapSkin, useBeatmapColour, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
}
@@ -70,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(false, true)]
[TestCase(true, false)]
[TestCase(false, false)]
- public override void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
+ public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
{
- TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, false);
- base.TestBeatmapNoComboColoursSkinOverride(useBeatmapSkin, useBeatmapColour);
+ PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, false));
+ ConfigureTest(useBeatmapSkin, useBeatmapColour, true);
AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 8dbb48c048..6c6f05c5c5 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("enable user provider", () => testUserSkin.Enabled = true);
- AddStep("enable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, true));
+ AddStep("enable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, true));
checkNextHitObject("beatmap");
- AddStep("disable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, false));
+ AddStep("disable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, false));
checkNextHitObject("user");
AddStep("disable user provider", () => testUserSkin.Enabled = false);
@@ -57,20 +57,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("enable user provider", () => testUserSkin.Enabled = true);
- AddStep("enable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, true));
- AddStep("enable beatmap colours", () => LocalConfig.Set(OsuSetting.BeatmapColours, true));
+ AddStep("enable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, true));
+ AddStep("enable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, true));
checkNextHitObject("beatmap");
- AddStep("enable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, true));
- AddStep("disable beatmap colours", () => LocalConfig.Set(OsuSetting.BeatmapColours, false));
+ AddStep("enable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, true));
+ AddStep("disable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, false));
checkNextHitObject("beatmap");
- AddStep("disable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, false));
- AddStep("enable beatmap colours", () => LocalConfig.Set(OsuSetting.BeatmapColours, true));
+ AddStep("disable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, false));
+ AddStep("enable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, true));
checkNextHitObject("user");
- AddStep("disable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, false));
- AddStep("disable beatmap colours", () => LocalConfig.Set(OsuSetting.BeatmapColours, false));
+ AddStep("disable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, false));
+ AddStep("disable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, false));
checkNextHitObject("user");
AddStep("disable user provider", () => testUserSkin.Enabled = false);
@@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
- public Sample GetSample(ISampleInfo sampleInfo) => null;
+ public ISample GetSample(ISampleInfo sampleInfo) => null;
public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default;
public IBindable GetConfig(TLookup lookup) => null;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index d40484f5ed..fc5fcf2358 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -30,54 +30,54 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestVariousSliders()
{
- AddStep("Big Single", () => SetContents(() => testSimpleBig()));
- AddStep("Medium Single", () => SetContents(() => testSimpleMedium()));
- AddStep("Small Single", () => SetContents(() => testSimpleSmall()));
- AddStep("Big 1 Repeat", () => SetContents(() => testSimpleBig(1)));
- AddStep("Medium 1 Repeat", () => SetContents(() => testSimpleMedium(1)));
- AddStep("Small 1 Repeat", () => SetContents(() => testSimpleSmall(1)));
- AddStep("Big 2 Repeats", () => SetContents(() => testSimpleBig(2)));
- AddStep("Medium 2 Repeats", () => SetContents(() => testSimpleMedium(2)));
- AddStep("Small 2 Repeats", () => SetContents(() => testSimpleSmall(2)));
+ AddStep("Big Single", () => SetContents(_ => testSimpleBig()));
+ AddStep("Medium Single", () => SetContents(_ => testSimpleMedium()));
+ AddStep("Small Single", () => SetContents(_ => testSimpleSmall()));
+ AddStep("Big 1 Repeat", () => SetContents(_ => testSimpleBig(1)));
+ AddStep("Medium 1 Repeat", () => SetContents(_ => testSimpleMedium(1)));
+ AddStep("Small 1 Repeat", () => SetContents(_ => testSimpleSmall(1)));
+ AddStep("Big 2 Repeats", () => SetContents(_ => testSimpleBig(2)));
+ AddStep("Medium 2 Repeats", () => SetContents(_ => testSimpleMedium(2)));
+ AddStep("Small 2 Repeats", () => SetContents(_ => testSimpleSmall(2)));
- AddStep("Slow Slider", () => SetContents(testSlowSpeed)); // slow long sliders take ages already so no repeat steps
- AddStep("Slow Short Slider", () => SetContents(() => testShortSlowSpeed()));
- AddStep("Slow Short Slider 1 Repeats", () => SetContents(() => testShortSlowSpeed(1)));
- AddStep("Slow Short Slider 2 Repeats", () => SetContents(() => testShortSlowSpeed(2)));
+ AddStep("Slow Slider", () => SetContents(_ => testSlowSpeed())); // slow long sliders take ages already so no repeat steps
+ AddStep("Slow Short Slider", () => SetContents(_ => testShortSlowSpeed()));
+ AddStep("Slow Short Slider 1 Repeats", () => SetContents(_ => testShortSlowSpeed(1)));
+ AddStep("Slow Short Slider 2 Repeats", () => SetContents(_ => testShortSlowSpeed(2)));
- AddStep("Fast Slider", () => SetContents(() => testHighSpeed()));
- AddStep("Fast Slider 1 Repeat", () => SetContents(() => testHighSpeed(1)));
- AddStep("Fast Slider 2 Repeats", () => SetContents(() => testHighSpeed(2)));
- AddStep("Fast Short Slider", () => SetContents(() => testShortHighSpeed()));
- AddStep("Fast Short Slider 1 Repeat", () => SetContents(() => testShortHighSpeed(1)));
- AddStep("Fast Short Slider 2 Repeats", () => SetContents(() => testShortHighSpeed(2)));
- AddStep("Fast Short Slider 6 Repeats", () => SetContents(() => testShortHighSpeed(6)));
+ AddStep("Fast Slider", () => SetContents(_ => testHighSpeed()));
+ AddStep("Fast Slider 1 Repeat", () => SetContents(_ => testHighSpeed(1)));
+ AddStep("Fast Slider 2 Repeats", () => SetContents(_ => testHighSpeed(2)));
+ AddStep("Fast Short Slider", () => SetContents(_ => testShortHighSpeed()));
+ AddStep("Fast Short Slider 1 Repeat", () => SetContents(_ => testShortHighSpeed(1)));
+ AddStep("Fast Short Slider 2 Repeats", () => SetContents(_ => testShortHighSpeed(2)));
+ AddStep("Fast Short Slider 6 Repeats", () => SetContents(_ => testShortHighSpeed(6)));
- AddStep("Perfect Curve", () => SetContents(() => testPerfect()));
- AddStep("Perfect Curve 1 Repeat", () => SetContents(() => testPerfect(1)));
- AddStep("Perfect Curve 2 Repeats", () => SetContents(() => testPerfect(2)));
+ AddStep("Perfect Curve", () => SetContents(_ => testPerfect()));
+ AddStep("Perfect Curve 1 Repeat", () => SetContents(_ => testPerfect(1)));
+ AddStep("Perfect Curve 2 Repeats", () => SetContents(_ => testPerfect(2)));
- AddStep("Linear Slider", () => SetContents(() => testLinear()));
- AddStep("Linear Slider 1 Repeat", () => SetContents(() => testLinear(1)));
- AddStep("Linear Slider 2 Repeats", () => SetContents(() => testLinear(2)));
+ AddStep("Linear Slider", () => SetContents(_ => testLinear()));
+ AddStep("Linear Slider 1 Repeat", () => SetContents(_ => testLinear(1)));
+ AddStep("Linear Slider 2 Repeats", () => SetContents(_ => testLinear(2)));
- AddStep("Bezier Slider", () => SetContents(() => testBezier()));
- AddStep("Bezier Slider 1 Repeat", () => SetContents(() => testBezier(1)));
- AddStep("Bezier Slider 2 Repeats", () => SetContents(() => testBezier(2)));
+ AddStep("Bezier Slider", () => SetContents(_ => testBezier()));
+ AddStep("Bezier Slider 1 Repeat", () => SetContents(_ => testBezier(1)));
+ AddStep("Bezier Slider 2 Repeats", () => SetContents(_ => testBezier(2)));
- AddStep("Linear Overlapping", () => SetContents(() => testLinearOverlapping()));
- AddStep("Linear Overlapping 1 Repeat", () => SetContents(() => testLinearOverlapping(1)));
- AddStep("Linear Overlapping 2 Repeats", () => SetContents(() => testLinearOverlapping(2)));
+ AddStep("Linear Overlapping", () => SetContents(_ => testLinearOverlapping()));
+ AddStep("Linear Overlapping 1 Repeat", () => SetContents(_ => testLinearOverlapping(1)));
+ AddStep("Linear Overlapping 2 Repeats", () => SetContents(_ => testLinearOverlapping(2)));
- AddStep("Catmull Slider", () => SetContents(() => testCatmull()));
- AddStep("Catmull Slider 1 Repeat", () => SetContents(() => testCatmull(1)));
- AddStep("Catmull Slider 2 Repeats", () => SetContents(() => testCatmull(2)));
+ AddStep("Catmull Slider", () => SetContents(_ => testCatmull()));
+ AddStep("Catmull Slider 1 Repeat", () => SetContents(_ => testCatmull(1)));
+ AddStep("Catmull Slider 2 Repeats", () => SetContents(_ => testCatmull(2)));
- AddStep("Big Single, Large StackOffset", () => SetContents(() => testSimpleBigLargeStackOffset()));
- AddStep("Big 1 Repeat, Large StackOffset", () => SetContents(() => testSimpleBigLargeStackOffset(1)));
+ AddStep("Big Single, Large StackOffset", () => SetContents(_ => testSimpleBigLargeStackOffset()));
+ AddStep("Big 1 Repeat, Large StackOffset", () => SetContents(_ => testSimpleBigLargeStackOffset(1)));
- AddStep("Distance Overflow", () => SetContents(() => testDistanceOverflow()));
- AddStep("Distance Overflow 1 Repeat", () => SetContents(() => testDistanceOverflow(1)));
+ AddStep("Distance Overflow", () => SetContents(_ => testDistanceOverflow()));
+ AddStep("Distance Overflow 1 Repeat", () => SetContents(_ => testDistanceOverflow(1)));
}
[Test]
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
index aac6db60fe..e698766aac 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(300, 0),
}),
RepeatCount = 1
- }), null));
+ })));
}
[Test]
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 2cc031405e..590d159300 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -34,6 +34,18 @@ namespace osu.Game.Rulesets.Osu.Tests
private List judgementResults;
+ [Test]
+ public void TestPressBothKeysSimultaneouslyAndReleaseOne()
+ {
+ performTest(new List
+ {
+ new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start },
+ new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
+ });
+
+ AddAssert("Tracking retained", assertMaxJudge);
+ }
+
///
/// Scenario:
/// - Press a key before a slider starts
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index f697a77d94..b21b7a6f4a 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -5,12 +5,14 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@@ -27,16 +29,26 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestVariousSpinners(bool autoplay)
{
string term = autoplay ? "Hit" : "Miss";
- AddStep($"{term} Big", () => SetContents(() => testSingle(2, autoplay)));
- AddStep($"{term} Medium", () => SetContents(() => testSingle(5, autoplay)));
- AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay)));
+ AddStep($"{term} Big", () => SetContents(_ => testSingle(2, autoplay)));
+ AddStep($"{term} Medium", () => SetContents(_ => testSingle(5, autoplay)));
+ AddStep($"{term} Small", () => SetContents(_ => testSingle(7, autoplay)));
+ }
+
+ [Test]
+ public void TestSpinningSamplePitchShift()
+ {
+ AddStep("Add spinner", () => SetContents(_ => testSingle(5, true, 4000)));
+ AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
+ AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
+
+ PausableSkinnableSound getSpinningSample() => drawableSpinner.ChildrenOfType().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
}
[TestCase(false)]
[TestCase(true)]
public void TestLongSpinner(bool autoplay)
{
- AddStep("Very long spinner", () => SetContents(() => testSingle(5, autoplay, 4000)));
+ AddStep("Very long spinner", () => SetContents(_ => testSingle(5, autoplay, 4000)));
AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
AddUntilStep("Check correct progress", () => drawableSpinner.Progress == (autoplay ? 1 : 0));
}
@@ -45,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(true)]
public void TestSuperShortSpinner(bool autoplay)
{
- AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 200)));
+ AddStep("Very short spinner", () => SetContents(_ => testSingle(5, autoplay, 200)));
AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1);
}
@@ -93,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
base.Update();
if (auto)
- RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3));
+ RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 2));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs
index d7fbc7ac48..8c97c02049 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
ComboIndex = 1,
Duration = 1000,
- }), null));
+ })));
AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0);
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index ac8d5c81bc..8ff21057b5 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
@@ -168,13 +169,13 @@ namespace osu.Game.Rulesets.Osu.Tests
double estimatedSpm = 0;
addSeekStep(1000);
- AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
+ AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
addSeekStep(2000);
- AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
addSeekStep(1000);
- AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
}
[TestCase(0.5)]
@@ -188,16 +189,16 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("retrieve spinner state", () =>
{
expectedProgress = drawableSpinner.Progress;
- expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
+ expectedSpm = drawableSpinner.SpinsPerMinute.Value;
});
addSeekStep(0);
- AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate);
+ AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate);
addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
- AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
+ AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
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 b4c686ccea..ebe642803b 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,8 +2,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
index e8272057f3..9589fd576f 100644
--- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
@@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Configuration
protected override void InitialiseDefaults()
{
base.InitialiseDefaults();
- Set(OsuRulesetSetting.SnakingInSliders, true);
- Set(OsuRulesetSetting.SnakingOutSliders, true);
- Set(OsuRulesetSetting.ShowCursorTrail, true);
- Set(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
+ SetDefault(OsuRulesetSetting.SnakingInSliders, true);
+ SetDefault(OsuRulesetSetting.SnakingOutSliders, true);
+ SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
+ SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 90cba13c7c..cb819ec090 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
///
- public class Aim : Skill
+ public class Aim : StrainSkill
{
private const double angle_bonus_begin = Math.PI / 3;
private const double timing_threshold = 107;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 200bc7997d..fbac080fc6 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
///
- public class Speed : Skill
+ public class Speed : StrainSkill
{
private const double single_spacing_threshold = 125;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
index abbb54e3c1..b21a3e038e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
@@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
protected readonly HitCirclePiece CirclePiece;
- public HitCircleSelectionBlueprint(DrawableHitCircle drawableCircle)
- : base(drawableCircle)
+ public HitCircleSelectionBlueprint(HitCircle circle)
+ : base(circle)
{
InternalChild = CirclePiece = new HitCirclePiece();
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
index 8dd550bb96..994c5cebeb 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
@@ -2,20 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints
{
- public abstract class OsuSelectionBlueprint : OverlaySelectionBlueprint
+ public abstract class OsuSelectionBlueprint : HitObjectSelectionBlueprint
where T : OsuHitObject
{
- protected new T HitObject => (T)DrawableObject.HitObject;
+ protected new DrawableOsuHitObject DrawableObject => (DrawableOsuHitObject)base.DrawableObject;
protected override bool AlwaysShowWhenSelected => true;
- protected OsuSelectionBlueprint(DrawableHitObject drawableObject)
- : base(drawableObject)
+ protected OsuSelectionBlueprint(T hitObject)
+ : base(hitObject)
{
}
}
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 e9838de63d..48e4db11ca 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -2,16 +2,22 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Utils;
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.Objects;
using osu.Game.Screens.Edit;
using osuTK;
@@ -23,9 +29,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
/// A visualisation of a single in a .
///
- public class PathControlPointPiece : BlueprintPiece
+ public class PathControlPointPiece : BlueprintPiece, IHasTooltip
{
public Action RequestSelection;
+ public List PointsInSegment;
public readonly BindableBool IsSelected = new BindableBool();
public readonly PathControlPoint ControlPoint;
@@ -52,6 +59,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
this.slider = slider;
ControlPoint = controlPoint;
+ // we don't want to run the path type update on construction as it may inadvertently change the slider.
+ cachePoints(slider);
+
+ slider.Path.Version.BindValueChanged(_ =>
+ {
+ cachePoints(slider);
+ updatePathType();
+ });
+
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
Origin = Anchor.Centre;
@@ -148,6 +164,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
private Vector2 dragStartPosition;
+ private PathType? dragPathType;
protected override bool OnDragStart(DragStartEvent e)
{
@@ -157,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (e.Button == MouseButton.Left)
{
dragStartPosition = ControlPoint.Position.Value;
+ dragPathType = PointsInSegment[0].Type.Value;
+
changeHandler?.BeginChange();
return true;
}
@@ -166,6 +185,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override void OnDrag(DragEvent e)
{
+ Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position.Value).ToArray();
+ var oldPosition = slider.Position;
+ var oldStartTime = slider.StartTime;
+
if (ControlPoint == slider.Path.ControlPoints[0])
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
@@ -182,10 +205,45 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
else
ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
+
+ if (!slider.Path.HasValidLength)
+ {
+ for (var i = 0; i < slider.Path.ControlPoints.Count; i++)
+ slider.Path.ControlPoints[i].Position.Value = oldControlPoints[i];
+
+ slider.Position = oldPosition;
+ slider.StartTime = oldStartTime;
+ return;
+ }
+
+ // Maintain the path type in case it got defaulted to bezier at some point during the drag.
+ PointsInSegment[0].Type.Value = dragPathType;
}
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
+ private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
+
+ ///
+ /// Handles correction of invalid path types.
+ ///
+ private void updatePathType()
+ {
+ if (ControlPoint.Type.Value != PathType.PerfectCurve)
+ return;
+
+ if (PointsInSegment.Count > 3)
+ ControlPoint.Type.Value = PathType.Bezier;
+
+ if (PointsInSegment.Count != 3)
+ return;
+
+ ReadOnlySpan points = PointsInSegment.Select(p => p.Position.Value).ToArray();
+ RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
+ if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
+ ControlPoint.Type.Value = PathType.Bezier;
+ }
+
///
/// Updates the state of the circular control point marker.
///
@@ -195,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
markerRing.Alpha = IsSelected.Value ? 1 : 0;
- Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
+ Color4 colour = getColourFromNodeType();
if (IsHovered || IsSelected.Value)
colour = colour.Lighten(1);
@@ -203,5 +261,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
marker.Colour = colour;
marker.Scale = new Vector2(slider.Scale);
}
+
+ private Color4 getColourFromNodeType()
+ {
+ if (!(ControlPoint.Type.Value is PathType pathType))
+ return colours.Yellow;
+
+ switch (pathType)
+ {
+ case PathType.Catmull:
+ return colours.Seafoam;
+
+ case PathType.Bezier:
+ return colours.Pink;
+
+ case PathType.PerfectCurve:
+ return colours.PurpleDark;
+
+ default:
+ return colours.Red;
+ }
+ }
+
+ public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
}
}
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 ce5dc4855e..c36768baba 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -153,6 +153,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
+ ///
+ /// Attempts to set the given control point piece to the given path type.
+ /// If that would fail, try to change the path such that it instead succeeds
+ /// in a UX-friendly way.
+ ///
+ /// The control point piece that we want to change the path type of.
+ /// The path type we want to assign to the given control point piece.
+ private void updatePathType(PathControlPointPiece piece, PathType? type)
+ {
+ int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
+
+ switch (type)
+ {
+ case PathType.PerfectCurve:
+ // Can't always create a circular arc out of 4 or more points,
+ // so we split the segment into one 3-point circular arc segment
+ // and one segment of the previous type.
+ int thirdPointIndex = indexInSegment + 2;
+
+ if (piece.PointsInSegment.Count > thirdPointIndex + 1)
+ piece.PointsInSegment[thirdPointIndex].Type.Value = piece.PointsInSegment[0].Type.Value;
+
+ break;
+ }
+
+ piece.ControlPoint.Type.Value = type;
+ }
+
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
@@ -215,10 +243,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
int totalCount = Pieces.Count(p => p.IsSelected.Value);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type);
- var item = new PathTypeMenuItem(type, () =>
+ var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ =>
{
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
- p.ControlPoint.Type.Value = type;
+ updatePathType(p, type);
});
if (countOfState == totalCount)
@@ -230,15 +258,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return item;
}
-
- private class PathTypeMenuItem : TernaryStateMenuItem
- {
- public PathTypeMenuItem(PathType? type, Action action)
- : base(type == null ? "Inherit" : type.ToString().Humanize(), changeState, MenuItemType.Standard, _ => action?.Invoke())
- {
- }
-
- private static TernaryState changeState(TernaryState state) => TernaryState.True;
- }
}
}
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 1c3d270c95..6e22c35ab3 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -26,6 +26,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
AccentColour = Color4.Transparent
};
+
+ // SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
+ // Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
+ AlwaysPresent = true;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs
similarity index 50%
rename from osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
rename to osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs
index dec9cd8622..241ff70a18 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs
@@ -1,36 +1,32 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics.Containers;
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 SliderCircleOverlay : CompositeDrawable
{
protected readonly HitCirclePiece CirclePiece;
+ private readonly Slider slider;
private readonly SliderPosition position;
- public SliderCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position)
- : base(slider)
+ public SliderCircleOverlay(Slider slider, SliderPosition position)
{
+ this.slider = slider;
this.position = position;
InternalChild = CirclePiece = new HitCirclePiece();
-
- Select();
}
protected override void Update()
{
base.Update();
- CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)HitObject.HeadCircle : HitObject.TailCircle);
+ CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.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 b71e1914f7..8b20df9a68 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private InputManager inputManager;
- private PlacementState state;
+ private SliderPlacementState state;
private PathControlPoint segmentStart;
private PathControlPoint cursor;
private int currentSegmentLength;
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPointVisualiser = new PathControlPointVisualiser(HitObject, false)
};
- setState(PlacementState.Initial);
+ setState(SliderPlacementState.Initial);
}
protected override void LoadComplete()
@@ -73,12 +73,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
switch (state)
{
- case PlacementState.Initial:
+ case SliderPlacementState.Initial:
BeginPlacement();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break;
- case PlacementState.Body:
+ case SliderPlacementState.Body:
updateCursor();
break;
}
@@ -91,11 +91,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
switch (state)
{
- case PlacementState.Initial:
+ case SliderPlacementState.Initial:
beginCurve();
break;
- case PlacementState.Body:
+ case SliderPlacementState.Body:
if (canPlaceNewControlPoint(out var lastPoint))
{
// Place a new point by detatching the current cursor.
@@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void OnMouseUp(MouseUpEvent e)
{
- if (state == PlacementState.Body && e.Button == MouseButton.Right)
+ if (state == SliderPlacementState.Body && e.Button == MouseButton.Right)
endCurve();
base.OnMouseUp(e);
}
@@ -129,19 +129,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void beginCurve()
{
BeginPlacement(commitStart: true);
- setState(PlacementState.Body);
+ setState(SliderPlacementState.Body);
}
private void endCurve()
{
updateSlider();
- EndPlacement(true);
+ EndPlacement(HitObject.Path.HasValidLength);
}
protected override void Update()
{
base.Update();
updateSlider();
+
+ // Maintain the path type in case it got defaulted to bezier at some point during the drag.
+ updatePathType();
}
private void updatePathType()
@@ -204,7 +207,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
lastPoint = last;
- return lastPiece?.IsHovered != true;
+ return lastPiece.IsHovered != true;
}
private void updateSlider()
@@ -216,12 +219,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
tailCirclePiece.UpdateFrom(HitObject.TailCircle);
}
- private void setState(PlacementState newState)
+ private void setState(SliderPlacementState newState)
{
state = newState;
}
- private enum PlacementState
+ private enum SliderPlacementState
{
Initial,
Body,
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 3d3dff653a..e810d2fe0c 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -25,12 +26,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public class SliderSelectionBlueprint : OsuSelectionBlueprint
{
- protected SliderBodyPiece BodyPiece { get; private set; }
- protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; }
- protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; }
- protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
+ protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
- private readonly DrawableSlider slider;
+ protected SliderBodyPiece BodyPiece { get; private set; }
+ protected SliderCircleOverlay HeadOverlay { get; private set; }
+ protected SliderCircleOverlay TailOverlay { get; private set; }
+
+ [CanBeNull]
+ protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
@@ -49,10 +52,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private readonly BindableList controlPoints = new BindableList();
private readonly IBindable pathVersion = new Bindable();
- public SliderSelectionBlueprint(DrawableSlider slider)
+ public SliderSelectionBlueprint(Slider slider)
: base(slider)
{
- this.slider = slider;
}
[BackgroundDependencyLoader]
@@ -61,8 +63,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
InternalChildren = new Drawable[]
{
BodyPiece = new SliderBodyPiece(),
- HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
- TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
+ HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start),
+ TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
};
}
@@ -100,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void OnSelected()
{
- AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(slider.HitObject, true)
+ AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
{
RemoveControlPointsRequested = removeControlPoints
});
@@ -114,6 +116,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// throw away frame buffers on deselection.
ControlPointVisualiser?.Expire();
+ ControlPointVisualiser = null;
+
BodyPiece.RecyclePath();
}
@@ -210,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted
- if (controlPoints.Count <= 1)
+ if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)
{
placementHandler?.Delete(HitObject);
return;
@@ -235,11 +239,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
};
- public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
+ // Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
+ public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
+ ?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;
- protected virtual SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new SliderCircleSelectionBlueprint(slider, position);
+ protected virtual SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new SliderCircleOverlay(slider, position);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs
index f05d4f8435..ee573d1a01 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs
@@ -3,7 +3,6 @@
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
@@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
{
private readonly SpinnerPiece piece;
- public SpinnerSelectionBlueprint(DrawableSpinner spinner)
+ public SpinnerSelectionBlueprint(Spinner spinner)
: base(spinner)
{
InternalChild = piece = new SpinnerPiece();
diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs
new file mode 100644
index 0000000000..a342c2a821
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs
@@ -0,0 +1,115 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Edit.Checks
+{
+ public class CheckOffscreenObjects : ICheck
+ {
+ // A close approximation for the bounding box of the screen in gameplay on 4:3 aspect ratio.
+ // Uses gameplay space coordinates (512 x 384 playfield / 640 x 480 screen area).
+ // See https://github.com/ppy/osu/pull/12361#discussion_r612199777 for reference.
+ private const int min_x = -67;
+ private const int min_y = -60;
+ private const int max_x = 579;
+ private const int max_y = 428;
+
+ // The amount of milliseconds to step through a slider path at a time
+ // (higher = more performant, but higher false-negative chance).
+ private const int path_step_size = 5;
+
+ public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Offscreen hitobjects");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateOffscreenCircle(this),
+ new IssueTemplateOffscreenSlider(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ foreach (var hitobject in context.Beatmap.HitObjects)
+ {
+ switch (hitobject)
+ {
+ case Slider slider:
+ {
+ foreach (var issue in sliderIssues(slider))
+ yield return issue;
+
+ break;
+ }
+
+ case HitCircle circle:
+ {
+ if (isOffscreen(circle.StackedPosition, circle.Radius))
+ yield return new IssueTemplateOffscreenCircle(this).Create(circle);
+
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Steps through points on the slider to ensure the entire path is on-screen.
+ /// Returns at most one issue.
+ ///
+ /// The slider whose path to check.
+ ///
+ private IEnumerable sliderIssues(Slider slider)
+ {
+ for (int i = 0; i < slider.Distance; i += path_step_size)
+ {
+ double progress = i / slider.Distance;
+ Vector2 position = slider.StackedPositionAt(progress);
+
+ if (!isOffscreen(position, slider.Radius))
+ continue;
+
+ // `SpanDuration` ensures we don't include reverses.
+ double time = slider.StartTime + progress * slider.SpanDuration;
+ yield return new IssueTemplateOffscreenSlider(this).Create(slider, time);
+
+ yield break;
+ }
+
+ // Above loop may skip the last position in the slider due to step size.
+ if (!isOffscreen(slider.StackedEndPosition, slider.Radius))
+ yield break;
+
+ yield return new IssueTemplateOffscreenSlider(this).Create(slider, slider.EndTime);
+ }
+
+ private bool isOffscreen(Vector2 position, double radius)
+ {
+ return position.X - radius < min_x || position.X + radius > max_x ||
+ position.Y - radius < min_y || position.Y + radius > max_y;
+ }
+
+ public class IssueTemplateOffscreenCircle : IssueTemplate
+ {
+ public IssueTemplateOffscreenCircle(ICheck check)
+ : base(check, IssueType.Problem, "This circle goes offscreen on a 4:3 aspect ratio.")
+ {
+ }
+
+ public Issue Create(HitCircle circle) => new Issue(circle, this);
+ }
+
+ public class IssueTemplateOffscreenSlider : IssueTemplate
+ {
+ public IssueTemplateOffscreenSlider(ICheck check)
+ : base(check, IssueType.Problem, "This slider goes offscreen here on a 4:3 aspect ratio.")
+ {
+ }
+
+ public Issue Create(Slider slider, double offscreenTime) => new Issue(slider, this) { Time = offscreenTime };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
deleted file mode 100644
index 5fdb79cbbd..0000000000
--- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using 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.Drawables;
-using osu.Game.Rulesets.Osu.UI;
-using osu.Game.Rulesets.UI;
-using osuTK;
-
-namespace osu.Game.Rulesets.Osu.Edit
-{
- public class DrawableOsuEditRuleset : DrawableOsuRuleset
- {
- public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods)
- : base(ruleset, beatmap, mods)
- {
- }
-
- protected override Playfield CreatePlayfield() => new OsuEditPlayfield();
-
- public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { Size = Vector2.One };
-
- private class OsuEditPlayfield : OsuPlayfield
- {
- protected override GameplayCursorContainer CreateCursor() => null;
-
- protected override void OnNewDrawableHitObject(DrawableHitObject d)
- {
- d.ApplyCustomUpdateState += updateState;
- }
-
- ///
- /// 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 = 700;
-
- private void updateState(DrawableHitObject hitObject, ArmedState state)
- {
- if (state == ArmedState.Idle)
- return;
-
- // adjust the visuals of certain object types to make them stay on screen for longer than usual.
- switch (hitObject)
- {
- default:
- // there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.)
- return;
-
- case DrawableSlider _:
- // no specifics to sliders but let them fade slower below.
- break;
-
- case DrawableHitCircle circle: // also handles slider heads
- circle.ApproachCircle
- .FadeOutFromOne(editor_hit_object_fade_out_extension)
- .Expire();
- break;
- }
-
- // Get the existing fade out transform
- var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
-
- if (existing == null)
- return;
-
- hitObject.RemoveTransform(existing);
-
- using (hitObject.BeginAbsoluteSequence(existing.StartTime))
- hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs
new file mode 100644
index 0000000000..aeeae84d14
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.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 System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class DrawableOsuEditorRuleset : DrawableOsuRuleset
+ {
+ public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods)
+ : base(ruleset, beatmap, mods)
+ {
+ }
+
+ protected override Playfield CreatePlayfield() => new OsuEditorPlayfield();
+
+ public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { Size = Vector2.One };
+
+ private class OsuEditorPlayfield : OsuPlayfield
+ {
+ private Bindable hitAnimations;
+
+ protected override GameplayCursorContainer CreateCursor() => null;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ hitAnimations = config.GetBindable(OsuSetting.EditorHitAnimations);
+ }
+
+ protected override void OnNewDrawableHitObject(DrawableHitObject d)
+ {
+ d.ApplyCustomUpdateState += updateState;
+ }
+
+ ///
+ /// 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 = 700;
+
+ private void updateState(DrawableHitObject hitObject, ArmedState state)
+ {
+ if (state == ArmedState.Idle || hitAnimations.Value)
+ return;
+
+ if (hitObject is DrawableHitCircle circle)
+ {
+ circle.ApproachCircle
+ .FadeOutFromOne(editor_hit_object_fade_out_extension * 4)
+ .Expire();
+
+ circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
+ }
+
+ if (hitObject is IHasMainCirclePiece mainPieceContainer)
+ {
+ // clear any explode animation logic.
+ mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
+ mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
+ }
+
+ if (hitObject is DrawableSliderRepeat repeat)
+ {
+ repeat.Arrow.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
+ repeat.Arrow.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
+ }
+
+ // adjust the visuals of top-level object types to make them stay on screen for longer than usual.
+ switch (hitObject)
+ {
+ case DrawableSlider _:
+ case DrawableHitCircle _:
+ // Get the existing fade out transform
+ var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
+
+ if (existing == null)
+ return;
+
+ hitObject.RemoveTransform(existing);
+
+ using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
+ hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
new file mode 100644
index 0000000000..04e881fbf3
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Rulesets.Osu.Edit.Checks;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class OsuBeatmapVerifier : IBeatmapVerifier
+ {
+ private readonly List checks = new List
+ {
+ new CheckOffscreenObjects()
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ return checks.SelectMany(check => check.Run(context));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs
index a68ed34e6b..dc8c3d6107 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs
@@ -2,11 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Osu.Edit
@@ -18,23 +18,23 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
- protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler();
+ protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler();
- public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
+ public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
{
switch (hitObject)
{
- case DrawableHitCircle circle:
+ case HitCircle circle:
return new HitCircleSelectionBlueprint(circle);
- case DrawableSlider slider:
+ case Slider slider:
return new SliderSelectionBlueprint(slider);
- case DrawableSpinner spinner:
+ case Spinner spinner:
return new SpinnerSelectionBlueprint(spinner);
}
- return base.CreateBlueprintFor(hitObject);
+ return base.CreateHitObjectBlueprintFor(hitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 0490e8b8ce..806b7e6051 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
- => new DrawableOsuEditRuleset(ruleset, beatmap, mods);
+ => new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
{
@@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override ComposeBlueprintContainer CreateBlueprintContainer()
=> new OsuBlueprintContainer(this);
+ public override string ConvertSelectionToString()
+ => string.Join(',', selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
+
private DistanceSnapGrid distanceSnapGrid;
private Container distanceSnapGridContainer;
@@ -144,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (b.IsSelected)
continue;
- var hitObject = (OsuHitObject)b.HitObject;
+ var hitObject = (OsuHitObject)b.Item;
Vector2? snap = checkSnap(hitObject.Position);
if (snap == null && hitObject.Position != hitObject.EndPosition)
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 871339ae7b..57d0cd859d 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
+using osu.Game.Extensions;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
@@ -15,8 +15,19 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
- public class OsuSelectionHandler : SelectionHandler
+ public class OsuSelectionHandler : EditorSelectionHandler
{
+ ///
+ /// During a transform, the initial origin is stored so it can be used throughout the operation.
+ ///
+ private Vector2? referenceOrigin;
+
+ ///
+ /// During a transform, the initial path types of a single selected slider are stored so they
+ /// can be maintained throughout the operation.
+ ///
+ private List referencePathTypes;
+
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
@@ -33,15 +44,21 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.OnOperationEnded();
referenceOrigin = null;
+ referencePathTypes = null;
}
- public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
- moveSelection(moveEvent.InstantDelta);
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
+ {
+ var hitObjects = selectedMovableObjects;
- ///
- /// During a transform, the initial origin is stored so it can be used throughout the operation.
- ///
- private Vector2? referenceOrigin;
+ // this will potentially move the selection out of bounds...
+ foreach (var h in hitObjects)
+ h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
+
+ // but this will be corrected.
+ moveSelectionInBounds();
+ return true;
+ }
public override bool HandleReverse()
{
@@ -96,24 +113,10 @@ namespace osu.Game.Rulesets.Osu.Edit
var hitObjects = selectedMovableObjects;
var selectedObjectsQuad = getSurroundingQuad(hitObjects);
- var centre = selectedObjectsQuad.Centre;
foreach (var h in hitObjects)
{
- var pos = h.Position;
-
- switch (direction)
- {
- case Direction.Horizontal:
- pos.X = centre.X - (pos.X - centre.X);
- break;
-
- case Direction.Vertical:
- pos.Y = centre.Y - (pos.Y - centre.Y);
- break;
- }
-
- h.Position = pos;
+ h.Position = GetFlippedPosition(direction, selectedObjectsQuad, h.Position);
if (h is Slider slider)
{
@@ -140,36 +143,11 @@ namespace osu.Game.Rulesets.Osu.Edit
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
- {
- Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
- Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height);
-
- foreach (var point in slider.Path.ControlPoints)
- point.Position.Value *= pathRelativeDeltaScale;
- }
+ scaleSlider(slider, scale);
else
- {
- // move the selection before scaling if dragging from top or left anchors.
- if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false;
- if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false;
-
- Quad quad = getSurroundingQuad(hitObjects);
-
- foreach (var h in hitObjects)
- {
- var newPosition = h.Position;
-
- // guard against no-ops and NaN.
- if (scale.X != 0 && quad.Width > 0)
- newPosition.X = quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X);
-
- if (scale.Y != 0 && quad.Height > 0)
- newPosition.Y = quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y);
-
- h.Position = newPosition;
- }
- }
+ scaleHitObjects(hitObjects, reference, scale);
+ moveSelectionInBounds();
return true;
}
@@ -194,12 +172,12 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var h in hitObjects)
{
- h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
+ h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
if (h is IHasPath path)
{
foreach (var point in path.Path.ControlPoints)
- point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
+ point.Position.Value = RotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
}
}
@@ -207,28 +185,116 @@ namespace osu.Game.Rulesets.Osu.Edit
return true;
}
- private bool moveSelection(Vector2 delta)
+ private void scaleSlider(Slider slider, Vector2 scale)
+ {
+ referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList();
+
+ Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
+
+ // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
+ scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
+
+ Vector2 pathRelativeDeltaScale = new Vector2(
+ sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
+ sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
+
+ Queue oldControlPoints = new Queue();
+
+ foreach (var point in slider.Path.ControlPoints)
+ {
+ oldControlPoints.Enqueue(point.Position.Value);
+ point.Position.Value *= pathRelativeDeltaScale;
+ }
+
+ // Maintain the path types in case they were defaulted to bezier at some point during scaling
+ for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
+ slider.Path.ControlPoints[i].Type.Value = referencePathTypes[i];
+
+ //if sliderhead or sliderend end up outside playfield, revert scaling.
+ Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
+ (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
+
+ if (xInBounds && yInBounds && slider.Path.HasValidLength)
+ return;
+
+ foreach (var point in slider.Path.ControlPoints)
+ point.Position.Value = oldControlPoints.Dequeue();
+ }
+
+ private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
+ {
+ scale = getClampedScale(hitObjects, reference, scale);
+ Quad selectionQuad = getSurroundingQuad(hitObjects);
+
+ foreach (var h in hitObjects)
+ h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position);
+ }
+
+ private (bool X, bool Y) isQuadInBounds(Quad quad)
+ {
+ bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth);
+ bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight);
+
+ return (xInBounds, yInBounds);
+ }
+
+ private void moveSelectionInBounds()
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
- Vector2 newTopLeft = quad.TopLeft + delta;
- if (newTopLeft.X < 0)
- delta.X -= newTopLeft.X;
- if (newTopLeft.Y < 0)
- delta.Y -= newTopLeft.Y;
+ Vector2 delta = Vector2.Zero;
- Vector2 newBottomRight = quad.BottomRight + delta;
- if (newBottomRight.X > DrawWidth)
- delta.X -= newBottomRight.X - DrawWidth;
- if (newBottomRight.Y > DrawHeight)
- delta.Y -= newBottomRight.Y - DrawHeight;
+ if (quad.TopLeft.X < 0)
+ delta.X -= quad.TopLeft.X;
+ if (quad.TopLeft.Y < 0)
+ delta.Y -= quad.TopLeft.Y;
+
+ if (quad.BottomRight.X > DrawWidth)
+ delta.X -= quad.BottomRight.X - DrawWidth;
+ if (quad.BottomRight.Y > DrawHeight)
+ delta.Y -= quad.BottomRight.Y - DrawHeight;
foreach (var h in hitObjects)
h.Position += delta;
+ }
- return true;
+ ///
+ /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
+ ///
+ /// The hitobjects to be scaled
+ /// The anchor from which the scale operation is performed
+ /// The scale to be clamped
+ /// The clamped scale vector
+ private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
+ {
+ float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
+ float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
+
+ Quad selectionQuad = getSurroundingQuad(hitObjects);
+
+ //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
+ Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
+
+ //max Size -> playfield bounds
+ if (scaledQuad.TopLeft.X < 0)
+ scale.X += scaledQuad.TopLeft.X;
+ if (scaledQuad.TopLeft.Y < 0)
+ scale.Y += scaledQuad.TopLeft.Y;
+
+ if (scaledQuad.BottomRight.X > DrawWidth)
+ scale.X -= scaledQuad.BottomRight.X - DrawWidth;
+ if (scaledQuad.BottomRight.Y > DrawHeight)
+ scale.Y -= scaledQuad.BottomRight.Y - DrawHeight;
+
+ //min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale.
+ Vector2 scaledSize = selectionQuad.Size + scale;
+ Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON);
+
+ scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size;
+
+ return scale;
}
///
@@ -236,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Edit
///
/// The hit objects to calculate a quad for.
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
- getSurroundingQuad(hitObjects.SelectMany(h =>
+ GetSurroundingQuad(hitObjects.SelectMany(h =>
{
if (h is IHasPath path)
{
@@ -251,59 +317,11 @@ namespace osu.Game.Rulesets.Osu.Edit
return new[] { h.Position };
}));
- ///
- /// Returns a gamefield-space quad surrounding the provided points.
- ///
- /// The points to calculate a quad for.
- private Quad getSurroundingQuad(IEnumerable points)
- {
- if (!EditorBeatmap.SelectedHitObjects.Any())
- return new Quad();
-
- 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 p in points)
- {
- minPosition = Vector2.ComponentMin(minPosition, p);
- maxPosition = Vector2.ComponentMax(maxPosition, p);
- }
-
- Vector2 size = maxPosition - minPosition;
-
- return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
- }
-
///
/// All osu! hitobjects which can be moved/rotated/scaled.
///
- private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects
- .OfType()
+ private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType()
.Where(h => !(h is Spinner))
.ToArray();
-
- ///
- /// Rotate a point around an arbitrary origin.
- ///
- /// The point.
- /// The centre origin to rotate around.
- /// The angle to rotate (in degrees).
- private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
- {
- angle = -angle;
-
- point.X -= origin.X;
- point.Y -= origin.Y;
-
- Vector2 ret;
- ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
- ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
-
- ret.X += origin.X;
- ret.Y += origin.Y;
-
- return ret;
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs
index e58aacd86e..9f77175398 100644
--- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs
@@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.Judgements
///
public float RateAdjustedRotation;
+ ///
+ /// Time instant at which the spin was started (the first user input which caused an increase in spin).
+ ///
+ public double? TimeStarted;
+
///
/// Time instant at which the spinner has been completed (the user has executed all required spins).
/// Will be null if all required spins haven't been completed.
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 77de0cb45b..aac830801b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay) };
public bool PerformFail() => false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
new file mode 100644
index 0000000000..9ae9653e9b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModBarrelRoll : ModBarrelRoll, IApplicableToDrawableHitObjects
+ {
+ public void ApplyToDrawableHitObjects(IEnumerable drawables)
+ {
+ foreach (var d in drawables)
+ {
+ d.OnUpdate += _ =>
+ {
+ switch (d)
+ {
+ case DrawableHitCircle circle:
+ circle.CirclePiece.Rotation = -CurrentRotation;
+ break;
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
index 5470d0fcb4..77dea5b0dc 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
-using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -16,22 +15,8 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset
+ public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset
{
- public override string Name => "Classic";
-
- public override string Acronym => "CL";
-
- public override double ScoreMultiplier => 1;
-
- public override IconUsage? Icon => FontAwesome.Solid.History;
-
- public override string Description => "Feeling nostalgic?";
-
- public override bool Ranked => false;
-
- public override ModType Type => ModType.Conversion;
-
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true);
@@ -44,6 +29,9 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")]
public Bindable FixedFollowCircleHitArea { get; } = new BindableBool(true);
+ [SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
+ public Bindable AlwaysPlayTailSample { get; } = new BindableBool(true);
+
public void ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
@@ -79,6 +67,10 @@ namespace osu.Game.Rulesets.Osu.Mods
case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
break;
+
+ case DrawableSliderTail tail:
+ tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
+ break;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index 20c0818d03..683b35f282 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -9,10 +9,12 @@ using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
@@ -23,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private const float default_flashlight_size = 180;
+ private const double default_follow_delay = 120;
+
private OsuFlashlight flashlight;
public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight();
@@ -35,8 +39,25 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
+ public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ base.ApplyToDrawableRuleset(drawableRuleset);
+
+ flashlight.FollowDelay = FollowDelay.Value;
+ }
+
+ [SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")]
+ public BindableNumber FollowDelay { get; } = new BindableDouble(default_follow_delay)
+ {
+ MinValue = default_follow_delay,
+ MaxValue = default_follow_delay * 10,
+ Precision = default_follow_delay,
+ };
+
private class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition
{
+ public double FollowDelay { private get; set; }
+
public OsuFlashlight()
{
FlashlightSize = new Vector2(0, getSizeFor(0));
@@ -50,13 +71,11 @@ namespace osu.Game.Rulesets.Osu.Mods
protected override bool OnMouseMove(MouseMoveEvent e)
{
- const double follow_delay = 120;
-
var position = FlashlightPosition;
var destination = e.MousePosition;
FlashlightPosition = Interpolation.ValueAt(
- Math.Min(Math.Abs(Clock.ElapsedFrameTime), follow_delay), position, destination, 0, follow_delay, Easing.Out);
+ Math.Min(Math.Abs(Clock.ElapsedFrameTime), FollowDelay), position, destination, 0, FollowDelay, Easing.Out);
return base.OnMouseMove(e);
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 45f314af7b..2752feb0a1 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -43,13 +43,11 @@ namespace osu.Game.Rulesets.Osu.Mods
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
- base.ApplyIncreasedVisibilityState(hitObject, state);
applyState(hitObject, true);
}
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
- base.ApplyNormalVisibilityState(hitObject, state);
applyState(hitObject, false);
}
@@ -60,20 +58,20 @@ namespace osu.Game.Rulesets.Osu.Mods
OsuHitObject hitObject = drawableOsuObject.HitObject;
- (double startTime, double duration) fadeOut = getFadeOutParameters(drawableOsuObject);
+ (double fadeStartTime, double fadeDuration) = getFadeOutParameters(drawableOsuObject);
switch (drawableObject)
{
case DrawableSliderTail _:
- using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true))
- drawableObject.FadeOut(fadeOut.duration);
+ using (drawableObject.BeginAbsoluteSequence(fadeStartTime))
+ drawableObject.FadeOut(fadeDuration);
break;
case DrawableSliderRepeat sliderRepeat:
- using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true))
+ using (drawableObject.BeginAbsoluteSequence(fadeStartTime))
// only apply to circle piece – reverse arrow is not affected by hidden.
- sliderRepeat.CirclePiece.FadeOut(fadeOut.duration);
+ sliderRepeat.CirclePiece.FadeOut(fadeDuration);
break;
@@ -88,23 +86,23 @@ namespace osu.Game.Rulesets.Osu.Mods
else
{
// we don't want to see the approach circle
- using (circle.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt, true))
+ using (circle.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt))
circle.ApproachCircle.Hide();
}
- using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true))
- fadeTarget.FadeOut(fadeOut.duration);
+ using (drawableObject.BeginAbsoluteSequence(fadeStartTime))
+ fadeTarget.FadeOut(fadeDuration);
break;
case DrawableSlider slider:
- using (slider.BeginAbsoluteSequence(fadeOut.startTime, true))
- slider.Body.FadeOut(fadeOut.duration, Easing.Out);
+ using (slider.BeginAbsoluteSequence(fadeStartTime))
+ slider.Body.FadeOut(fadeDuration, Easing.Out);
break;
case DrawableSliderTick sliderTick:
- using (sliderTick.BeginAbsoluteSequence(fadeOut.startTime, true))
- sliderTick.FadeOut(fadeOut.duration);
+ using (sliderTick.BeginAbsoluteSequence(fadeStartTime))
+ sliderTick.FadeOut(fadeDuration);
break;
@@ -112,14 +110,14 @@ namespace osu.Game.Rulesets.Osu.Mods
// hide elements we don't care about.
// todo: hide background
- using (spinner.BeginAbsoluteSequence(fadeOut.startTime, true))
- spinner.FadeOut(fadeOut.duration);
+ using (spinner.BeginAbsoluteSequence(fadeStartTime))
+ spinner.FadeOut(fadeDuration);
break;
}
}
- private (double startTime, double duration) getFadeOutParameters(DrawableOsuHitObject drawableObject)
+ private (double fadeStartTime, double fadeDuration) getFadeOutParameters(DrawableOsuHitObject drawableObject)
{
switch (drawableObject)
{
@@ -137,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Mods
return getParameters(drawableObject.HitObject);
}
- static (double startTime, double duration) getParameters(OsuHitObject hitObject)
+ static (double fadeStartTime, double fadeDuration) getParameters(OsuHitObject hitObject)
{
var fadeOutStartTime = hitObject.StartTime - hitObject.TimePreempt + hitObject.TimeFadeIn;
var fadeOutDuration = hitObject.TimePreempt * fade_out_duration_multiplier;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
index f0db548e74..3b16e9d2b7 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
@@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Touch Device";
public override string Acronym => "TD";
+ public override string Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index df0a41455f..4b0939db16 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
namespace osu.Game.Rulesets.Osu.Mods
{
- internal class OsuModTraceable : ModWithVisibilityAdjustment
+ public class OsuModTraceable : ModWithVisibilityAdjustment
{
public override string Name => "Traceable";
public override string Acronym => "TC";
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index 9c5e41f245..a01cec4bb3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Mods
var osuObject = (OsuHitObject)drawable.HitObject;
Vector2 origin = drawable.Position;
- // Wiggle the repeat points with the slider instead of independently.
+ // Wiggle the repeat points and the tail with the slider instead of independently.
// Also fixes an issue with repeat points being positioned incorrectly.
- if (osuObject is SliderRepeat)
+ if (osuObject is SliderRepeat || osuObject is SliderTailCircle)
return;
Random objRand = new Random((int)osuObject.StartTime);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 5541d0e790..cda4715280 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
Entry = null;
}
- private void onEntryInvalidated() => refreshPoints();
+ private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
private void refreshPoints()
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 77094f928b..236af4b3f1 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -19,7 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableHitCircle : DrawableOsuHitObject
+ public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece
{
public OsuAction? HitAction => HitArea.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
@@ -66,7 +66,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
- CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece()),
+ CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
ApproachCircle = new ApproachCircle
{
Alpha = 0,
@@ -164,28 +168,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApproachCircle.Expire(true);
}
+ protected override void UpdateStartTimeStateTransforms()
+ {
+ base.UpdateStartTimeStateTransforms();
+
+ ApproachCircle.FadeOut(50);
+ }
+
protected override void UpdateHitStateTransforms(ArmedState state)
{
Debug.Assert(HitObject.HitWindows != null);
+ // todo: temporary / arbitrary, used for lifetime optimisation.
+ this.Delay(800).FadeOut();
+
switch (state)
{
case ArmedState.Idle:
- this.Delay(HitObject.TimePreempt).FadeOut(500);
HitArea.HitAction = null;
break;
case ArmedState.Miss:
- ApproachCircle.FadeOut(50);
this.FadeOut(100);
break;
-
- case ArmedState.Hit:
- ApproachCircle.FadeOut(50);
-
- // todo: temporary / arbitrary
- this.Delay(800).FadeOut();
- break;
}
Expire();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 9122f347d0..0bec33bf77 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osuTK;
@@ -33,7 +34,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
- private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody;
+ [CanBeNull]
+ public PlaySliderBody SliderBody => Body.Drawable as PlaySliderBody;
public IBindable PathVersion => pathVersion;
private readonly Bindable pathVersion = new Bindable();
@@ -108,16 +110,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void LoadSamples()
{
- base.LoadSamples();
+ // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
- var firstSample = HitObject.Samples.FirstOrDefault();
-
- if (firstSample != null)
+ if (HitObject.SampleControlPoint == null)
{
- var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide");
-
- slidingSample.Samples = new ISampleInfo[] { clone };
+ throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
+
+ Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
+
+ var slidingSamples = new List();
+
+ var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
+ if (normalSample != null)
+ slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide"));
+
+ var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
+ if (whistleSample != null)
+ slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle"));
+
+ slidingSample.Samples = slidingSamples.ToArray();
}
public override void StopAllSamples()
@@ -203,16 +216,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
Ball.UpdateProgress(completionProgress);
- sliderBody?.UpdateProgress(completionProgress);
+ SliderBody?.UpdateProgress(completionProgress);
foreach (DrawableHitObject hitObject in NestedHitObjects)
{
- if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(sliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(sliderBody?.SnakedEnd ?? 0));
+ if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking;
}
- Size = sliderBody?.Size ?? Vector2.Zero;
- OriginPosition = sliderBody?.PathOffset ?? Vector2.Zero;
+ Size = SliderBody?.Size ?? Vector2.Zero;
+ OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero;
if (DrawSize != Vector2.Zero)
{
@@ -226,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override void OnKilled()
{
base.OnKilled();
- sliderBody?.RecyclePath();
+ SliderBody?.RecyclePath();
}
protected override void ApplySkin(ISkinSource skin, bool allowFallback)
@@ -280,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
// rather than doing it this way, we should probably attach the sample to the tail circle.
// this can only be done after we stop using LegacyLastTick.
- if (TailCircle.IsHit)
+ if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit)
base.PlaySamples();
}
@@ -312,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
case ArmedState.Hit:
Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
- if (sliderBody?.SnakingOut.Value == true)
+ if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break;
}
@@ -320,7 +333,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
}
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => sliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
private class DefaultSliderBody : PlaySliderBody
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index 76490e0de1..b7458b5695 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -15,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
+ public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking, IHasMainCirclePiece
{
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
@@ -26,9 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private double animDuration;
- public Drawable CirclePiece { get; private set; }
+ public SkinnableDrawable CirclePiece { get; private set; }
+
+ public ReverseArrowPiece Arrow { get; private set; }
+
private Drawable scaleContainer;
- private ReverseArrowPiece arrow;
public override bool DisplayResult => false;
@@ -53,11 +55,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Children = new[]
+ Children = new Drawable[]
{
// no default for this; only visible in legacy skins.
- CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()),
- arrow = new ReverseArrowPiece(),
+ CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ Arrow = new ReverseArrowPiece(),
}
};
@@ -102,8 +108,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
case ArmedState.Hit:
- this.FadeOut(animDuration, Easing.Out)
- .ScaleTo(Scale * 1.5f, animDuration, Easing.Out);
+ this.FadeOut(animDuration, Easing.Out);
+
+ const float final_scale = 1.5f;
+
+ Arrow.ScaleTo(Scale * final_scale, animDuration, Easing.Out);
+ CirclePiece.ScaleTo(Scale * final_scale, animDuration, Easing.Out);
break;
}
}
@@ -139,18 +149,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
- while (Math.Abs(aimRotation - arrow.Rotation) > 180)
- aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
+ while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
+ aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;
if (!hasRotation)
{
- arrow.Rotation = aimRotation;
+ Arrow.Rotation = aimRotation;
hasRotation = true;
}
else
{
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
- arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
+ Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index 6a8e02e886..ec1387eb54 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -7,12 +7,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
+ public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, IHasMainCirclePiece
{
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
@@ -26,9 +28,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public override bool DisplayResult => false;
+ ///
+ /// Whether the hit samples only play on successful hits.
+ /// If false, the hit samples will also play on misses.
+ ///
+ public bool SamplePlaysOnlyOnHit { get; set; } = true;
+
public bool Tracking { get; set; }
- private SkinnableDrawable circlePiece;
+ public SkinnableDrawable CirclePiece { get; private set; }
+
private Container scaleContainer;
public DrawableSliderTail()
@@ -57,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Children = new Drawable[]
{
// no default for this; only visible in legacy skins.
- circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
+ CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
}
},
};
@@ -69,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateInitialTransforms();
- circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
+ CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
}
protected override void UpdateHitStateTransforms(ArmedState state)
@@ -101,7 +110,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
- public void UpdateSnakingPosition(Vector2 start, Vector2 end) =>
- Position = HitObject.RepeatIndex % 2 == 0 ? end : start;
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ if (Slider != null)
+ Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index d02376b6c3..19cee61f26 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -30,15 +30,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
public SpinnerRotationTracker RotationTracker { get; private set; }
- public SpinnerSpmCounter SpmCounter { get; private set; }
+
+ private SpinnerSpmCalculator spmCalculator;
private Container ticks;
- private SpinnerBonusDisplay bonusDisplay;
private PausableSkinnableSound spinningSample;
private Bindable isSpinning;
private bool spinnerFrequencyModulate;
+ private const float spinning_sample_initial_frequency = 1.0f;
+ private const float spinning_sample_modulated_base_frequency = 0.5f;
+
+ ///
+ /// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
+ ///
+ public IBindable GainedBonus => gainedBonus;
+
+ private readonly Bindable gainedBonus = new BindableDouble();
+
+ ///
+ /// The number of spins per minute this spinner is spinning at, for display purposes.
+ ///
+ public readonly IBindable SpinsPerMinute = new BindableDouble();
+
+ private const double fade_out_duration = 160;
+
public DrawableSpinner()
: this(null)
{
@@ -55,8 +72,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
+ spmCalculator = new SpinnerSpmCalculator
+ {
+ Result = { BindTarget = SpinsPerMinute },
+ },
ticks = new Container(),
new AspectContainer
{
@@ -65,30 +86,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{
- new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()),
+ new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
RotationTracker = new SpinnerRotationTracker(this)
}
},
- SpmCounter = new SpinnerSpmCounter
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Y = 120,
- Alpha = 0
- },
- bonusDisplay = new SpinnerBonusDisplay
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Y = -120,
- },
spinningSample = new PausableSkinnableSound
{
Volume = { Value = 0 },
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
}
- };
+ });
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
}
@@ -101,9 +109,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample);
}
- private const float spinning_sample_initial_frequency = 1.0f;
- private const float spinning_sample_modulated_base_frequency = 0.5f;
-
protected override void OnFree()
{
base.OnFree();
@@ -131,12 +136,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (tracking.NewValue)
{
if (!spinningSample.IsPlaying)
- spinningSample?.Play();
- spinningSample?.VolumeTo(1, 300);
+ spinningSample.Play();
+
+ spinningSample.VolumeTo(1, 300);
}
else
{
- spinningSample?.VolumeTo(0, 300).OnComplete(_ => spinningSample.Stop());
+ spinningSample.VolumeTo(0, fade_out_duration);
}
}
@@ -162,7 +168,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
- this.FadeOut(160).Expire();
+ this.FadeOut(fade_out_duration).OnComplete(_ =>
+ {
+ // looping sample should be stopped here as it is safer than running in the OnComplete
+ // of the volume transition above.
+ spinningSample.Stop();
+ });
+
+ Expire();
// skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
isSpinning?.TriggerChange();
@@ -261,14 +274,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateAfterChildren();
- if (!SpmCounter.IsPresent && RotationTracker.Tracking)
- SpmCounter.FadeIn(HitObject.TimeFadeIn);
+ if (Result.TimeStarted == null && RotationTracker.Tracking)
+ Result.TimeStarted = Time.Current;
- SpmCounter.SetRotation(Result.RateAdjustedRotation);
+ // don't update after end time to avoid the rate display dropping during fade out.
+ // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
+ if (Time.Current <= HitObject.EndTime)
+ spmCalculator.SetRotation(Result.RateAdjustedRotation);
updateBonusScore();
}
+ private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
+
private int wholeSpins;
private void updateBonusScore()
@@ -293,8 +311,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (tick != null)
{
tick.TriggerResult(true);
+
if (tick is DrawableSpinnerBonusTick)
- bonusDisplay.SetBonusCount(spins - HitObject.SpinsRequired);
+ gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired);
}
wholeSpins++;
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index e2b6c84896..8ba9597dc3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -81,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.Objects
public List> NodeSamples { get; set; } = new List>();
+ [JsonIgnore]
+ public IList TailSamples { get; private set; }
+
private int repeatCount;
public int RepeatCount
@@ -143,11 +146,6 @@ namespace osu.Game.Rulesets.Osu.Objects
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
-
- // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
- // For now, the samples are attached to and played by the slider itself at the correct end time.
- // ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live).
- Samples = this.GetNodeSamples(repeatCount + 1).ToArray();
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@@ -238,6 +236,10 @@ namespace osu.Game.Rulesets.Osu.Objects
if (HeadCircle != null)
HeadCircle.Samples = this.GetNodeSamples(0);
+
+ // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
+ // For now, the samples are played by the slider itself at the correct end time.
+ TailSamples = this.GetNodeSamples(repeatCount + 1);
}
public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 18324a18a8..465d6d7155 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -29,6 +29,7 @@ using osu.Game.Scoring;
using osu.Game.Skinning;
using System;
using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.Statistics;
@@ -58,52 +59,52 @@ namespace osu.Game.Rulesets.Osu
public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
- if (mods.HasFlag(LegacyMods.Nightcore))
+ if (mods.HasFlagFast(LegacyMods.Nightcore))
yield return new OsuModNightcore();
- else if (mods.HasFlag(LegacyMods.DoubleTime))
+ else if (mods.HasFlagFast(LegacyMods.DoubleTime))
yield return new OsuModDoubleTime();
- if (mods.HasFlag(LegacyMods.Perfect))
+ if (mods.HasFlagFast(LegacyMods.Perfect))
yield return new OsuModPerfect();
- else if (mods.HasFlag(LegacyMods.SuddenDeath))
+ else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
yield return new OsuModSuddenDeath();
- if (mods.HasFlag(LegacyMods.Autopilot))
+ if (mods.HasFlagFast(LegacyMods.Autopilot))
yield return new OsuModAutopilot();
- if (mods.HasFlag(LegacyMods.Cinema))
+ if (mods.HasFlagFast(LegacyMods.Cinema))
yield return new OsuModCinema();
- else if (mods.HasFlag(LegacyMods.Autoplay))
+ else if (mods.HasFlagFast(LegacyMods.Autoplay))
yield return new OsuModAutoplay();
- if (mods.HasFlag(LegacyMods.Easy))
+ if (mods.HasFlagFast(LegacyMods.Easy))
yield return new OsuModEasy();
- if (mods.HasFlag(LegacyMods.Flashlight))
+ if (mods.HasFlagFast(LegacyMods.Flashlight))
yield return new OsuModFlashlight();
- if (mods.HasFlag(LegacyMods.HalfTime))
+ if (mods.HasFlagFast(LegacyMods.HalfTime))
yield return new OsuModHalfTime();
- if (mods.HasFlag(LegacyMods.HardRock))
+ if (mods.HasFlagFast(LegacyMods.HardRock))
yield return new OsuModHardRock();
- if (mods.HasFlag(LegacyMods.Hidden))
+ if (mods.HasFlagFast(LegacyMods.Hidden))
yield return new OsuModHidden();
- if (mods.HasFlag(LegacyMods.NoFail))
+ if (mods.HasFlagFast(LegacyMods.NoFail))
yield return new OsuModNoFail();
- if (mods.HasFlag(LegacyMods.Relax))
+ if (mods.HasFlagFast(LegacyMods.Relax))
yield return new OsuModRelax();
- if (mods.HasFlag(LegacyMods.SpunOut))
+ if (mods.HasFlagFast(LegacyMods.SpunOut))
yield return new OsuModSpunOut();
- if (mods.HasFlag(LegacyMods.Target))
+ if (mods.HasFlagFast(LegacyMods.Target))
yield return new OsuModTarget();
- if (mods.HasFlag(LegacyMods.TouchDevice))
+ if (mods.HasFlagFast(LegacyMods.TouchDevice))
yield return new OsuModTouchDevice();
}
@@ -184,6 +185,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModGrow(), new OsuModDeflate()),
new MultiMod(new ModWindUp(), new ModWindDown()),
new OsuModTraceable(),
+ new OsuModBarrelRoll(),
};
case ModType.System:
@@ -205,6 +207,8 @@ namespace osu.Game.Rulesets.Osu
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
+ public override IBeatmapVerifier CreateBeatmapVerifier() => new OsuBeatmapVerifier();
+
public override string Description => "osu!";
public override string ShortName => SHORT_NAME;
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 2883f0c187..fcb544fa5b 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Osu
SliderFollowCircle,
SliderBall,
SliderBody,
- SpinnerBody
+ SpinnerBody,
}
}
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index 693943a08a..7b0cf651c8 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -71,8 +71,6 @@ namespace osu.Game.Rulesets.Osu.Replays
buttonIndex = 0;
- AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500)));
- AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500)));
AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500)));
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
index cf48dc053f..7d696dfb79 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
@@ -2,13 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using osu.Framework.Input.StateChanges;
using osu.Framework.Utils;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
-using osuTK;
namespace osu.Game.Rulesets.Osu.Replays
{
@@ -21,24 +19,11 @@ namespace osu.Game.Rulesets.Osu.Replays
protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any();
- protected Vector2? Position
- {
- get
- {
- var frame = CurrentFrame;
-
- if (frame == null)
- return null;
-
- Debug.Assert(CurrentTime != null);
-
- return NextFrame != null ? Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time) : frame.Position;
- }
- }
-
public override void CollectPendingInputs(List inputs)
{
- inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) });
+ var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
+
+ inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(position) });
inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
}
}
diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json
index 96e4bf1637..1a0bd66246 100644
--- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json
+++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json
@@ -1,5 +1,18 @@
{
"Mappings": [{
+ "StartTime": 114993,
+ "Objects": [{
+ "StartTime": 114993,
+ "EndTime": 114993,
+ "X": 493,
+ "Y": 92
+ }, {
+ "StartTime": 115290,
+ "EndTime": 115290,
+ "X": 451.659241,
+ "Y": 267.188
+ }]
+ }, {
"StartTime": 118858.0,
"Objects": [{
"StartTime": 118858.0,
diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu
index 8c3edc9571..dd35098502 100644
--- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu
+++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu
@@ -9,7 +9,9 @@ SliderMultiplier:1.87
SliderTickRate:1
[TimingPoints]
-49051,230.769230769231,4,2,1,15,1,0
+114000,346.820809248555,4,2,1,71,1,0
+118000,230.769230769231,4,2,1,15,1,0
[HitObjects]
+493,92,114993,2,0,P|472:181|442:308,1,180,12|0,0:0|0:0,0:0:0:0:
219,215,118858,2,0,P|224:170|244:-10,1,187,8|2,0:0|0:0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json
new file mode 100644
index 0000000000..8a056b3039
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json
@@ -0,0 +1,36 @@
+{
+ "Mappings": [{
+ "StartTime": 347893,
+ "Objects": [{
+ "StartTime": 347893,
+ "EndTime": 347893,
+ "X": 329,
+ "Y": 245,
+ "StackOffset": {
+ "X": 0,
+ "Y": 0
+ }
+ },
+ {
+ "StartTime": 348193,
+ "EndTime": 348193,
+ "X": 183.0447,
+ "Y": 245.24292,
+ "StackOffset": {
+ "X": 0,
+ "Y": 0
+ }
+ },
+ {
+ "StartTime": 348457,
+ "EndTime": 348457,
+ "X": 329,
+ "Y": 245,
+ "StackOffset": {
+ "X": 0,
+ "Y": 0
+ }
+ }
+ ]
+ }]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider.osu
new file mode 100644
index 0000000000..843c32b8ef
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider.osu
@@ -0,0 +1,17 @@
+osu file format v14
+
+[General]
+Mode: 0
+
+[Difficulty]
+CircleSize:4
+OverallDifficulty:7
+ApproachRate:8
+SliderMultiplier:2
+SliderTickRate:1
+
+[TimingPoints]
+337093,300,4,2,1,40,1,0
+
+[HitObjects]
+329,245,347893,2,0,B|319:311|199:343|183:245|183:245,2,200,8|8|8,0:0|0:0|0:0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
new file mode 100644
index 0000000000..ae8c03dad1
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
@@ -0,0 +1,132 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Globalization;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+ public class DefaultSpinner : CompositeDrawable
+ {
+ private DrawableSpinner drawableSpinner;
+
+ private OsuSpriteText bonusCounter;
+
+ private Container spmContainer;
+ private OsuSpriteText spmCounter;
+
+ public DefaultSpinner()
+ {
+ RelativeSizeAxes = Axes.Both;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableHitObject;
+
+ AddRangeInternal(new Drawable[]
+ {
+ new DefaultSpinnerDisc
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ bonusCounter = new OsuSpriteText
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Numeric.With(size: 24),
+ Y = -120,
+ },
+ spmContainer = new Container
+ {
+ Alpha = 0f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 120,
+ Children = new[]
+ {
+ spmCounter = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"0",
+ Font = OsuFont.Numeric.With(size: 24)
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"SPINS PER MINUTE",
+ Font = OsuFont.Numeric.With(size: 12),
+ Y = 30
+ }
+ }
+ }
+ });
+ }
+
+ private IBindable gainedBonus;
+ private IBindable spinsPerMinute;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
+ gainedBonus.BindValueChanged(bonus =>
+ {
+ bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
+ bonusCounter.FadeOutFromOne(1500);
+ bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
+ });
+
+ spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
+ fadeCounterOnTimeStart();
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
+ fadeCounterOnTimeStart();
+ }
+
+ private void fadeCounterOnTimeStart()
+ {
+ if (drawableSpinner.Result?.TimeStarted is double startTime)
+ {
+ using (BeginAbsoluteSequence(startTime))
+ spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
index 667fee1495..542f3eff0d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
@@ -40,14 +40,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public DefaultSpinnerDisc()
{
- RelativeSizeAxes = Axes.Both;
-
// we are slightly bigger than our parent, to clip the top and bottom of the circle
// this should probably be revisited when scaled spinners are a thing.
Scale = new Vector2(initial_scale);
-
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/IHasMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/IHasMainCirclePiece.cs
new file mode 100644
index 0000000000..8bb7629542
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/IHasMainCirclePiece.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.Skinning;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+ public interface IHasMainCirclePiece
+ {
+ SkinnableDrawable CirclePiece { get; }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
index fcbe4c1b28..b52dc749f0 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
@@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private readonly IBindable accentColour = new Bindable();
private readonly IBindable indexInCurrentCombo = new Bindable();
+ private readonly IBindable armedState = new Bindable();
[Resolved]
private DrawableHitObject drawableObject { get; set; }
@@ -53,6 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
+ armedState.BindTo(drawableObject.State);
}
protected override void LoadComplete()
@@ -68,17 +70,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
- drawableObject.ApplyCustomUpdateState += updateState;
- updateState(drawableObject, drawableObject.State.Value);
+ armedState.BindValueChanged(animate, true);
}
- private void updateState(DrawableHitObject drawableObject, ArmedState state)
+ private void animate(ValueChangedEvent state)
{
- using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
- {
+ ClearTransforms(true);
+
+ using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
glow.FadeOut(400);
- switch (state)
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
+ switch (state.NewValue)
{
case ArmedState.Hit:
const double flash_in = 40;
@@ -91,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
explode.FadeIn(flash_in);
this.ScaleTo(1.5f, 400, Easing.OutQuad);
- using (BeginDelayedSequence(flash_in, true))
+ using (BeginDelayedSequence(flash_in))
{
// after the flash, we can hide some elements that were behind it
ring.FadeOut();
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs
index bea6186501..43d8d1e27f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public string Text
{
- get => number.Text;
+ get => number.Text.ToString();
set => number.Text = value;
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
index 82b677e12c..8feeca56e8 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
@@ -2,6 +2,7 @@
// 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.Bindables;
@@ -134,6 +135,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
///
private double? timeToAcceptAnyKeyAfter;
+ ///
+ /// The actions that were pressed in the previous frame.
+ ///
+ private readonly List lastPressedActions = new List();
+
protected override void Update()
{
base.Update();
@@ -152,8 +158,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton;
- // we can return to accepting all keys if the initial head circle key is the *only* key pressed, or all keys have been released.
- if (actions?.Contains(otherKey) != true)
+ // we can start accepting any key once all other keys have been released in the previous frame.
+ if (!lastPressedActions.Contains(otherKey))
timeToAcceptAnyKeyAfter = Time.Current;
}
@@ -164,6 +170,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
lastScreenSpaceMousePosition.HasValue && followCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
// valid action
(actions?.Any(isValidTrackingAction) ?? false);
+
+ lastPressedActions.Clear();
+ if (actions != null)
+ lastPressedActions.AddRange(actions);
}
///
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs
deleted file mode 100644
index c0db6228ef..0000000000
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.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.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Osu.Objects;
-
-namespace osu.Game.Rulesets.Osu.Skinning.Default
-{
- ///
- /// Shows incremental bonus score achieved for a spinner.
- ///
- public class SpinnerBonusDisplay : CompositeDrawable
- {
- private static readonly int score_per_tick = new SpinnerBonusTick().CreateJudgement().MaxNumericResult;
-
- private readonly OsuSpriteText bonusCounter;
-
- public SpinnerBonusDisplay()
- {
- AutoSizeAxes = Axes.Both;
-
- InternalChild = bonusCounter = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Font = OsuFont.Numeric.With(size: 24),
- Alpha = 0,
- };
- }
-
- private int displayedCount;
-
- public void SetBonusCount(int count)
- {
- if (displayedCount == count)
- return;
-
- displayedCount = count;
- bonusCounter.Text = $"{score_per_tick * count}";
- bonusCounter.FadeOutFromOne(1500);
- bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
similarity index 61%
rename from osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
index 69355f624b..a5205bbb8c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
@@ -1,77 +1,37 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
- public class SpinnerSpmCounter : Container
+ public class SpinnerSpmCalculator : Component
{
+ private readonly Queue records = new Queue();
+ private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
+
+ ///
+ /// The resultant spins per minute value, which is updated via .
+ ///
+ public IBindable Result => result;
+
+ private readonly Bindable result = new BindableDouble();
+
[Resolved]
private DrawableHitObject drawableSpinner { get; set; }
- private readonly OsuSpriteText spmText;
-
- public SpinnerSpmCounter()
- {
- Children = new Drawable[]
- {
- spmText = new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = @"0",
- Font = OsuFont.Numeric.With(size: 24)
- },
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = @"SPINS PER MINUTE",
- Font = OsuFont.Numeric.With(size: 12),
- Y = 30
- }
- };
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
drawableSpinner.HitObjectApplied += resetState;
}
- private double spm;
-
- public double SpinsPerMinute
- {
- get => spm;
- private set
- {
- if (value == spm) return;
-
- spm = value;
- spmText.Text = Math.Truncate(value).ToString(@"#0");
- }
- }
-
- private struct RotationRecord
- {
- public float Rotation;
- public double Time;
- }
-
- private readonly Queue records = new Queue();
- private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
-
public void SetRotation(float currentRotation)
{
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
@@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
record = records.Dequeue();
- SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
+ result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
}
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
@@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private void resetState(DrawableHitObject hitObject)
{
- SpinsPerMinute = 0;
+ result.Value = 0;
records.Clear();
}
@@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
if (drawableSpinner != null)
drawableSpinner.HitObjectApplied -= resetState;
}
+
+ private struct RotationRecord
+ {
+ public float Rotation;
+ public double Time;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs
index 314139d02a..7a8555d991 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
+ bool centre = skin.GetConfig(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
spin = skin.GetConfig(OsuSkinConfiguration.CursorRotate)?.Value ?? true;
InternalChildren = new[]
@@ -32,13 +33,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Texture = skin.GetTexture("cursor"),
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ Origin = centre ? Anchor.Centre : Anchor.TopLeft,
},
new NonPlayfieldSprite
{
Texture = skin.GetTexture("cursormiddle"),
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ Origin = centre ? Anchor.Centre : Anchor.TopLeft,
},
};
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
index af9ea99232..0025576325 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
@@ -26,7 +26,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
- Blending = !disjointTrail ? BlendingParameters.Additive : BlendingParameters.Inherit;
+ if (disjointTrail)
+ {
+ bool centre = skin.GetConfig(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
+
+ TrailOrigin = centre ? Anchor.Centre : Anchor.TopLeft;
+ Blending = BlendingParameters.Inherit;
+ }
+ else
+ {
+ Blending = BlendingParameters.Additive;
+ }
if (Texture != null)
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index 545e80a709..ffbeea5e0e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly Bindable accentColour = new Bindable();
private readonly IBindable indexInCurrentCombo = new Bindable();
+ private readonly IBindable armedState = new Bindable();
[Resolved]
private DrawableHitObject drawableObject { get; set; }
@@ -114,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
+ armedState.BindTo(drawableObject.State);
Texture getTextureWithFallback(string name)
{
@@ -139,17 +141,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
- drawableObject.ApplyCustomUpdateState += updateState;
- updateState(drawableObject, drawableObject.State.Value);
+ armedState.BindValueChanged(animate, true);
}
- private void updateState(DrawableHitObject drawableObject, ArmedState state)
+ private void animate(ValueChangedEvent state)
{
const double legacy_fade_duration = 240;
- using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
+ ClearTransforms(true);
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
- switch (state)
+ switch (state.NewValue)
{
case ArmedState.Hit:
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index efeca53969..22fb3aab86 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -37,9 +37,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
AddInternal(scaleContainer = new Container
{
Scale = new Vector2(SPRITE_SCALE),
- Anchor = Anchor.Centre,
+ Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
+ Y = SPINNER_Y_CENTRE,
Children = new Drawable[]
{
glow = new Sprite
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
index 4e07cb60b3..19cb55c16e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
@@ -33,47 +33,38 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
- AddInternal(new Container
+ AddRangeInternal(new Drawable[]
{
- // the old-style spinner relied heavily on absolute screen-space coordinate values.
- // wrap everything in a container simulating absolute coords to preserve alignment
- // as there are skins that depend on it.
- Width = 640,
- Height = 480,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new Drawable[]
+ new Sprite
{
- new Sprite
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-background"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Y = SPINNER_Y_CENTRE,
+ },
+ disc = new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-circle"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Y = SPINNER_Y_CENTRE,
+ },
+ metre = new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ // this anchor makes no sense, but that's what stable uses.
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET },
+ Masking = true,
+ Child = metreSprite = new Sprite
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-background"),
- Scale = new Vector2(SPRITE_SCALE)
- },
- disc = new Sprite
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-circle"),
- Scale = new Vector2(SPRITE_SCALE)
- },
- metre = new Container
- {
- AutoSizeAxes = Axes.Both,
- // this anchor makes no sense, but that's what stable uses.
+ Texture = source.GetTexture("spinner-metre"),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
- // adjustment for stable (metre has additional offset)
- Margin = new MarginPadding { Top = 20 },
- Masking = true,
- Child = metreSprite = new Sprite
- {
- Texture = source.GetTexture("spinner-metre"),
- Anchor = Anchor.TopLeft,
- Origin = Anchor.TopLeft,
- Scale = new Vector2(SPRITE_SCALE)
- }
+ Scale = new Vector2(SPRITE_SCALE)
}
}
});
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index ec7ecb0d28..959589620b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -16,50 +17,115 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public abstract class LegacySpinner : CompositeDrawable
{
+ ///
+ /// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
+ /// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
+ /// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
+ ///
+ protected const float SPINNER_TOP_OFFSET = 45f - 16f;
+
+ protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
+
protected const float SPRITE_SCALE = 0.625f;
+ private const float spm_hide_offset = 50f;
+
protected DrawableSpinner DrawableSpinner { get; private set; }
private Sprite spin;
private Sprite clear;
+ private LegacySpriteText bonusCounter;
+
+ private Sprite spmBackground;
+ private LegacySpriteText spmCounter;
+
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
{
- RelativeSizeAxes = Axes.Both;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ // osu!stable positions spinner components in window-space (as opposed to gamefield-space). This is a 640x480 area taking up the entire screen.
+ // In lazer, the gamefield-space positional transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to make this area take up the entire window space.
+ Size = new Vector2(640, 480);
+ Position = new Vector2(0, -8f);
DrawableSpinner = (DrawableSpinner)drawableHitObject;
- AddRangeInternal(new[]
+ AddInternal(new Container
{
- spin = new Sprite
+ Depth = float.MinValue,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Depth = float.MinValue,
- Texture = source.GetTexture("spinner-spin"),
- Scale = new Vector2(SPRITE_SCALE),
- Y = 120 - 45 // offset temporarily to avoid overlapping default spin counter
- },
- clear = new Sprite
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Depth = float.MinValue,
- Alpha = 0,
- Texture = source.GetTexture("spinner-clear"),
- Scale = new Vector2(SPRITE_SCALE),
- Y = -60
- },
+ spin = new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-spin"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Y = SPINNER_TOP_OFFSET + 335,
+ },
+ clear = new Sprite
+ {
+ Alpha = 0,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-clear"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Y = SPINNER_TOP_OFFSET + 115,
+ },
+ bonusCounter = new LegacySpriteText(LegacyFont.Score)
+ {
+ Alpha = 0f,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(SPRITE_SCALE),
+ Y = SPINNER_TOP_OFFSET + 299,
+ }.With(s => s.Font = s.Font.With(fixedWidth: false)),
+ spmBackground = new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopLeft,
+ Texture = source.GetTexture("spinner-rpm"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Position = new Vector2(-87, 445 + spm_hide_offset),
+ },
+ spmCounter = new LegacySpriteText(LegacyFont.Score)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopRight,
+ Scale = new Vector2(SPRITE_SCALE * 0.9f),
+ Position = new Vector2(80, 448 + spm_hide_offset),
+ }.With(s => s.Font = s.Font.With(fixedWidth: false)),
+ }
});
}
+ private IBindable gainedBonus;
+ private IBindable spinsPerMinute;
+
private readonly Bindable completed = new Bindable();
protected override void LoadComplete()
{
base.LoadComplete();
+ gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy();
+ gainedBonus.BindValueChanged(bonus =>
+ {
+ bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
+ bonusCounter.FadeOutFromOne(800, Easing.Out);
+ bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
+ });
+
+ spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
completed.BindValueChanged(onCompletedChanged, true);
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
@@ -103,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (drawableHitObject)
{
case DrawableSpinner d:
- double fadeOutLength = Math.Min(400, d.HitObject.Duration);
+ using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn))
+ {
+ spmBackground.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
+ spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
+ }
- using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true))
- spin.FadeOutFromOne(fadeOutLength);
+ double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
+
+ using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
+ spin.FadeOutFromOne(spinFadeOutLength);
break;
case DrawableSpinnerTick d:
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index d74f885573..ffd4f78400 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -34,93 +34,90 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
public override Drawable GetDrawableComponent(ISkinComponent component)
{
- if (!(component is OsuSkinComponent osuComponent))
- return null;
-
- switch (osuComponent.Component)
+ if (component is OsuSkinComponent osuComponent)
{
- case OsuSkinComponents.FollowPoint:
- return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false);
+ switch (osuComponent.Component)
+ {
+ case OsuSkinComponents.FollowPoint:
+ return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false);
- case OsuSkinComponents.SliderFollowCircle:
- var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
- if (followCircle != null)
- // follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
- followCircle.Scale *= 0.5f;
- return followCircle;
+ case OsuSkinComponents.SliderFollowCircle:
+ var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
+ if (followCircle != null)
+ // follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
+ followCircle.Scale *= 0.5f;
+ return followCircle;
- case OsuSkinComponents.SliderBall:
- var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
+ case OsuSkinComponents.SliderBall:
+ var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
- // todo: slider ball has a custom frame delay based on velocity
- // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
+ // todo: slider ball has a custom frame delay based on velocity
+ // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
- if (sliderBallContent != null)
- return new LegacySliderBall(sliderBallContent);
+ if (sliderBallContent != null)
+ return new LegacySliderBall(sliderBallContent);
- return null;
+ return null;
- case OsuSkinComponents.SliderBody:
- if (hasHitCircle.Value)
- return new LegacySliderBody();
+ case OsuSkinComponents.SliderBody:
+ if (hasHitCircle.Value)
+ return new LegacySliderBody();
- return null;
+ return null;
- case OsuSkinComponents.SliderTailHitCircle:
- if (hasHitCircle.Value)
- return new LegacyMainCirclePiece("sliderendcircle", false);
+ case OsuSkinComponents.SliderTailHitCircle:
+ if (hasHitCircle.Value)
+ return new LegacyMainCirclePiece("sliderendcircle", false);
- return null;
+ return null;
- case OsuSkinComponents.SliderHeadHitCircle:
- if (hasHitCircle.Value)
- return new LegacyMainCirclePiece("sliderstartcircle");
+ case OsuSkinComponents.SliderHeadHitCircle:
+ if (hasHitCircle.Value)
+ return new LegacyMainCirclePiece("sliderstartcircle");
- return null;
+ return null;
- case OsuSkinComponents.HitCircle:
- if (hasHitCircle.Value)
- return new LegacyMainCirclePiece();
+ case OsuSkinComponents.HitCircle:
+ if (hasHitCircle.Value)
+ return new LegacyMainCirclePiece();
- return null;
+ return null;
- case OsuSkinComponents.Cursor:
- if (Source.GetTexture("cursor") != null)
- return new LegacyCursor();
+ case OsuSkinComponents.Cursor:
+ if (Source.GetTexture("cursor") != null)
+ return new LegacyCursor();
- return null;
+ return null;
- case OsuSkinComponents.CursorTrail:
- if (Source.GetTexture("cursortrail") != null)
- return new LegacyCursorTrail();
+ case OsuSkinComponents.CursorTrail:
+ if (Source.GetTexture("cursortrail") != null)
+ return new LegacyCursorTrail();
- return null;
+ return null;
- case OsuSkinComponents.HitCircleText:
- var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
- var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
+ case OsuSkinComponents.HitCircleText:
+ if (!this.HasFont(LegacyFont.HitCircle))
+ return null;
- return !this.HasFont(font)
- ? null
- : new LegacySpriteText(Source, font)
+ return new LegacySpriteText(LegacyFont.HitCircle)
{
// stable applies a blanket 0.8x scale to hitcircle fonts
Scale = new Vector2(0.8f),
- Spacing = new Vector2(-overlap, 0)
};
- case OsuSkinComponents.SpinnerBody:
- bool hasBackground = Source.GetTexture("spinner-background") != null;
+ case OsuSkinComponents.SpinnerBody:
+ bool hasBackground = Source.GetTexture("spinner-background") != null;
- if (Source.GetTexture("spinner-top") != null && !hasBackground)
- return new LegacyNewStyleSpinner();
- else if (hasBackground)
- return new LegacyOldStyleSpinner();
+ if (Source.GetTexture("spinner-top") != null && !hasBackground)
+ return new LegacyNewStyleSpinner();
+ else if (hasBackground)
+ return new LegacyOldStyleSpinner();
- return null;
+ return null;
+ }
}
- return null;
+ return Source.GetDrawableComponent(component);
}
public override IBindable GetConfig(TLookup lookup)
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 63c9b53278..6953e66b5c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -5,11 +5,10 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public enum OsuSkinConfiguration
{
- HitCirclePrefix,
- HitCircleOverlap,
SliderBorderSize,
SliderPathRadius,
AllowSliderBallTint,
+ CursorCentre,
CursorExpand,
CursorRotate,
HitCircleOverlayAboveNumber,
diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
index 88c855d768..cb769c31b8 100644
--- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
+++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
@@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
var point = new HitPoint(pointType, this)
{
- Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255)
+ BaseColour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255)
};
points[r][c] = point;
@@ -234,6 +234,11 @@ namespace osu.Game.Rulesets.Osu.Statistics
private class HitPoint : Circle
{
+ ///
+ /// The base colour which will be lightened/darkened depending on the value of this .
+ ///
+ public Color4 BaseColour;
+
private readonly HitPointType pointType;
private readonly AccuracyHeatmap heatmap;
@@ -284,7 +289,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
Alpha = Math.Min(amount / lighten_cutoff, 1);
if (pointType == HitPointType.Hit)
- Colour = ((Color4)Colour).Lighten(Math.Max(0, amount - lighten_cutoff));
+ Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff));
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 0b30c28b8d..7f86e9daf7 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Vertices;
@@ -31,6 +32,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private double timeOffset;
private float time;
+ private Anchor trailOrigin = Anchor.Centre;
+
+ protected Anchor TrailOrigin
+ {
+ get => trailOrigin;
+ set
+ {
+ trailOrigin = value;
+ Invalidate(Invalidation.DrawNode);
+ }
+ }
+
public CursorTrail()
{
// as we are currently very dependent on having a running clock, let's make our own clock for the time being.
@@ -197,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size;
+ private Vector2 originPosition;
+
private readonly QuadBatch vertexBatch = new QuadBatch(max_sprites, 1);
public TrailDrawNode(CursorTrail source)
@@ -213,6 +228,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
size = Source.partSize;
time = Source.time;
+ originPosition = Vector2.Zero;
+
+ if (Source.TrailOrigin.HasFlagFast(Anchor.x1))
+ originPosition.X = 0.5f;
+ else if (Source.TrailOrigin.HasFlagFast(Anchor.x2))
+ originPosition.X = 1f;
+
+ if (Source.TrailOrigin.HasFlagFast(Anchor.y1))
+ originPosition.Y = 0.5f;
+ else if (Source.TrailOrigin.HasFlagFast(Anchor.y2))
+ originPosition.Y = 1f;
+
Source.parts.CopyTo(parts, 0);
}
@@ -237,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
- Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y + size.Y / 2),
+ Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
@@ -246,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
- Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y + size.Y / 2),
+ Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear,
@@ -255,7 +282,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
- Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y - size.Y / 2),
+ Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear,
@@ -264,7 +291,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
- Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y - size.Y / 2),
+ Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear,
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index b1069149f3..8993a9b18a 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -42,6 +42,9 @@ namespace osu.Game.Rulesets.Osu.UI
public OsuPlayfield()
{
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
@@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.UI
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
- poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded));
+ poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
AddRangeInternal(poolDictionary.Values);
@@ -99,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.UI
}
}
- private void onJudgmentLoaded(DrawableOsuJudgement judgement)
+ private void onJudgementLoaded(DrawableOsuJudgement judgement)
{
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
index ec7751d2b4..27d48d1296 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
@@ -33,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.UI
{
Add(cursorScaleContainer = new Container
{
- RelativePositionAxes = Axes.Both,
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
});
}
@@ -43,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.UI
base.PopIn();
GameplayCursor.ActiveCursor.Hide();
- cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
+ cursorScaleContainer.Position = ToLocalSpace(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre);
clickToResumeCursor.Appear();
if (localCursorContainer == null)
diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
index 392442b713..4d4dabebe6 100644
--- a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
@@ -14,6 +14,11 @@
Properties\AndroidManifest.xml
armeabi-v7a;x86;arm64-v8a
+
+ None
+ cjk;mideast;other;rare;west
+ true
+
@@ -35,5 +40,10 @@
osu.Game
+
+
+ 5.0.0
+
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs
index a1d000386f..ac01508081 100644
--- a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[SetUpSteps]
public void SetUp()
- => AddStep("clear SHOC", () => hitObjectContainer.Clear(false));
+ => AddStep("clear SHOC", () => hitObjectContainer.Clear());
protected void AddHitObject(DrawableHitObject hitObject)
=> AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject));
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
index ff309f278e..f9b8e9a985 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- AddStep("Bar line", () => SetContents(() =>
+ AddStep("Bar line", () => SetContents(_ =>
{
ScrollingHitObjectContainer hoc;
@@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
return cont;
}));
- AddStep("Bar line (major)", () => SetContents(() =>
+ AddStep("Bar line (major)", () => SetContents(_ =>
{
ScrollingHitObjectContainer hoc;
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
index 44646e5fc9..26a4e85fe5 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- AddStep("Drum roll", () => SetContents(() =>
+ AddStep("Drum roll", () => SetContents(_ =>
{
var hoc = new ScrollingHitObjectContainer();
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
return hoc;
}));
- AddStep("Drum roll (strong)", () => SetContents(() =>
+ AddStep("Drum roll (strong)", () => SetContents(_ =>
{
var hoc = new ScrollingHitObjectContainer();
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
index 9930d97d31..c4ee68206c 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
@@ -17,25 +17,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- AddStep("Centre hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
+ AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
- AddStep("Centre hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
+ AddStep("Centre hit (strong)", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
- AddStep("Rim hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
+ AddStep("Rim hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
- AddStep("Rim hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
+ AddStep("Rim hit (strong)", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
index 8d1aafdcc2..b976735223 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
@@ -54,16 +54,16 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
AddStep("set beatmap", () => setBeatmap());
- AddStep("clear state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Clear)));
- AddStep("idle state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Idle)));
- AddStep("kiai state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai)));
- AddStep("fail state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Fail)));
+ AddStep("clear state", () => SetContents(_ => new TaikoMascotAnimation(TaikoMascotAnimationState.Clear)));
+ AddStep("idle state", () => SetContents(_ => new TaikoMascotAnimation(TaikoMascotAnimationState.Idle)));
+ AddStep("kiai state", () => SetContents(_ => new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai)));
+ AddStep("fail state", () => SetContents(_ => new TaikoMascotAnimation(TaikoMascotAnimationState.Fail)));
}
[Test]
public void TestInitialState()
{
- AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
+ AddStep("create mascot", () => SetContents(_ => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
AddAssert("mascot initially idle", () => allMascotsIn(TaikoMascotAnimationState.Idle));
}
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
AddStep("set beatmap", () => setBeatmap());
- AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
+ AddStep("create mascot", () => SetContents(_ => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }));
@@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
Beatmap.Value.Track.Start();
- SetContents(() =>
+ SetContents(_ =>
{
var ruleset = new TaikoRuleset();
return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo));
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
index fecb5d4a74..1cba6c9008 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
@@ -2,8 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
@@ -13,19 +17,24 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture]
public class TestSceneHitExplosion : TaikoSkinnableTestScene
{
+ protected override double TimePerAction => 100;
+
[Test]
public void TestNormalHit()
{
- AddStep("Great", () => SetContents(() => getContentFor(createHit(HitResult.Great))));
- AddStep("Ok", () => SetContents(() => getContentFor(createHit(HitResult.Ok))));
- AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss))));
+ AddStep("Great", () => SetContents(_ => getContentFor(createHit(HitResult.Great))));
+ AddStep("Ok", () => SetContents(_ => getContentFor(createHit(HitResult.Ok))));
+ AddStep("Miss", () => SetContents(_ => getContentFor(createHit(HitResult.Miss))));
}
- [Test]
- public void TestStrongHit([Values(false, true)] bool hitBoth)
+ [TestCase(HitResult.Great)]
+ [TestCase(HitResult.Ok)]
+ public void TestStrongHit(HitResult type)
{
- AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth))));
- AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Ok, hitBoth))));
+ AddStep("create hit", () => SetContents(_ => getContentFor(createStrongHit(type))));
+ AddStep("visualise second hit",
+ () => this.ChildrenOfType()
+ .ForEach(e => e.VisualiseSecondHit(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement()))));
}
private Drawable getContentFor(DrawableTestHit hit)
@@ -38,17 +47,17 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
// the hit needs to be added to hierarchy in order for nested objects to be created correctly.
// setting zero alpha is supposed to prevent the test from looking broken.
hit.With(h => h.Alpha = 0),
- new HitExplosion(hit, hit.Type)
+ new HitExplosion(hit.Type)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- }
+ }.With(explosion => explosion.Apply(hit))
}
};
}
private DrawableTestHit createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
- private DrawableTestHit createStrongHit(HitResult type, bool hitBoth) => new DrawableTestStrongHit(Time.Current, type, hitBoth);
+ private DrawableTestHit createStrongHit(HitResult type) => new DrawableTestStrongHit(Time.Current, type);
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
index fa6c9da174..055a292fe8 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new TaikoInputManager(new RulesetInfo { ID = 1 })
+ SetContents(_ => new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{
RelativeSizeAxes = Axes.Both,
Child = new Container
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs
index b558709592..419e100296 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs
@@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestKiaiHits()
{
- AddStep("rim hit", () => SetContents(() => getContentFor(createHit(HitType.Rim))));
- AddStep("centre hit", () => SetContents(() => getContentFor(createHit(HitType.Centre))));
+ AddStep("rim hit", () => SetContents(_ => getContentFor(createHit(HitType.Rim))));
+ AddStep("centre hit", () => SetContents(_ => getContentFor(createHit(HitType.Centre))));
}
private Drawable getContentFor(DrawableTestHit hit)
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
index 7b7e2c43d1..f96297a06d 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Beatmap.Value.Track.Start();
});
- AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo())
+ AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield(new ControlPointInfo())
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
index 4ae3cbd418..9882c7bc90 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
@@ -5,6 +5,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
using osu.Game.Skinning;
@@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
public TestSceneTaikoScroller()
{
- AddStep("Load scroller", () => SetContents(() =>
+ AddStep("Load scroller", () => SetContents(_ =>
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
{
Clock = new FramedClock(clock),
@@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}));
AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value =
- new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Great : HitResult.Miss }));
+ new JudgementResult(new HitObject(), new Judgement()) { Type = passing ? HitResult.Great : HitResult.Miss }));
AddToggleStep("toggle playback direction", reversed => this.reversed = reversed);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index eb21c02d5f..dd3c6b317a 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(3.1473940254109078d, "diffcalc-test")]
- [TestCase(3.1473940254109078d, "diffcalc-test-strong")]
+ [TestCase(3.1704781712282624d, "diffcalc-test")]
+ [TestCase(3.1704781712282624d, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs
index a970965141..f33c738b04 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
StartTime = 400,
Major = true
- }), null));
+ })));
AddHitObject(barLine);
RemoveHitObject(barLine);
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
StartTime = 200,
Major = false
- }), null));
+ })));
AddHitObject(barLine);
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs
index 54450e27db..c389a05566 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Duration = 500,
IsStrong = false,
TickRate = 2
- }), null));
+ })));
AddHitObject(drumRoll);
RemoveHitObject(drumRoll);
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Duration = 400,
IsStrong = true,
TickRate = 16
- }), null));
+ })));
AddHitObject(drumRoll);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs
index 52fd440857..c2f251fcb6 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Type = HitType.Rim,
IsStrong = false,
StartTime = 300
- }), null));
+ })));
AddHitObject(hit);
RemoveHitObject(hit);
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Type = HitType.Centre,
IsStrong = true,
StartTime = 500
- }), null));
+ })));
AddHitObject(hit);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
index 7695ca067b..87c936d386 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
@@ -11,7 +11,6 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
@@ -108,12 +107,12 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
- Hit hit = new Hit();
+ Hit hit = new Hit { StartTime = DrawableRuleset.Playfield.Time.Current };
var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
DrawableRuleset.Playfield.Add(h);
- ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
+ ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(hit, new TaikoJudgement()) { Type = hitResult });
}
private void addStrongHitJudgement(bool kiai)
@@ -122,6 +121,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit hit = new Hit
{
+ StartTime = DrawableRuleset.Playfield.Time.Current,
IsStrong = true,
Samples = createSamples(strong: true)
};
@@ -129,8 +129,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
DrawableRuleset.Playfield.Add(h);
- ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
- ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
+ ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(hit, new TaikoJudgement()) { Type = hitResult });
+ ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(hit.NestedHitObjects.Single(), new TaikoStrongJudgement()) { Type = HitResult.Great });
}
private void addMissJudgement()
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs
index 7089ea6619..221d715a35 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
- protected override IResourceStore Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples)));
+ protected override IResourceStore RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples)));
[TestCase("taiko-normal-hitnormal")]
[TestCase("normal-hitnormal")]
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 2b084f3bee..8fb167ba10 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,8 +2,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index b51f096d7d..90c99316b1 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -79,8 +79,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// Old osu! used hit sounding to determine various hit type information
IList samples = obj.Samples;
- bool strong = samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
-
switch (obj)
{
case IHasDistance distanceData:
@@ -94,15 +92,11 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
{
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);
yield return new Hit
{
StartTime = j,
- Type = isRim ? HitType.Rim : HitType.Centre,
Samples = currentSamples,
- IsStrong = strong
};
i = (i + 1) % allSamples.Count;
@@ -117,7 +111,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
StartTime = obj.StartTime,
Samples = obj.Samples,
- IsStrong = strong,
Duration = taikoDuration,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4
};
@@ -143,16 +136,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
default:
{
- bool isRimDefinition(HitSampleInfo s) => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE;
-
- bool isRim = samples.Any(isRimDefinition);
-
yield return new Hit
{
StartTime = obj.StartTime,
- Type = isRim ? HitType.Rim : HitType.Centre,
Samples = samples,
- IsStrong = strong
};
break;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
index cc0738e252..769d021362 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Calculates the colour coefficient of taiko difficulty.
///
- public class Colour : Skill
+ public class Colour : StrainSkill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
index f2b8309ac5..a32f6ebe0d 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Calculates the rhythm coefficient of taiko difficulty.
///
- public class Rhythm : Skill
+ public class Rhythm : StrainSkill
{
protected override double SkillMultiplier => 10;
protected override double StrainDecayBase => 0;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index c34cce0cd6..4cceadb23f 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
///
- public class Stamina : Skill
+ public class Stamina : StrainSkill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index fc198d2493..6b3e31c5d5 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -133,11 +133,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
List peaks = new List();
- for (int i = 0; i < colour.StrainPeaks.Count; i++)
+ var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
+ var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
+ var staminaRightPeaks = staminaRight.GetCurrentStrainPeaks().ToList();
+ var staminaLeftPeaks = staminaLeft.GetCurrentStrainPeaks().ToList();
+
+ for (int i = 0; i < colourPeaks.Count; i++)
{
- double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier;
- double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier;
- double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
+ double colourPeak = colourPeaks[i] * colour_skill_multiplier;
+ double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
+ double staminaPeak = (staminaRightPeaks[i] + staminaLeftPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak));
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
index 17e7fb81f6..0d0fd136a7 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
@@ -14,10 +14,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{
private readonly HitPiece piece;
- private static Hit hit;
+ public new Hit HitObject => (Hit)base.HitObject;
public HitPlacementBlueprint()
- : base(hit = new Hit())
+ : base(new Hit())
{
InternalChild = piece = new HitPiece
{
@@ -30,12 +30,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
switch (e.Button)
{
case MouseButton.Left:
- hit.Type = HitType.Centre;
+ HitObject.Type = HitType.Centre;
EndPlacement(true);
return true;
case MouseButton.Right:
- hit.Type = HitType.Rim;
+ HitObject.Type = HitType.Rim;
EndPlacement(true);
return true;
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs
index 62f69122cc..01b90c4bca 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs
@@ -3,14 +3,14 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{
- public class TaikoSelectionBlueprint : OverlaySelectionBlueprint
+ public class TaikoSelectionBlueprint : HitObjectSelectionBlueprint
{
- public TaikoSelectionBlueprint(DrawableHitObject hitObject)
+ public TaikoSelectionBlueprint(HitObject hitObject)
: base(hitObject)
{
RelativeSizeAxes = Axes.None;
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
index e53b331f46..59249e6bf4 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{
base.UpdateTimeAndPosition(result);
- if (PlacementActive)
+ if (PlacementActive == PlacementState.Active)
{
if (result.Time is double dragTime)
{
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs
index 8b41448c9d..a465638779 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
using osu.Game.Screens.Edit.Compose.Components;
@@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
- protected override SelectionHandler CreateSelectionHandler() => new TaikoSelectionHandler();
+ protected override SelectionHandler CreateSelectionHandler() => new TaikoSelectionHandler();
- public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) =>
+ public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) =>
new TaikoSelectionBlueprint(hitObject);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
index 3fbcee44af..ab3b729307 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
@@ -8,12 +8,13 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Taiko.Edit
{
- public class TaikoSelectionHandler : SelectionHandler
+ public class TaikoSelectionHandler : EditorSelectionHandler
{
private readonly Bindable selectionRimState = new Bindable();
private readonly Bindable selectionStrongState = new Bindable();
@@ -52,44 +53,43 @@ namespace osu.Game.Rulesets.Taiko.Edit
public void SetStrongState(bool state)
{
- var hits = EditorBeatmap.SelectedHitObjects.OfType();
-
- EditorBeatmap.BeginChange();
-
- foreach (var h in hits)
+ EditorBeatmap.PerformOnSelection(h =>
{
- if (h.IsStrong != state)
- {
- h.IsStrong = state;
- EditorBeatmap.Update(h);
- }
- }
+ if (!(h is Hit taikoHit)) return;
- EditorBeatmap.EndChange();
+ if (taikoHit.IsStrong != state)
+ {
+ taikoHit.IsStrong = state;
+ EditorBeatmap.Update(taikoHit);
+ }
+ });
}
public void SetRimState(bool state)
{
- var hits = EditorBeatmap.SelectedHitObjects.OfType();
-
- EditorBeatmap.BeginChange();
-
- foreach (var h in hits)
- h.Type = state ? HitType.Rim : HitType.Centre;
-
- EditorBeatmap.EndChange();
+ EditorBeatmap.PerformOnSelection(h =>
+ {
+ if (h is Hit taikoHit)
+ {
+ taikoHit.Type = state ? HitType.Rim : HitType.Centre;
+ EditorBeatmap.Update(h);
+ }
+ });
}
- protected override IEnumerable