1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 05:39:53 +08:00

Compare commits

...

228 Commits

123 changed files with 5204 additions and 2582 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ContentModelUserStore">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" />
</modules>
</component>
</project>
+1 -1
View File
@@ -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).
+1 -1
View File
@@ -1,4 +1,4 @@
# osu-templates
# Templates
Templates for use when creating osu! dependent projects. Create a fully-testable (and ready for git) custom ruleset in just two lines.
+179 -6
View File
@@ -12,16 +12,189 @@ trim_trailing_whitespace = true
#PascalCase for public and protected members
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event,delegate
dotnet_naming_rule.public_members_pascalcase.severity = suggestion
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.public_members_pascalcase.severity = error
dotnet_naming_rule.public_members_pascalcase.symbols = public_members
dotnet_naming_rule.public_members_pascalcase.style = pascalcase
#camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_symbols.private_members.applicable_accessibilities = private
dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event,delegate
dotnet_naming_rule.private_members_camelcase.severity = suggestion
dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.private_members_camelcase.severity = warning
dotnet_naming_rule.private_members_camelcase.symbols = private_members
dotnet_naming_rule.private_members_camelcase.style = camelcase
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
File diff suppressed because it is too large Load Diff
@@ -12,16 +12,189 @@ trim_trailing_whitespace = true
#PascalCase for public and protected members
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event,delegate
dotnet_naming_rule.public_members_pascalcase.severity = suggestion
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.public_members_pascalcase.severity = error
dotnet_naming_rule.public_members_pascalcase.symbols = public_members
dotnet_naming_rule.public_members_pascalcase.style = pascalcase
#camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_symbols.private_members.applicable_accessibilities = private
dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event,delegate
dotnet_naming_rule.private_members_camelcase.severity = suggestion
dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.private_members_camelcase.severity = warning
dotnet_naming_rule.private_members_camelcase.symbols = private_members
dotnet_naming_rule.private_members_camelcase.style = camelcase
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
File diff suppressed because it is too large Load Diff
@@ -12,16 +12,189 @@ trim_trailing_whitespace = true
#PascalCase for public and protected members
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event,delegate
dotnet_naming_rule.public_members_pascalcase.severity = suggestion
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.public_members_pascalcase.severity = error
dotnet_naming_rule.public_members_pascalcase.symbols = public_members
dotnet_naming_rule.public_members_pascalcase.style = pascalcase
#camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_symbols.private_members.applicable_accessibilities = private
dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event,delegate
dotnet_naming_rule.private_members_camelcase.severity = suggestion
dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.private_members_camelcase.severity = warning
dotnet_naming_rule.private_members_camelcase.symbols = private_members
dotnet_naming_rule.private_members_camelcase.style = camelcase
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
@@ -12,16 +12,189 @@ trim_trailing_whitespace = true
#PascalCase for public and protected members
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event,delegate
dotnet_naming_rule.public_members_pascalcase.severity = suggestion
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.public_members_pascalcase.severity = error
dotnet_naming_rule.public_members_pascalcase.symbols = public_members
dotnet_naming_rule.public_members_pascalcase.style = pascalcase
#camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_symbols.private_members.applicable_accessibilities = private
dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event,delegate
dotnet_naming_rule.private_members_camelcase.severity = suggestion
dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.private_members_camelcase.severity = warning
dotnet_naming_rule.private_members_camelcase.symbols = private_members
dotnet_naming_rule.private_members_camelcase.style = camelcase
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
+2 -2
View File
@@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.402.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.410.0" />
</ItemGroup>
</Project>
+5
View File
@@ -20,6 +20,11 @@
<AndroidDexTool>d8</AndroidDexTool>
<AndroidLinkTool>r8</AndroidLinkTool>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<AndroidLinkMode>None</AndroidLinkMode>
<MandroidI18n>cjk;mideast;other;rare;west</MandroidI18n>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<Compile Include="GameplayScreenRotationLocker.cs" />
<Compile Include="OsuGameActivity.cs" />
+11 -2
View File
@@ -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"
]
}
}
}
@@ -14,6 +14,11 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<AndroidSupportedAbis>armeabi-v7a;x86;arm64-v8a</AndroidSupportedAbis>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<AndroidLinkMode>None</AndroidLinkMode>
<MandroidI18n>cjk;mideast;other;rare;west</MandroidI18n>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<Compile Include="MainActivity.cs" />
</ItemGroup>
@@ -0,0 +1,53 @@
// 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.
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;
}
}
}
@@ -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)
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
public class Movement : Skill
public class Movement : StrainSkill
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
protected override double DecayWeight => 0.94;
protected override int SectionLength => 750;
protected readonly float HalfCatcherWidth;
private float? lastPlayerPosition;
+4 -1
View File
@@ -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,
};
}
@@ -14,6 +14,11 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<AndroidSupportedAbis>armeabi-v7a;x86;arm64-v8a</AndroidSupportedAbis>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<AndroidLinkMode>None</AndroidLinkMode>
<MandroidI18n>cjk;mideast;other;rare;west</MandroidI18n>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<Compile Include="MainActivity.cs" />
</ItemGroup>
@@ -7,11 +7,10 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Strain : Skill
public class Strain : StrainSkill
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
@@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
var endTime = maniaCurrent.EndTime;
var column = maniaCurrent.BaseObject.Column;
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
@@ -46,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
for (int i = 0; i < holdEndTimes.Length; ++i)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
@@ -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;
}
}
@@ -14,6 +14,11 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<AndroidSupportedAbis>armeabi-v7a;x86;arm64-v8a</AndroidSupportedAbis>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<AndroidLinkMode>None</AndroidLinkMode>
<MandroidI18n>cjk;mideast;other;rare;west</MandroidI18n>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<Compile Include="MainActivity.cs" />
</ItemGroup>
@@ -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();
});
}
}
}
@@ -0,0 +1,175 @@
// 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.
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)
{
}
}
}
}
@@ -276,6 +276,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)
@@ -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<ConvertValue> CreateConvertValue(HitObject hitObject)
@@ -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;
}
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// <summary>
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
/// </summary>
public class Aim : Skill
public class Aim : StrainSkill
{
private const double angle_bonus_begin = Math.PI / 3;
private const double timing_threshold = 107;
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// <summary>
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
/// </summary>
public class Speed : Skill
public class Speed : StrainSkill
{
private const double single_spacing_threshold = 125;
@@ -2,14 +2,18 @@
// 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;
@@ -28,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip
{
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
public List<PathControlPoint> PointsInSegment;
public readonly BindableBool IsSelected = new BindableBool();
public readonly PathControlPoint ControlPoint;
@@ -54,6 +59,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
this.slider = slider;
ControlPoint = controlPoint;
slider.Path.Version.BindValueChanged(_ =>
{
PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
updatePathType();
}, runOnceImmediately: true);
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
Origin = Anchor.Centre;
@@ -150,6 +161,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)
{
@@ -159,6 +171,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;
}
@@ -184,10 +198,33 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
else
ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
// 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();
/// <summary>
/// Handles correction of invalid path types.
/// </summary>
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<Vector2> 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;
}
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>
@@ -153,6 +153,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="piece">The control point piece that we want to change the path type of.</param>
/// <param name="type">The path type we want to assign to the given control point piece.</param>
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)
@@ -142,6 +142,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.Update();
updateSlider();
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
updatePathType();
}
private void updatePathType()
@@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.OnOperationEnded();
referenceOrigin = null;
referencePathTypes = null;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent)
@@ -53,6 +54,12 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
private Vector2? referenceOrigin;
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?> referencePathTypes;
public override bool HandleReverse()
{
var hitObjects = EditorBeatmap.SelectedHitObjects;
@@ -194,6 +201,8 @@ namespace osu.Game.Rulesets.Osu.Edit
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.
@@ -209,6 +218,10 @@ namespace osu.Game.Rulesets.Osu.Edit
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);
@@ -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;
@@ -108,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<ISampleInfo>().ToArray();
var slidingSamples = new List<ISampleInfo>();
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()
+7 -5
View File
@@ -81,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.Objects
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
[JsonIgnore]
public IList<HitSampleInfo> TailSamples { get; private set; }
private int repeatCount;
public int RepeatCount
@@ -143,11 +146,6 @@ namespace osu.Game.Rulesets.Osu.Objects
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
// For now, the samples are attached to and played by the slider itself at the correct end time.
// ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live).
Samples = this.GetNodeSamples(repeatCount + 1).ToArray();
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@@ -238,6 +236,10 @@ namespace osu.Game.Rulesets.Osu.Objects
if (HeadCircle != null)
HeadCircle.Samples = this.GetNodeSamples(0);
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
// For now, the samples are played by the slider itself at the correct end time.
TailSamples = this.GetNodeSamples(repeatCount + 1);
}
public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
@@ -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
}
}
]
}]
}
@@ -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:
@@ -14,6 +14,11 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<AndroidSupportedAbis>armeabi-v7a;x86;arm64-v8a</AndroidSupportedAbis>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<AndroidLinkMode>None</AndroidLinkMode>
<MandroidI18n>cjk;mideast;other;rare;west</MandroidI18n>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<Compile Include="MainActivity.cs" />
</ItemGroup>
@@ -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
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <summary>
/// Calculates the colour coefficient of taiko difficulty.
/// </summary>
public class Colour : Skill
public class Colour : StrainSkill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <summary>
/// Calculates the rhythm coefficient of taiko difficulty.
/// </summary>
public class Rhythm : Skill
public class Rhythm : StrainSkill
{
protected override double SkillMultiplier => 10;
protected override double StrainDecayBase => 0;
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <remarks>
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
/// </remarks>
public class Stamina : Skill
public class Stamina : StrainSkill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
@@ -133,11 +133,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
List<double> peaks = new List<double>();
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));
}
@@ -23,6 +23,11 @@
<PropertyGroup>
<NoWarn>$(NoWarn);CA2007</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<AndroidLinkMode>None</AndroidLinkMode>
<MandroidI18n>cjk;mideast;other;rare;west</MandroidI18n>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\osu.Game.Tests\**\Beatmaps\**\*.cs">
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
@@ -707,6 +707,69 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null));
Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0)));
Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null));
// Last control point duplicated
var fourth = ((IHasPath)decoded.HitObjects[3]).Path;
Assert.That(fourth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(fourth.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
Assert.That(fourth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(1, 1)));
Assert.That(fourth.ControlPoints[1].Type.Value, Is.EqualTo(null));
Assert.That(fourth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(2, 2)));
Assert.That(fourth.ControlPoints[2].Type.Value, Is.EqualTo(null));
Assert.That(fourth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(3, 3)));
Assert.That(fourth.ControlPoints[3].Type.Value, Is.EqualTo(null));
Assert.That(fourth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(3, 3)));
Assert.That(fourth.ControlPoints[4].Type.Value, Is.EqualTo(null));
// Last control point in segment duplicated
var fifth = ((IHasPath)decoded.HitObjects[4]).Path;
Assert.That(fifth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(fifth.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
Assert.That(fifth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(1, 1)));
Assert.That(fifth.ControlPoints[1].Type.Value, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(2, 2)));
Assert.That(fifth.ControlPoints[2].Type.Value, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(3, 3)));
Assert.That(fifth.ControlPoints[3].Type.Value, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(3, 3)));
Assert.That(fifth.ControlPoints[4].Type.Value, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(4, 4)));
Assert.That(fifth.ControlPoints[5].Type.Value, Is.EqualTo(PathType.Bezier));
Assert.That(fifth.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(5, 5)));
Assert.That(fifth.ControlPoints[6].Type.Value, Is.EqualTo(null));
// Implicit perfect-curve multi-segment(Should convert to bezier to match stable)
var sixth = ((IHasPath)decoded.HitObjects[5]).Path;
Assert.That(sixth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(sixth.ControlPoints[0].Type.Value == PathType.Bezier);
Assert.That(sixth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(75, 145)));
Assert.That(sixth.ControlPoints[1].Type.Value == null);
Assert.That(sixth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(170, 75)));
Assert.That(sixth.ControlPoints[2].Type.Value == PathType.Bezier);
Assert.That(sixth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(300, 145)));
Assert.That(sixth.ControlPoints[3].Type.Value == null);
Assert.That(sixth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(410, 20)));
Assert.That(sixth.ControlPoints[4].Type.Value == null);
// Explicit perfect-curve multi-segment(Should not convert to bezier)
var seventh = ((IHasPath)decoded.HitObjects[6]).Path;
Assert.That(seventh.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(seventh.ControlPoints[0].Type.Value == PathType.PerfectCurve);
Assert.That(seventh.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(75, 145)));
Assert.That(seventh.ControlPoints[1].Type.Value == null);
Assert.That(seventh.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(170, 75)));
Assert.That(seventh.ControlPoints[2].Type.Value == PathType.PerfectCurve);
Assert.That(seventh.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(300, 145)));
Assert.That(seventh.ControlPoints[3].Type.Value == null);
Assert.That(seventh.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(410, 20)));
Assert.That(seventh.ControlPoints[4].Type.Value == null);
}
}
}
@@ -18,10 +18,14 @@ using osu.Game.IO;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Beatmaps.Formats
{
@@ -45,6 +49,33 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
}
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
var beatmap = new Beatmap
{
HitObjects =
{
new Slider
{
Position = new Vector2(0.6f),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.Bezier),
new PathControlPoint(new Vector2(0.5f)),
new PathControlPoint(new Vector2(0.51f)), // This is actually on the same position as the previous one in legacy beatmaps (truncated to int).
new PathControlPoint(new Vector2(1f), PathType.Bezier),
new PathControlPoint(new Vector2(2f))
})
},
}
};
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty))), string.Empty);
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(5));
}
private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
{
// equal to null, no need to SequenceEqual
@@ -1,115 +0,0 @@
// 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.
using System;
using NUnit.Framework;
using osu.Game.Rulesets.Difficulty.Utils;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class LimitedCapacityStackTest
{
private const int capacity = 3;
private LimitedCapacityStack<int> stack;
[SetUp]
public void Setup()
{
stack = new LimitedCapacityStack<int>(capacity);
}
[Test]
public void TestEmptyStack()
{
Assert.AreEqual(0, stack.Count);
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
int unused = stack[0];
});
int count = 0;
foreach (var unused in stack)
count++;
Assert.AreEqual(0, count);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public void TestInRangeElements(int count)
{
// e.g. 0 -> 1 -> 2
for (int i = 0; i < count; i++)
stack.Push(i);
Assert.AreEqual(count, stack.Count);
// e.g. 2 -> 1 -> 0 (reverse order)
for (int i = 0; i < stack.Count; i++)
Assert.AreEqual(count - 1 - i, stack[i]);
// e.g. indices 3, 4, 5, 6 (out of range)
for (int i = stack.Count; i < stack.Count + capacity; i++)
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
int unused = stack[i];
});
}
}
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
public void TestOverflowElements(int count)
{
// e.g. 0 -> 1 -> 2 -> 3
for (int i = 0; i < count; i++)
stack.Push(i);
Assert.AreEqual(capacity, stack.Count);
// e.g. 3 -> 2 -> 1 (reverse order)
for (int i = 0; i < stack.Count; i++)
Assert.AreEqual(count - 1 - i, stack[i]);
// e.g. indices 3, 4, 5, 6 (out of range)
for (int i = stack.Count; i < stack.Count + capacity; i++)
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
int unused = stack[i];
});
}
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
public void TestEnumerator(int count)
{
// e.g. 0 -> 1 -> 2 -> 3
for (int i = 0; i < count; i++)
stack.Push(i);
int enumeratorCount = 0;
int expectedValue = count - 1;
foreach (var item in stack)
{
Assert.AreEqual(expectedValue, item);
enumeratorCount++;
expectedValue--;
}
Assert.AreEqual(stack.Count, enumeratorCount);
}
}
}
@@ -0,0 +1,143 @@
// 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.
using System;
using NUnit.Framework;
using osu.Game.Rulesets.Difficulty.Utils;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class ReverseQueueTest
{
private ReverseQueue<char> queue;
[SetUp]
public void Setup()
{
queue = new ReverseQueue<char>(4);
}
[Test]
public void TestEmptyQueue()
{
Assert.AreEqual(0, queue.Count);
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
char unused = queue[0];
});
int count = 0;
foreach (var unused in queue)
count++;
Assert.AreEqual(0, count);
}
[Test]
public void TestEnqueue()
{
// Assert correct values and reverse index after enqueueing
queue.Enqueue('a');
queue.Enqueue('b');
queue.Enqueue('c');
Assert.AreEqual('c', queue[0]);
Assert.AreEqual('b', queue[1]);
Assert.AreEqual('a', queue[2]);
// Assert correct values and reverse index after enqueueing beyond initial capacity of 4
queue.Enqueue('d');
queue.Enqueue('e');
queue.Enqueue('f');
Assert.AreEqual('f', queue[0]);
Assert.AreEqual('e', queue[1]);
Assert.AreEqual('d', queue[2]);
Assert.AreEqual('c', queue[3]);
Assert.AreEqual('b', queue[4]);
Assert.AreEqual('a', queue[5]);
}
[Test]
public void TestDequeue()
{
queue.Enqueue('a');
queue.Enqueue('b');
queue.Enqueue('c');
queue.Enqueue('d');
queue.Enqueue('e');
queue.Enqueue('f');
// Assert correct item return and no longer in queue after dequeueing
Assert.AreEqual('a', queue[5]);
var dequeuedItem = queue.Dequeue();
Assert.AreEqual('a', dequeuedItem);
Assert.AreEqual(5, queue.Count);
Assert.AreEqual('f', queue[0]);
Assert.AreEqual('b', queue[4]);
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
char unused = queue[5];
});
// Assert correct state after enough enqueues and dequeues to wrap around array (queue.start = 0 again)
queue.Enqueue('g');
queue.Enqueue('h');
queue.Enqueue('i');
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
Assert.AreEqual(1, queue.Count);
Assert.AreEqual('i', queue[0]);
}
[Test]
public void TestClear()
{
queue.Enqueue('a');
queue.Enqueue('b');
queue.Enqueue('c');
queue.Enqueue('d');
queue.Enqueue('e');
queue.Enqueue('f');
// Assert queue is empty after clearing
queue.Clear();
Assert.AreEqual(0, queue.Count);
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
char unused = queue[0];
});
}
[Test]
public void TestEnumerator()
{
queue.Enqueue('a');
queue.Enqueue('b');
queue.Enqueue('c');
queue.Enqueue('d');
queue.Enqueue('e');
queue.Enqueue('f');
char[] expectedValues = { 'f', 'e', 'd', 'c', 'b', 'a' };
int expectedValueIndex = 0;
// Assert items are enumerated in correct order
foreach (var item in queue)
{
Assert.AreEqual(expectedValues[expectedValueIndex], item);
expectedValueIndex++;
}
}
}
}
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Online
private BeatmapSetInfo testBeatmapSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
private OnlinePlayBeatmapAvailablilityTracker availablilityTracker;
private OnlinePlayBeatmapAvailabilityTracker availabilityTracker;
[BackgroundDependencyLoader]
private void load(AudioManager audio, GameHost host)
@@ -67,7 +67,7 @@ namespace osu.Game.Tests.Online
Ruleset = { Value = testBeatmapInfo.Ruleset },
};
Child = availablilityTracker = new OnlinePlayBeatmapAvailablilityTracker
Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
{
SelectedItem = { BindTarget = selectedItem, }
};
@@ -118,7 +118,7 @@ namespace osu.Game.Tests.Online
});
addAvailabilityCheckStep("state still not downloaded", BeatmapAvailability.NotDownloaded);
AddStep("recreate tracker", () => Child = availablilityTracker = new OnlinePlayBeatmapAvailablilityTracker
AddStep("recreate tracker", () => Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
{
SelectedItem = { BindTarget = selectedItem }
});
@@ -127,7 +127,7 @@ namespace osu.Game.Tests.Online
private void addAvailabilityCheckStep(string description, Func<BeatmapAvailability> expected)
{
AddAssert(description, () => availablilityTracker.Availability.Value.Equals(expected.Invoke()));
AddAssert(description, () => availabilityTracker.Availability.Value.Equals(expected.Invoke()));
}
private static BeatmapInfo getTestBeatmapInfo(string archiveFile)
@@ -9,3 +9,16 @@ osu file format v128
// Implicit multi-segment
32,192,3000,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800
// Last control point duplicated
0,0,4000,2,0,B|1:1|2:2|3:3|3:3,2,200
// Last control point in segment duplicated
0,0,5000,2,0,B|1:1|2:2|3:3|3:3|B|4:4|5:5,2,200
// Implicit perfect-curve multi-segment (Should convert to bezier to match stable)
0,0,6000,2,0,P|75:145|170:75|170:75|300:145|410:20,1,475,0:0:0:0:
// Explicit perfect-curve multi-segment (Should not convert to bezier)
0,0,7000,2,0,P|75:145|P|170:75|300:145|410:20,1,650,0:0:0:0:
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 478_571)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 492_857)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0)
@@ -89,7 +89,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 535)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 594)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] // (((3 * 300) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] // (((3 * 350) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] // (0 * 1 * 300) * (1 + 0 / 25)
@@ -0,0 +1,27 @@
// 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.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerMatchFooter : MultiplayerTestScene
{
[Cached]
private readonly OnlinePlayBeatmapAvailabilityTracker availablilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
[BackgroundDependencyLoader]
private void load()
{
Child = new MultiplayerMatchFooter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = 50
};
}
}
}
@@ -3,13 +3,21 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
@@ -18,11 +26,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerMatchSubScreen screen;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
public TestSceneMultiplayerMatchSubScreen()
: base(false)
{
}
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
@@ -71,7 +93,48 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
AddWaitStep("wait", 10);
AddUntilStep("wait for join", () => Client.Room != null);
}
[Test]
public void TestStartMatchWhileSpectating()
{
AddStep("set playlist", () =>
{
Room.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
});
});
AddStep("click create button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for room join", () => Client.Room != null);
AddStep("join other user (ready)", () =>
{
Client.AddUser(new User { Id = 55 });
Client.ChangeUserState(55, MultiplayerUserState.Ready);
});
AddStep("click spectate button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerSpectateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("click ready button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerReadyButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
}
}
}
@@ -126,6 +126,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().Single().IsPresent);
}
[Test]
public void TestToggleSpectateState()
{
AddStep("make user spectating", () => Client.ChangeState(MultiplayerUserState.Spectating));
AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle));
}
[Test]
public void TestCrownChangesStateWhenHostTransferred()
{
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
{
private MultiplayerReadyButton button;
private OnlinePlayBeatmapAvailablilityTracker beatmapTracker;
private OnlinePlayBeatmapAvailabilityTracker beatmapTracker;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
@@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
Add(beatmapTracker = new OnlinePlayBeatmapAvailablilityTracker
Add(beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker
{
SelectedItem = { BindTarget = selectedItem }
});
@@ -209,9 +209,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
addClickButtonStep();
AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddAssert("ready button disabled", () => !button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddAssert("ready button disabled", () => !button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddStep("transitioned to gameplay", () => readyClickOperation.Dispose());
AddStep("finish gameplay", () =>
{
Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
});
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
}
@@ -0,0 +1,193 @@
// 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.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
{
private MultiplayerSpectateButton spectateButton;
private MultiplayerReadyButton readyButton;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
private BeatmapSetInfo importedSet;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private IDisposable readyClickOperation;
protected override Container<Drawable> Content => content;
private readonly Container content;
public TestSceneMultiplayerSpectateButton()
{
base.Content.Add(content = new Container
{
RelativeSizeAxes = Axes.Both
});
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
return dependencies;
}
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
var beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = selectedItem } };
base.Content.Add(beatmapTracker);
Dependencies.Cache(beatmapTracker);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem
{
Beatmap = { Value = Beatmap.Value.BeatmapInfo },
Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset },
};
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
spectateButton = new MultiplayerSpectateButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
OnSpectateClick = async () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
await Client.ToggleSpectate();
readyClickOperation.Dispose();
}
},
readyButton = new MultiplayerReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
OnReadyClick = async () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
{
await Client.StartMatch();
return;
}
await Client.ToggleReady();
readyClickOperation.Dispose();
}
}
}
};
});
[Test]
public void TestEnabledWhenRoomOpen()
{
assertSpectateButtonEnablement(true);
}
[TestCase(MultiplayerUserState.Idle)]
[TestCase(MultiplayerUserState.Ready)]
public void TestToggleWhenIdle(MultiplayerUserState initialState)
{
addClickSpectateButtonStep();
AddAssert("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating);
addClickSpectateButtonStep();
AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(MultiplayerRoomState.WaitingForLoad)]
[TestCase(MultiplayerRoomState.Playing)]
[TestCase(MultiplayerRoomState.Closed)]
public void TestDisabledDuringGameplayOrClosed(MultiplayerRoomState roomState)
{
AddStep($"change user to {roomState}", () => Client.ChangeRoomState(roomState));
assertSpectateButtonEnablement(false);
}
[Test]
public void TestReadyButtonDisabledWhenHostAndNoReadyUsers()
{
addClickSpectateButtonStep();
assertReadyButtonEnablement(false);
}
[Test]
public void TestReadyButtonEnabledWhenHostAndUsersReady()
{
AddStep("add user", () => Client.AddUser(new User { Id = 55 }));
AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready));
addClickSpectateButtonStep();
assertReadyButtonEnablement(true);
}
[Test]
public void TestReadyButtonDisabledWhenNotHostAndUsersReady()
{
AddStep("add user and transfer host", () =>
{
Client.AddUser(new User { Id = 55 });
Client.TransferHost(55);
});
AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready));
addClickSpectateButtonStep();
assertReadyButtonEnablement(false);
}
private void addClickSpectateButtonStep() => AddStep("click spectate button", () =>
{
InputManager.MoveMouseTo(spectateButton);
InputManager.Click(MouseButton.Left);
});
private void assertSpectateButtonEnablement(bool shouldBeEnabled)
=> AddAssert($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
private void assertReadyButtonEnablement(bool shouldBeEnabled)
=> AddAssert($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
}
}
@@ -36,6 +36,8 @@ namespace osu.Game.Tests.Visual.Navigation
protected override bool UseFreshStoragePerRun => true;
protected override bool CreateNestedActionContainer => false;
[BackgroundDependencyLoader]
private void load(GameHost host)
{
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
@@ -43,6 +44,20 @@ namespace osu.Game.Tests.Visual.Navigation
exitViaEscapeAndConfirm();
}
/// <summary>
/// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding
/// but should be handled *after* song select).
/// </summary>
[Test]
public void TestOpenModSelectOverlayUsingAction()
{
TestSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect());
AddStep("Show mods overlay", () => InputManager.Key(Key.F1));
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
}
[Test]
public void TestRetryCountIncrements()
{
@@ -146,7 +161,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition));
// BackButton handles hover using its child button, so this checks whether or not any of BackButton's children are hovered.
AddUntilStep("Back button is hovered", () => InputManager.HoveredDrawables.Any(d => d.Parent == Game.BackButton));
AddUntilStep("Back button is hovered", () => Game.ChildrenOfType<BackButton>().First().Children.Any(c => c.IsHovered));
AddStep("Click back button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
@@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.SongSelect
@@ -14,6 +15,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
private BeatmapDetails details;
private DummyAPIAccess api => (DummyAPIAccess)API;
[SetUp]
public void Setup() => Schedule(() =>
{
@@ -173,6 +176,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
OnlineBeatmapID = 162,
});
AddStep("set online", () => api.SetState(APIState.Online));
AddStep("set offline", () => api.SetState(APIState.Offline));
}
}
}
@@ -0,0 +1,35 @@
// 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.
using osu.Framework.Graphics;
using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneSongSelectFooter : OsuManualInputManagerTestScene
{
public TestSceneSongSelectFooter()
{
AddStep("Create footer", () =>
{
Footer footer;
AddRange(new Drawable[]
{
footer = new Footer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
footer.AddButton(new FooterButtonMods(), null);
footer.AddButton(new FooterButtonRandom
{
NextRandom = () => { },
PreviousRandom = () => { },
}, null);
footer.AddButton(new FooterButtonOptions(), null);
});
}
}
}
+1 -33
View File
@@ -27,8 +27,6 @@ namespace osu.Game.Beatmaps
public IBeatmap Beatmap { get; }
private CancellationToken cancellationToken;
protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
{
Beatmap = beatmap;
@@ -41,8 +39,6 @@ namespace osu.Game.Beatmaps
public IBeatmap Convert(CancellationToken cancellationToken = default)
{
this.cancellationToken = cancellationToken;
// We always operate on a clone of the original beatmap, to not modify it game-wide
return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
}
@@ -55,19 +51,6 @@ namespace osu.Game.Beatmaps
/// <returns>The converted Beatmap.</returns>
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
#pragma warning disable 618
return ConvertBeatmap(original);
#pragma warning restore 618
}
/// <summary>
/// Performs the conversion of a Beatmap using this Beatmap Converter.
/// </summary>
/// <param name="original">The un-converted Beatmap.</param>
/// <returns>The converted Beatmap.</returns>
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original)
{
var beatmap = CreateBeatmap();
beatmap.BeatmapInfo = original.BeatmapInfo;
@@ -121,21 +104,6 @@ namespace osu.Game.Beatmaps
/// <param name="beatmap">The un-converted Beatmap.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The converted hit object.</returns>
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{
#pragma warning disable 618
return ConvertHitObject(original, beatmap);
#pragma warning restore 618
}
/// <summary>
/// Performs the conversion of a hit object.
/// This method is generally executed sequentially for all objects in a beatmap.
/// </summary>
/// <param name="original">The hit object to convert.</param>
/// <param name="beatmap">The un-converted Beatmap.</param>
/// <returns>The converted hit object.</returns>
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap) => Enumerable.Empty<T>();
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) => Enumerable.Empty<T>();
}
}
-12
View File
@@ -3,16 +3,11 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osuTK;
namespace osu.Game.Beatmaps
{
public class BeatmapStatistic
{
[Obsolete("Use CreateIcon instead")] // can be removed 20210203
public IconUsage Icon = FontAwesome.Regular.QuestionCircle;
/// <summary>
/// A function to create the icon for display purposes. Use default icons available via <see cref="BeatmapStatisticIcon"/> whenever possible for conformity.
/// </summary>
@@ -20,12 +15,5 @@ namespace osu.Game.Beatmaps
public string Content;
public string Name;
public BeatmapStatistic()
{
#pragma warning disable 618
CreateIcon = () => new SpriteIcon { Icon = Icon, Scale = new Vector2(0.7f) };
#pragma warning restore 618
}
}
}
@@ -273,7 +273,7 @@ namespace osu.Game.Beatmaps.Formats
if (hitObject is IHasPath path)
{
addPathData(writer, path, position);
writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true));
writer.Write(getSampleBank(hitObject.Samples));
}
else
{
@@ -329,7 +329,26 @@ namespace osu.Game.Beatmaps.Formats
if (point.Type.Value != null)
{
if (point.Type.Value != lastType)
// We've reached a new (explicit) segment!
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
bool needsExplicitSegment = point.Type.Value != lastType || point.Type.Value == PathType.PerfectCurve;
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.
if (i > 1)
{
// We need to use the absolute control point position to determine equality, otherwise floating point issues may arise.
Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position.Value;
Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position.Value;
if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y)
needsExplicitSegment = true;
}
if (needsExplicitSegment)
{
switch (point.Type.Value)
{
@@ -401,15 +420,15 @@ namespace osu.Game.Beatmaps.Formats
writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}"));
}
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false, bool zeroBanks = false)
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false)
{
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank);
StringBuilder sb = new StringBuilder();
sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)normalBank)}:"));
sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)addBank)}"));
sb.Append(FormattableString.Invariant($"{(int)normalBank}:"));
sb.Append(FormattableString.Invariant($"{(int)addBank}"));
if (!banksOnly)
{
@@ -36,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface
public override void PlayHoverSample()
{
sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08);
sampleHover.Frequency.Value = 0.98 + RNG.NextDouble(0.04);
sampleHover.Play();
}
}
@@ -53,6 +53,9 @@ namespace osu.Game.Graphics.UserInterface
Soft,
[Description("-toolbar")]
Toolbar
Toolbar,
[Description("-songselect")]
SongSelect
}
}
@@ -64,12 +64,20 @@ namespace osu.Game.Input.Bindings
protected override void ReloadMappings()
{
var defaults = DefaultKeyBindings.ToList();
if (ruleset != null && !ruleset.ID.HasValue)
// if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings.
// fallback to defaults instead.
KeyBindings = DefaultKeyBindings;
KeyBindings = defaults;
else
KeyBindings = store.Query(ruleset?.ID, variant).ToList();
{
KeyBindings = store.Query(ruleset?.ID, variant)
// this ordering is important to ensure that we read entries from the database in the order
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
// have been eaten by the music controller due to query order.
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.IntAction)).ToList();
}
}
}
}
@@ -13,6 +13,7 @@ namespace osu.Game.Input.Bindings
public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput
{
private readonly Drawable handler;
private InputManager parentInputManager;
public GlobalActionContainer(OsuGameBase game)
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
@@ -21,7 +22,18 @@ namespace osu.Game.Input.Bindings
handler = game;
}
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings);
protected override void LoadComplete()
{
base.LoadComplete();
parentInputManager = GetContainingInputManager();
}
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
.Concat(EditorKeyBindings)
.Concat(InGameKeyBindings)
.Concat(SongSelectKeyBindings)
.Concat(AudioControlKeyBindings);
public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
{
@@ -74,6 +86,14 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
};
public IEnumerable<KeyBinding> SongSelectKeyBindings => new[]
{
new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection),
new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom),
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom),
new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions)
};
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume),
@@ -91,8 +111,20 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.F3, GlobalAction.MusicPlay)
};
protected override IEnumerable<Drawable> KeyBindingInputQueue =>
handler == null ? base.KeyBindingInputQueue : base.KeyBindingInputQueue.Prepend(handler);
protected override IEnumerable<Drawable> KeyBindingInputQueue
{
get
{
// To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content.
// It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly
// allow the whole game to handle these actions.
// An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging.
var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue;
return handler != null ? inputQueue.Prepend(handler) : inputQueue;
}
}
}
public enum GlobalAction
@@ -204,5 +236,18 @@ namespace osu.Game.Input.Bindings
[Description("Toggle in-game interface")]
ToggleInGameInterface,
// Song select keybindings
[Description("Toggle Mod Select")]
ToggleModSelection,
[Description("Random")]
SelectNextRandom,
[Description("Rewind")]
SelectPreviousRandom,
[Description("Beatmap Options")]
ToggleBeatmapOptions,
}
}
@@ -96,6 +96,9 @@ namespace osu.Game.Online.Multiplayer
if (!IsConnected.Value)
return Task.CompletedTask;
if (newState == MultiplayerUserState.Spectating)
return Task.CompletedTask; // Not supported yet.
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
}
@@ -55,5 +55,10 @@ namespace osu.Game.Online.Multiplayer
/// The user is currently viewing results. This is a reserved state, and is set by the server.
/// </summary>
Results,
/// <summary>
/// The user is currently spectating this room.
/// </summary>
Spectating
}
}
@@ -249,6 +249,33 @@ namespace osu.Game.Online.Multiplayer
}
}
/// <summary>
/// Toggles the <see cref="LocalUser"/>'s spectating state.
/// </summary>
/// <exception cref="InvalidOperationException">If a toggle of the spectating state is not valid at this time.</exception>
public async Task ToggleSpectate()
{
var localUser = LocalUser;
if (localUser == null)
return;
switch (localUser.State)
{
case MultiplayerUserState.Idle:
case MultiplayerUserState.Ready:
await ChangeState(MultiplayerUserState.Spectating).ConfigureAwait(false);
return;
case MultiplayerUserState.Spectating:
await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
return;
default:
throw new InvalidOperationException($"Cannot toggle spectate when in {localUser.State}");
}
}
public abstract Task TransferHost(int userId);
public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
+7 -3
View File
@@ -23,13 +23,17 @@ namespace osu.Game.Online
private readonly string placeholderMessage;
private Placeholder placeholder;
private Drawable placeholder;
private const double transform_duration = 300;
[Resolved]
protected IAPIProvider API { get; private set; }
/// <summary>
/// Construct a new instance of an online view container.
/// </summary>
/// <param name="placeholderMessage">The message to display when not logged in. If empty, no button will display.</param>
public OnlineViewContainer(string placeholderMessage)
{
this.placeholderMessage = placeholderMessage;
@@ -40,10 +44,10 @@ namespace osu.Game.Online
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
{
InternalChildren = new Drawable[]
InternalChildren = new[]
{
Content,
placeholder = new LoginPlaceholder(placeholderMessage),
placeholder = string.IsNullOrEmpty(placeholderMessage) ? Empty() : new LoginPlaceholder(placeholderMessage),
LoadingSpinner = new LoadingSpinner
{
Alpha = 0,
@@ -16,7 +16,7 @@ namespace osu.Game.Online.Rooms
/// This differs from a regular download tracking composite as this accounts for the
/// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap.
/// </summary>
public class OnlinePlayBeatmapAvailablilityTracker : DownloadTrackingComposite<BeatmapSetInfo, BeatmapManager>
public class OnlinePlayBeatmapAvailabilityTracker : DownloadTrackingComposite<BeatmapSetInfo, BeatmapManager>
{
public readonly IBindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
@@ -1,6 +1,7 @@
// 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.
using System.Globalization;
using System.Net.Http;
using osu.Framework.IO.Network;
using osu.Game.Online.API;
@@ -11,11 +12,13 @@ namespace osu.Game.Online.Solo
public class CreateSoloScoreRequest : APIRequest<APIScoreToken>
{
private readonly int beatmapId;
private readonly int rulesetId;
private readonly string versionHash;
public CreateSoloScoreRequest(int beatmapId, string versionHash)
public CreateSoloScoreRequest(int beatmapId, int rulesetId, string versionHash)
{
this.beatmapId = beatmapId;
this.rulesetId = rulesetId;
this.versionHash = versionHash;
}
@@ -24,9 +27,10 @@ namespace osu.Game.Online.Solo
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.AddParameter("version_hash", versionHash);
req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture));
return req;
}
protected override string Target => $@"solo/{beatmapId}/scores";
protected override string Target => $@"beatmaps/{beatmapId}/solo/scores";
}
}
@@ -40,6 +40,6 @@ namespace osu.Game.Online.Solo
return req;
}
protected override string Target => $@"solo/{beatmapId}/scores/{scoreId}";
protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{scoreId}";
}
}
-8
View File
@@ -27,7 +27,6 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Collections;
@@ -879,13 +878,6 @@ namespace osu.Game
return component;
}
protected override bool OnScroll(ScrollEvent e)
{
// forward any unhandled mouse scroll events to the volume control.
volume.Adjust(GlobalAction.IncreaseVolume, e.ScrollDelta.Y, e.IsPrecise);
return true;
}
public bool OnPressed(GlobalAction action)
{
if (introScreen == null) return false;
+7 -6
View File
@@ -308,17 +308,18 @@ namespace osu.Game
AddInternal(RulesetConfigCache);
MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both };
GlobalActionContainer globalBindings;
MenuCursorContainer.Child = globalBindings = new GlobalActionContainer(this)
var mainContent = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }
MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both },
// to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything.
globalBindings = new GlobalActionContainer(this)
};
base.Content.Add(CreateScalingContainer().WithChild(MenuCursorContainer));
MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both };
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
KeyBindingStore.Register(globalBindings);
dependencies.Cache(globalBindings);
@@ -21,6 +21,7 @@ namespace osu.Game.Overlays.KeyBinding
{
Add(new DefaultBindingsSubsection(manager));
Add(new AudioControlKeyBindingsSubsection(manager));
Add(new SongSelectKeyBindingSubsection(manager));
Add(new InGameKeyBindingsSubsection(manager));
Add(new EditorKeyBindingsSubsection(manager));
}
@@ -36,6 +37,17 @@ namespace osu.Game.Overlays.KeyBinding
}
}
private class SongSelectKeyBindingSubsection : KeyBindingsSubsection
{
protected override string Header => "Song Select";
public SongSelectKeyBindingSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.SongSelectKeyBindings;
}
}
private class InGameKeyBindingsSubsection : KeyBindingsSubsection
{
protected override string Header => "In Game";
@@ -57,13 +57,6 @@ namespace osu.Game.Overlays.Settings
}
}
[Obsolete("Use Current instead")] // Can be removed 20210406
public Bindable<T> Bindable
{
get => Current;
set => Current = value;
}
public virtual Bindable<T> Current
{
get => controlWithCurrent.Current;
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Volume
@@ -17,6 +18,13 @@ namespace osu.Game.Overlays.Volume
public bool OnPressed(GlobalAction action) =>
ActionRequested?.Invoke(action) ?? false;
protected override bool OnScroll(ScrollEvent e)
{
// forward any unhandled mouse scroll events to the volume control.
ScrollActionRequested?.Invoke(GlobalAction.IncreaseVolume, e.ScrollDelta.Y, e.IsPrecise);
return true;
}
public bool OnScroll(GlobalAction action, float amount, bool isPrecise) =>
ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false;
@@ -16,11 +16,6 @@ namespace osu.Game.Rulesets.Difficulty
{
public abstract class DifficultyCalculator
{
/// <summary>
/// The length of each strain section.
/// </summary>
protected virtual int SectionLength => 400;
private readonly Ruleset ruleset;
private readonly WorkingBeatmap beatmap;
@@ -71,32 +66,14 @@ namespace osu.Game.Rulesets.Difficulty
var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList();
double sectionLength = SectionLength * clockRate;
// The first object doesn't generate a strain, so we begin with an incremented section end
double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
foreach (DifficultyHitObject h in difficultyHitObjects)
foreach (var hitObject in difficultyHitObjects)
{
while (h.BaseObject.StartTime > currentSectionEnd)
foreach (var skill in skills)
{
foreach (Skill s in skills)
{
s.SaveCurrentPeak();
s.StartNewSectionFrom(currentSectionEnd / clockRate);
}
currentSectionEnd += sectionLength;
skill.ProcessInternal(hitObject);
}
foreach (Skill s in skills)
s.Process(h);
}
// The peak strain will not be saved for the last section in the above loop
foreach (Skill s in skills)
s.SaveCurrentPeak();
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
}
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
public readonly HitObject LastObject;
/// <summary>
/// Amount of time elapsed between <see cref="BaseObject"/> and <see cref="LastObject"/>.
/// Amount of time elapsed between <see cref="BaseObject"/> and <see cref="LastObject"/>, adjusted by clockrate.
/// </summary>
public readonly double DeltaTime;
+16 -89
View File
@@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
@@ -11,123 +9,52 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty.Skills
{
/// <summary>
/// Used to processes strain values of <see cref="DifficultyHitObject"/>s, keep track of strain levels caused by the processed objects
/// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
/// A bare minimal abstract skill for fully custom skill implementations.
/// </summary>
public abstract class Skill
{
/// <summary>
/// The peak strain for each <see cref="DifficultyCalculator.SectionLength"/> section of the beatmap.
/// </summary>
public IReadOnlyList<double> StrainPeaks => strainPeaks;
/// <summary>
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
/// </summary>
protected abstract double SkillMultiplier { get; }
/// <summary>
/// Determines how quickly strain decays for the given skill.
/// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
/// </summary>
protected abstract double StrainDecayBase { get; }
/// <summary>
/// The weight by which each strain value decays.
/// </summary>
protected virtual double DecayWeight => 0.9;
/// <summary>
/// <see cref="DifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
/// </summary>
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet
protected readonly ReverseQueue<DifficultyHitObject> Previous;
/// <summary>
/// The current strain level.
/// Number of previous <see cref="DifficultyHitObject"/>s to keep inside the <see cref="Previous"/> queue.
/// </summary>
protected double CurrentStrain { get; private set; } = 1;
protected virtual int HistoryLength => 1;
/// <summary>
/// Mods for use in skill calculations.
/// </summary>
protected IReadOnlyList<Mod> Mods => mods;
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private readonly List<double> strainPeaks = new List<double>();
private readonly Mod[] mods;
protected Skill(Mod[] mods)
{
this.mods = mods;
Previous = new ReverseQueue<DifficultyHitObject>(HistoryLength + 1);
}
/// <summary>
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
/// </summary>
public void Process(DifficultyHitObject current)
internal void ProcessInternal(DifficultyHitObject current)
{
CurrentStrain *= strainDecay(current.DeltaTime);
CurrentStrain += StrainValueOf(current) * SkillMultiplier;
while (Previous.Count > HistoryLength)
Previous.Dequeue();
currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
Process(current);
Previous.Push(current);
Previous.Enqueue(current);
}
/// <summary>
/// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
/// Process a <see cref="DifficultyHitObject"/>.
/// </summary>
public void SaveCurrentPeak()
{
if (Previous.Count > 0)
strainPeaks.Add(currentSectionPeak);
}
/// <param name="current">The <see cref="DifficultyHitObject"/> to process.</param>
protected abstract void Process(DifficultyHitObject current);
/// <summary>
/// Sets the initial strain level for a new section.
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
/// </summary>
/// <param name="time">The beginning of the new section in milliseconds, adjusted by clockrate.</param>
public void StartNewSectionFrom(double time)
{
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
if (Previous.Count > 0)
currentSectionPeak = GetPeakStrain(time);
}
/// <summary>
/// Retrieves the peak strain at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the peak strain at, adjusted by clockrate.</param>
/// <returns>The peak strain.</returns>
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime);
/// <summary>
/// Returns the calculated difficulty value representing all processed <see cref="DifficultyHitObject"/>s.
/// </summary>
public double DifficultyValue()
{
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
foreach (double strain in strainPeaks.OrderByDescending(d => d))
{
difficulty += strain * weight;
weight *= DecayWeight;
}
return difficulty;
}
/// <summary>
/// Calculates the strain value of a <see cref="DifficultyHitObject"/>. This value is affected by previously processed objects.
/// </summary>
protected abstract double StrainValueOf(DifficultyHitObject current);
private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
public abstract double DifficultyValue();
}
}
@@ -0,0 +1,135 @@
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty.Skills
{
/// <summary>
/// Used to processes strain values of <see cref="DifficultyHitObject"/>s, keep track of strain levels caused by the processed objects
/// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
/// </summary>
public abstract class StrainSkill : Skill
{
/// <summary>
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
/// </summary>
protected abstract double SkillMultiplier { get; }
/// <summary>
/// Determines how quickly strain decays for the given skill.
/// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
/// </summary>
protected abstract double StrainDecayBase { get; }
/// <summary>
/// The weight by which each strain value decays.
/// </summary>
protected virtual double DecayWeight => 0.9;
/// <summary>
/// The current strain level.
/// </summary>
protected double CurrentStrain { get; private set; } = 1;
/// <summary>
/// The length of each strain section.
/// </summary>
protected virtual int SectionLength => 400;
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private double currentSectionEnd;
private readonly List<double> strainPeaks = new List<double>();
protected StrainSkill(Mod[] mods)
: base(mods)
{
}
/// <summary>
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
/// </summary>
protected sealed override void Process(DifficultyHitObject current)
{
// The first object doesn't generate a strain, so we begin with an incremented section end
if (Previous.Count == 0)
currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
while (current.StartTime > currentSectionEnd)
{
saveCurrentPeak();
startNewSectionFrom(currentSectionEnd);
currentSectionEnd += SectionLength;
}
CurrentStrain *= strainDecay(current.DeltaTime);
CurrentStrain += StrainValueOf(current) * SkillMultiplier;
currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
}
/// <summary>
/// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
/// </summary>
private void saveCurrentPeak()
{
strainPeaks.Add(currentSectionPeak);
}
/// <summary>
/// Sets the initial strain level for a new section.
/// </summary>
/// <param name="time">The beginning of the new section in milliseconds.</param>
private void startNewSectionFrom(double time)
{
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
currentSectionPeak = GetPeakStrain(time);
}
/// <summary>
/// Retrieves the peak strain at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the peak strain at.</param>
/// <returns>The peak strain.</returns>
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime);
/// <summary>
/// Returns a live enumerable of the peak strains for each <see cref="SectionLength"/> section of the beatmap,
/// including the peak of the current section.
/// </summary>
public IEnumerable<double> GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak);
/// <summary>
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
/// </summary>
public sealed override double DifficultyValue()
{
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d))
{
difficulty += strain * weight;
weight *= DecayWeight;
}
return difficulty;
}
/// <summary>
/// Calculates the strain value of a <see cref="DifficultyHitObject"/>. This value is affected by previously processed objects.
/// </summary>
protected abstract double StrainValueOf(DifficultyHitObject current);
private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
}
}
@@ -1,92 +0,0 @@
// 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.
using System;
using System.Collections;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Difficulty.Utils
{
/// <summary>
/// An indexed stack with limited depth. Indexing starts at the top of the stack.
/// </summary>
public class LimitedCapacityStack<T> : IEnumerable<T>
{
/// <summary>
/// The number of elements in the stack.
/// </summary>
public int Count { get; private set; }
private readonly T[] array;
private readonly int capacity;
private int marker; // Marks the position of the most recently added item.
/// <summary>
/// Constructs a new <see cref="LimitedCapacityStack{T}"/>.
/// </summary>
/// <param name="capacity">The number of items the stack can hold.</param>
public LimitedCapacityStack(int capacity)
{
if (capacity < 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
this.capacity = capacity;
array = new T[capacity];
marker = capacity; // Set marker to the end of the array, outside of the indexed range by one.
}
/// <summary>
/// Retrieves the item at an index in the stack.
/// </summary>
/// <param name="i">The index of the item to retrieve. The top of the stack is returned at index 0.</param>
public T this[int i]
{
get
{
if (i < 0 || i > Count - 1)
throw new ArgumentOutOfRangeException(nameof(i));
i += marker;
if (i > capacity - 1)
i -= capacity;
return array[i];
}
}
/// <summary>
/// Pushes an item to this <see cref="LimitedCapacityStack{T}"/>.
/// </summary>
/// <param name="item">The item to push.</param>
public void Push(T item)
{
// Overwrite the oldest item instead of shifting every item by one with every addition.
if (marker == 0)
marker = capacity - 1;
else
--marker;
array[marker] = item;
if (Count < capacity)
++Count;
}
/// <summary>
/// Returns an enumerator which enumerates items in the history starting from the most recently added one.
/// </summary>
public IEnumerator<T> GetEnumerator()
{
for (int i = marker; i < capacity; ++i)
yield return array[i];
if (Count == capacity)
{
for (int i = 0; i < marker; ++i)
yield return array[i];
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
@@ -0,0 +1,133 @@
// 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.
using System;
using System.Collections;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Difficulty.Utils
{
/// <summary>
/// An indexed queue where items are indexed beginning from the most recently enqueued item.
/// Enqueuing an item pushes all existing indexes up by one and inserts the item at index 0.
/// Dequeuing an item removes the item from the highest index and returns it.
/// </summary>
public class ReverseQueue<T> : IEnumerable<T>
{
/// <summary>
/// The number of elements in the <see cref="ReverseQueue{T}"/>.
/// </summary>
public int Count { get; private set; }
private T[] items;
private int capacity;
private int start;
public ReverseQueue(int initialCapacity)
{
if (initialCapacity <= 0)
throw new ArgumentOutOfRangeException(nameof(initialCapacity));
items = new T[initialCapacity];
capacity = initialCapacity;
start = 0;
Count = 0;
}
/// <summary>
/// Retrieves the item at an index in the <see cref="ReverseQueue{T}"/>.
/// </summary>
/// <param name="index">The index of the item to retrieve. The most recently enqueued item is at index 0.</param>
public T this[int index]
{
get
{
if (index < 0 || index > Count - 1)
throw new ArgumentOutOfRangeException(nameof(index));
int reverseIndex = Count - 1 - index;
return items[(start + reverseIndex) % capacity];
}
}
/// <summary>
/// Enqueues an item to this <see cref="ReverseQueue{T}"/>.
/// </summary>
/// <param name="item">The item to enqueue.</param>
public void Enqueue(T item)
{
if (Count == capacity)
{
// Double the buffer size
var buffer = new T[capacity * 2];
// Copy items to new queue
for (int i = 0; i < Count; i++)
{
buffer[i] = items[(start + i) % capacity];
}
// Replace array with new buffer
items = buffer;
capacity *= 2;
start = 0;
}
items[(start + Count) % capacity] = item;
Count++;
}
/// <summary>
/// Dequeues the least recently enqueued item from the <see cref="ReverseQueue{T}"/> and returns it.
/// </summary>
/// <returns>The item dequeued from the <see cref="ReverseQueue{T}"/>.</returns>
public T Dequeue()
{
var item = items[start];
start = (start + 1) % capacity;
Count--;
return item;
}
/// <summary>
/// Clears the <see cref="ReverseQueue{T}"/> of all items.
/// </summary>
public void Clear()
{
start = 0;
Count = 0;
}
/// <summary>
/// Returns an enumerator which enumerates items in the <see cref="ReverseQueue{T}"/> starting from the most recently enqueued item.
/// </summary>
public IEnumerator<T> GetEnumerator() => new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public struct Enumerator : IEnumerator<T>
{
private ReverseQueue<T> reverseQueue;
private int currentIndex;
internal Enumerator(ReverseQueue<T> reverseQueue)
{
this.reverseQueue = reverseQueue;
currentIndex = -1; // The first MoveNext() should bring the iterator to 0
}
public bool MoveNext() => ++currentIndex < reverseQueue.Count;
public void Reset() => currentIndex = -1;
public readonly T Current => reverseQueue[currentIndex];
readonly object IEnumerator.Current => Current;
public void Dispose()
{
reverseQueue = null;
}
}
}
}
+1 -13
View File
@@ -28,18 +28,6 @@ namespace osu.Game.Rulesets.Judgements
/// </summary>
protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05;
/// <summary>
/// Whether this <see cref="Judgement"/> should affect the current combo.
/// </summary>
[Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328
public virtual bool AffectsCombo => true;
/// <summary>
/// Whether this <see cref="Judgement"/> should be counted as base (combo) or bonus score.
/// </summary>
[Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328
public virtual bool IsBonus => !AffectsCombo;
/// <summary>
/// The maximum <see cref="HitResult"/> that can be achieved.
/// </summary>
@@ -181,7 +169,7 @@ namespace osu.Game.Rulesets.Judgements
return 300;
case HitResult.Perfect:
return 350;
return 315;
case HitResult.SmallBonus:
return SMALL_BONUS_SCORE;
@@ -11,7 +11,6 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
@@ -736,24 +735,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (!Result.HasResult)
throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}.");
// Some (especially older) rulesets use scorable judgements instead of the newer ignorehit/ignoremiss judgements.
// Can be removed 20210328
if (Result.Judgement.MaxResult == HitResult.IgnoreHit)
{
HitResult originalType = Result.Type;
if (Result.Type == HitResult.Miss)
Result.Type = HitResult.IgnoreMiss;
else if (Result.Type >= HitResult.Meh && Result.Type <= HitResult.Perfect)
Result.Type = HitResult.IgnoreHit;
if (Result.Type != originalType)
{
Logger.Log($"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n"
+ $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2021-03-28 onwards.", level: LogLevel.Important);
}
}
if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult))
{
throw new InvalidOperationException(
-9
View File
@@ -139,15 +139,6 @@ namespace osu.Game.Rulesets.Objects
}
protected virtual void CreateNestedHitObjects(CancellationToken cancellationToken)
{
// ReSharper disable once MethodSupportsCancellation (https://youtrack.jetbrains.com/issue/RIDER-44520)
#pragma warning disable 618
CreateNestedHitObjects();
#pragma warning restore 618
}
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
protected virtual void CreateNestedHitObjects()
{
}
@@ -336,9 +336,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
while (++endIndex < vertices.Length - endPointLength)
{
// Keep incrementing while an implicit segment doesn't need to be started
if (vertices[endIndex].Position.Value != vertices[endIndex - 1].Position.Value)
continue;
// The last control point of each segment is not allowed to start a new implicit segment.
if (endIndex == vertices.Length - endPointLength - 1)
continue;
// Force a type on the last point, and return the current control point set as a segment.
vertices[endIndex - 1].Type.Value = type;
yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex);
+33
View File
@@ -156,6 +156,39 @@ namespace osu.Game.Rulesets.Objects
return interpolateVertices(indexOfDistance(d), d);
}
/// <summary>
/// Returns the control points belonging to the same segment as the one given.
/// The first point has a PathType which all other points inherit.
/// </summary>
/// <param name="controlPoint">One of the control points in the segment.</param>
/// <returns></returns>
public List<PathControlPoint> PointsInSegment(PathControlPoint controlPoint)
{
bool found = false;
List<PathControlPoint> pointsInCurrentSegment = new List<PathControlPoint>();
foreach (PathControlPoint point in ControlPoints)
{
if (point.Type.Value != null)
{
if (!found)
pointsInCurrentSegment.Clear();
else
{
pointsInCurrentSegment.Add(point);
break;
}
}
pointsInCurrentSegment.Add(point);
if (point == controlPoint)
found = true;
}
return pointsInCurrentSegment;
}
private void invalidate()
{
pathCache.Invalidate();
@@ -346,12 +346,6 @@ namespace osu.Game.Rulesets.Scoring
score.HitEvents = hitEvents;
}
/// <summary>
/// Create a <see cref="HitWindows"/> for this processor.
/// </summary>
[Obsolete("Method is now unused.")] // Can be removed 20210328
public virtual HitWindows CreateHitWindows() => new HitWindows();
}
public enum ScoringMode
@@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -91,6 +92,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
},
ticks = new TimelineTickDisplay(),
controlPoints = new TimelineControlPointDisplay(),
new Box
{
Name = "zero marker",
RelativeSizeAxes = Axes.Y,
Width = 2,
Origin = Anchor.TopCentre,
Colour = colours.YellowDarker,
},
}
},
});
@@ -6,7 +6,9 @@ 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.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
@@ -16,6 +18,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
@@ -35,7 +38,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Bindable<HitObject> placement;
private SelectionBlueprint placementBlueprint;
private readonly Box backgroundBox;
private SelectableAreaBackground backgroundBox;
// we only care about checking vertical validity.
// this allows selecting and dragging selections before time=0.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
float localY = ToLocalSpace(screenSpacePos).Y;
return DrawRectangle.Top <= localY && DrawRectangle.Bottom >= localY;
}
public TimelineBlueprintContainer(HitObjectComposer composer)
: base(composer)
@@ -45,12 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Origin = Anchor.Centre;
Height = 0.6f;
}
AddInternal(backgroundBox = new Box
[BackgroundDependencyLoader]
private void load()
{
AddInternal(backgroundBox = new SelectableAreaBackground
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f,
Colour = Color4.Black
});
}
@@ -195,6 +208,33 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
private class SelectableAreaBackground : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
Alpha = 0.1f;
AddRangeInternal(new[]
{
// fade out over intro time, outside the valid time bounds.
new Box
{
RelativeSizeAxes = Axes.Y,
Width = 200,
Origin = Anchor.TopRight,
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White),
},
new Box
{
Colour = Color4.White,
RelativeSizeAxes = Axes.Both,
}
});
}
}
internal class TimelineSelectionHandler : SelectionHandler
{
// for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation
@@ -5,8 +5,8 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
@@ -18,15 +18,13 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledSliderBar<float> approachRateSlider;
private LabelledSliderBar<float> overallDifficultySlider;
public override LocalisableString Title => "Difficulty";
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Difficulty settings"
},
circleSizeSlider = new LabelledSliderBar<float>
{
Label = "Object Size",
@@ -2,12 +2,16 @@
// 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.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
@@ -17,27 +21,27 @@ namespace osu.Game.Screens.Edit.Setup
/// <summary>
/// A labelled textbox which reveals an inline file chooser when clicked.
/// </summary>
internal class FileChooserLabelledTextBox : LabelledTextBox
internal class FileChooserLabelledTextBox : LabelledTextBox, ICanAcceptFiles
{
private readonly string[] handledExtensions;
public IEnumerable<string> HandledExtensions => handledExtensions;
/// <summary>
/// The target container to display the file chooser in.
/// </summary>
public Container Target;
private readonly IBindable<FileInfo> currentFile = new Bindable<FileInfo>();
private readonly Bindable<FileInfo> currentFile = new Bindable<FileInfo>();
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private SectionsContainer<SetupSection> sectionsContainer { get; set; }
public FileChooserLabelledTextBox()
public FileChooserLabelledTextBox(params string[] handledExtensions)
{
currentFile.BindValueChanged(onFileSelected);
}
private void onFileSelected(ValueChangedEvent<FileInfo> file)
{
if (file.NewValue == null)
return;
Target.Clear();
Current.Value = file.NewValue.FullName;
this.handledExtensions = handledExtensions;
}
protected override OsuTextBox CreateTextBox() =>
@@ -54,7 +58,7 @@ namespace osu.Game.Screens.Edit.Setup
{
FileSelector fileSelector;
Target.Child = fileSelector = new FileSelector(currentFile.Value?.DirectoryName, ResourcesSection.AudioExtensions)
Target.Child = fileSelector = new FileSelector(currentFile.Value?.DirectoryName, handledExtensions)
{
RelativeSizeAxes = Axes.X,
Height = 400,
@@ -64,6 +68,37 @@ namespace osu.Game.Screens.Edit.Setup
sectionsContainer.ScrollTo(fileSelector);
}
protected override void LoadComplete()
{
base.LoadComplete();
game.RegisterImportHandler(this);
currentFile.BindValueChanged(onFileSelected);
}
private void onFileSelected(ValueChangedEvent<FileInfo> file)
{
if (file.NewValue == null)
return;
Target.Clear();
Current.Value = file.NewValue.FullName;
}
Task ICanAcceptFiles.Import(params string[] paths)
{
Schedule(() => currentFile.Value = new FileInfo(paths.First()));
return Task.CompletedTask;
}
Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
game.UnregisterImportHandler(this);
}
internal class FileChooserOsuTextBox : OsuTextBox
{
public Action OnFocused;
@@ -5,7 +5,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
@@ -17,15 +17,13 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledTextBox creatorTextBox;
private LabelledTextBox difficultyTextBox;
public override LocalisableString Title => "Metadata";
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Beatmap metadata"
},
artistTextBox = new LabelledTextBox
{
Label = "Artist",
+63 -117
View File
@@ -1,40 +1,25 @@
// 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.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
namespace osu.Game.Screens.Edit.Setup
{
internal class ResourcesSection : SetupSection, ICanAcceptFiles
internal class ResourcesSection : SetupSection
{
private LabelledTextBox audioTrackTextBox;
private Container backgroundSpriteContainer;
private LabelledTextBox backgroundTextBox;
public IEnumerable<string> HandledExtensions => ImageExtensions.Concat(AudioExtensions);
public static string[] ImageExtensions { get; } = { ".jpg", ".jpeg", ".png" };
public static string[] AudioExtensions { get; } = { ".mp3", ".ogg" };
[Resolved]
private OsuGameBase game { get; set; }
public override LocalisableString Title => "Resources";
[Resolved]
private MusicController music { get; set; }
@@ -48,70 +33,66 @@ namespace osu.Game.Screens.Edit.Setup
[Resolved(canBeNull: true)]
private Editor editor { get; set; }
[Resolved]
private SetupScreenHeader header { get; set; }
[BackgroundDependencyLoader]
private void load()
{
Container audioTrackFileChooserContainer = new Container
Container audioTrackFileChooserContainer = createFileChooserContainer();
Container backgroundFileChooserContainer = createFileChooserContainer();
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
backgroundTextBox = new FileChooserLabelledTextBox(".jpg", ".jpeg", ".png")
{
Label = "Background",
PlaceholderText = "Click to select a background image",
Current = { Value = working.Value.Metadata.BackgroundFile },
Target = backgroundFileChooserContainer,
TabbableContentContainer = this
},
backgroundFileChooserContainer,
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
audioTrackTextBox = new FileChooserLabelledTextBox(".mp3", ".ogg")
{
Label = "Audio Track",
PlaceholderText = "Click to select a track",
Current = { Value = working.Value.Metadata.AudioFile },
Target = audioTrackFileChooserContainer,
TabbableContentContainer = this
},
audioTrackFileChooserContainer,
}
}
};
backgroundTextBox.Current.BindValueChanged(backgroundChanged);
audioTrackTextBox.Current.BindValueChanged(audioTrackChanged);
}
private static Container createFileChooserContainer() =>
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
Children = new Drawable[]
{
backgroundSpriteContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = 250,
Masking = true,
CornerRadius = 10,
},
new OsuSpriteText
{
Text = "Resources"
},
audioTrackTextBox = new FileChooserLabelledTextBox
{
Label = "Audio Track",
PlaceholderText = "Click to select a track",
Current = { Value = working.Value.Metadata.AudioFile },
Target = audioTrackFileChooserContainer,
TabbableContentContainer = this
},
audioTrackFileChooserContainer,
};
updateBackgroundSprite();
audioTrackTextBox.Current.BindValueChanged(audioTrackChanged);
}
Task ICanAcceptFiles.Import(params string[] paths)
{
Schedule(() =>
{
var firstFile = new FileInfo(paths.First());
if (ImageExtensions.Contains(firstFile.Extension))
{
ChangeBackgroundImage(firstFile.FullName);
}
else if (AudioExtensions.Contains(firstFile.Extension))
{
audioTrackTextBox.Text = firstFile.FullName;
}
});
return Task.CompletedTask;
}
Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException();
protected override void LoadComplete()
{
base.LoadComplete();
game.RegisterImportHandler(this);
}
public bool ChangeBackgroundImage(string path)
{
var info = new FileInfo(path);
@@ -134,17 +115,11 @@ namespace osu.Game.Screens.Edit.Setup
}
working.Value.Metadata.BackgroundFile = info.Name;
updateBackgroundSprite();
header.Background.UpdateBackground();
return true;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
game?.UnregisterImportHandler(this);
}
public bool ChangeAudioTrack(string path)
{
var info = new FileInfo(path);
@@ -174,45 +149,16 @@ namespace osu.Game.Screens.Edit.Setup
return true;
}
private void backgroundChanged(ValueChangedEvent<string> filePath)
{
if (!ChangeBackgroundImage(filePath.NewValue))
backgroundTextBox.Current.Value = filePath.OldValue;
}
private void audioTrackChanged(ValueChangedEvent<string> filePath)
{
if (!ChangeAudioTrack(filePath.NewValue))
audioTrackTextBox.Current.Value = filePath.OldValue;
}
private void updateBackgroundSprite()
{
LoadComponentAsync(new BeatmapBackgroundSprite(working.Value)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
}, background =>
{
if (background.Texture != null)
backgroundSpriteContainer.Child = background;
else
{
backgroundSpriteContainer.Children = new Drawable[]
{
new Box
{
Colour = Colours.GreySeafoamDarker,
RelativeSizeAxes = Axes.Both,
},
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 24))
{
Text = "Drag image here to set beatmap background!",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.X,
}
};
}
background.FadeInFromZero(500);
});
}
}
}
+12 -19
View File
@@ -13,16 +13,24 @@ namespace osu.Game.Screens.Edit.Setup
{
public class SetupScreen : EditorScreen
{
public const int HORIZONTAL_PADDING = 100;
[Resolved]
private OsuColour colours { get; set; }
[Cached]
protected readonly OverlayColourProvider ColourProvider;
[Cached]
private SectionsContainer<SetupSection> sections = new SectionsContainer<SetupSection>();
[Cached]
private SetupScreenHeader header = new SetupScreenHeader();
public SetupScreen()
: base(EditorScreenMode.SongSetup)
{
ColourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
ColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
}
[BackgroundDependencyLoader]
@@ -41,12 +49,12 @@ namespace osu.Game.Screens.Edit.Setup
{
new Box
{
Colour = colours.GreySeafoamDark,
Colour = ColourProvider.Dark4,
RelativeSizeAxes = Axes.Both,
},
new SectionsContainer<SetupSection>
sections = new SectionsContainer<SetupSection>
{
FixedHeader = new SetupScreenHeader(),
FixedHeader = header,
RelativeSizeAxes = Axes.Both,
Children = new SetupSection[]
{
@@ -60,19 +68,4 @@ namespace osu.Game.Screens.Edit.Setup
};
}
}
internal class SetupScreenHeader : OverlayHeader
{
protected override OverlayTitle CreateTitle() => new SetupScreenTitle();
private class SetupScreenTitle : OverlayTitle
{
public SetupScreenTitle()
{
Title = "beatmap setup";
Description = "change general settings of your beatmap";
IconTexture = "Icons/Hexacons/social";
}
}
}
}
@@ -0,0 +1,120 @@
// 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.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Setup
{
internal class SetupScreenHeader : OverlayHeader
{
public SetupScreenHeaderBackground Background { get; private set; }
[Resolved]
private SectionsContainer<SetupSection> sections { get; set; }
private SetupScreenTabControl tabControl;
protected override OverlayTitle CreateTitle() => new SetupScreenTitle();
protected override Drawable CreateContent() => new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
// reverse flow is used to ensure that the tab control's expandable bars extend over the background chooser.
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
tabControl = new SetupScreenTabControl
{
RelativeSizeAxes = Axes.X,
Height = 30
},
Background = new SetupScreenHeaderBackground
{
RelativeSizeAxes = Axes.X,
Height = 120
}
}
}
};
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
tabControl.AccentColour = colourProvider.Highlight1;
tabControl.BackgroundColour = colourProvider.Dark5;
foreach (var section in sections)
tabControl.AddItem(section);
}
protected override void LoadComplete()
{
base.LoadComplete();
sections.SelectedSection.BindValueChanged(section => tabControl.Current.Value = section.NewValue);
tabControl.Current.BindValueChanged(section =>
{
if (section.NewValue != sections.SelectedSection.Value)
sections.ScrollTo(section.NewValue);
});
}
private class SetupScreenTitle : OverlayTitle
{
public SetupScreenTitle()
{
Title = "beatmap setup";
Description = "change general settings of your beatmap";
IconTexture = "Icons/Hexacons/social";
}
}
internal class SetupScreenTabControl : OverlayTabControl<SetupSection>
{
private readonly Box background;
public Color4 BackgroundColour
{
get => background.Colour;
set => background.Colour = value;
}
public SetupScreenTabControl()
{
TabContainer.Margin = new MarginPadding { Horizontal = SetupScreen.HORIZONTAL_PADDING };
AddInternal(background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = 1
});
}
protected override TabItem<SetupSection> CreateTabItem(SetupSection value) => new SetupScreenTabItem(value)
{
AccentColour = AccentColour
};
private class SetupScreenTabItem : OverlayTabItem
{
public SetupScreenTabItem(SetupSection value)
: base(value)
{
Text.Text = value.Title;
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More