diff --git a/.editorconfig b/.editorconfig
index a5f7795882..0cdf3b92d3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -194,4 +194,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
\ No newline at end of file
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/02-feature-request-issues.md
similarity index 62%
rename from .github/ISSUE_TEMPLATE/03-feature-request-issues.md
rename to .github/ISSUE_TEMPLATE/02-feature-request-issues.md
index 54c4ff94e5..c3357dd780 100644
--- a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md
+++ b/.github/ISSUE_TEMPLATE/02-feature-request-issues.md
@@ -1,6 +1,6 @@
---
name: Feature Request
-about: Features you would like to see in the game!
+about: Propose a feature you would like to see in the game!
---
**Describe the new feature:**
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 efca075042..eb790ca18f 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
# osu!
[](https://ci.appveyor.com/project/peppy/osu)
-[]()
+[](https://github.com/ppy/osu/releases/latest)
[](https://www.codefactor.io/repository/github/ppy/osu)
[](https://discord.gg/ppy)
@@ -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).
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..992f954a3a
--- /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..6d8d4215a2
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Replays;
+using osu.Game.Rulesets.EmptyFreeform.Objects;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Replays
+{
+ public class EmptyFreeformAutoGenerator : AutoGenerator
+ {
+ protected Replay Replay;
+ protected List Frames => Replay.Frames;
+
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public EmptyFreeformAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ Replay = new Replay();
+ }
+
+ public override Replay Generate()
+ {
+ 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.
+ });
+ }
+
+ return Replay;
+ }
+ }
+}
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..7571d1827a
--- /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..9c54b82e38
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonAutoGenerator : AutoGenerator
+ {
+ protected Replay Replay;
+ protected List Frames => Replay.Frames;
+
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public PippidonAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ Replay = new Replay();
+ }
+
+ public override Replay Generate()
+ {
+ Frames.Add(new PippidonReplayFrame());
+
+ foreach (PippidonHitObject hitObject in Beatmap.HitObjects)
+ {
+ Frames.Add(new PippidonReplayFrame
+ {
+ Time = hitObject.StartTime,
+ Position = hitObject.Position,
+ });
+ }
+
+ return Replay;
+ }
+ }
+}
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/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs
new file mode 100644
index 0000000000..1c4fe698c2
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.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.Scoring;
+
+namespace osu.Game.Rulesets.Pippidon.Scoring
+{
+ public class PippidonScoreProcessor : ScoreProcessor
+ {
+ }
+}
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..1c8ed54440
--- /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..7923918842
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Replays;
+using osu.Game.Rulesets.EmptyScrolling.Objects;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Replays
+{
+ public class EmptyScrollingAutoGenerator : AutoGenerator
+ {
+ protected Replay Replay;
+ protected List Frames => Replay.Frames;
+
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public EmptyScrollingAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ Replay = new Replay();
+ }
+
+ public override Replay Generate()
+ {
+ Frames.Add(new EmptyScrollingReplayFrame());
+
+ foreach (EmptyScrollingHitObject hitObject in Beatmap.HitObjects)
+ {
+ Frames.Add(new EmptyScrollingReplayFrame
+ {
+ Time = hitObject.StartTime
+ // todo: add required inputs and extra frames.
+ });
+ }
+
+ return Replay;
+ }
+ }
+}
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..7571d1827a
--- /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..bd99cdcdbd
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
@@ -0,0 +1,68 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Replays;
+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
+ {
+ protected Replay Replay;
+ protected List Frames => Replay.Frames;
+
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public PippidonAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ Replay = new Replay();
+ }
+
+ public override Replay Generate()
+ {
+ 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;
+ }
+
+ return Replay;
+ }
+
+ 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 9ad5946311..5aee9e15cc 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 788e5f82be..cffcea22c2 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -17,10 +17,10 @@ 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" })]
+ [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" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
@@ -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/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
index 63b12fb84b..832d26b0ef 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -105,7 +105,7 @@ namespace osu.Desktop
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
presence.Assets.LargeImageText = string.Empty;
else
- presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty);
+ presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
// update ruleset
presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom";
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index d1515acafa..4a28ab3722 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,8 @@ 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
{
@@ -32,7 +36,7 @@ namespace osu.Desktop
noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false;
}
- public override Storage GetStorageForStableInstall()
+ public override StableStorage GetStorageForStableInstall()
{
try
{
@@ -40,7 +44,7 @@ namespace osu.Desktop
{
string stablePath = getStableInstallPath();
if (!string.IsNullOrEmpty(stablePath))
- return new DesktopStorage(stablePath, desktopHost);
+ return new StableStorage(stablePath, desktopHost);
}
}
catch (Exception)
@@ -110,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)
@@ -135,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 4554f8b83a..ad5c323e9b 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,16 +24,14 @@
+
+
-
+
-
-
-
-
-
+
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 7805bfcefc..bfcf4ef35e 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -8,7 +8,7 @@
-
+
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 ee416e5a38..5580358f89 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
+using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Tests.Beatmaps;
@@ -17,6 +18,10 @@ namespace osu.Game.Rulesets.Catch.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
+ [TestCase(5.169743871843191d, "diffcalc-test")]
+ public void TestClockRateAdjusted(double expected, string name)
+ => Test(expected, name, new CatchModDoubleTime());
+
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new CatchRuleset();
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/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..ad404e1f63 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,6 +90,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{
circleSize = size;
+ 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/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 54fddc297e..77e9d672e3 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..e3c457693e 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:
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index a317ef252d..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)
@@ -69,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
}
}
- protected override Skill[] CreateSkills(IBeatmap beatmap)
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
{
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
@@ -78,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new Skill[]
{
- new Movement(halfCatcherWidth),
+ new Movement(mods, halfCatcherWidth),
};
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index e679231638..75e17f6c48 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -5,10 +5,11 @@ using System;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
+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;
@@ -19,13 +20,16 @@ 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;
private float lastDistanceMoved;
private double lastStrainTime;
- public Movement(float halfCatcherWidth)
+ public Movement(Mod[] mods, float halfCatcherWidth)
+ : base(mods)
{
HalfCatcherWidth = halfCatcherWidth;
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
index 692e63fa69..e1eceea606 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
index 3bc1ee5bf5..d53d019e90 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModCinema : ModCinema
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
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/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index 438d17dbc5..5f1736450a 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
- public BindableNumber CircleSize { get; } = new BindableFloat
+ public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
- public BindableNumber ApproachRate { get; } = new BindableFloat
+ public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
+ protected override void ApplyLimits(bool extended)
+ {
+ base.ApplyLimits(extended);
+
+ CircleSize.MaxValue = extended ? 11 : 10;
+ ApproachRate.MaxValue = extended ? 11 : 10;
+ }
+
public override string SettingDescription
{
get
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..bba42dea97 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,6 +20,14 @@ 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 ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
base.ApplyNormalVisibilityState(hitObject, state);
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 32e8ab5da7..10230b6b78 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -45,6 +45,11 @@ namespace osu.Game.Rulesets.Catch.Replays
float positionChange = Math.Abs(lastPosition - h.EffectiveX);
double timeAvailable = h.StartTime - lastTime;
+ if (timeAvailable < 0)
+ {
+ return;
+ }
+
// So we can either make it there without a dash or not.
// If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too)
// The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour.
@@ -120,10 +125,6 @@ namespace osu.Game.Rulesets.Catch.Replays
private void addFrame(double time, float? position = null, bool dashing = false)
{
- // todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
- if (Replay.Frames.Count == 0)
- Replay.Frames.Add(new CatchReplayFrame(time - 1, position, false, null));
-
var last = currentFrame;
currentFrame = new CatchReplayFrame(time, position, dashing, last);
Replay.Frames.Add(currentFrame);
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
index 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/Default/BorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs
index 7308d6b499..8d8ee49af7 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs
@@ -29,4 +29,3 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
}
}
}
-
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs
index d160956a6e..c8895f32f4 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs
@@ -19,4 +19,3 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
}
}
}
-
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index 41fd0fe776..1b48832ed6 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -5,7 +5,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Skinning;
using osuTK.Graphics;
-using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
@@ -14,7 +13,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)
@@ -69,7 +68,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter:
-
if (providesComboCounter)
return new LegacyCatchComboCounter(Source);
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
index f797ae75c2..28ee7bd813 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(skin, 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(skin, 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/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
index a25551f854..6e6500a339 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
@@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Difficulty;
+using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
@@ -17,6 +18,10 @@ namespace osu.Game.Rulesets.Mania.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
+ [TestCase(2.7879104989252959d, "diffcalc-test")]
+ public void TestClockRateAdjusted(double expected, string name)
+ => Test(expected, name, new ManiaModDoubleTime());
+
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs
new file mode 100644
index 0000000000..60363aaeef
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.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.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Rulesets.UI.Scrolling.Algorithms;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Mods
+{
+ public class TestSceneManiaModConstantSpeed : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ [Test]
+ public void TestConstantScroll() => CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModConstantSpeed(),
+ PassCondition = () =>
+ {
+ var hitObject = Player.ChildrenOfType().FirstOrDefault();
+ return hitObject?.Dependencies.Get().Algorithm is ConstantScrollAlgorithm;
+ }
+ });
+ }
+}
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/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 596430f9e5..668487f673 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -5,11 +5,13 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
@@ -286,17 +288,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,6 +413,14 @@ 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 head", () => currentPlayer.GameplayClockContainer.GameplayClock.CurrentTime >= time_head);
+ AddAssert("head is visible",
+ () => currentPlayer.ChildrenOfType()
+ .Single(note => note.HitObject == beatmap.HitObjects[0])
+ .Head
+ .Alpha == 1);
+
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
@@ -352,6 +428,8 @@ namespace osu.Game.Rulesets.Mania.Tests
{
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/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/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index d55b4fe08a..8f8b99b092 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..39d0f4bae4 100644
--- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
@@ -20,8 +20,8 @@ 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);
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index ade830764d..8c0b9ed8b7 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable SortObjects(IEnumerable input) => input;
- protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
{
- new Strain(((ManiaBeatmap)beatmap).TotalColumns)
+ new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
};
protected override Mod[] DifficultyAdjustmentMods
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
index 7ebc1ff752..2ba2ee6b4a 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -6,11 +6,11 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Mods;
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;
@@ -24,7 +24,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
private double individualStrain;
private double overallStrain;
- public Strain(int totalColumns)
+ public Strain(Mod[] mods, int totalColumns)
+ : base(mods)
{
holdEndTimes = new double[totalColumns];
individualStrains = new double[totalColumns];
@@ -34,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
@@ -44,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
@@ -71,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/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/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/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/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..2689ed4112 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -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,11 @@ 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 =>
+ {
+ if (h is ManiaHitObject maniaObj)
+ maniaObj.Column += columnDelta;
+ });
}
}
}
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 59c766fd84..b3889bc7d3 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;
@@ -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,7 +239,9 @@ namespace osu.Game.Rulesets.Mania
new ManiaModDualStages(),
new ManiaModMirror(),
new ManiaModDifficultyAdjust(),
+ new ManiaModClassic(),
new ManiaModInvert(),
+ new ManiaModConstantSpeed()
};
case ModType.Automation:
@@ -380,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/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
index c05e979e9a..105d88129c 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
index 02c1fc1b79..064c55ed8d 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModCinema : ModCinema
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
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/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
new file mode 100644
index 0000000000..078394b1d8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.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 osu.Framework.Graphics.Sprites;
+using osu.Game.Configuration;
+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 ManiaModConstantSpeed : Mod, IApplicableToDrawableRuleset
+ {
+ public override string Name => "Constant Speed";
+
+ public override string Acronym => "CS";
+
+ public override double ScoreMultiplier => 1;
+
+ public override string Description => "No more tricky speed changes!";
+
+ public override IconUsage? Icon => FontAwesome.Solid.Equals;
+
+ public override ModType Type => ModType.Conversion;
+
+ public override bool Ranked => false;
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
+ maniaRuleset.ScrollMethod = ScrollVisualisationMethod.Constant;
+ }
+ }
+}
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..87501d07a5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
@@ -0,0 +1,43 @@
+// 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.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;
+ }));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 4f062753a6..02829d87bd 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -221,7 +221,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 +233,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..35ba2465fa 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Objects.Drawables;
+
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
///
@@ -25,6 +27,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/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index 3ebbe5af8e..ada84dfac2 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -5,6 +5,7 @@ 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;
@@ -69,10 +70,6 @@ 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()));
}
@@ -85,20 +82,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 7e2a8823b6..24ccae895d 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -140,11 +140,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return animation == null ? null : new LegacyManiaJudgementPiece(result, animation);
}
- public override SampleChannel GetSample(ISampleInfo sampleInfo)
+ public override ISample GetSample(ISampleInfo sampleInfo)
{
// layered hit sounds never play in mania
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
- return new SampleChannelVirtual();
+ return new SampleVirtual();
return Source.GetSample(sampleInfo);
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 941ac9816c..4ee060e91e 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@@ -11,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -49,6 +51,22 @@ namespace osu.Game.Rulesets.Mania.UI
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
+ public ScrollVisualisationMethod ScrollMethod
+ {
+ get => scrollMethod;
+ set
+ {
+ if (IsLoaded)
+ throw new InvalidOperationException($"Can't alter {nameof(ScrollMethod)} after ruleset is already loaded");
+
+ scrollMethod = value;
+ }
+ }
+
+ private ScrollVisualisationMethod scrollMethod = ScrollVisualisationMethod.Sequential;
+
+ protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
+
private readonly Bindable configDirection = new Bindable();
private readonly Bindable configTimeRange = new BindableDouble();
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..6139b0e676
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs
@@ -0,0 +1,246 @@
+// 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.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)
+ {
+ Assert.That(check.Run(beatmap, new TestWorkingBeatmap(beatmap)), Is.Empty);
+ }
+
+ private void assertOffscreenCircle(IBeatmap beatmap)
+ {
+ var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
+ }
+
+ private void assertOffscreenSlider(IBeatmap beatmap)
+ {
+ var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).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/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..d7dfc3bd42
--- /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(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 TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
+ public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
+ public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
+
+ public TestSliderBlueprint(DrawableSlider slider)
+ : base(slider)
+ {
+ }
+
+ protected override SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new TestSliderCircleBlueprint(slider, position);
+ }
+
+ private class TestSliderCircleBlueprint : SliderCircleSelectionBlueprint
+ {
+ public new HitCirclePiece CirclePiece => base.CirclePiece;
+
+ public TestSliderCircleBlueprint(DrawableSlider slider, SliderPosition position)
+ : base(slider, position)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/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/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
new file mode 100644
index 0000000000..0ba775e5c7
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -0,0 +1,65 @@
+// 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.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Rulesets.Osu.UI;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModAutoplay : OsuModTestScene
+ {
+ [Test]
+ public void TestSpmUnaffectedByRateAdjust()
+ => runSpmTest(new OsuModDaycore
+ {
+ SpeedChange = { Value = 0.88 }
+ });
+
+ [Test]
+ public void TestSpmUnaffectedByTimeRamp()
+ => runSpmTest(new ModWindUp
+ {
+ InitialRate = { Value = 0.7 },
+ FinalRate = { Value = 1.3 }
+ });
+
+ private void runSpmTest(Mod mod)
+ {
+ SpinnerSpmCalculator spmCalculator = null;
+
+ CreateModTest(new ModTestData
+ {
+ Autoplay = true,
+ Mod = mod,
+ Beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new Spinner
+ {
+ Duration = 2000,
+ Position = OsuPlayfield.BASE_SIZE / 2
+ }
+ }
+ },
+ PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
+ });
+
+ AddUntilStep("fetch SPM calculator", () =>
+ {
+ spmCalculator = this.ChildrenOfType().SingleOrDefault();
+ return spmCalculator != null;
+ });
+
+ 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 85a41137d4..afd94f4570 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
@@ -14,11 +15,16 @@ 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.7212283220412345d, "diffcalc-test")]
+ [TestCase(1.3212137158641493d, "zero-length-sliders")]
+ public void TestClockRateAdjusted(double expected, string name)
+ => Test(expected, name, new OsuModDoubleTime());
+
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new OsuRuleset();
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 fefe983f97..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 SampleChannel 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..4395ca6281 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);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 461779b185..9a77292aff 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/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/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
index 39deba2f57..af67ab5839 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
@@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
@@ -65,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestAutoMod : OsuModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
- Replay = new MissingAutoGenerator(beatmap).Generate()
+ Replay = new MissingAutoGenerator(beatmap, mods).Generate()
};
}
@@ -76,8 +78,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap;
- public MissingAutoGenerator(IBeatmap beatmap)
- : base(beatmap)
+ public MissingAutoGenerator(IBeatmap beatmap, IReadOnlyList mods)
+ : base(beatmap, mods)
{
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
new file mode 100644
index 0000000000..77a68b714b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
@@ -0,0 +1,491 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
+ {
+ private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
+ private const double late_miss_window = 500; // time after +500 is considered a miss
+
+ ///
+ /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleBeforeFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Miss);
+ addJudgementOffsetAssert(hitObjects[0], late_miss_window);
+ }
+
+ ///
+ /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAtFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Miss);
+ addJudgementOffsetAssert(hitObjects[0], late_miss_window);
+ }
+
+ ///
+ /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAfterFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Miss);
+ addJudgementOffsetAssert(hitObjects[0], late_miss_window);
+ }
+
+ ///
+ /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
+ }
+
+ ///
+ /// Tests clicking a future circle after the first circle's start time, while the first circle HAS been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAfterFirstCircleTimeWithFirstCircleJudged()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
+ addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
+ }
+
+ ///
+ /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
+ ///
+ [Test]
+ public void TestMissSliderHeadAndHitAllSliderTicks()
+ {
+ const double time_slider = 1500;
+ const double time_circle = 1510;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
+ }
+
+ ///
+ /// Tests clicking hitting future slider ticks before a circle.
+ ///
+ [Test]
+ public void TestHitSliderTicksBeforeCircle()
+ {
+ const double time_slider = 1500;
+ const double time_circle = 1510;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(30);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
+ }
+
+ ///
+ /// Tests clicking a future circle before a spinner.
+ ///
+ [Test]
+ public void TestHitCircleBeforeSpinner()
+ {
+ const double time_spinner = 1500;
+ const double time_circle = 1800;
+ Vector2 positionCircle = Vector2.Zero;
+
+ var hitObjects = new List
+ {
+ new TestSpinner
+ {
+ StartTime = time_spinner,
+ Position = new Vector2(256, 192),
+ EndTime = time_spinner + 1000,
+ },
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ }
+
+ [Test]
+ public void TestHitSliderHeadBeforeHitCircle()
+ {
+ const double time_circle = 1000;
+ const double time_slider = 1200;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ }
+
+ private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
+ }
+
+ private void addJudgementAssert(string name, Func hitObject, HitResult result)
+ {
+ AddAssert($"{name} judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
+ }
+
+ private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
+ () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
+ }
+
+ private ScoreAccessibleReplayPlayer currentPlayer;
+ private List judgementResults;
+
+ private void performTest(List hitObjects, List frames)
+ {
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ HitObjects = hitObjects,
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
+ Ruleset = new OsuRuleset().RulesetInfo
+ },
+ });
+
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
+ SelectedMods.Value = new[] { new OsuModClassic() };
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults.Add(result);
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ judgementResults = new List();
+ });
+
+ 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);
+ }
+
+ private class TestHitCircle : HitCircle
+ {
+ protected override HitWindows CreateHitWindows() => new TestHitWindows();
+ }
+
+ private class TestSlider : Slider
+ {
+ public TestSlider()
+ {
+ DefaultsApplied += _ =>
+ {
+ HeadCircle.HitWindows = new TestHitWindows();
+ TailCircle.HitWindows = new TestHitWindows();
+
+ HeadCircle.HitWindows.SetDifficulty(0);
+ TailCircle.HitWindows.SetDifficulty(0);
+ };
+ }
+ }
+
+ private class TestSpinner : Spinner
+ {
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+ SpinsRequired = 1;
+ }
+ }
+
+ private class TestHitWindows : HitWindows
+ {
+ private static readonly DifficultyRange[] ranges =
+ {
+ new DifficultyRange(HitResult.Great, 500, 500, 500),
+ new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
+ };
+
+ public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
+
+ protected override DifficultyRange[] GetRanges() => ranges;
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, new PlayerConfiguration
+ {
+ AllowPause = false,
+ ShowResults = false,
+ })
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 10baca438d..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 SampleChannel 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/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/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/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
similarity index 99%
rename from osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
rename to osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
index 296b421a11..177a4f50a1 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
@@ -25,7 +25,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
- public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene
+ public class TestSceneStartTimeOrderedHitPolicy : RateAdjustedBeatmapTestScene
{
private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
private const double late_miss_window = 500; // time after +500 is considered a miss
@@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
- addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
+ addJudgementOffsetAssert(hitObjects[1], -200); // time_second_circle - first_circle_time - 100
}
///
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 345c3e6d35..e01e858873 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/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 6a7d76151c..75d6786d95 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -79,10 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
}
- protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
{
- new Aim(),
- new Speed()
+ new Aim(mods),
+ new Speed(mods)
};
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index e74f4933b2..cb819ec090 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -4,6 +4,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@@ -12,11 +13,16 @@ 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;
+ public Aim(Mod[] mods)
+ : base(mods)
+ {
+ }
+
protected override double SkillMultiplier => 26.25;
protected override double StrainDecayBase => 0.15;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 01f2fb8dc8..fbac080fc6 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -4,6 +4,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@@ -12,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;
@@ -27,6 +28,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private const double max_speed_bonus = 45; // ~330BPM
private const double speed_balancing_factor = 40;
+ public Speed(Mod[] mods)
+ : base(mods)
+ {
+ }
+
protected override double StrainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
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..44c3056910 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; }
@@ -218,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
var item = new PathTypeMenuItem(type, () =>
{
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
- p.ControlPoint.Type.Value = type;
+ updatePathType(p, type);
});
if (countOfState == totalCount)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
index a0392fe536..dec9cd8622 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.Update();
- CirclePiece.UpdateFrom(position == SliderPosition.Start ? HitObject.HeadCircle : HitObject.TailCircle);
+ CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)HitObject.HeadCircle : HitObject.TailCircle);
}
// Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input.
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..88fcb1e715 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;
@@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected SliderBodyPiece BodyPiece { get; private set; }
protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; }
protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; }
+
+ [CanBeNull]
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
private readonly DrawableSlider slider;
@@ -114,6 +117,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// throw away frame buffers on deselection.
ControlPointVisualiser?.Expire();
+ ControlPointVisualiser = null;
+
BodyPiece.RecyclePath();
}
@@ -210,7 +215,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 || !slider.HitObject.Path.HasValidLength)
{
placementHandler?.Delete(HitObject);
return;
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..4b0a7531a1
--- /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.Beatmaps;
+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(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
+ {
+ foreach (var hitobject in playableBeatmap.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..dab6483179
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.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 System.Linq;
+using osu.Game.Beatmaps;
+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(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
+ {
+ return checks.SelectMany(check => check.Run(playableBeatmap, workingBeatmap));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 0490e8b8ce..7b67d7aaf1 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;
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 871339ae7b..de0a4682a3 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -33,16 +33,33 @@ 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;
+
+ // this will potentially move the selection out of bounds...
+ foreach (var h in hitObjects)
+ h.Position += moveEvent.InstantDelta;
+
+ // but this will be corrected.
+ moveSelectionInBounds();
+ return true;
+ }
///
/// 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;
+
public override bool HandleReverse()
{
var hitObjects = EditorBeatmap.SelectedHitObjects;
@@ -140,36 +157,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;
}
@@ -207,28 +199,132 @@ 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);
+
+ // move the selection before scaling if dragging from top or left anchors.
+ float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
+ float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
+
+ Quad selectionQuad = getSurroundingQuad(hitObjects);
+
+ foreach (var h in hitObjects)
+ {
+ var newPosition = h.Position;
+
+ // guard against no-ops and NaN.
+ if (scale.X != 0 && selectionQuad.Width > 0)
+ newPosition.X = selectionQuad.TopLeft.X + xOffset + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
+
+ if (scale.Y != 0 && selectionQuad.Height > 0)
+ newPosition.Y = selectionQuad.TopLeft.Y + yOffset + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
+
+ h.Position = newPosition;
+ }
+ }
+
+ 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;
}
///
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/Judgements/SliderTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs
new file mode 100644
index 0000000000..a088696784
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Judgements
+{
+ public class SliderTickJudgement : OsuJudgement
+ {
+ public override HitResult MaxResult => HitResult.LargeTickHit;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 8c819c4773..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;
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
inputManager.AllowUserCursorMovement = false;
// Generate the replay frames the cursor should follow
- replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap).Generate().Frames.Cast().ToList();
+ replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast().ToList();
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index bea2bbcb32..3b1f271d41 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.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.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -16,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
- Replay = new OsuAutoGenerator(beatmap).Generate()
+ Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
new file mode 100644
index 0000000000..37ba401d42
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
@@ -0,0 +1,68 @@
+// 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.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+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.Osu.UI;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModBarrelRoll : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToDrawableHitObjects
+ {
+ private float currentRotation;
+
+ [SettingSource("Roll speed", "Rotations per minute")]
+ public BindableNumber SpinSpeed { get; } = new BindableDouble(0.5)
+ {
+ MinValue = 0.02,
+ MaxValue = 12,
+ Precision = 0.01,
+ };
+
+ [SettingSource("Direction", "The direction of rotation")]
+ public Bindable Direction { get; } = new Bindable(RotationDirection.Clockwise);
+
+ public override string Name => "Barrel Roll";
+ public override string Acronym => "BR";
+ public override string Description => "The whole playfield is on a wheel!";
+ public override double ScoreMultiplier => 1;
+
+ public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
+
+ public void Update(Playfield playfield)
+ {
+ playfield.Rotation = currentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
+ }
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ // scale the playfield to allow all hitobjects to stay within the visible region.
+ drawableRuleset.Playfield.Scale = new Vector2(OsuPlayfield.BASE_SIZE.Y / OsuPlayfield.BASE_SIZE.X);
+ }
+
+ 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/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
index 5d9a524577..df06988b70 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.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.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -16,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
- Replay = new OsuAutoGenerator(beatmap).Generate()
+ Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
new file mode 100644
index 0000000000..77dea5b0dc
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.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 System.Linq;
+using osu.Framework.Bindables;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset
+ {
+ [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
+ public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true);
+
+ [SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")]
+ public Bindable NoSliderHeadMovement { get; } = new BindableBool(true);
+
+ [SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
+ public Bindable ClassicNoteLock { get; } = new BindableBool(true);
+
+ [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)
+ {
+ case Slider slider:
+ slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value;
+
+ foreach (var head in slider.NestedHitObjects.OfType())
+ head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value;
+
+ break;
+ }
+ }
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
+
+ if (ClassicNoteLock.Value)
+ osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
+ }
+
+ public void ApplyToDrawableHitObjects(IEnumerable drawables)
+ {
+ foreach (var obj in drawables)
+ {
+ switch (obj)
+ {
+ case DrawableSlider slider:
+ slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
+ break;
+
+ case DrawableSliderHead head:
+ head.TrackFollowCircle = !NoSliderHeadMovement.Value;
+ break;
+
+ case DrawableSliderTail tail:
+ tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
index a638234dbd..1cb25edecf 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
- public BindableNumber CircleSize { get; } = new BindableFloat
+ public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
- public BindableNumber ApproachRate { get; } = new BindableFloat
+ public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Osu.Mods
Value = 5,
};
+ protected override void ApplyLimits(bool extended)
+ {
+ base.ApplyLimits(extended);
+
+ CircleSize.MaxValue = extended ? 11 : 10;
+ ApproachRate.MaxValue = extended ? 11 : 10;
+ }
+
public override string SettingDescription
{
get
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/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 6e7b1050cb..5541d0e790 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -110,8 +110,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
double startTime = start.GetEndTime();
double duration = end.StartTime - startTime;
+ // Preempt time can go below 800ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
+ // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear preempt function (see: OsuHitObject).
+ // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good.
+ double preempt = PREEMPT * Math.Min(1, start.TimePreempt / OsuHitObject.PREEMPT_MIN);
+
fadeOutTime = startTime + fraction * duration;
- fadeInTime = fadeOutTime - PREEMPT;
+ fadeInTime = fadeOutTime - preempt;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 3c0260f5f5..1bf9e76d7d 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,
@@ -123,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
}
- var result = HitObject.HitWindows.ResultFor(timeOffset);
+ var result = ResultFor(timeOffset);
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
@@ -146,6 +150,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
+ ///
+ /// Retrieves the for a time offset.
+ ///
+ /// The time offset.
+ /// The hit result, or if doesn't result in a judgement.
+ protected virtual HitResult ResultFor(double timeOffset) => HitObject.HitWindows.ResultFor(timeOffset);
+
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
@@ -157,28 +168,31 @@ 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();
+
+ (CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
+
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/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 13f5960bd4..79655c33e4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (JudgedObject?.HitObject is OsuHitObject osuObject)
{
- Position = osuObject.StackedPosition;
+ Position = osuObject.StackedEndPosition;
Scale = new Vector2(osuObject.Scale);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 511cbc2347..82b5492de6 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;
@@ -15,6 +16,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning;
@@ -30,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SliderBall Ball { get; private set; }
public SkinnableDrawable Body { get; private set; }
- public override bool DisplayResult => false;
+ public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody;
@@ -107,16 +109,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()
@@ -249,14 +262,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (userTriggered || Time.Current < HitObject.EndTime)
return;
- ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
+ // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
+ // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
+ if (HitObject.OnlyJudgeNestedObjects)
+ {
+ ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
+ return;
+ }
+
+ // Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
+ ApplyResult(r =>
+ {
+ int totalTicks = NestedHitObjects.Count;
+ int hitTicks = NestedHitObjects.Count(h => h.IsHit);
+
+ if (hitTicks == totalTicks)
+ r.Type = HitResult.Great;
+ else if (hitTicks == 0)
+ r.Type = HitResult.Miss;
+ else
+ {
+ double hitFraction = (double)hitTicks / totalTicks;
+ r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh;
+ }
+ });
}
public override void PlaySamples()
{
// 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();
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index acc95ab036..01c0d988ee 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -7,16 +7,27 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderHead : DrawableHitCircle
{
+ public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject;
+
[CanBeNull]
public Slider Slider => DrawableSlider?.HitObject;
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
+ public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
+
+ ///
+ /// Makes this track the follow circle when the start time is reached.
+ /// If false, this will be pinned to its initial position in the slider.
+ ///
+ public bool TrackFollowCircle = true;
+
private readonly IBindable pathVersion = new Bindable();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
@@ -59,12 +70,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.Update();
Debug.Assert(Slider != null);
+ Debug.Assert(HitObject != null);
- double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
+ if (TrackFollowCircle)
+ {
+ double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
- //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
- if (!IsHit)
- Position = Slider.CurvePositionAt(completionProgress);
+ //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
+ if (!IsHit)
+ Position = Slider.CurvePositionAt(completionProgress);
+ }
+ }
+
+ protected override HitResult ResultFor(double timeOffset)
+ {
+ Debug.Assert(HitObject != null);
+
+ if (HitObject.JudgeAsNormalHitCircle)
+ return base.ResultFor(timeOffset);
+
+ // If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring.
+ var result = base.ResultFor(timeOffset);
+ return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
}
public Action OnShake;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index 76490e0de1..7b4188edab 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(),
}
};
@@ -91,6 +97,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
+ (CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
+
switch (state)
{
case ArmedState.Idle:
@@ -102,8 +110,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 +151,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..d81af053d1 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -7,12 +7,13 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
+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, ITrackSnaking, IHasMainCirclePiece
{
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
@@ -26,9 +27,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 +65,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 +77,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateInitialTransforms();
- circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
+ CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
}
protected override void UpdateHitStateTransforms(ArmedState state)
@@ -78,6 +86,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Debug.Assert(HitObject.HitWindows != null);
+ (CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
+
switch (state)
{
case ArmedState.Idle:
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 56aedebed3..3a4753761a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -30,15 +30,29 @@ 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;
+ ///
+ /// 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 +69,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 +83,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);
}
@@ -130,12 +135,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
if (tracking.NewValue)
{
- spinningSample?.Play(!spinningSample.IsPlaying);
- spinningSample?.VolumeTo(1, 300);
+ if (!spinningSample.IsPlaying)
+ spinningSample.Play();
+
+ spinningSample.VolumeTo(1, 300);
}
else
{
- spinningSample?.VolumeTo(0, 300).OnComplete(_ => spinningSample.Stop());
+ spinningSample.VolumeTo(0, fade_out_duration);
}
}
@@ -161,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();
@@ -243,7 +257,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.Update();
if (HandleUserInput)
- RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
+ {
+ bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime;
+ bool correctButtonPressed = (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
+
+ RotationTracker.Tracking = !Result.HasResult
+ && correctButtonPressed
+ && isValidSpinningTime;
+ }
if (spinningSample != null && spinnerFrequencyModulate)
spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
@@ -253,13 +274,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateAfterChildren();
- if (!SpmCounter.IsPresent && RotationTracker.Tracking)
- SpmCounter.FadeIn(HitObject.TimeFadeIn);
- SpmCounter.SetRotation(Result.RateAdjustedRotation);
+ if (Result.TimeStarted == null && RotationTracker.Tracking)
+ Result.TimeStarted = Time.Current;
+
+ // 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()
@@ -284,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/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 15af141c99..22b64af3df 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@@ -25,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects
///
internal const float BASE_SCORING_DISTANCE = 100;
+ ///
+ /// Minimum preempt time at AR=10.
+ ///
+ public const double PREEMPT_MIN = 450;
+
public double TimePreempt = 600;
public double TimeFadeIn = 400;
@@ -112,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
- TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
- TimeFadeIn = 400; // as per osu-stable
+ TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
+
+ // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
+ // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.
+ // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good.
+ // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in.
+ TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN);
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 1670df24a8..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
@@ -114,8 +117,14 @@ namespace osu.Game.Rulesets.Osu.Objects
///
public double TickDistanceMultiplier = 1;
+ ///
+ /// Whether this 's judgement is fully handled by its nested s.
+ /// If false, this will be judged proportionally to the number of nested s hit.
+ ///
+ public bool OnlyJudgeNestedObjects = true;
+
[JsonIgnore]
- public HitCircle HeadCircle { get; protected set; }
+ public SliderHeadCircle HeadCircle { get; protected set; }
[JsonIgnore]
public SliderTailCircle TailCircle { get; protected set; }
@@ -137,10 +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.
- Samples = this.GetNodeSamples(repeatCount + 1);
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@@ -231,9 +236,13 @@ 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() => new OsuIgnoreJudgement();
+ public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
index f6d46aeef5..5672283230 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
@@ -1,9 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.Judgements;
+
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderHeadCircle : HitCircle
{
+ ///
+ /// Whether to treat this as a normal for judgement purposes.
+ /// If false, this will be judged as a instead.
+ ///
+ public bool JudgeAsNormalHitCircle = true;
+
+ public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement();
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
index a427ee1955..725dbe81fb 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
@@ -33,10 +33,5 @@ namespace osu.Game.Rulesets.Osu.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderTickJudgement();
-
- public class SliderTickJudgement : OsuJudgement
- {
- public override HitResult MaxResult => HitResult.LargeTickHit;
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index c8fe4f41ca..7314021a14 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu
protected override bool Handle(UIEvent e)
{
- if (e is MouseMoveEvent && !AllowUserCursorMovement) return false;
+ if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false;
return base.Handle(e);
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index cba0c5be14..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();
}
@@ -163,6 +164,7 @@ namespace osu.Game.Rulesets.Osu
{
new OsuModTarget(),
new OsuModDifficultyAdjust(),
+ new OsuModClassic()
};
case ModType.Automation:
@@ -183,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:
@@ -204,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 954a217473..7b0cf651c8 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -6,10 +6,12 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Scoring;
@@ -33,11 +35,6 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Constants
- ///
- /// The "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
- ///
- private readonly double reactionTime;
-
private readonly HitWindows defaultHitWindows;
///
@@ -49,12 +46,9 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Construction / Initialisation
- public OsuAutoGenerator(IBeatmap beatmap)
- : base(beatmap)
+ public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList mods)
+ : base(beatmap, mods)
{
- // Already superhuman, but still somewhat realistic
- reactionTime = ApplyModsToRate(100);
-
defaultHitWindows = new OsuHitWindows();
defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
}
@@ -77,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++)
@@ -240,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Replays
OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1];
// Wait until Auto could "see and react" to the next note.
- double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime);
+ double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt));
if (waitTime > lastFrame.Time)
{
@@ -250,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Vector2 lastPosition = lastFrame.Position;
- double timeDifference = ApplyModsToTime(h.StartTime - lastFrame.Time);
+ double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
// Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
if (timeDifference > 0 && // Sanity checks
@@ -258,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Replays
timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
{
// Perform eased movement
- for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay)
+ for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time))
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing);
AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions });
@@ -272,6 +264,14 @@ namespace osu.Game.Rulesets.Osu.Replays
}
}
+ ///
+ /// Calculates the "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
+ ///
+ ///
+ /// Already superhuman, but still somewhat realistic.
+ ///
+ private double getReactionTime(double timeInstant) => ApplyModsToRate(timeInstant, 100);
+
// Add frames to click the hitobject
private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection)
{
@@ -341,17 +341,23 @@ namespace osu.Game.Rulesets.Osu.Replays
float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X);
double t;
+ double previousFrame = h.StartTime;
- for (double j = h.StartTime + FrameDelay; j < spinner.EndTime; j += FrameDelay)
+ for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame))
{
- t = ApplyModsToTime(j - h.StartTime) * spinnerDirection;
+ t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection;
+ angle += (float)t / 20;
- Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS);
- AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action));
+ Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
+ AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action));
+
+ previousFrame = nextFrame;
}
- t = ApplyModsToTime(spinner.EndTime - h.StartTime) * spinnerDirection;
- Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS);
+ t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection;
+ angle += (float)t / 20;
+
+ Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action));
@@ -359,7 +365,7 @@ namespace osu.Game.Rulesets.Osu.Replays
break;
case Slider slider:
- for (double j = FrameDelay; j < slider.Duration; j += FrameDelay)
+ for (double j = GetFrameDelay(slider.StartTime); j < slider.Duration; j += GetFrameDelay(slider.StartTime + j))
{
Vector2 pos = slider.StackedPositionAt(j / slider.Duration);
AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action));
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
index 3356a0fbe0..1cb3208c30 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
@@ -5,7 +5,9 @@ using osuTK;
using osu.Game.Beatmaps;
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
@@ -22,33 +24,61 @@ namespace osu.Game.Rulesets.Osu.Replays
public const float SPIN_RADIUS = 50;
- ///
- /// The time in ms between each ReplayFrame.
- ///
- protected readonly double FrameDelay;
-
#endregion
#region Construction / Initialisation
protected Replay Replay;
protected List Frames => Replay.Frames;
+ private readonly IReadOnlyList timeAffectingMods;
- protected OsuAutoGeneratorBase(IBeatmap beatmap)
+ protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList mods)
: base(beatmap)
{
Replay = new Replay();
- // We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps.
- FrameDelay = ApplyModsToRate(1000.0 / 60.0);
+ timeAffectingMods = mods.OfType().ToList();
}
#endregion
#region Utilities
- protected double ApplyModsToTime(double v) => v;
- protected double ApplyModsToRate(double v) => v;
+ ///
+ /// Returns the real duration of time between and
+ /// after applying rate-affecting mods.
+ ///
+ ///
+ /// This method should only be used when and are very close.
+ /// That is because the track rate might be changing with time,
+ /// and the method used here is a rough instantaneous approximation.
+ ///
+ /// The start time of the time delta, in original track time.
+ /// The end time of the time delta, in original track time.
+ protected double ApplyModsToTimeDelta(double startTime, double endTime)
+ {
+ double delta = endTime - startTime;
+
+ foreach (var mod in timeAffectingMods)
+ delta /= mod.ApplyToRate(startTime);
+
+ return delta;
+ }
+
+ protected double ApplyModsToRate(double time, double rate)
+ {
+ foreach (var mod in timeAffectingMods)
+ rate = mod.ApplyToRate(time, rate);
+ return rate;
+ }
+
+ ///
+ /// Calculates the interval after which the next should be generated,
+ /// in milliseconds.
+ ///
+ /// The time of the previous frame.
+ protected double GetFrameDelay(double time)
+ => ApplyModsToRate(time, 1000.0 / 60);
private class ReplayFrameComparer : IComparer
{
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/IMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/IMainCirclePiece.cs
new file mode 100644
index 0000000000..17a1e29094
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/IMainCirclePiece.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+ public interface IMainCirclePiece
+ {
+ ///
+ /// Begins animating this .
+ ///
+ /// The of the related .
+ void Animate(ArmedState state);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
index fcbe4c1b28..b46baa00ba 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
@@ -13,7 +13,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
- public class MainCirclePiece : CompositeDrawable
+ public class MainCirclePiece : CompositeDrawable, IMainCirclePiece
{
private readonly CirclePiece circle;
private readonly RingPiece ring;
@@ -67,17 +67,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
-
- drawableObject.ApplyCustomUpdateState += updateState;
- updateState(drawableObject, drawableObject.State.Value);
}
- private void updateState(DrawableHitObject drawableObject, ArmedState state)
+ public void Animate(ArmedState state)
{
- using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
- {
+ using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
glow.FadeOut(400);
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
switch (state)
{
case ArmedState.Hit:
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/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
index e77c93c721..4dd7b2d69c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
+ private readonly Bindable configSnakingOut = new Bindable();
+
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableObject)
{
@@ -36,10 +38,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
- config?.BindWith(OsuRulesetSetting.SnakingOutSliders, SnakingOut);
+ config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
+
+ SnakingOut.BindTo(configSnakingOut);
BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
+
+ drawableObject.HitObjectApplied += onHitObjectApplied;
+ }
+
+ private void onHitObjectApplied(DrawableHitObject obj)
+ {
+ var drawableSlider = (DrawableSlider)obj;
+ if (drawableSlider.HitObject == null)
+ return;
+
+ // When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way.
+ if (!drawableSlider.HeadCircle.TrackFollowCircle)
+ {
+ SnakingOut.UnbindFrom(configSnakingOut);
+ SnakingOut.Value = false;
+ }
}
private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour)
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
index a96beb66d4..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;
@@ -31,6 +32,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
set => ball.Colour = value;
}
+ ///
+ /// Whether to track accurately to the visual size of this .
+ /// If false, tracking will be performed at the final scale at all times.
+ ///
+ public bool InputTracksVisualSize = true;
+
private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider;
private readonly Drawable ball;
@@ -94,7 +101,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
tracking = value;
- followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
+ if (InputTracksVisualSize)
+ followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
+ else
+ {
+ // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration.
+ followCircle.ScaleTo(tracking ? 2.4f : 1f);
+ }
+
followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
}
}
@@ -121,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();
@@ -139,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;
}
@@ -151,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 54%
rename from osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
index e5952ecf97..a5205bbb8c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
@@ -1,66 +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 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
- }
- };
- }
-
- 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
+ ///
+ /// 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; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ drawableSpinner.HitObjectApplied += resetState;
+ }
+
public void SetRotation(float currentRotation)
{
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
@@ -77,10 +48,30 @@ 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 });
}
+
+ private void resetState(DrawableHitObject hitObject)
+ {
+ result.Value = 0;
+ records.Clear();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ 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..cf62165929 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
@@ -12,6 +12,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@@ -19,7 +20,7 @@ using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
- public class LegacyMainCirclePiece : CompositeDrawable
+ public class LegacyMainCirclePiece : CompositeDrawable, IMainCirclePiece
{
private readonly string priorityLookup;
private readonly bool hasNumber;
@@ -138,12 +139,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
-
- drawableObject.ApplyCustomUpdateState += updateState;
- updateState(drawableObject, drawableObject.State.Value);
}
- private void updateState(DrawableHitObject drawableObject, ArmedState state)
+ public void Animate(ArmedState state)
{
const double legacy_fade_duration = 240;
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..7eb6898abc 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(source, 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(source, 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..ffe238c507 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -97,17 +97,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
case OsuSkinComponents.HitCircleText:
- var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
- var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
+ if (!this.HasFont(LegacyFont.HitCircle))
+ return null;
- return !this.HasFont(font)
- ? null
- : new LegacySpriteText(Source, font)
- {
- // stable applies a blanket 0.8x scale to hitcircle fonts
- Scale = new Vector2(0.8f),
- Spacing = new Vector2(-overlap, 0)
- };
+ return new LegacySpriteText(Source, LegacyFont.HitCircle)
+ {
+ // stable applies a blanket 0.8x scale to hitcircle fonts
+ Scale = new Vector2(0.8f),
+ };
case OsuSkinComponents.SpinnerBody:
bool hasBackground = Source.GetTexture("spinner-background") != null;
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/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/IHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs
new file mode 100644
index 0000000000..5d8ea035a7
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/IHitPolicy.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 osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.UI
+{
+ public interface IHitPolicy
+ {
+ ///
+ /// The containing the s which this applies to.
+ ///
+ IHitObjectContainer HitObjectContainer { set; }
+
+ ///
+ /// Determines whether a can be hit at a point in time.
+ ///
+ /// The to check.
+ /// The time to check.
+ /// Whether can be hit at the given .
+ bool IsHittable(DrawableHitObject hitObject, double time);
+
+ ///
+ /// Handles a being hit.
+ ///
+ /// The that was hit.
+ void HandleHit(DrawableHitObject hitObject);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs
new file mode 100644
index 0000000000..83f205deac
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.UI
+{
+ ///
+ /// Ensures that s are hit in order of appearance. The classic note lock.
+ ///
+ /// Hits will be blocked until the previous s have been judged.
+ ///
+ ///
+ public class ObjectOrderedHitPolicy : IHitPolicy
+ {
+ public IHitObjectContainer HitObjectContainer { get; set; }
+
+ public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged);
+
+ public void HandleHit(DrawableHitObject hitObject)
+ {
+ }
+
+ private IEnumerable enumerateHitObjectsUpTo(double targetTime)
+ {
+ foreach (var obj in HitObjectContainer.AliveObjects)
+ {
+ if (obj.HitObject.StartTime >= targetTime)
+ yield break;
+
+ switch (obj)
+ {
+ case DrawableSpinner _:
+ continue;
+
+ case DrawableSlider slider:
+ yield return slider.HeadCircle;
+
+ break;
+
+ default:
+ yield return obj;
+
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 975b444699..ea3eb5eb5c 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
private readonly FollowPointRenderer followPoints;
- private readonly OrderedHitPolicy hitPolicy;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -43,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 },
@@ -54,10 +56,9 @@ namespace osu.Game.Rulesets.Osu.UI
approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
};
- hitPolicy = new OrderedHitPolicy(HitObjectContainer);
+ HitPolicy = new StartTimeOrderedHitPolicy();
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));
@@ -66,6 +67,18 @@ namespace osu.Game.Rulesets.Osu.UI
NewResult += onNewResult;
}
+ private IHitPolicy hitPolicy;
+
+ public IHitPolicy HitPolicy
+ {
+ get => hitPolicy;
+ set
+ {
+ hitPolicy = value ?? throw new ArgumentNullException(nameof(value));
+ hitPolicy.HitObjectContainer = HitObjectContainer;
+ }
+ }
+
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
{
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
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.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs
similarity index 76%
rename from osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
rename to osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs
index 8e4f81347d..0173156246 100644
--- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs
@@ -11,28 +11,17 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
///
- /// Ensures that s are hit in-order. Affectionately known as "note lock".
+ /// Ensures that s are hit in-order of their start times. Affectionately known as "note lock".
/// If a is hit out of order:
///
/// - The hit is blocked if it occurred earlier than the previous 's start time.
/// - The hit causes all previous s to missed otherwise.
///
///
- public class OrderedHitPolicy
+ public class StartTimeOrderedHitPolicy : IHitPolicy
{
- private readonly HitObjectContainer hitObjectContainer;
+ public IHitObjectContainer HitObjectContainer { get; set; }
- public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
- {
- this.hitObjectContainer = hitObjectContainer;
- }
-
- ///
- /// Determines whether a can be hit at a point in time.
- ///
- /// The to check.
- /// The time to check.
- /// Whether can be hit at the given .
public bool IsHittable(DrawableHitObject hitObject, double time)
{
DrawableHitObject blockingObject = null;
@@ -54,10 +43,6 @@ namespace osu.Game.Rulesets.Osu.UI
return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
}
- ///
- /// Handles a being hit to potentially miss all earlier s.
- ///
- /// The that was hit.
public void HandleHit(DrawableHitObject hitObject)
{
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
@@ -67,6 +52,7 @@ namespace osu.Game.Rulesets.Osu.UI
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
+ // Miss all hitobjects prior to the hit one.
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
if (obj.Judged)
@@ -86,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.UI
private IEnumerable enumerateHitObjectsUpTo(double targetTime)
{
- foreach (var obj in hitObjectContainer.AliveObjects)
+ foreach (var obj in HitObjectContainer.AliveObjects)
{
if (obj.HitObject.StartTime >= targetTime)
yield break;
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/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
index fecb5d4a74..61ea8b664d 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,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture]
public class TestSceneHitExplosion : TaikoSkinnableTestScene
{
+ protected override double TimePerAction => 100;
+
[Test]
public void TestNormalHit()
{
@@ -21,11 +27,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
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..9b36b064bc 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/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index 71b3c23b50..dd3c6b317a 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Taiko.Difficulty;
+using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests
@@ -18,6 +19,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
+ [TestCase(3.1704781712282624d, "diffcalc-test")]
+ [TestCase(3.1704781712282624d, "diffcalc-test-strong")]
+ public void TestClockRateAdjusted(double expected, string name)
+ => Test(expected, name, new TaikoModDoubleTime());
+
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new TaikoRuleset();
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/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 2a5a2e2fdb..2dfa1dfbb7 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/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
index 32421ee00a..769d021362 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
@@ -5,6 +5,7 @@ using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
@@ -13,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;
@@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
private int currentMonoLength;
+ public Colour(Mod[] mods)
+ : base(mods)
+ {
+ }
+
protected override double StrainValueOf(DifficultyHitObject current)
{
// changing from/to a drum roll or a swell does not constitute a colour change.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
index 5569b27ad5..a32f6ebe0d 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -5,6 +5,7 @@ using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
@@ -13,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;
@@ -47,6 +48,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
private int notesSinceRhythmChange;
+ public Rhythm(Mod[] mods)
+ : base(mods)
+ {
+ }
+
protected override double StrainValueOf(DifficultyHitObject current)
{
// drum rolls and swells are exempt.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index 0b61eb9930..4cceadb23f 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -5,6 +5,7 @@ using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
@@ -16,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;
@@ -48,8 +49,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Creates a skill.
///
+ /// Mods for use in skill calculations.
/// Whether this instance is performing calculations for the right hand.
- public Stamina(bool rightHand)
+ public Stamina(Mod[] mods, bool rightHand)
+ : base(mods)
{
hand = rightHand ? 1 : 0;
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index e5485db4df..6b3e31c5d5 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
}
- protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
{
- new Colour(),
- new Rhythm(),
- new Stamina(true),
- new Stamina(false),
+ new Colour(mods),
+ new Rhythm(mods),
+ new Stamina(mods, true),
+ new Stamina(mods, false),
};
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
@@ -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/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/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
index 3fbcee44af..ac2dd4bdb6 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
@@ -52,32 +52,24 @@ 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;
+ });
}
protected override IEnumerable