diff --git a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml index 27ba142e96..7b08163ceb 100644 --- a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml +++ b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml @@ -1,6 +1,6 @@ - + diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml deleted file mode 100644 index 680312ad27..0000000000 --- a/.idea/.idea.osu.Desktop/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index e09b4d86a5..0d6af2aeba 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ If your platform is not listed above, there is still a chance you can manually b ## Developing a custom ruleset -osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu-templates). +osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates). You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852). diff --git a/Templates/Directory.Build.props b/Templates/Directory.Build.props new file mode 100644 index 0000000000..0e470106e8 --- /dev/null +++ b/Templates/Directory.Build.props @@ -0,0 +1,3 @@ + + + diff --git a/Templates/README.md b/Templates/README.md new file mode 100644 index 0000000000..cf25a89273 --- /dev/null +++ b/Templates/README.md @@ -0,0 +1,21 @@ +# Templates + +Templates for use when creating osu! dependent projects. Create a fully-testable (and ready for git) custom ruleset in just two lines. + +## Usage + +```bash +# install (or update) templates package. +# this only needs to be done once +dotnet new -i ppy.osu.Game.Templates + +# create an empty freeform ruleset +dotnet new ruleset -n MyCoolRuleset +# create an empty scrolling ruleset (which provides the basics for a scrolling ←↑→↓ ruleset) +dotnet new ruleset-scrolling -n MyCoolRuleset + +# ..or start with a working sample freeform game +dotnet new ruleset-example -n MyCoolWorkingRuleset +# ..or a working sample scrolling game +dotnet new ruleset-scrolling-example -n MyCoolWorkingRuleset +``` diff --git a/Templates/Rulesets/ruleset-empty/.editorconfig b/Templates/Rulesets/ruleset-empty/.editorconfig new file mode 100644 index 0000000000..f3badda9b3 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/.editorconfig @@ -0,0 +1,200 @@ +# EditorConfig is awesome: http://editorconfig.org +root = true + +[*.cs] +end_of_line = crlf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +#Roslyn naming styles + +#PascalCase for public and protected members +dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.public_members_pascalcase.severity = error +dotnet_naming_rule.public_members_pascalcase.symbols = public_members +dotnet_naming_rule.public_members_pascalcase.style = pascalcase + +#camelCase for private members +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_symbols.private_members.applicable_accessibilities = private +dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.private_members_camelcase.severity = warning +dotnet_naming_rule.private_members_camelcase.symbols = private_members +dotnet_naming_rule.private_members_camelcase.style = camelcase + +dotnet_naming_symbols.local_function.applicable_kinds = local_function +dotnet_naming_rule.local_function_camelcase.severity = warning +dotnet_naming_rule.local_function_camelcase.symbols = local_function +dotnet_naming_rule.local_function_camelcase.style = camelcase + +#all_lower for private and local constants/static readonlys +dotnet_naming_style.all_lower.capitalization = all_lower +dotnet_naming_style.all_lower.word_separator = _ + +dotnet_naming_symbols.private_constants.applicable_accessibilities = private +dotnet_naming_symbols.private_constants.required_modifiers = const +dotnet_naming_symbols.private_constants.applicable_kinds = field +dotnet_naming_rule.private_const_all_lower.severity = warning +dotnet_naming_rule.private_const_all_lower.symbols = private_constants +dotnet_naming_rule.private_const_all_lower.style = all_lower + +dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.private_static_readonly.applicable_kinds = field +dotnet_naming_rule.private_static_readonly_all_lower.severity = warning +dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly +dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.required_modifiers = const +dotnet_naming_rule.local_const_all_lower.severity = warning +dotnet_naming_rule.local_const_all_lower.symbols = local_constants +dotnet_naming_rule.local_const_all_lower.style = all_lower + +#ALL_UPPER for non private constants/static readonlys +dotnet_naming_style.all_upper.capitalization = all_upper +dotnet_naming_style.all_upper.word_separator = _ + +dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_constants.required_modifiers = const +dotnet_naming_symbols.public_constants.applicable_kinds = field +dotnet_naming_rule.public_const_all_upper.severity = warning +dotnet_naming_rule.public_const_all_upper.symbols = public_constants +dotnet_naming_rule.public_const_all_upper.style = all_upper + +dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.public_static_readonly.applicable_kinds = field +dotnet_naming_rule.public_static_readonly_all_upper.severity = warning +dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly +dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper + +#Roslyn formating options + +#Formatting - indentation options +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +#Formatting - new line options +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_open_brace = all +#csharp_new_line_before_members_in_anonymous_types = true +#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing +csharp_new_line_between_query_expression_clauses = true + +#Formatting - organize using options +dotnet_sort_system_directives_first = true + +#Formatting - spacing options +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#Roslyn language styles + +#Style - this. qualification +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning + +#Style - type names +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_for_built_in_types = true:none +csharp_style_var_elsewhere = true:silent + +#Style - modifiers +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning + +#Style - parentheses +# Skipped because roslyn cannot separate +-*/ with << >> + +#Style - expression bodies +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_local_functions = true:silent + +#Style - expression preferences +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_compound_assignment = true:warning + +#Style - null/type checks +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_throw_expression = true:silent +csharp_style_conditional_delegate_call = true:warning + +#Style - unused +dotnet_style_readonly_field = true:silent +dotnet_code_quality_unused_parameters = non_public:silent +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:warning + +#Style - variable declaration +csharp_style_inlined_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = true:warning + +#Style - other C# 7.x features +dotnet_style_prefer_inferred_tuple_names = true:warning +csharp_prefer_simple_default_expression = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent + +#Style - C# 8 features +csharp_prefer_static_local_function = true:warning +csharp_prefer_simple_using_statement = true:silent +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_switch_expression = false:none + +#Supressing roslyn built-in analyzers +# Suppress: EC112 + +#Private method is unused +dotnet_diagnostic.IDE0051.severity = silent +#Private member is unused +dotnet_diagnostic.IDE0052.severity = silent + +#Rules for disposable +dotnet_diagnostic.IDE0067.severity = none +dotnet_diagnostic.IDE0068.severity = none +dotnet_diagnostic.IDE0069.severity = none + +#Disable operator overloads requiring alternate named methods +dotnet_diagnostic.CA2225.severity = none + +# Banned APIs +dotnet_diagnostic.RS0030.severity = error \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-empty/.gitignore b/Templates/Rulesets/ruleset-empty/.gitignore new file mode 100644 index 0000000000..940794e60f --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/.gitignore @@ -0,0 +1,288 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs diff --git a/Templates/Rulesets/ruleset-empty/.template.config/template.json b/Templates/Rulesets/ruleset-empty/.template.config/template.json new file mode 100644 index 0000000000..6bfe2e19dc --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "ppy Pty Ltd", + "classifications": [ + "Console" + ], + "name": "osu! ruleset", + "identity": "ppy.osu.Game.Templates.Rulesets", + "shortName": "ruleset", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "EmptyFreeform", + "preferNameDirectory": true +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json new file mode 100644 index 0000000000..fd03878699 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "VisualTests (Debug)", + "type": "coreclr", + "request": "launch", + "program": "dotnet", + "args": [ + "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll" + ], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Debug)", + "env": {}, + "console": "internalConsole" + }, + { + "name": "VisualTests (Release)", + "type": "coreclr", + "request": "launch", + "program": "dotnet", + "args": [ + "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll" + ], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Release)", + "env": {}, + "console": "internalConsole" + } + ] +} \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/tasks.json new file mode 100644 index 0000000000..509df6a510 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/tasks.json @@ -0,0 +1,47 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Build (Debug)", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "--no-restore", + "osu.Game.Rulesets.EmptyFreeformRuleset.Tests.csproj", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Build (Release)", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "--no-restore", + "osu.Game.Rulesets.EmptyFreeformRuleset.Tests.csproj", + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Restore", + "type": "shell", + "command": "dotnet", + "args": [ + "restore" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs new file mode 100644 index 0000000000..9c512a01ea --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Platform; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.EmptyFreeform.Tests +{ + public class TestSceneOsuGame : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(GameHost host, OsuGameBase gameBase) + { + OsuGame game = new OsuGame(); + game.SetHost(host); + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + game + }; + } + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuPlayer.cs new file mode 100644 index 0000000000..0f2ddf82a5 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuPlayer.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.EmptyFreeform.Tests +{ + [TestFixture] + public class TestSceneOsuPlayer : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new EmptyFreeformRuleset(); + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs new file mode 100644 index 0000000000..4f810ce17f --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework; +using osu.Framework.Platform; +using osu.Game.Tests; + +namespace osu.Game.Rulesets.EmptyFreeform.Tests +{ + public static class VisualTestRunner + { + [STAThread] + public static int Main(string[] args) + { + using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + { + host.Run(new OsuTestBrowser()); + return 0; + } + } + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj new file mode 100644 index 0000000000..98a32f9b3a --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -0,0 +1,26 @@ + + + osu.Game.Rulesets.EmptyFreeform.Tests.VisualTestRunner + + + + + + false + + + + + + + + + + + + + WinExe + net5.0 + osu.Game.Rulesets.EmptyFreeform.Tests + + \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln new file mode 100644 index 0000000000..706df08472 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln @@ -0,0 +1,96 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29123.88 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.EmptyFreeform", "osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyFreeform.Tests", "osu.Game.Rulesets.EmptyFreeform.Tests\osu.Game.Rulesets.EmptyFreeform.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + VisualTests|Any CPU = VisualTests|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.EolMarker = Windows + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = True + $2.NewLinesForBracesInProperties = True + $2.NewLinesForBracesInAccessors = True + $2.NewLinesForBracesInAnonymousMethods = True + $2.NewLinesForBracesInControlBlocks = True + $2.NewLinesForBracesInAnonymousTypes = True + $2.NewLinesForBracesInObjectCollectionArrayInitializers = True + $2.NewLinesForBracesInLambdaExpressionBody = True + $2.NewLineForElse = True + $2.NewLineForCatch = True + $2.NewLineForFinally = True + $2.NewLineForMembersInObjectInit = True + $2.NewLineForMembersInAnonymousTypes = True + $2.NewLineForClausesInQuery = True + $2.SpacingAfterMethodDeclarationName = False + $2.SpaceAfterMethodCallName = False + $2.SpaceBeforeOpenSquareBracket = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.EolMarker = Windows + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = True + $2.NewLinesForBracesInProperties = True + $2.NewLinesForBracesInAccessors = True + $2.NewLinesForBracesInAnonymousMethods = True + $2.NewLinesForBracesInControlBlocks = True + $2.NewLinesForBracesInAnonymousTypes = True + $2.NewLinesForBracesInObjectCollectionArrayInitializers = True + $2.NewLinesForBracesInLambdaExpressionBody = True + $2.NewLineForElse = True + $2.NewLineForCatch = True + $2.NewLineForFinally = True + $2.NewLineForMembersInObjectInit = True + $2.NewLineForMembersInAnonymousTypes = True + $2.NewLineForClausesInQuery = True + $2.SpacingAfterMethodDeclarationName = False + $2.SpaceAfterMethodCallName = False + $2.SpaceBeforeOpenSquareBracket = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + EndGlobalSection +EndGlobal diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings new file mode 100644 index 0000000000..aa8f8739c1 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings @@ -0,0 +1,934 @@ + + True + True + True + True + ExplicitlyExcluded + ExplicitlyExcluded + SOLUTION + WARNING + WARNING + WARNING + HINT + HINT + WARNING + WARNING + True + WARNING + WARNING + HINT + DO_NOT_SHOW + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + SUGGESTION + HINT + HINT + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + WARNING + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + DO_NOT_SHOW + WARNING + HINT + DO_NOT_SHOW + HINT + HINT + ERROR + WARNING + HINT + HINT + HINT + WARNING + WARNING + HINT + DO_NOT_SHOW + HINT + HINT + HINT + HINT + WARNING + WARNING + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + HINT + HINT + DO_NOT_SHOW + HINT + HINT + WARNING + WARNING + HINT + WARNING + WARNING + WARNING + WARNING + HINT + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + ERROR + WARNING + WARNING + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + WARNING + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + + True + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + HINT + WARNING + WARNING + <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile> + Code Cleanup (peppy) + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + Explicit + ExpressionBody + BlockBody + True + NEXT_LINE + True + True + True + True + True + True + True + True + NEXT_LINE + 1 + 1 + NEXT_LINE + MULTILINE + True + True + True + True + NEXT_LINE + 1 + 1 + True + NEXT_LINE + NEVER + NEVER + True + False + True + NEVER + False + False + True + False + False + True + True + False + False + CHOP_IF_LONG + True + 200 + CHOP_IF_LONG + False + False + AABB + API + BPM + GC + GL + GLSL + HID + HTML + HUD + ID + IL + IOS + IP + IPC + JIT + LTRB + MD5 + NS + OS + PM + RGB + RNG + SHA + SRGB + TK + SS + PP + GMT + QAT + BNG + UI + False + HINT + <?xml version="1.0" encoding="utf-16"?> +<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="COM interfaces or structs"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" /> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry Priority="100" DisplayName="Test Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="Default Pattern"> + <Group DisplayName="Fields/Properties"> + <Group DisplayName="Public Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Public Properties"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Internal Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Internal Properties"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Protected Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Protected Properties"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Private Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Private Properties"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Constructor/Destructor"> + <Entry DisplayName="Ctor"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + </Entry> + <Region Name="Disposal"> + <Entry DisplayName="Dtor"> + <Entry.Match> + <Kind Is="Destructor" /> + </Entry.Match> + </Entry> + <Entry DisplayName="Dispose()"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Method" /> + <Name Is="Dispose" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Dispose(true)"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Or> + <Virtual /> + <Override /> + </Or> + <Kind Is="Method" /> + <Name Is="Dispose" /> + </And> + </Entry.Match> + </Entry> + </Region> + </Group> + <Group DisplayName="Methods"> + <Group DisplayName="Public"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Internal"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Protected"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Private"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + </Group> + </TypePattern> +</Patterns> + Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +See the LICENCE file in the repository root for full licence text. + + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> + <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + + True + True + True + True + True + True + True + True + True + True + True + TestFolder + True + True + o!f – Object Initializer: Anchor&Origin + True + constant("Centre") + 0 + True + True + 2.0 + InCSharpFile + ofao + True + Anchor = Anchor.$anchor$, +Origin = Anchor.$anchor$, + True + True + o!f – InternalChildren = [] + True + True + 2.0 + InCSharpFile + ofic + True + InternalChildren = new Drawable[] +{ + $END$ +}; + True + True + o!f – new GridContainer { .. } + True + True + 2.0 + InCSharpFile + ofgc + True + new GridContainer +{ + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { $END$ }, + new Drawable[] { } + } +}; + True + True + o!f – new FillFlowContainer { .. } + True + True + 2.0 + InCSharpFile + offf + True + new FillFlowContainer +{ + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – new Container { .. } + True + True + 2.0 + InCSharpFile + ofcont + True + new Container +{ + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – BackgroundDependencyLoader load() + True + True + 2.0 + InCSharpFile + ofbdl + True + [BackgroundDependencyLoader] +private void load() +{ + $END$ +} + True + True + o!f – new Box { .. } + True + True + 2.0 + InCSharpFile + ofbox + True + new Box +{ + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, +}, + True + True + o!f – Children = [] + True + True + 2.0 + InCSharpFile + ofc + True + Children = new Drawable[] +{ + $END$ +}; + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Beatmaps/EmptyFreeformBeatmapConverter.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Beatmaps/EmptyFreeformBeatmapConverter.cs new file mode 100644 index 0000000000..a441438d80 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Beatmaps/EmptyFreeformBeatmapConverter.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Threading; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.EmptyFreeform.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.EmptyFreeform.Beatmaps +{ + public class EmptyFreeformBeatmapConverter : BeatmapConverter + { + public EmptyFreeformBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) + { + } + + // todo: Check for conversion types that should be supported (ie. Beatmap.HitObjects.Any(h => h is IHasXPosition)) + // https://github.com/ppy/osu/tree/master/osu.Game/Rulesets/Objects/Types + public override bool CanConvert() => true; + + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) + { + yield return new EmptyFreeformHitObject + { + Samples = original.Samples, + StartTime = original.StartTime, + Position = (original as IHasPosition)?.Position ?? Vector2.Zero, + }; + } + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs new file mode 100644 index 0000000000..59a68245a6 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.EmptyFreeform +{ + public class EmptyFreeformDifficultyCalculator : DifficultyCalculator + { + public EmptyFreeformDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) + { + } + + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + { + return new DifficultyAttributes(mods, skills, 0); + } + + protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty(); + + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformInputManager.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformInputManager.cs new file mode 100644 index 0000000000..b292a28c0d --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformInputManager.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.EmptyFreeform +{ + public class EmptyFreeformInputManager : RulesetInputManager + { + public EmptyFreeformInputManager(RulesetInfo ruleset) + : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + } + + public enum EmptyFreeformAction + { + [Description("Button 1")] + Button1, + + [Description("Button 2")] + Button2, + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs new file mode 100644 index 0000000000..96675e3e99 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.EmptyFreeform.Beatmaps; +using osu.Game.Rulesets.EmptyFreeform.Mods; +using osu.Game.Rulesets.EmptyFreeform.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.EmptyFreeform +{ + public class EmptyFreeformRuleset : Ruleset + { + public override string Description => "a very emptyfreeformruleset ruleset"; + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => + new DrawableEmptyFreeformRuleset(this, beatmap, mods); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => + new EmptyFreeformBeatmapConverter(beatmap, this); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => + new EmptyFreeformDifficultyCalculator(this, beatmap); + + public override IEnumerable GetModsFor(ModType type) + { + switch (type) + { + case ModType.Automation: + return new[] { new EmptyFreeformModAutoplay() }; + + default: + return new Mod[] { null }; + } + } + + public override string ShortName => "emptyfreeformruleset"; + + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] + { + new KeyBinding(InputKey.Z, EmptyFreeformAction.Button1), + new KeyBinding(InputKey.X, EmptyFreeformAction.Button2), + }; + + public override Drawable CreateIcon() => new Icon(ShortName[0]); + + public class Icon : CompositeDrawable + { + public Icon(char c) + { + InternalChildren = new Drawable[] + { + new Circle + { + Size = new Vector2(20), + Colour = Color4.White, + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = c.ToString(), + Font = OsuFont.Default.With(size: 18) + } + }; + } + } + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs new file mode 100644 index 0000000000..d5c1e9bd15 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.EmptyFreeform.Objects; +using osu.Game.Rulesets.EmptyFreeform.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Rulesets.EmptyFreeform.Mods +{ + public class EmptyFreeformModAutoplay : ModAutoplay + { + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score + { + ScoreInfo = new ScoreInfo + { + User = new User { Username = "sample" }, + }, + Replay = new EmptyFreeformAutoGenerator(beatmap).Generate(), + }; + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs new file mode 100644 index 0000000000..0f38e9fdf8 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.EmptyFreeform.Objects.Drawables +{ + public class DrawableEmptyFreeformHitObject : DrawableHitObject + { + public DrawableEmptyFreeformHitObject(EmptyFreeformHitObject hitObject) + : base(hitObject) + { + Size = new Vector2(40); + Origin = Anchor.Centre; + + Position = hitObject.Position; + + // todo: add visuals. + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (timeOffset >= 0) + // todo: implement judgement logic + ApplyResult(r => r.Type = HitResult.Perfect); + } + + protected override void UpdateHitStateTransforms(ArmedState state) + { + const double duration = 1000; + + switch (state) + { + case ArmedState.Hit: + this.FadeOut(duration, Easing.OutQuint).Expire(); + break; + + case ArmedState.Miss: + this.FadeColour(Color4.Red, duration); + this.FadeOut(duration, Easing.InQuint).Expire(); + break; + } + } + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs new file mode 100644 index 0000000000..9cd18d2d9f --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.EmptyFreeform.Objects +{ + public class EmptyFreeformHitObject : HitObject, IHasPosition + { + public override Judgement CreateJudgement() => new Judgement(); + + public Vector2 Position { get; set; } + + public float X => Position.X; + public float Y => Position.Y; + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs new file mode 100644 index 0000000000..6d8d4215a2 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.EmptyFreeform.Objects; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.EmptyFreeform.Replays +{ + public class EmptyFreeformAutoGenerator : AutoGenerator + { + protected Replay Replay; + protected List Frames => Replay.Frames; + + public new Beatmap Beatmap => (Beatmap)base.Beatmap; + + public EmptyFreeformAutoGenerator(IBeatmap beatmap) + : base(beatmap) + { + Replay = new Replay(); + } + + public override Replay Generate() + { + Frames.Add(new EmptyFreeformReplayFrame()); + + foreach (EmptyFreeformHitObject hitObject in Beatmap.HitObjects) + { + Frames.Add(new EmptyFreeformReplayFrame + { + Time = hitObject.StartTime, + Position = hitObject.Position, + // todo: add required inputs and extra frames. + }); + } + + return Replay; + } + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs new file mode 100644 index 0000000000..f25ea6ec62 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Input.StateChanges; +using osu.Framework.Utils; +using osu.Game.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.EmptyFreeform.Replays +{ + public class EmptyFreeformFramedReplayInputHandler : FramedReplayInputHandler + { + public EmptyFreeformFramedReplayInputHandler(Replay replay) + : base(replay) + { + } + + protected override bool IsImportant(EmptyFreeformReplayFrame frame) => frame.Actions.Any(); + + protected Vector2 Position + { + get + { + var frame = CurrentFrame; + + if (frame == null) + return Vector2.Zero; + + Debug.Assert(CurrentTime != null); + + return Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time); + } + } + + public override void CollectPendingInputs(List inputs) + { + inputs.Add(new MousePositionAbsoluteInput + { + Position = GamefieldToScreenSpace(Position), + }); + inputs.Add(new ReplayState + { + PressedActions = CurrentFrame?.Actions ?? new List(), + }); + } + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs new file mode 100644 index 0000000000..c84101ca70 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.EmptyFreeform.Replays +{ + public class EmptyFreeformReplayFrame : ReplayFrame + { + public List Actions = new List(); + public Vector2 Position; + + public EmptyFreeformReplayFrame(EmptyFreeformAction? button = null) + { + if (button.HasValue) + Actions.Add(button.Value); + } + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/DrawableEmptyFreeformRuleset.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/DrawableEmptyFreeformRuleset.cs new file mode 100644 index 0000000000..290f35f516 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/DrawableEmptyFreeformRuleset.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Input.Handlers; +using osu.Game.Replays; +using osu.Game.Rulesets.EmptyFreeform.Objects; +using osu.Game.Rulesets.EmptyFreeform.Objects.Drawables; +using osu.Game.Rulesets.EmptyFreeform.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.EmptyFreeform.UI +{ + [Cached] + public class DrawableEmptyFreeformRuleset : DrawableRuleset + { + public DrawableEmptyFreeformRuleset(EmptyFreeformRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + : base(ruleset, beatmap, mods) + { + } + + protected override Playfield CreatePlayfield() => new EmptyFreeformPlayfield(); + + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new EmptyFreeformFramedReplayInputHandler(replay); + + public override DrawableHitObject CreateDrawableRepresentation(EmptyFreeformHitObject h) => new DrawableEmptyFreeformHitObject(h); + + protected override PassThroughInputManager CreateInputManager() => new EmptyFreeformInputManager(Ruleset?.RulesetInfo); + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/EmptyFreeformPlayfield.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/EmptyFreeformPlayfield.cs new file mode 100644 index 0000000000..9df5935c45 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/EmptyFreeformPlayfield.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.EmptyFreeform.UI +{ + [Cached] + public class EmptyFreeformPlayfield : Playfield + { + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + HitObjectContainer, + }); + } + } +} diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj new file mode 100644 index 0000000000..cfe2bd1cb2 --- /dev/null +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj @@ -0,0 +1,15 @@ + + + netstandard2.1 + osu.Game.Rulesets.Sample + Library + AnyCPU + osu.Game.Rulesets.EmptyFreeform + + + + + + + + \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-example/.editorconfig b/Templates/Rulesets/ruleset-example/.editorconfig new file mode 100644 index 0000000000..f3badda9b3 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/.editorconfig @@ -0,0 +1,200 @@ +# EditorConfig is awesome: http://editorconfig.org +root = true + +[*.cs] +end_of_line = crlf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +#Roslyn naming styles + +#PascalCase for public and protected members +dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.public_members_pascalcase.severity = error +dotnet_naming_rule.public_members_pascalcase.symbols = public_members +dotnet_naming_rule.public_members_pascalcase.style = pascalcase + +#camelCase for private members +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_symbols.private_members.applicable_accessibilities = private +dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.private_members_camelcase.severity = warning +dotnet_naming_rule.private_members_camelcase.symbols = private_members +dotnet_naming_rule.private_members_camelcase.style = camelcase + +dotnet_naming_symbols.local_function.applicable_kinds = local_function +dotnet_naming_rule.local_function_camelcase.severity = warning +dotnet_naming_rule.local_function_camelcase.symbols = local_function +dotnet_naming_rule.local_function_camelcase.style = camelcase + +#all_lower for private and local constants/static readonlys +dotnet_naming_style.all_lower.capitalization = all_lower +dotnet_naming_style.all_lower.word_separator = _ + +dotnet_naming_symbols.private_constants.applicable_accessibilities = private +dotnet_naming_symbols.private_constants.required_modifiers = const +dotnet_naming_symbols.private_constants.applicable_kinds = field +dotnet_naming_rule.private_const_all_lower.severity = warning +dotnet_naming_rule.private_const_all_lower.symbols = private_constants +dotnet_naming_rule.private_const_all_lower.style = all_lower + +dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.private_static_readonly.applicable_kinds = field +dotnet_naming_rule.private_static_readonly_all_lower.severity = warning +dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly +dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.required_modifiers = const +dotnet_naming_rule.local_const_all_lower.severity = warning +dotnet_naming_rule.local_const_all_lower.symbols = local_constants +dotnet_naming_rule.local_const_all_lower.style = all_lower + +#ALL_UPPER for non private constants/static readonlys +dotnet_naming_style.all_upper.capitalization = all_upper +dotnet_naming_style.all_upper.word_separator = _ + +dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_constants.required_modifiers = const +dotnet_naming_symbols.public_constants.applicable_kinds = field +dotnet_naming_rule.public_const_all_upper.severity = warning +dotnet_naming_rule.public_const_all_upper.symbols = public_constants +dotnet_naming_rule.public_const_all_upper.style = all_upper + +dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.public_static_readonly.applicable_kinds = field +dotnet_naming_rule.public_static_readonly_all_upper.severity = warning +dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly +dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper + +#Roslyn formating options + +#Formatting - indentation options +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +#Formatting - new line options +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_open_brace = all +#csharp_new_line_before_members_in_anonymous_types = true +#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing +csharp_new_line_between_query_expression_clauses = true + +#Formatting - organize using options +dotnet_sort_system_directives_first = true + +#Formatting - spacing options +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#Roslyn language styles + +#Style - this. qualification +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning + +#Style - type names +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_for_built_in_types = true:none +csharp_style_var_elsewhere = true:silent + +#Style - modifiers +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning + +#Style - parentheses +# Skipped because roslyn cannot separate +-*/ with << >> + +#Style - expression bodies +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_local_functions = true:silent + +#Style - expression preferences +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_compound_assignment = true:warning + +#Style - null/type checks +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_throw_expression = true:silent +csharp_style_conditional_delegate_call = true:warning + +#Style - unused +dotnet_style_readonly_field = true:silent +dotnet_code_quality_unused_parameters = non_public:silent +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:warning + +#Style - variable declaration +csharp_style_inlined_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = true:warning + +#Style - other C# 7.x features +dotnet_style_prefer_inferred_tuple_names = true:warning +csharp_prefer_simple_default_expression = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent + +#Style - C# 8 features +csharp_prefer_static_local_function = true:warning +csharp_prefer_simple_using_statement = true:silent +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_switch_expression = false:none + +#Supressing roslyn built-in analyzers +# Suppress: EC112 + +#Private method is unused +dotnet_diagnostic.IDE0051.severity = silent +#Private member is unused +dotnet_diagnostic.IDE0052.severity = silent + +#Rules for disposable +dotnet_diagnostic.IDE0067.severity = none +dotnet_diagnostic.IDE0068.severity = none +dotnet_diagnostic.IDE0069.severity = none + +#Disable operator overloads requiring alternate named methods +dotnet_diagnostic.CA2225.severity = none + +# Banned APIs +dotnet_diagnostic.RS0030.severity = error \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-example/.gitignore b/Templates/Rulesets/ruleset-example/.gitignore new file mode 100644 index 0000000000..940794e60f --- /dev/null +++ b/Templates/Rulesets/ruleset-example/.gitignore @@ -0,0 +1,288 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs diff --git a/Templates/Rulesets/ruleset-example/.template.config/template.json b/Templates/Rulesets/ruleset-example/.template.config/template.json new file mode 100644 index 0000000000..5d2f6f1ebd --- /dev/null +++ b/Templates/Rulesets/ruleset-example/.template.config/template.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "ppy Pty Ltd", + "classifications": [ + "Console" + ], + "name": "osu! ruleset (pippidon example)", + "identity": "ppy.osu.Game.Templates.Rulesets.Pippidon", + "shortName": "ruleset-example", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Pippidon", + "preferNameDirectory": true +} + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json new file mode 100644 index 0000000000..bd9db14259 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "VisualTests (Debug)", + "type": "coreclr", + "request": "launch", + "program": "dotnet", + "args": [ + "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll" + ], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Debug)", + "env": {}, + "console": "internalConsole" + }, + { + "name": "VisualTests (Release)", + "type": "coreclr", + "request": "launch", + "program": "dotnet", + "args": [ + "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll" + ], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Release)", + "env": {}, + "console": "internalConsole" + } + ] +} \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json new file mode 100644 index 0000000000..0ee07c1036 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json @@ -0,0 +1,47 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Build (Debug)", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "--no-restore", + "osu.Game.Rulesets.Pippidon.Tests.csproj", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Build (Release)", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "--no-restore", + "osu.Game.Rulesets.Pippidon.Tests.csproj", + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Restore", + "type": "shell", + "command": "dotnet", + "args": [ + "restore" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs new file mode 100644 index 0000000000..270d906b01 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Platform; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Pippidon.Tests +{ + public class TestSceneOsuGame : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(GameHost host, OsuGameBase gameBase) + { + OsuGame game = new OsuGame(); + game.SetHost(host); + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + game + }; + } + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs new file mode 100644 index 0000000000..f00528900c --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Pippidon.Tests +{ + [TestFixture] + public class TestSceneOsuPlayer : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new PippidonRuleset(); + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs new file mode 100644 index 0000000000..fd6bd9b714 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework; +using osu.Framework.Platform; +using osu.Game.Tests; + +namespace osu.Game.Rulesets.Pippidon.Tests +{ + public static class VisualTestRunner + { + [STAThread] + public static int Main(string[] args) + { + using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + { + host.Run(new OsuTestBrowser()); + return 0; + } + } + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj new file mode 100644 index 0000000000..afa7b03536 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -0,0 +1,26 @@ + + + osu.Game.Rulesets.Pippidon.Tests.VisualTestRunner + + + + + + false + + + + + + + + + + + + + WinExe + net5.0 + osu.Game.Rulesets.Pippidon.Tests + + \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln new file mode 100644 index 0000000000..bccffcd7ff --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln @@ -0,0 +1,96 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29123.88 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Pippidon", "osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon.Tests", "osu.Game.Rulesets.Pippidon.Tests\osu.Game.Rulesets.Pippidon.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + VisualTests|Any CPU = VisualTests|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.EolMarker = Windows + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = True + $2.NewLinesForBracesInProperties = True + $2.NewLinesForBracesInAccessors = True + $2.NewLinesForBracesInAnonymousMethods = True + $2.NewLinesForBracesInControlBlocks = True + $2.NewLinesForBracesInAnonymousTypes = True + $2.NewLinesForBracesInObjectCollectionArrayInitializers = True + $2.NewLinesForBracesInLambdaExpressionBody = True + $2.NewLineForElse = True + $2.NewLineForCatch = True + $2.NewLineForFinally = True + $2.NewLineForMembersInObjectInit = True + $2.NewLineForMembersInAnonymousTypes = True + $2.NewLineForClausesInQuery = True + $2.SpacingAfterMethodDeclarationName = False + $2.SpaceAfterMethodCallName = False + $2.SpaceBeforeOpenSquareBracket = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.EolMarker = Windows + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = True + $2.NewLinesForBracesInProperties = True + $2.NewLinesForBracesInAccessors = True + $2.NewLinesForBracesInAnonymousMethods = True + $2.NewLinesForBracesInControlBlocks = True + $2.NewLinesForBracesInAnonymousTypes = True + $2.NewLinesForBracesInObjectCollectionArrayInitializers = True + $2.NewLinesForBracesInLambdaExpressionBody = True + $2.NewLineForElse = True + $2.NewLineForCatch = True + $2.NewLineForFinally = True + $2.NewLineForMembersInObjectInit = True + $2.NewLineForMembersInAnonymousTypes = True + $2.NewLineForClausesInQuery = True + $2.SpacingAfterMethodDeclarationName = False + $2.SpaceAfterMethodCallName = False + $2.SpaceBeforeOpenSquareBracket = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + EndGlobalSection +EndGlobal diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings new file mode 100644 index 0000000000..aa8f8739c1 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -0,0 +1,934 @@ + + True + True + True + True + ExplicitlyExcluded + ExplicitlyExcluded + SOLUTION + WARNING + WARNING + WARNING + HINT + HINT + WARNING + WARNING + True + WARNING + WARNING + HINT + DO_NOT_SHOW + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + SUGGESTION + HINT + HINT + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + WARNING + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + DO_NOT_SHOW + WARNING + HINT + DO_NOT_SHOW + HINT + HINT + ERROR + WARNING + HINT + HINT + HINT + WARNING + WARNING + HINT + DO_NOT_SHOW + HINT + HINT + HINT + HINT + WARNING + WARNING + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + HINT + HINT + DO_NOT_SHOW + HINT + HINT + WARNING + WARNING + HINT + WARNING + WARNING + WARNING + WARNING + HINT + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + ERROR + WARNING + WARNING + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + WARNING + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + + True + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + HINT + WARNING + WARNING + <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile> + Code Cleanup (peppy) + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + Explicit + ExpressionBody + BlockBody + True + NEXT_LINE + True + True + True + True + True + True + True + True + NEXT_LINE + 1 + 1 + NEXT_LINE + MULTILINE + True + True + True + True + NEXT_LINE + 1 + 1 + True + NEXT_LINE + NEVER + NEVER + True + False + True + NEVER + False + False + True + False + False + True + True + False + False + CHOP_IF_LONG + True + 200 + CHOP_IF_LONG + False + False + AABB + API + BPM + GC + GL + GLSL + HID + HTML + HUD + ID + IL + IOS + IP + IPC + JIT + LTRB + MD5 + NS + OS + PM + RGB + RNG + SHA + SRGB + TK + SS + PP + GMT + QAT + BNG + UI + False + HINT + <?xml version="1.0" encoding="utf-16"?> +<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="COM interfaces or structs"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" /> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry Priority="100" DisplayName="Test Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="Default Pattern"> + <Group DisplayName="Fields/Properties"> + <Group DisplayName="Public Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Public Properties"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Internal Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Internal Properties"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Protected Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Protected Properties"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Private Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Private Properties"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Constructor/Destructor"> + <Entry DisplayName="Ctor"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + </Entry> + <Region Name="Disposal"> + <Entry DisplayName="Dtor"> + <Entry.Match> + <Kind Is="Destructor" /> + </Entry.Match> + </Entry> + <Entry DisplayName="Dispose()"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Method" /> + <Name Is="Dispose" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Dispose(true)"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Or> + <Virtual /> + <Override /> + </Or> + <Kind Is="Method" /> + <Name Is="Dispose" /> + </And> + </Entry.Match> + </Entry> + </Region> + </Group> + <Group DisplayName="Methods"> + <Group DisplayName="Public"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Internal"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Protected"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Private"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + </Group> + </TypePattern> +</Patterns> + Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +See the LICENCE file in the repository root for full licence text. + + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> + <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + + True + True + True + True + True + True + True + True + True + True + True + TestFolder + True + True + o!f – Object Initializer: Anchor&Origin + True + constant("Centre") + 0 + True + True + 2.0 + InCSharpFile + ofao + True + Anchor = Anchor.$anchor$, +Origin = Anchor.$anchor$, + True + True + o!f – InternalChildren = [] + True + True + 2.0 + InCSharpFile + ofic + True + InternalChildren = new Drawable[] +{ + $END$ +}; + True + True + o!f – new GridContainer { .. } + True + True + 2.0 + InCSharpFile + ofgc + True + new GridContainer +{ + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { $END$ }, + new Drawable[] { } + } +}; + True + True + o!f – new FillFlowContainer { .. } + True + True + 2.0 + InCSharpFile + offf + True + new FillFlowContainer +{ + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – new Container { .. } + True + True + 2.0 + InCSharpFile + ofcont + True + new Container +{ + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – BackgroundDependencyLoader load() + True + True + 2.0 + InCSharpFile + ofbdl + True + [BackgroundDependencyLoader] +private void load() +{ + $END$ +} + True + True + o!f – new Box { .. } + True + True + 2.0 + InCSharpFile + ofbox + True + new Box +{ + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, +}, + True + True + o!f – Children = [] + True + True + 2.0 + InCSharpFile + ofc + True + Children = new Drawable[] +{ + $END$ +}; + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs new file mode 100644 index 0000000000..a2a4784603 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Pippidon.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Pippidon.Beatmaps +{ + public class PippidonBeatmapConverter : BeatmapConverter + { + public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) + { + } + + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition); + + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) + { + yield return new PippidonHitObject + { + Samples = original.Samples, + StartTime = original.StartTime, + Position = (original as IHasPosition)?.Position ?? Vector2.Zero, + }; + } + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs new file mode 100644 index 0000000000..8ea334c99c --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Pippidon.Objects; +using osu.Game.Rulesets.Pippidon.Replays; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Rulesets.Pippidon.Mods +{ + public class PippidonModAutoplay : ModAutoplay + { + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score + { + ScoreInfo = new ScoreInfo + { + User = new User { Username = "sample" }, + }, + Replay = new PippidonAutoGenerator(beatmap).Generate(), + }; + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs new file mode 100644 index 0000000000..399d6adda2 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Pippidon.Objects.Drawables +{ + public class DrawablePippidonHitObject : DrawableHitObject + { + private const double time_preempt = 600; + private const double time_fadein = 400; + + public override bool HandlePositionalInput => true; + + public DrawablePippidonHitObject(PippidonHitObject hitObject) + : base(hitObject) + { + Size = new Vector2(80); + + Origin = Anchor.Centre; + Position = hitObject.Position; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + AddInternal(new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get("coin"), + }); + } + + public override IEnumerable GetSamples() => new[] + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK) + }; + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (timeOffset >= 0) + ApplyResult(r => r.Type = IsHovered ? HitResult.Perfect : HitResult.Miss); + } + + protected override double InitialLifetimeOffset => time_preempt; + + protected override void UpdateInitialTransforms() => this.FadeInFromZero(time_fadein); + + protected override void UpdateHitStateTransforms(ArmedState state) + { + switch (state) + { + case ArmedState.Hit: + this.ScaleTo(5, 1500, Easing.OutQuint).FadeOut(1500, Easing.OutQuint).Expire(); + break; + + case ArmedState.Miss: + const double duration = 1000; + + this.ScaleTo(0.8f, duration, Easing.OutQuint); + this.MoveToOffset(new Vector2(0, 10), duration, Easing.In); + this.FadeColour(Color4.Red.Opacity(0.5f), duration / 2, Easing.OutQuint).Then().FadeOut(duration / 2, Easing.InQuint).Expire(); + break; + } + } + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs new file mode 100644 index 0000000000..0c22554e82 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.Pippidon.Objects +{ + public class PippidonHitObject : HitObject, IHasPosition + { + public override Judgement CreateJudgement() => new Judgement(); + + public Vector2 Position { get; set; } + + public float X => Position.X; + public float Y => Position.Y; + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs new file mode 100644 index 0000000000..f6340f6c25 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Pippidon +{ + public class PippidonDifficultyCalculator : DifficultyCalculator + { + public PippidonDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) + { + } + + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + { + return new DifficultyAttributes(mods, skills, 0); + } + + protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty(); + + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs new file mode 100644 index 0000000000..aa7fa3188b --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Pippidon +{ + public class PippidonInputManager : RulesetInputManager + { + public PippidonInputManager(RulesetInfo ruleset) + : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + } + + public enum PippidonAction + { + [Description("Button 1")] + Button1, + + [Description("Button 2")] + Button2, + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs new file mode 100644 index 0000000000..89fed791cd --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Bindings; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Pippidon.Beatmaps; +using osu.Game.Rulesets.Pippidon.Mods; +using osu.Game.Rulesets.Pippidon.UI; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Pippidon +{ + public class PippidonRuleset : Ruleset + { + public override string Description => "gather the osu!coins"; + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => + new DrawablePippidonRuleset(this, beatmap, mods); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => + new PippidonBeatmapConverter(beatmap, this); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => + new PippidonDifficultyCalculator(this, beatmap); + + public override IEnumerable GetModsFor(ModType type) + { + switch (type) + { + case ModType.Automation: + return new[] { new PippidonModAutoplay() }; + + default: + return new Mod[] { null }; + } + } + + public override string ShortName => "pippidon"; + + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] + { + new KeyBinding(InputKey.Z, PippidonAction.Button1), + new KeyBinding(InputKey.X, PippidonAction.Button2), + }; + + public override Drawable CreateIcon() => new Sprite + { + Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"), + }; + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs new file mode 100644 index 0000000000..9c54b82e38 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Pippidon.Objects; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.Pippidon.Replays +{ + public class PippidonAutoGenerator : AutoGenerator + { + protected Replay Replay; + protected List Frames => Replay.Frames; + + public new Beatmap Beatmap => (Beatmap)base.Beatmap; + + public PippidonAutoGenerator(IBeatmap beatmap) + : base(beatmap) + { + Replay = new Replay(); + } + + public override Replay Generate() + { + Frames.Add(new PippidonReplayFrame()); + + foreach (PippidonHitObject hitObject in Beatmap.HitObjects) + { + Frames.Add(new PippidonReplayFrame + { + Time = hitObject.StartTime, + Position = hitObject.Position, + }); + } + + return Replay; + } + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs new file mode 100644 index 0000000000..18efa6b885 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.Input.StateChanges; +using osu.Framework.Utils; +using osu.Game.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.Pippidon.Replays +{ + public class PippidonFramedReplayInputHandler : FramedReplayInputHandler + { + public PippidonFramedReplayInputHandler(Replay replay) + : base(replay) + { + } + + protected override bool IsImportant(PippidonReplayFrame frame) => true; + + protected Vector2 Position + { + get + { + var frame = CurrentFrame; + + if (frame == null) + return Vector2.Zero; + + Debug.Assert(CurrentTime != null); + + return NextFrame != null ? Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time) : frame.Position; + } + } + + public override void CollectPendingInputs(List inputs) + { + inputs.Add(new MousePositionAbsoluteInput + { + Position = GamefieldToScreenSpace(Position) + }); + } + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs new file mode 100644 index 0000000000..949ca160be --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.Pippidon.Replays +{ + public class PippidonReplayFrame : ReplayFrame + { + public Vector2 Position; + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 new file mode 100644 index 0000000000..90b13d1f73 Binary files /dev/null and b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 differ diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png new file mode 100644 index 0000000000..e79d2528ec Binary files /dev/null and b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png differ diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png new file mode 100644 index 0000000000..3cd89c6ce6 Binary files /dev/null and b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png differ diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs new file mode 100644 index 0000000000..1c4fe698c2 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Pippidon.Scoring +{ + public class PippidonScoreProcessor : ScoreProcessor + { + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs new file mode 100644 index 0000000000..d923963bef --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Input.Handlers; +using osu.Game.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Pippidon.Objects; +using osu.Game.Rulesets.Pippidon.Objects.Drawables; +using osu.Game.Rulesets.Pippidon.Replays; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Pippidon.UI +{ + [Cached] + public class DrawablePippidonRuleset : DrawableRuleset + { + public DrawablePippidonRuleset(PippidonRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + : base(ruleset, beatmap, mods) + { + } + + public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new PippidonPlayfieldAdjustmentContainer(); + + protected override Playfield CreatePlayfield() => new PippidonPlayfield(); + + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new PippidonFramedReplayInputHandler(replay); + + public override DrawableHitObject CreateDrawableRepresentation(PippidonHitObject h) => new DrawablePippidonHitObject(h); + + protected override PassThroughInputManager CreateInputManager() => new PippidonInputManager(Ruleset?.RulesetInfo); + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonCursorContainer.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonCursorContainer.cs new file mode 100644 index 0000000000..9de3f4ba14 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonCursorContainer.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Pippidon.UI +{ + public class PippidonCursorContainer : GameplayCursorContainer + { + private Sprite cursorSprite; + private Texture cursorTexture; + + protected override Drawable CreateCursor() => cursorSprite = new Sprite + { + Scale = new Vector2(0.5f), + Origin = Anchor.Centre, + Texture = cursorTexture, + }; + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + cursorTexture = textures.Get("character"); + + if (cursorSprite != null) + cursorSprite.Texture = cursorTexture; + } + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs new file mode 100644 index 0000000000..b5a97c5ea3 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Pippidon.UI +{ + [Cached] + public class PippidonPlayfield : Playfield + { + protected override GameplayCursorContainer CreateCursor() => new PippidonCursorContainer(); + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + HitObjectContainer, + }); + } + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfieldAdjustmentContainer.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfieldAdjustmentContainer.cs new file mode 100644 index 0000000000..9236683827 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfieldAdjustmentContainer.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Pippidon.UI +{ + public class PippidonPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer + { + public PippidonPlayfieldAdjustmentContainer() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Size = new Vector2(0.8f); + } + } +} diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj new file mode 100644 index 0000000000..61b859f45b --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj @@ -0,0 +1,15 @@ + + + netstandard2.1 + osu.Game.Rulesets.Sample + Library + AnyCPU + osu.Game.Rulesets.Pippidon + + + + + + + + \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig new file mode 100644 index 0000000000..f3badda9b3 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig @@ -0,0 +1,200 @@ +# EditorConfig is awesome: http://editorconfig.org +root = true + +[*.cs] +end_of_line = crlf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +#Roslyn naming styles + +#PascalCase for public and protected members +dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.public_members_pascalcase.severity = error +dotnet_naming_rule.public_members_pascalcase.symbols = public_members +dotnet_naming_rule.public_members_pascalcase.style = pascalcase + +#camelCase for private members +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_symbols.private_members.applicable_accessibilities = private +dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.private_members_camelcase.severity = warning +dotnet_naming_rule.private_members_camelcase.symbols = private_members +dotnet_naming_rule.private_members_camelcase.style = camelcase + +dotnet_naming_symbols.local_function.applicable_kinds = local_function +dotnet_naming_rule.local_function_camelcase.severity = warning +dotnet_naming_rule.local_function_camelcase.symbols = local_function +dotnet_naming_rule.local_function_camelcase.style = camelcase + +#all_lower for private and local constants/static readonlys +dotnet_naming_style.all_lower.capitalization = all_lower +dotnet_naming_style.all_lower.word_separator = _ + +dotnet_naming_symbols.private_constants.applicable_accessibilities = private +dotnet_naming_symbols.private_constants.required_modifiers = const +dotnet_naming_symbols.private_constants.applicable_kinds = field +dotnet_naming_rule.private_const_all_lower.severity = warning +dotnet_naming_rule.private_const_all_lower.symbols = private_constants +dotnet_naming_rule.private_const_all_lower.style = all_lower + +dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.private_static_readonly.applicable_kinds = field +dotnet_naming_rule.private_static_readonly_all_lower.severity = warning +dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly +dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.required_modifiers = const +dotnet_naming_rule.local_const_all_lower.severity = warning +dotnet_naming_rule.local_const_all_lower.symbols = local_constants +dotnet_naming_rule.local_const_all_lower.style = all_lower + +#ALL_UPPER for non private constants/static readonlys +dotnet_naming_style.all_upper.capitalization = all_upper +dotnet_naming_style.all_upper.word_separator = _ + +dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_constants.required_modifiers = const +dotnet_naming_symbols.public_constants.applicable_kinds = field +dotnet_naming_rule.public_const_all_upper.severity = warning +dotnet_naming_rule.public_const_all_upper.symbols = public_constants +dotnet_naming_rule.public_const_all_upper.style = all_upper + +dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.public_static_readonly.applicable_kinds = field +dotnet_naming_rule.public_static_readonly_all_upper.severity = warning +dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly +dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper + +#Roslyn formating options + +#Formatting - indentation options +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +#Formatting - new line options +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_open_brace = all +#csharp_new_line_before_members_in_anonymous_types = true +#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing +csharp_new_line_between_query_expression_clauses = true + +#Formatting - organize using options +dotnet_sort_system_directives_first = true + +#Formatting - spacing options +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#Roslyn language styles + +#Style - this. qualification +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning + +#Style - type names +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_for_built_in_types = true:none +csharp_style_var_elsewhere = true:silent + +#Style - modifiers +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning + +#Style - parentheses +# Skipped because roslyn cannot separate +-*/ with << >> + +#Style - expression bodies +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_local_functions = true:silent + +#Style - expression preferences +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_compound_assignment = true:warning + +#Style - null/type checks +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_throw_expression = true:silent +csharp_style_conditional_delegate_call = true:warning + +#Style - unused +dotnet_style_readonly_field = true:silent +dotnet_code_quality_unused_parameters = non_public:silent +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:warning + +#Style - variable declaration +csharp_style_inlined_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = true:warning + +#Style - other C# 7.x features +dotnet_style_prefer_inferred_tuple_names = true:warning +csharp_prefer_simple_default_expression = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent + +#Style - C# 8 features +csharp_prefer_static_local_function = true:warning +csharp_prefer_simple_using_statement = true:silent +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_switch_expression = false:none + +#Supressing roslyn built-in analyzers +# Suppress: EC112 + +#Private method is unused +dotnet_diagnostic.IDE0051.severity = silent +#Private member is unused +dotnet_diagnostic.IDE0052.severity = silent + +#Rules for disposable +dotnet_diagnostic.IDE0067.severity = none +dotnet_diagnostic.IDE0068.severity = none +dotnet_diagnostic.IDE0069.severity = none + +#Disable operator overloads requiring alternate named methods +dotnet_diagnostic.CA2225.severity = none + +# Banned APIs +dotnet_diagnostic.RS0030.severity = error \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.gitignore b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore new file mode 100644 index 0000000000..940794e60f --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore @@ -0,0 +1,288 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.template.config/template.json b/Templates/Rulesets/ruleset-scrolling-empty/.template.config/template.json new file mode 100644 index 0000000000..3eb99a1f9d --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "ppy Pty Ltd", + "classifications": [ + "Console" + ], + "name": "osu! ruleset (scrolling)", + "identity": "ppy.osu.Game.Templates.Rulesets.Scrolling", + "shortName": "ruleset-scrolling", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "EmptyScrolling", + "preferNameDirectory": true +} \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json new file mode 100644 index 0000000000..24e4873ed6 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "VisualTests (Debug)", + "type": "coreclr", + "request": "launch", + "program": "dotnet", + "args": [ + "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll" + ], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Debug)", + "env": {}, + "console": "internalConsole" + }, + { + "name": "VisualTests (Release)", + "type": "coreclr", + "request": "launch", + "program": "dotnet", + "args": [ + "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll" + ], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Release)", + "env": {}, + "console": "internalConsole" + } + ] +} \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/tasks.json new file mode 100644 index 0000000000..00d0dc7d9b --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/tasks.json @@ -0,0 +1,47 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Build (Debug)", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "--no-restore", + "osu.Game.Rulesets.EmptyScrolling.Tests.csproj", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Build (Release)", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "--no-restore", + "osu.Game.Rulesets.EmptyScrolling.Tests.csproj", + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Restore", + "type": "shell", + "command": "dotnet", + "args": [ + "restore" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs new file mode 100644 index 0000000000..aed6abb6bf --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Platform; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.EmptyScrolling.Tests +{ + public class TestSceneOsuGame : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(GameHost host, OsuGameBase gameBase) + { + OsuGame game = new OsuGame(); + game.SetHost(host); + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + game + }; + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuPlayer.cs new file mode 100644 index 0000000000..9460576196 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuPlayer.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.EmptyScrolling.Tests +{ + [TestFixture] + public class TestSceneOsuPlayer : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new EmptyScrollingRuleset(); + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs new file mode 100644 index 0000000000..65cfb2bff4 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework; +using osu.Framework.Platform; +using osu.Game.Tests; + +namespace osu.Game.Rulesets.EmptyScrolling.Tests +{ + public static class VisualTestRunner + { + [STAThread] + public static int Main(string[] args) + { + using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + { + host.Run(new OsuTestBrowser()); + return 0; + } + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj new file mode 100644 index 0000000000..c9f87a8551 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -0,0 +1,26 @@ + + + osu.Game.Rulesets.EmptyScrolling.Tests.VisualTestRunner + + + + + + false + + + + + + + + + + + + + WinExe + net5.0 + osu.Game.Rulesets.EmptyScrolling.Tests + + \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln new file mode 100644 index 0000000000..97361e1a7b --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln @@ -0,0 +1,96 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29123.88 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.EmptyScrolling", "osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyScrolling.Tests", "osu.Game.Rulesets.EmptyScrolling.Tests\osu.Game.Rulesets.EmptyScrolling.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + VisualTests|Any CPU = VisualTests|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.EolMarker = Windows + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = True + $2.NewLinesForBracesInProperties = True + $2.NewLinesForBracesInAccessors = True + $2.NewLinesForBracesInAnonymousMethods = True + $2.NewLinesForBracesInControlBlocks = True + $2.NewLinesForBracesInAnonymousTypes = True + $2.NewLinesForBracesInObjectCollectionArrayInitializers = True + $2.NewLinesForBracesInLambdaExpressionBody = True + $2.NewLineForElse = True + $2.NewLineForCatch = True + $2.NewLineForFinally = True + $2.NewLineForMembersInObjectInit = True + $2.NewLineForMembersInAnonymousTypes = True + $2.NewLineForClausesInQuery = True + $2.SpacingAfterMethodDeclarationName = False + $2.SpaceAfterMethodCallName = False + $2.SpaceBeforeOpenSquareBracket = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.EolMarker = Windows + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = True + $2.NewLinesForBracesInProperties = True + $2.NewLinesForBracesInAccessors = True + $2.NewLinesForBracesInAnonymousMethods = True + $2.NewLinesForBracesInControlBlocks = True + $2.NewLinesForBracesInAnonymousTypes = True + $2.NewLinesForBracesInObjectCollectionArrayInitializers = True + $2.NewLinesForBracesInLambdaExpressionBody = True + $2.NewLineForElse = True + $2.NewLineForCatch = True + $2.NewLineForFinally = True + $2.NewLineForMembersInObjectInit = True + $2.NewLineForMembersInAnonymousTypes = True + $2.NewLineForClausesInQuery = True + $2.SpacingAfterMethodDeclarationName = False + $2.SpaceAfterMethodCallName = False + $2.SpaceBeforeOpenSquareBracket = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + EndGlobalSection +EndGlobal diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings new file mode 100644 index 0000000000..aa8f8739c1 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings @@ -0,0 +1,934 @@ + + True + True + True + True + ExplicitlyExcluded + ExplicitlyExcluded + SOLUTION + WARNING + WARNING + WARNING + HINT + HINT + WARNING + WARNING + True + WARNING + WARNING + HINT + DO_NOT_SHOW + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + SUGGESTION + HINT + HINT + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + WARNING + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + DO_NOT_SHOW + WARNING + HINT + DO_NOT_SHOW + HINT + HINT + ERROR + WARNING + HINT + HINT + HINT + WARNING + WARNING + HINT + DO_NOT_SHOW + HINT + HINT + HINT + HINT + WARNING + WARNING + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + HINT + HINT + DO_NOT_SHOW + HINT + HINT + WARNING + WARNING + HINT + WARNING + WARNING + WARNING + WARNING + HINT + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + ERROR + WARNING + WARNING + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + WARNING + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + + True + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + HINT + WARNING + WARNING + <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile> + Code Cleanup (peppy) + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + Explicit + ExpressionBody + BlockBody + True + NEXT_LINE + True + True + True + True + True + True + True + True + NEXT_LINE + 1 + 1 + NEXT_LINE + MULTILINE + True + True + True + True + NEXT_LINE + 1 + 1 + True + NEXT_LINE + NEVER + NEVER + True + False + True + NEVER + False + False + True + False + False + True + True + False + False + CHOP_IF_LONG + True + 200 + CHOP_IF_LONG + False + False + AABB + API + BPM + GC + GL + GLSL + HID + HTML + HUD + ID + IL + IOS + IP + IPC + JIT + LTRB + MD5 + NS + OS + PM + RGB + RNG + SHA + SRGB + TK + SS + PP + GMT + QAT + BNG + UI + False + HINT + <?xml version="1.0" encoding="utf-16"?> +<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="COM interfaces or structs"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" /> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry Priority="100" DisplayName="Test Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="Default Pattern"> + <Group DisplayName="Fields/Properties"> + <Group DisplayName="Public Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Public Properties"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Internal Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Internal Properties"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Protected Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Protected Properties"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Private Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Private Properties"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Constructor/Destructor"> + <Entry DisplayName="Ctor"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + </Entry> + <Region Name="Disposal"> + <Entry DisplayName="Dtor"> + <Entry.Match> + <Kind Is="Destructor" /> + </Entry.Match> + </Entry> + <Entry DisplayName="Dispose()"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Method" /> + <Name Is="Dispose" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Dispose(true)"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Or> + <Virtual /> + <Override /> + </Or> + <Kind Is="Method" /> + <Name Is="Dispose" /> + </And> + </Entry.Match> + </Entry> + </Region> + </Group> + <Group DisplayName="Methods"> + <Group DisplayName="Public"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Internal"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Protected"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Private"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + </Group> + </TypePattern> +</Patterns> + Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +See the LICENCE file in the repository root for full licence text. + + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> + <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + + True + True + True + True + True + True + True + True + True + True + True + TestFolder + True + True + o!f – Object Initializer: Anchor&Origin + True + constant("Centre") + 0 + True + True + 2.0 + InCSharpFile + ofao + True + Anchor = Anchor.$anchor$, +Origin = Anchor.$anchor$, + True + True + o!f – InternalChildren = [] + True + True + 2.0 + InCSharpFile + ofic + True + InternalChildren = new Drawable[] +{ + $END$ +}; + True + True + o!f – new GridContainer { .. } + True + True + 2.0 + InCSharpFile + ofgc + True + new GridContainer +{ + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { $END$ }, + new Drawable[] { } + } +}; + True + True + o!f – new FillFlowContainer { .. } + True + True + 2.0 + InCSharpFile + offf + True + new FillFlowContainer +{ + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – new Container { .. } + True + True + 2.0 + InCSharpFile + ofcont + True + new Container +{ + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – BackgroundDependencyLoader load() + True + True + 2.0 + InCSharpFile + ofbdl + True + [BackgroundDependencyLoader] +private void load() +{ + $END$ +} + True + True + o!f – new Box { .. } + True + True + 2.0 + InCSharpFile + ofbox + True + new Box +{ + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, +}, + True + True + o!f – Children = [] + True + True + 2.0 + InCSharpFile + ofc + True + Children = new Drawable[] +{ + $END$ +}; + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Beatmaps/EmptyScrollingBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Beatmaps/EmptyScrollingBeatmapConverter.cs new file mode 100644 index 0000000000..02fb9a9dd5 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Beatmaps/EmptyScrollingBeatmapConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Threading; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.EmptyScrolling.Objects; + +namespace osu.Game.Rulesets.EmptyScrolling.Beatmaps +{ + public class EmptyScrollingBeatmapConverter : BeatmapConverter + { + public EmptyScrollingBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) + { + } + + // todo: Check for conversion types that should be supported (ie. Beatmap.HitObjects.Any(h => h is IHasXPosition)) + // https://github.com/ppy/osu/tree/master/osu.Game/Rulesets/Objects/Types + public override bool CanConvert() => true; + + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) + { + yield return new EmptyScrollingHitObject + { + Samples = original.Samples, + StartTime = original.StartTime, + }; + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs new file mode 100644 index 0000000000..7f29c4e712 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.EmptyScrolling +{ + public class EmptyScrollingDifficultyCalculator : DifficultyCalculator + { + public EmptyScrollingDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) + { + } + + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + { + return new DifficultyAttributes(mods, skills, 0); + } + + protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty(); + + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingInputManager.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingInputManager.cs new file mode 100644 index 0000000000..632e04f301 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingInputManager.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.EmptyScrolling +{ + public class EmptyScrollingInputManager : RulesetInputManager + { + public EmptyScrollingInputManager(RulesetInfo ruleset) + : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + } + + public enum EmptyScrollingAction + { + [Description("Button 1")] + Button1, + + [Description("Button 2")] + Button2, + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs new file mode 100644 index 0000000000..c1d4de52b7 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.EmptyScrolling.Beatmaps; +using osu.Game.Rulesets.EmptyScrolling.Mods; +using osu.Game.Rulesets.EmptyScrolling.UI; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.EmptyScrolling +{ + public class EmptyScrollingRuleset : Ruleset + { + public override string Description => "a very emptyscrolling ruleset"; + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableEmptyScrollingRuleset(this, beatmap, mods); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new EmptyScrollingBeatmapConverter(beatmap, this); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new EmptyScrollingDifficultyCalculator(this, beatmap); + + public override IEnumerable GetModsFor(ModType type) + { + switch (type) + { + case ModType.Automation: + return new[] { new EmptyScrollingModAutoplay() }; + + default: + return new Mod[] { null }; + } + } + + public override string ShortName => "emptyscrolling"; + + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] + { + new KeyBinding(InputKey.Z, EmptyScrollingAction.Button1), + new KeyBinding(InputKey.X, EmptyScrollingAction.Button2), + }; + + public override Drawable CreateIcon() => new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = ShortName[0].ToString(), + Font = OsuFont.Default.With(size: 18), + }; + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs new file mode 100644 index 0000000000..6dad1ff43b --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.EmptyScrolling.Objects; +using osu.Game.Rulesets.EmptyScrolling.Replays; +using osu.Game.Scoring; +using osu.Game.Users; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.EmptyScrolling.Mods +{ + public class EmptyScrollingModAutoplay : ModAutoplay + { + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score + { + ScoreInfo = new ScoreInfo + { + User = new User { Username = "sample" }, + }, + Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(), + }; + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs new file mode 100644 index 0000000000..b5ff0cde7c --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.EmptyScrolling.Objects.Drawables +{ + public class DrawableEmptyScrollingHitObject : DrawableHitObject + { + public DrawableEmptyScrollingHitObject(EmptyScrollingHitObject hitObject) + : base(hitObject) + { + Size = new Vector2(40); + Origin = Anchor.Centre; + + // todo: add visuals. + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (timeOffset >= 0) + // todo: implement judgement logic + ApplyResult(r => r.Type = HitResult.Perfect); + } + + protected override void UpdateHitStateTransforms(ArmedState state) + { + const double duration = 1000; + + switch (state) + { + case ArmedState.Hit: + this.FadeOut(duration, Easing.OutQuint).Expire(); + break; + + case ArmedState.Miss: + + this.FadeColour(Color4.Red, duration); + this.FadeOut(duration, Easing.InQuint).Expire(); + break; + } + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/EmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/EmptyScrollingHitObject.cs new file mode 100644 index 0000000000..9b469be496 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/EmptyScrollingHitObject.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.EmptyScrolling.Objects +{ + public class EmptyScrollingHitObject : HitObject + { + public override Judgement CreateJudgement() => new Judgement(); + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs new file mode 100644 index 0000000000..7923918842 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.EmptyScrolling.Objects; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.EmptyScrolling.Replays +{ + public class EmptyScrollingAutoGenerator : AutoGenerator + { + protected Replay Replay; + protected List Frames => Replay.Frames; + + public new Beatmap Beatmap => (Beatmap)base.Beatmap; + + public EmptyScrollingAutoGenerator(IBeatmap beatmap) + : base(beatmap) + { + Replay = new Replay(); + } + + public override Replay Generate() + { + Frames.Add(new EmptyScrollingReplayFrame()); + + foreach (EmptyScrollingHitObject hitObject in Beatmap.HitObjects) + { + Frames.Add(new EmptyScrollingReplayFrame + { + Time = hitObject.StartTime + // todo: add required inputs and extra frames. + }); + } + + return Replay; + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs new file mode 100644 index 0000000000..4b998cfca3 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.StateChanges; +using osu.Game.Replays; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.EmptyScrolling.Replays +{ + public class EmptyScrollingFramedReplayInputHandler : FramedReplayInputHandler + { + public EmptyScrollingFramedReplayInputHandler(Replay replay) + : base(replay) + { + } + + protected override bool IsImportant(EmptyScrollingReplayFrame frame) => frame.Actions.Any(); + + public override void CollectPendingInputs(List inputs) + { + inputs.Add(new ReplayState + { + PressedActions = CurrentFrame?.Actions ?? new List(), + }); + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs new file mode 100644 index 0000000000..2f19cffd2a --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.EmptyScrolling.Replays +{ + public class EmptyScrollingReplayFrame : ReplayFrame + { + public List Actions = new List(); + + public EmptyScrollingReplayFrame(EmptyScrollingAction? button = null) + { + if (button.HasValue) + Actions.Add(button.Value); + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/DrawableEmptyScrollingRuleset.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/DrawableEmptyScrollingRuleset.cs new file mode 100644 index 0000000000..620a4abc51 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/DrawableEmptyScrollingRuleset.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Input.Handlers; +using osu.Game.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.EmptyScrolling.Objects; +using osu.Game.Rulesets.EmptyScrolling.Objects.Drawables; +using osu.Game.Rulesets.EmptyScrolling.Replays; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.EmptyScrolling.UI +{ + [Cached] + public class DrawableEmptyScrollingRuleset : DrawableScrollingRuleset + { + public DrawableEmptyScrollingRuleset(EmptyScrollingRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + : base(ruleset, beatmap, mods) + { + Direction.Value = ScrollingDirection.Left; + TimeRange.Value = 6000; + } + + protected override Playfield CreatePlayfield() => new EmptyScrollingPlayfield(); + + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new EmptyScrollingFramedReplayInputHandler(replay); + + public override DrawableHitObject CreateDrawableRepresentation(EmptyScrollingHitObject h) => new DrawableEmptyScrollingHitObject(h); + + protected override PassThroughInputManager CreateInputManager() => new EmptyScrollingInputManager(Ruleset?.RulesetInfo); + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/EmptyScrollingPlayfield.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/EmptyScrollingPlayfield.cs new file mode 100644 index 0000000000..56620e44b3 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/EmptyScrollingPlayfield.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.EmptyScrolling.UI +{ + [Cached] + public class EmptyScrollingPlayfield : ScrollingPlayfield + { + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + HitObjectContainer, + }); + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj new file mode 100644 index 0000000000..9dce3c9a0a --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj @@ -0,0 +1,15 @@ + + + netstandard2.1 + osu.Game.Rulesets.Sample + Library + AnyCPU + osu.Game.Rulesets.EmptyScrolling + + + + + + + + \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig new file mode 100644 index 0000000000..f3badda9b3 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig @@ -0,0 +1,200 @@ +# EditorConfig is awesome: http://editorconfig.org +root = true + +[*.cs] +end_of_line = crlf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +#Roslyn naming styles + +#PascalCase for public and protected members +dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.public_members_pascalcase.severity = error +dotnet_naming_rule.public_members_pascalcase.symbols = public_members +dotnet_naming_rule.public_members_pascalcase.style = pascalcase + +#camelCase for private members +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_symbols.private_members.applicable_accessibilities = private +dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.private_members_camelcase.severity = warning +dotnet_naming_rule.private_members_camelcase.symbols = private_members +dotnet_naming_rule.private_members_camelcase.style = camelcase + +dotnet_naming_symbols.local_function.applicable_kinds = local_function +dotnet_naming_rule.local_function_camelcase.severity = warning +dotnet_naming_rule.local_function_camelcase.symbols = local_function +dotnet_naming_rule.local_function_camelcase.style = camelcase + +#all_lower for private and local constants/static readonlys +dotnet_naming_style.all_lower.capitalization = all_lower +dotnet_naming_style.all_lower.word_separator = _ + +dotnet_naming_symbols.private_constants.applicable_accessibilities = private +dotnet_naming_symbols.private_constants.required_modifiers = const +dotnet_naming_symbols.private_constants.applicable_kinds = field +dotnet_naming_rule.private_const_all_lower.severity = warning +dotnet_naming_rule.private_const_all_lower.symbols = private_constants +dotnet_naming_rule.private_const_all_lower.style = all_lower + +dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.private_static_readonly.applicable_kinds = field +dotnet_naming_rule.private_static_readonly_all_lower.severity = warning +dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly +dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.required_modifiers = const +dotnet_naming_rule.local_const_all_lower.severity = warning +dotnet_naming_rule.local_const_all_lower.symbols = local_constants +dotnet_naming_rule.local_const_all_lower.style = all_lower + +#ALL_UPPER for non private constants/static readonlys +dotnet_naming_style.all_upper.capitalization = all_upper +dotnet_naming_style.all_upper.word_separator = _ + +dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_constants.required_modifiers = const +dotnet_naming_symbols.public_constants.applicable_kinds = field +dotnet_naming_rule.public_const_all_upper.severity = warning +dotnet_naming_rule.public_const_all_upper.symbols = public_constants +dotnet_naming_rule.public_const_all_upper.style = all_upper + +dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.public_static_readonly.applicable_kinds = field +dotnet_naming_rule.public_static_readonly_all_upper.severity = warning +dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly +dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper + +#Roslyn formating options + +#Formatting - indentation options +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +#Formatting - new line options +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_open_brace = all +#csharp_new_line_before_members_in_anonymous_types = true +#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing +csharp_new_line_between_query_expression_clauses = true + +#Formatting - organize using options +dotnet_sort_system_directives_first = true + +#Formatting - spacing options +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#Roslyn language styles + +#Style - this. qualification +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning + +#Style - type names +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_for_built_in_types = true:none +csharp_style_var_elsewhere = true:silent + +#Style - modifiers +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning + +#Style - parentheses +# Skipped because roslyn cannot separate +-*/ with << >> + +#Style - expression bodies +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_local_functions = true:silent + +#Style - expression preferences +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_compound_assignment = true:warning + +#Style - null/type checks +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_throw_expression = true:silent +csharp_style_conditional_delegate_call = true:warning + +#Style - unused +dotnet_style_readonly_field = true:silent +dotnet_code_quality_unused_parameters = non_public:silent +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:warning + +#Style - variable declaration +csharp_style_inlined_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = true:warning + +#Style - other C# 7.x features +dotnet_style_prefer_inferred_tuple_names = true:warning +csharp_prefer_simple_default_expression = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent + +#Style - C# 8 features +csharp_prefer_static_local_function = true:warning +csharp_prefer_simple_using_statement = true:silent +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_switch_expression = false:none + +#Supressing roslyn built-in analyzers +# Suppress: EC112 + +#Private method is unused +dotnet_diagnostic.IDE0051.severity = silent +#Private member is unused +dotnet_diagnostic.IDE0052.severity = silent + +#Rules for disposable +dotnet_diagnostic.IDE0067.severity = none +dotnet_diagnostic.IDE0068.severity = none +dotnet_diagnostic.IDE0069.severity = none + +#Disable operator overloads requiring alternate named methods +dotnet_diagnostic.CA2225.severity = none + +# Banned APIs +dotnet_diagnostic.RS0030.severity = error \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-example/.gitignore b/Templates/Rulesets/ruleset-scrolling-example/.gitignore new file mode 100644 index 0000000000..940794e60f --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/.gitignore @@ -0,0 +1,288 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs diff --git a/Templates/Rulesets/ruleset-scrolling-example/.template.config/template.json b/Templates/Rulesets/ruleset-scrolling-example/.template.config/template.json new file mode 100644 index 0000000000..a1c097f1c8 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "ppy Pty Ltd", + "classifications": [ + "Console" + ], + "name": "osu! ruleset (scrolling pippidon example)", + "identity": "ppy.osu.Game.Templates.Rulesets.Scrolling.Pippidon", + "shortName": "ruleset-scrolling-example", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Pippidon", + "preferNameDirectory": true +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json new file mode 100644 index 0000000000..bd9db14259 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "VisualTests (Debug)", + "type": "coreclr", + "request": "launch", + "program": "dotnet", + "args": [ + "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll" + ], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Debug)", + "env": {}, + "console": "internalConsole" + }, + { + "name": "VisualTests (Release)", + "type": "coreclr", + "request": "launch", + "program": "dotnet", + "args": [ + "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll" + ], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Release)", + "env": {}, + "console": "internalConsole" + } + ] +} \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json new file mode 100644 index 0000000000..0ee07c1036 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json @@ -0,0 +1,47 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Build (Debug)", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "--no-restore", + "osu.Game.Rulesets.Pippidon.Tests.csproj", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Build (Release)", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "--no-restore", + "osu.Game.Rulesets.Pippidon.Tests.csproj", + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Restore", + "type": "shell", + "command": "dotnet", + "args": [ + "restore" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs new file mode 100644 index 0000000000..270d906b01 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Platform; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Pippidon.Tests +{ + public class TestSceneOsuGame : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(GameHost host, OsuGameBase gameBase) + { + OsuGame game = new OsuGame(); + game.SetHost(host); + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + game + }; + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs new file mode 100644 index 0000000000..f00528900c --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Pippidon.Tests +{ + [TestFixture] + public class TestSceneOsuPlayer : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new PippidonRuleset(); + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs new file mode 100644 index 0000000000..fd6bd9b714 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework; +using osu.Framework.Platform; +using osu.Game.Tests; + +namespace osu.Game.Rulesets.Pippidon.Tests +{ + public static class VisualTestRunner + { + [STAThread] + public static int Main(string[] args) + { + using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + { + host.Run(new OsuTestBrowser()); + return 0; + } + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj new file mode 100644 index 0000000000..afa7b03536 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -0,0 +1,26 @@ + + + osu.Game.Rulesets.Pippidon.Tests.VisualTestRunner + + + + + + false + + + + + + + + + + + + + WinExe + net5.0 + osu.Game.Rulesets.Pippidon.Tests + + \ No newline at end of file diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln new file mode 100644 index 0000000000..bccffcd7ff --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln @@ -0,0 +1,96 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29123.88 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Pippidon", "osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon.Tests", "osu.Game.Rulesets.Pippidon.Tests\osu.Game.Rulesets.Pippidon.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + VisualTests|Any CPU = VisualTests|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU + {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU + {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.EolMarker = Windows + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = True + $2.NewLinesForBracesInProperties = True + $2.NewLinesForBracesInAccessors = True + $2.NewLinesForBracesInAnonymousMethods = True + $2.NewLinesForBracesInControlBlocks = True + $2.NewLinesForBracesInAnonymousTypes = True + $2.NewLinesForBracesInObjectCollectionArrayInitializers = True + $2.NewLinesForBracesInLambdaExpressionBody = True + $2.NewLineForElse = True + $2.NewLineForCatch = True + $2.NewLineForFinally = True + $2.NewLineForMembersInObjectInit = True + $2.NewLineForMembersInAnonymousTypes = True + $2.NewLineForClausesInQuery = True + $2.SpacingAfterMethodDeclarationName = False + $2.SpaceAfterMethodCallName = False + $2.SpaceBeforeOpenSquareBracket = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.EolMarker = Windows + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = True + $2.NewLinesForBracesInProperties = True + $2.NewLinesForBracesInAccessors = True + $2.NewLinesForBracesInAnonymousMethods = True + $2.NewLinesForBracesInControlBlocks = True + $2.NewLinesForBracesInAnonymousTypes = True + $2.NewLinesForBracesInObjectCollectionArrayInitializers = True + $2.NewLinesForBracesInLambdaExpressionBody = True + $2.NewLineForElse = True + $2.NewLineForCatch = True + $2.NewLineForFinally = True + $2.NewLineForMembersInObjectInit = True + $2.NewLineForMembersInAnonymousTypes = True + $2.NewLineForClausesInQuery = True + $2.SpacingAfterMethodDeclarationName = False + $2.SpaceAfterMethodCallName = False + $2.SpaceBeforeOpenSquareBracket = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + EndGlobalSection +EndGlobal diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings new file mode 100644 index 0000000000..aa8f8739c1 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -0,0 +1,934 @@ + + True + True + True + True + ExplicitlyExcluded + ExplicitlyExcluded + SOLUTION + WARNING + WARNING + WARNING + HINT + HINT + WARNING + WARNING + True + WARNING + WARNING + HINT + DO_NOT_SHOW + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + SUGGESTION + HINT + HINT + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + WARNING + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + DO_NOT_SHOW + WARNING + HINT + DO_NOT_SHOW + HINT + HINT + ERROR + WARNING + HINT + HINT + HINT + WARNING + WARNING + HINT + DO_NOT_SHOW + HINT + HINT + HINT + HINT + WARNING + WARNING + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + HINT + HINT + DO_NOT_SHOW + HINT + HINT + WARNING + WARNING + HINT + WARNING + WARNING + WARNING + WARNING + HINT + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + ERROR + WARNING + WARNING + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + WARNING + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + WARNING + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + HINT + DO_NOT_SHOW + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + + True + WARNING + WARNING + WARNING + WARNING + WARNING + HINT + HINT + WARNING + WARNING + <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile> + Code Cleanup (peppy) + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + Explicit + ExpressionBody + BlockBody + True + NEXT_LINE + True + True + True + True + True + True + True + True + NEXT_LINE + 1 + 1 + NEXT_LINE + MULTILINE + True + True + True + True + NEXT_LINE + 1 + 1 + True + NEXT_LINE + NEVER + NEVER + True + False + True + NEVER + False + False + True + False + False + True + True + False + False + CHOP_IF_LONG + True + 200 + CHOP_IF_LONG + False + False + AABB + API + BPM + GC + GL + GLSL + HID + HTML + HUD + ID + IL + IOS + IP + IPC + JIT + LTRB + MD5 + NS + OS + PM + RGB + RNG + SHA + SRGB + TK + SS + PP + GMT + QAT + BNG + UI + False + HINT + <?xml version="1.0" encoding="utf-16"?> +<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="COM interfaces or structs"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" /> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry Priority="100" DisplayName="Test Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="Default Pattern"> + <Group DisplayName="Fields/Properties"> + <Group DisplayName="Public Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Public Properties"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Internal Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Internal Properties"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Protected Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Protected Properties"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + <Group DisplayName="Private Fields"> + <Entry DisplayName="Constant Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Or> + <Kind Is="Constant" /> + <Readonly /> + <And> + <Static /> + <Readonly /> + </And> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Static Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Static /> + <Not> + <Readonly /> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Normal Fields"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Not> + <Or> + <Static /> + <Readonly /> + </Or> + </Not> + <Kind Is="Field" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Entry DisplayName="Private Properties"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Kind Is="Property" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Constructor/Destructor"> + <Entry DisplayName="Ctor"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + </Entry> + <Region Name="Disposal"> + <Entry DisplayName="Dtor"> + <Entry.Match> + <Kind Is="Destructor" /> + </Entry.Match> + </Entry> + <Entry DisplayName="Dispose()"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Method" /> + <Name Is="Dispose" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Dispose(true)"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Or> + <Virtual /> + <Override /> + </Or> + <Kind Is="Method" /> + <Name Is="Dispose" /> + </And> + </Entry.Match> + </Entry> + </Region> + </Group> + <Group DisplayName="Methods"> + <Group DisplayName="Public"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Internal"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Internal" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Protected"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Protected" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + <Group DisplayName="Private"> + <Entry DisplayName="Static Methods"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Static /> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Methods"> + <Entry.Match> + <And> + <Access Is="Private" /> + <Not> + <Static /> + </Not> + <Kind Is="Method" /> + </And> + </Entry.Match> + </Entry> + </Group> + </Group> + </TypePattern> +</Patterns> + Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +See the LICENCE file in the repository root for full licence text. + + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> + <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + + True + True + True + True + True + True + True + True + True + True + True + TestFolder + True + True + o!f – Object Initializer: Anchor&Origin + True + constant("Centre") + 0 + True + True + 2.0 + InCSharpFile + ofao + True + Anchor = Anchor.$anchor$, +Origin = Anchor.$anchor$, + True + True + o!f – InternalChildren = [] + True + True + 2.0 + InCSharpFile + ofic + True + InternalChildren = new Drawable[] +{ + $END$ +}; + True + True + o!f – new GridContainer { .. } + True + True + 2.0 + InCSharpFile + ofgc + True + new GridContainer +{ + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { $END$ }, + new Drawable[] { } + } +}; + True + True + o!f – new FillFlowContainer { .. } + True + True + 2.0 + InCSharpFile + offf + True + new FillFlowContainer +{ + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – new Container { .. } + True + True + 2.0 + InCSharpFile + ofcont + True + new Container +{ + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – BackgroundDependencyLoader load() + True + True + 2.0 + InCSharpFile + ofbdl + True + [BackgroundDependencyLoader] +private void load() +{ + $END$ +} + True + True + o!f – new Box { .. } + True + True + 2.0 + InCSharpFile + ofbox + True + new Box +{ + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, +}, + True + True + o!f – Children = [] + True + True + 2.0 + InCSharpFile + ofc + True + Children = new Drawable[] +{ + $END$ +}; + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs new file mode 100644 index 0000000000..8f0b31ef1b --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Pippidon.Objects; +using osu.Game.Rulesets.Pippidon.UI; +using osuTK; + +namespace osu.Game.Rulesets.Pippidon.Beatmaps +{ + public class PippidonBeatmapConverter : BeatmapConverter + { + private readonly float minPosition; + private readonly float maxPosition; + + public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) + { + minPosition = beatmap.HitObjects.Min(getUsablePosition); + maxPosition = beatmap.HitObjects.Max(getUsablePosition); + } + + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition); + + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) + { + yield return new PippidonHitObject + { + Samples = original.Samples, + StartTime = original.StartTime, + Lane = getLane(original) + }; + } + + private int getLane(HitObject hitObject) => (int)MathHelper.Clamp( + (getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1); + + private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X; + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs new file mode 100644 index 0000000000..8ea334c99c --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Pippidon.Objects; +using osu.Game.Rulesets.Pippidon.Replays; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Rulesets.Pippidon.Mods +{ + public class PippidonModAutoplay : ModAutoplay + { + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score + { + ScoreInfo = new ScoreInfo + { + User = new User { Username = "sample" }, + }, + Replay = new PippidonAutoGenerator(beatmap).Generate(), + }; + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs new file mode 100644 index 0000000000..e458cacef9 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Pippidon.UI; +using osu.Game.Rulesets.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Pippidon.Objects.Drawables +{ + public class DrawablePippidonHitObject : DrawableHitObject + { + private BindableNumber currentLane; + + public DrawablePippidonHitObject(PippidonHitObject hitObject) + : base(hitObject) + { + Size = new Vector2(40); + + Origin = Anchor.Centre; + Y = hitObject.Lane * PippidonPlayfield.LANE_HEIGHT; + } + + [BackgroundDependencyLoader] + private void load(PippidonPlayfield playfield, TextureStore textures) + { + AddInternal(new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get("coin"), + }); + + currentLane = playfield.CurrentLane.GetBoundCopy(); + } + + public override IEnumerable GetSamples() => new[] + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK) + }; + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (timeOffset >= 0) + ApplyResult(r => r.Type = currentLane.Value == HitObject.Lane ? HitResult.Perfect : HitResult.Miss); + } + + protected override void UpdateHitStateTransforms(ArmedState state) + { + switch (state) + { + case ArmedState.Hit: + this.ScaleTo(5, 1500, Easing.OutQuint).FadeOut(1500, Easing.OutQuint).Expire(); + break; + + case ArmedState.Miss: + + const double duration = 1000; + + this.ScaleTo(0.8f, duration, Easing.OutQuint); + this.MoveToOffset(new Vector2(0, 10), duration, Easing.In); + this.FadeColour(Color4.Red, duration / 2, Easing.OutQuint).Then().FadeOut(duration / 2, Easing.InQuint).Expire(); + break; + } + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs new file mode 100644 index 0000000000..9dd135479f --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Pippidon.Objects +{ + public class PippidonHitObject : HitObject + { + /// + /// Range = [-1,1] + /// + public int Lane; + + public override Judgement CreateJudgement() => new Judgement(); + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs new file mode 100644 index 0000000000..f6340f6c25 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Pippidon +{ + public class PippidonDifficultyCalculator : DifficultyCalculator + { + public PippidonDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) + { + } + + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + { + return new DifficultyAttributes(mods, skills, 0); + } + + protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty(); + + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs new file mode 100644 index 0000000000..c9e6e6faaa --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Pippidon +{ + public class PippidonInputManager : RulesetInputManager + { + public PippidonInputManager(RulesetInfo ruleset) + : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + } + + public enum PippidonAction + { + [Description("Move up")] + MoveUp, + + [Description("Move down")] + MoveDown, + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs new file mode 100644 index 0000000000..ede00f1510 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Bindings; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Pippidon.Beatmaps; +using osu.Game.Rulesets.Pippidon.Mods; +using osu.Game.Rulesets.Pippidon.UI; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Pippidon +{ + public class PippidonRuleset : Ruleset + { + public override string Description => "gather the osu!coins"; + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawablePippidonRuleset(this, beatmap, mods); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new PippidonBeatmapConverter(beatmap, this); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new PippidonDifficultyCalculator(this, beatmap); + + public override IEnumerable GetModsFor(ModType type) + { + switch (type) + { + case ModType.Automation: + return new[] { new PippidonModAutoplay() }; + + default: + return new Mod[] { null }; + } + } + + public override string ShortName => "pippidon"; + + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] + { + new KeyBinding(InputKey.W, PippidonAction.MoveUp), + new KeyBinding(InputKey.S, PippidonAction.MoveDown), + }; + + public override Drawable CreateIcon() => new Sprite + { + Margin = new MarginPadding { Top = 3 }, + Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"), + }; + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs new file mode 100644 index 0000000000..bd99cdcdbd --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Pippidon.Objects; +using osu.Game.Rulesets.Pippidon.UI; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.Pippidon.Replays +{ + public class PippidonAutoGenerator : AutoGenerator + { + protected Replay Replay; + protected List Frames => Replay.Frames; + + public new Beatmap Beatmap => (Beatmap)base.Beatmap; + + public PippidonAutoGenerator(IBeatmap beatmap) + : base(beatmap) + { + Replay = new Replay(); + } + + public override Replay Generate() + { + int currentLane = 0; + + Frames.Add(new PippidonReplayFrame()); + + foreach (PippidonHitObject hitObject in Beatmap.HitObjects) + { + if (currentLane == hitObject.Lane) + continue; + + int totalTravel = Math.Abs(hitObject.Lane - currentLane); + var direction = hitObject.Lane > currentLane ? PippidonAction.MoveDown : PippidonAction.MoveUp; + + double time = hitObject.StartTime - 5; + + if (totalTravel == PippidonPlayfield.LANE_COUNT - 1) + addFrame(time, direction == PippidonAction.MoveDown ? PippidonAction.MoveUp : PippidonAction.MoveDown); + else + { + time -= totalTravel * KEY_UP_DELAY; + + for (int i = 0; i < totalTravel; i++) + { + addFrame(time, direction); + time += KEY_UP_DELAY; + } + } + + currentLane = hitObject.Lane; + } + + return Replay; + } + + private void addFrame(double time, PippidonAction direction) + { + Frames.Add(new PippidonReplayFrame(direction) { Time = time }); + Frames.Add(new PippidonReplayFrame { Time = time + KEY_UP_DELAY }); //Release the keys as well + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs new file mode 100644 index 0000000000..7652357b4d --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.StateChanges; +using osu.Game.Replays; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.Pippidon.Replays +{ + public class PippidonFramedReplayInputHandler : FramedReplayInputHandler + { + public PippidonFramedReplayInputHandler(Replay replay) + : base(replay) + { + } + + protected override bool IsImportant(PippidonReplayFrame frame) => frame.Actions.Any(); + + public override void CollectPendingInputs(List inputs) + { + inputs.Add(new ReplayState + { + PressedActions = CurrentFrame?.Actions ?? new List(), + }); + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs new file mode 100644 index 0000000000..468ac9c725 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.Pippidon.Replays +{ + public class PippidonReplayFrame : ReplayFrame + { + public List Actions = new List(); + + public PippidonReplayFrame(PippidonAction? button = null) + { + if (button.HasValue) + Actions.Add(button.Value); + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 new file mode 100644 index 0000000000..90b13d1f73 Binary files /dev/null and b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 differ diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png new file mode 100644 index 0000000000..e79d2528ec Binary files /dev/null and b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png differ diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png new file mode 100644 index 0000000000..3cd89c6ce6 Binary files /dev/null and b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png differ diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs new file mode 100644 index 0000000000..9a73dd7790 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Input.Handlers; +using osu.Game.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Pippidon.Objects; +using osu.Game.Rulesets.Pippidon.Objects.Drawables; +using osu.Game.Rulesets.Pippidon.Replays; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Pippidon.UI +{ + [Cached] + public class DrawablePippidonRuleset : DrawableScrollingRuleset + { + public DrawablePippidonRuleset(PippidonRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + : base(ruleset, beatmap, mods) + { + Direction.Value = ScrollingDirection.Left; + TimeRange.Value = 6000; + } + + protected override Playfield CreatePlayfield() => new PippidonPlayfield(); + + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new PippidonFramedReplayInputHandler(replay); + + public override DrawableHitObject CreateDrawableRepresentation(PippidonHitObject h) => new DrawablePippidonHitObject(h); + + protected override PassThroughInputManager CreateInputManager() => new PippidonInputManager(Ruleset?.RulesetInfo); + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs new file mode 100644 index 0000000000..dd0a20f1b4 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Bindings; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Rulesets.Pippidon.UI +{ + public class PippidonCharacter : BeatSyncedContainer, IKeyBindingHandler + { + public readonly BindableInt LanePosition = new BindableInt + { + Value = 0, + MinValue = 0, + MaxValue = PippidonPlayfield.LANE_COUNT - 1, + }; + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Size = new Vector2(PippidonPlayfield.LANE_HEIGHT); + + Child = new Sprite + { + FillMode = FillMode.Fit, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.2f), + RelativeSizeAxes = Axes.Both, + Texture = textures.Get("character") + }; + + LanePosition.BindValueChanged(e => { this.MoveToY(e.NewValue * PippidonPlayfield.LANE_HEIGHT); }); + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + if (effectPoint.KiaiMode) + { + bool direction = beatIndex % 2 == 1; + double duration = timingPoint.BeatLength / 2; + + Child.RotateTo(direction ? 10 : -10, duration * 2, Easing.InOutSine); + + Child.Animate(i => i.MoveToY(-10, duration, Easing.Out)) + .Then(i => i.MoveToY(0, duration, Easing.In)); + } + else + { + Child.ClearTransforms(); + Child.RotateTo(0, 500, Easing.Out); + Child.MoveTo(Vector2.Zero, 500, Easing.Out); + } + } + + public bool OnPressed(PippidonAction action) + { + switch (action) + { + case PippidonAction.MoveUp: + changeLane(-1); + return true; + + case PippidonAction.MoveDown: + changeLane(1); + return true; + + default: + return false; + } + } + + public void OnReleased(PippidonAction action) + { + } + + private void changeLane(int change) => LanePosition.Value = (LanePosition.Value + change + PippidonPlayfield.LANE_COUNT) % PippidonPlayfield.LANE_COUNT; + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs new file mode 100644 index 0000000000..0e50030162 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs @@ -0,0 +1,128 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Pippidon.UI +{ + [Cached] + public class PippidonPlayfield : ScrollingPlayfield + { + public const float LANE_HEIGHT = 70; + + public const int LANE_COUNT = 6; + + public BindableInt CurrentLane => pippidon.LanePosition; + + private PippidonCharacter pippidon; + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + AddRangeInternal(new Drawable[] + { + new LaneContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Left = 200, + Top = LANE_HEIGHT / 2, + Bottom = LANE_HEIGHT / 2 + }, + Children = new Drawable[] + { + HitObjectContainer, + pippidon = new PippidonCharacter + { + Origin = Anchor.Centre, + }, + } + }, + }, + }); + } + + private class LaneContainer : BeatSyncedContainer + { + private OsuColour colours; + private FillFlowContainer fill; + + private readonly Container content = new Container + { + RelativeSizeAxes = Axes.Both, + }; + + protected override Container Content => content; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + this.colours = colours; + + InternalChildren = new Drawable[] + { + fill = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Colour = colours.BlueLight, + Direction = FillDirection.Vertical, + }, + content, + }; + + for (int i = 0; i < LANE_COUNT; i++) + { + fill.Add(new Lane + { + RelativeSizeAxes = Axes.X, + Height = LANE_HEIGHT, + }); + } + } + + private class Lane : CompositeDrawable + { + public Lane() + { + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 0.95f, + }, + }; + } + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + if (effectPoint.KiaiMode) + fill.FlashColour(colours.PinkLight, 800, Easing.In); + } + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj new file mode 100644 index 0000000000..61b859f45b --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj @@ -0,0 +1,15 @@ + + + netstandard2.1 + osu.Game.Rulesets.Sample + Library + AnyCPU + osu.Game.Rulesets.Pippidon + + + + + + + + \ No newline at end of file diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj new file mode 100644 index 0000000000..31a24a301f --- /dev/null +++ b/Templates/osu.Game.Templates.csproj @@ -0,0 +1,24 @@ + + + Template + ppy.osu.Game.Templates + osu! templates + ppy Pty Ltd + https://github.com/ppy/osu/blob/master/LICENCE + https://github.com/ppy/osu/blob/master/Templates + https://github.com/ppy/osu + Automated release. + Copyright (c) 2021 ppy Pty Ltd + Templates to use when creating a ruleset for consumption in osu!. + dotnet-new;templates;osu + netstandard2.1 + true + false + content + + + + + + + diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml index 737e5c43ab..adf98848bc 100644 --- a/appveyor_deploy.yml +++ b/appveyor_deploy.yml @@ -16,6 +16,8 @@ environment: job_depends_on: osu-game - job_name: mania-ruleset job_depends_on: osu-game + - job_name: templates + job_depends_on: osu-game nuget: project_feed: true @@ -59,6 +61,22 @@ for: - cmd: dotnet remove osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj reference osu.Game\osu.Game.csproj - cmd: dotnet add osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% - cmd: dotnet pack osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% + - + matrix: + only: + - job_name: templates + build_script: + - cmd: dotnet remove Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game\osu.Game.csproj + - cmd: dotnet remove Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj + - cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game\osu.Game.csproj + - cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj + + - cmd: dotnet add Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% + - cmd: dotnet add Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% + - cmd: dotnet add Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% + - cmd: dotnet add Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% + + - cmd: dotnet pack Templates\osu.Game.Templates.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% artifacts: - path: '**\*.nupkg' diff --git a/osu.Android.props b/osu.Android.props index 75ac298626..196d122a2a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - - + + diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj index a2638e95c8..54857ac87d 100644 --- a/osu.Android/osu.Android.csproj +++ b/osu.Android/osu.Android.csproj @@ -20,6 +20,11 @@ d8 r8 + + None + cjk;mideast;other;rare;west + true + @@ -53,5 +58,10 @@ + + + 5.0.0 + + \ No newline at end of file diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf index d2c14d321a..503e5935f5 100644 --- a/osu.Desktop.slnf +++ b/osu.Desktop.slnf @@ -15,7 +15,16 @@ "osu.Game.Tests\\osu.Game.Tests.csproj", "osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj", "osu.Game.Tournament\\osu.Game.Tournament.csproj", - "osu.Game\\osu.Game.csproj" + "osu.Game\\osu.Game.csproj", + + "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj", + "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj", + "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", + "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj", + "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj", + "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj", + "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", + "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj" ] } -} \ No newline at end of file +} diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index b2487568ce..0c21c75290 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -18,6 +19,7 @@ using osu.Framework.Screens; using osu.Game.Screens.Menu; using osu.Game.Updater; using osu.Desktop.Windows; +using osu.Framework.Threading; using osu.Game.IO; namespace osu.Desktop @@ -144,13 +146,39 @@ namespace osu.Desktop desktopWindow.DragDrop += f => fileDrop(new[] { f }); } + private readonly List importableFiles = new List(); + private ScheduledDelegate importSchedule; + private void fileDrop(string[] filePaths) { - var firstExtension = Path.GetExtension(filePaths.First()); + lock (importableFiles) + { + var firstExtension = Path.GetExtension(filePaths.First()); - if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; + if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; - Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning); + importableFiles.AddRange(filePaths); + + Logger.Log($"Adding {filePaths.Length} files for import"); + + // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. + // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. + importSchedule?.Cancel(); + importSchedule = Scheduler.AddDelayed(handlePendingImports, 100); + } + } + + private void handlePendingImports() + { + lock (importableFiles) + { + Logger.Log($"Handling batch import of {importableFiles.Count} files"); + + var paths = importableFiles.ToArray(); + importableFiles.Clear(); + + Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); + } } } } diff --git a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj index 88b420ffad..94fdba4a3e 100644 --- a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj +++ b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj @@ -14,6 +14,11 @@ Properties\AndroidManifest.xml armeabi-v7a;x86;arm64-v8a + + None + cjk;mideast;other;rare;west + true + @@ -35,5 +40,10 @@ osu.Game + + + 5.0.0 + + \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index f4ee3f5a42..5580358f89 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Tests public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(5.0565038923984691d, "diffcalc-test")] + [TestCase(5.169743871843191d, "diffcalc-test")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new CatchModDoubleTime()); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs new file mode 100644 index 0000000000..a10371b0f7 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneCatchReplay : TestSceneCatchPlayer + { + protected override bool Autoplay => true; + + private const int object_count = 10; + + [Test] + public void TestReplayCatcherPositionIsFramePerfect() + { + AddUntilStep("caught all fruits", () => Player.ScoreProcessor.Combo.Value == object_count); + } + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = new Beatmap + { + BeatmapInfo = + { + Ruleset = ruleset, + } + }; + + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + + for (int i = 0; i < object_count / 2; i++) + { + beatmap.HitObjects.Add(new Fruit + { + StartTime = (i + 1) * 1000, + X = 0 + }); + beatmap.HitObjects.Add(new Fruit + { + StartTime = (i + 1) * 1000 + 1, + X = CatchPlayfield.WIDTH + }); + } + + return beatmap; + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 728af5124e..c2d9a923d9 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 10aae70722..f5cce47186 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty { private const double star_scaling_factor = 0.153; - protected override int SectionLength => 750; - private float halfCatcherWidth; public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 9ad719be1a..75e17f6c48 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -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; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs index 4b008d2734..bba42dea97 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs @@ -3,13 +3,16 @@ using System.Linq; using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModHidden : ModHidden + public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset { public override string Description => @"Play with fading fruits."; public override double ScoreMultiplier => 1.06; @@ -17,6 +20,14 @@ namespace osu.Game.Rulesets.Catch.Mods private const double fade_out_offset_multiplier = 0.6; private const double fade_out_duration_multiplier = 0.44; + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset; + var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield; + + catchPlayfield.CatcherArea.MovableCatcher.CatchFruitOnPlate = false; + } + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { base.ApplyNormalVisibilityState(hitObject, state); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 73420a9eda..0e1ef90737 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -51,8 +51,11 @@ namespace osu.Game.Rulesets.Catch.UI { droppedObjectContainer, CatcherArea.MovableCatcher.CreateProxiedContent(), - HitObjectContainer, + HitObjectContainer.CreateProxy(), + // This ordering (`CatcherArea` before `HitObjectContainer`) is important to + // make sure the up-to-date catcher position is used for the catcher catching logic of hit objects. CatcherArea, + HitObjectContainer, }; } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index ed875e7002..5d57e84b75 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Catch.UI /// public bool HyperDashing => hyperDashModifier != 1; + /// + /// Whether fruit should appear on the plate. + /// + public bool CatchFruitOnPlate { get; set; } = true; + /// /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. /// @@ -237,7 +242,8 @@ namespace osu.Game.Rulesets.Catch.UI { var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2); - placeCaughtObject(palpableObject, positionInStack); + if (CatchFruitOnPlate) + placeCaughtObject(palpableObject, positionInStack); if (hitLighting.Value) addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value); diff --git a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj index 0e557cb260..9674186039 100644 --- a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj +++ b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj @@ -14,6 +14,11 @@ Properties\AndroidManifest.xml armeabi-v7a;x86;arm64-v8a + + None + cjk;mideast;other;rare;west + true + @@ -35,5 +40,10 @@ osu.Game + + + 5.0.0 + + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index 09ca04be8a..6e6500a339 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(2.7646128945056723d, "diffcalc-test")] + [TestCase(2.7879104989252959d, "diffcalc-test")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new ManiaModDoubleTime()); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 7ae69bf7d7..42ea12214f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -288,17 +288,56 @@ namespace osu.Game.Rulesets.Mania.Tests .All(j => j.Type.IsHit())); } + [Test] + public void TestHitTailBeforeLastTick() + { + const int tick_rate = 8; + const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate; + const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1); + + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + } + }, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = tick_rate }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_last_tick - 5) + }, beatmap); + + assertHeadJudgement(HitResult.Perfect); + assertLastTickJudgement(HitResult.LargeTickMiss); + assertTailJudgement(HitResult.Ok); + } + private void assertHeadJudgement(HitResult result) - => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); + => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result); private void assertTailJudgement(HitResult result) - => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result); + => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type == result); private void assertNoteJudgement(HitResult result) - => AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result); + => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type == result); private void assertTickJudgement(HitResult result) - => AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick + => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Any(j => j.Type == result)); + + private void assertLastTickJudgement(HitResult result) + => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type == result); private ScoreAccessibleReplayPlayer currentPlayer; diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index af16f39563..64e934efd2 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 7a0e3b2b76..26393c8edb 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps if (IsForCurrentRuleset) { - TargetColumns = (int)Math.Max(1, roundedCircleSize); + TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo); if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS) { @@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps originalTargetColumns = TargetColumns; } + public static int GetColumnCountForNonConvert(BeatmapInfo beatmap) + { + var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize); + return (int)Math.Max(1, roundedCircleSize); + } + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index d6ea58ee78..2ba2ee6b4a 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -7,11 +7,10 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Difficulty.Skills { - public class Strain : Skill + public class Strain : StrainSkill { private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; @@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { var maniaCurrent = (ManiaDifficultyHitObject)current; - var endTime = maniaCurrent.BaseObject.GetEndTime(); + var endTime = maniaCurrent.EndTime; var column = maniaCurrent.BaseObject.Column; double holdFactor = 1.0; // Factor to all additional strains in case something else is held @@ -46,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills for (int i = 0; i < holdEndTimes.Length; ++i) { // If there is at least one other overlapping end or note, then we get an addition, buuuuuut... - if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1)) + if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1)) holdAddition = 1.0; // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1 @@ -73,8 +72,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills } protected override double GetPeakStrain(double offset) - => applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base) - + applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base); + => applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base) + + applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base); private double applyDecay(double value, double deltaTime, double decayBase) => value * Math.Pow(decayBase, deltaTime / 1000); diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 324670c4b2..d9570bf8be 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -119,5 +119,8 @@ namespace osu.Game.Rulesets.Mania.Edit beatSnapGrid.SelectionTimeRange = null; } } + + public override string ConvertSelectionToString() + => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); } } diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs new file mode 100644 index 0000000000..d9a278ef29 --- /dev/null +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Filter; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Rulesets.Mania +{ + public class ManiaFilterCriteria : IRulesetFilterCriteria + { + private FilterCriteria.OptionalRange keys; + + public bool Matches(BeatmapInfo beatmap) + { + return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap))); + } + + public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) + { + switch (key) + { + case "key": + case "keys": + return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value); + } + + return false; + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index d624e094ad..88b63606b9 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -22,6 +22,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; @@ -382,6 +383,11 @@ namespace osu.Game.Rulesets.Mania } } }; + + public override IRulesetFilterCriteria CreateRulesetFilterCriteria() + { + return new ManiaFilterCriteria(); + } } public enum PlayfieldType diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 4f062753a6..828ee7b03e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -233,6 +233,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { + foreach (var tick in tickContainer) + { + if (!tick.Judged) + tick.MissForcefully(); + } + ApplyResult(r => r.Type = r.Judgement.MaxResult); endHold(); } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 71cc0bdf1f..48b377c794 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -7,8 +7,8 @@ namespace osu.Game.Rulesets.Mania.Scoring { internal class ManiaScoreProcessor : ScoreProcessor { - protected override double DefaultAccuracyPortion => 0.95; + protected override double DefaultAccuracyPortion => 0.99; - protected override double DefaultComboPortion => 0.05; + protected override double DefaultComboPortion => 0.01; } } diff --git a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj index dcf1573522..f4b673f10b 100644 --- a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj +++ b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj @@ -14,6 +14,11 @@ Properties\AndroidManifest.xml armeabi-v7a;x86;arm64-v8a + + None + cjk;mideast;other;rare;west + true + @@ -35,5 +40,10 @@ osu.Game + + + 5.0.0 + + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 738a21b17e..35b79aa8ac 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -4,9 +4,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Visual; @@ -14,7 +16,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Editor { - public class TestScenePathControlPointVisualiser : OsuTestScene + public class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene { private Slider slider; private PathControlPointVisualiser visualiser; @@ -43,12 +45,145 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } + [Test] + public void TestPerfectCurveTooManyPoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + // Must be both hovering and selecting the control point for the context menu to work. + moveMouseToControlPoint(1); + AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(1, PathType.PerfectCurve); + assertControlPointPathType(3, PathType.Bezier); + } + + [Test] + public void TestPerfectCurveLastThreePoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(2); + AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(2, PathType.PerfectCurve); + assertControlPointPathType(4, null); + } + + [Test] + public void TestPerfectCurveLastTwoPoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(3); + AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Bezier); + AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null); + } + + [Test] + public void TestPerfectCurveTooManyPointsLinear() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Linear); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + // Must be both hovering and selecting the control point for the context menu to work. + moveMouseToControlPoint(1); + AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Linear); + assertControlPointPathType(1, PathType.PerfectCurve); + assertControlPointPathType(3, PathType.Linear); + } + + [Test] + public void TestPerfectCurveChangeToBezier() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300), PathType.PerfectCurve); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200), PathType.Bezier); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(3); + AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); + addContextMenuItemStep("Inherit"); + + assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(1, PathType.Bezier); + assertControlPointPathType(3, null); + } + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) { Anchor = Anchor.Centre, Origin = Anchor.Centre }); - private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position))); + private void addControlPointStep(Vector2 position) => addControlPointStep(position, null); + + private void addControlPointStep(Vector2 position, PathType? type) + { + AddStep($"add {type} control point at {position}", () => + { + slider.Path.ControlPoints.Add(new PathControlPoint(position, type)); + }); + } + + private void moveMouseToControlPoint(int index) + { + AddStep($"move mouse to control point {index}", () => + { + Vector2 position = slider.Path.ControlPoints[index].Position.Value; + InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position)); + }); + } + + private void assertControlPointPathType(int controlPointIndex, PathType? type) + { + AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type.Value == type); + } + + private void addContextMenuItemStep(string contextMenuText) + { + AddStep($"click context menu item \"{contextMenuText}\"", () => + { + MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); + + item?.Action?.Value(); + }); + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs new file mode 100644 index 0000000000..d7dfc3bd42 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -0,0 +1,175 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneSliderControlPointPiece : SelectionBlueprintTestScene + { + private Slider slider; + private DrawableSlider drawableObject; + + [SetUp] + public void Setup() => Schedule(() => + { + Clear(); + + slider = new Slider + { + Position = new Vector2(256, 192), + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(new Vector2(150, 150)), + new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(400, 0)), + new PathControlPoint(new Vector2(400, 150)) + }) + }; + + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 }); + + Add(drawableObject = new DrawableSlider(slider)); + AddBlueprint(new TestSliderBlueprint(drawableObject)); + }); + + [Test] + public void TestDragControlPoint() + { + moveMouseToControlPoint(1); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(150, 50)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(1, new Vector2(150, 50)); + assertControlPointType(0, PathType.PerfectCurve); + } + + [Test] + public void TestDragControlPointAlmostLinearlyExterior() + { + moveMouseToControlPoint(1); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(400, 0.01f)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(1, new Vector2(400, 0.01f)); + assertControlPointType(0, PathType.Bezier); + } + + [Test] + public void TestDragControlPointPathRecovery() + { + moveMouseToControlPoint(1); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(400, 0.01f)); + assertControlPointType(0, PathType.Bezier); + + addMovementStep(new Vector2(150, 50)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(1, new Vector2(150, 50)); + assertControlPointType(0, PathType.PerfectCurve); + } + + [Test] + public void TestDragControlPointPathRecoveryOtherSegment() + { + moveMouseToControlPoint(4); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(350, 0.01f)); + assertControlPointType(2, PathType.Bezier); + + addMovementStep(new Vector2(150, 150)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(4, new Vector2(150, 150)); + assertControlPointType(2, PathType.PerfectCurve); + } + + [Test] + public void TestDragControlPointPathAfterChangingType() + { + AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type.Value = PathType.Bezier); + AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10)))); + AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type.Value = PathType.PerfectCurve); + + moveMouseToControlPoint(4); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + assertControlPointType(3, PathType.PerfectCurve); + + addMovementStep(new Vector2(350, 0.01f)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(4, new Vector2(350, 0.01f)); + assertControlPointType(3, PathType.Bezier); + } + + private void addMovementStep(Vector2 relativePosition) + { + AddStep($"move mouse to {relativePosition}", () => + { + Vector2 position = slider.Position + relativePosition; + InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); + }); + } + + private void moveMouseToControlPoint(int index) + { + AddStep($"move mouse to control point {index}", () => + { + Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value; + InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); + }); + } + + private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type.Value == type); + + private void assertControlPointPosition(int index, Vector2 position) => + AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position.Value, 1)); + + private class TestSliderBlueprint : SliderSelectionBlueprint + { + public new SliderBodyPiece BodyPiece => base.BodyPiece; + public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint; + public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint; + public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; + + public TestSliderBlueprint(DrawableSlider slider) + : base(slider) + { + } + + protected override SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new TestSliderCircleBlueprint(slider, position); + } + + private class TestSliderCircleBlueprint : SliderCircleSelectionBlueprint + { + public new HitCirclePiece CirclePiece => base.CirclePiece; + + public TestSliderCircleBlueprint(DrawableSlider slider, SliderPosition position) + : base(slider, position) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 67a2e5a47c..d2c37061f0 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -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) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index 7d32895083..5f44e1b6b6 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase("repeat-slider")] [TestCase("uneven-repeat-slider")] [TestCase("old-stacking")] + [TestCase("multi-segment-slider")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index c2119585ab..afd94f4570 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.6228371119271454d, "diffcalc-test")] - [TestCase(1.2864585280364178d, "zero-length-sliders")] + [TestCase(8.7212283220412345d, "diffcalc-test")] + [TestCase(1.3212137158641493d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs index cad98185ce..233aaf2ed9 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Tests get { if (content == null) - base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); + base.Content.Add(content = new OsuInputManager(new OsuRuleset().RulesetInfo)); return content; } diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 3d2d1f3fec..f743d65db3 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 90cba13c7c..cb819ec090 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// /// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances. /// - public class Aim : Skill + public class Aim : StrainSkill { private const double angle_bonus_begin = Math.PI / 3; private const double timing_threshold = 107; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 200bc7997d..fbac080fc6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// /// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit. /// - public class Speed : Skill + public class Speed : StrainSkill { private const double single_spacing_threshold = 125; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 1390675a1a..6b78cff33e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -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, IHasTooltip { public Action RequestSelection; + public List 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(); + /// + /// Handles correction of invalid path types. + /// + private void updatePathType() + { + if (ControlPoint.Type.Value != PathType.PerfectCurve) + return; + + if (PointsInSegment.Count > 3) + ControlPoint.Type.Value = PathType.Bezier; + + if (PointsInSegment.Count != 3) + return; + + ReadOnlySpan points = PointsInSegment.Select(p => p.Position.Value).ToArray(); + RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); + if (boundingBox.Width >= 640 || boundingBox.Height >= 480) + ControlPoint.Type.Value = PathType.Bezier; + } + /// /// Updates the state of the circular control point marker. /// diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index ce5dc4855e..44c3056910 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -153,6 +153,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } + /// + /// Attempts to set the given control point piece to the given path type. + /// If that would fail, try to change the path such that it instead succeeds + /// in a UX-friendly way. + /// + /// The control point piece that we want to change the path type of. + /// The path type we want to assign to the given control point piece. + private void updatePathType(PathControlPointPiece piece, PathType? type) + { + int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); + + switch (type) + { + case PathType.PerfectCurve: + // Can't always create a circular arc out of 4 or more points, + // so we split the segment into one 3-point circular arc segment + // and one segment of the previous type. + int thirdPointIndex = indexInSegment + 2; + + if (piece.PointsInSegment.Count > thirdPointIndex + 1) + piece.PointsInSegment[thirdPointIndex].Type.Value = piece.PointsInSegment[0].Type.Value; + + break; + } + + piece.ControlPoint.Type.Value = type; + } + [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -218,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components var item = new PathTypeMenuItem(type, () => { foreach (var p in Pieces.Where(p => p.IsSelected.Value)) - p.ControlPoint.Type.Value = type; + updatePathType(p, type); }); if (countOfState == totalCount) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index b71e1914f7..16e2a52279 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -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() diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0490e8b8ce..396fd41377 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Osu.Edit protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(this); + public override string ConvertSelectionToString() + => string.Join(',', selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 871339ae7b..5a84bd6163 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -33,16 +33,33 @@ namespace osu.Game.Rulesets.Osu.Edit { base.OnOperationEnded(); referenceOrigin = null; + referencePathTypes = null; } - public override bool HandleMovement(MoveSelectionEvent moveEvent) => - moveSelection(moveEvent.InstantDelta); + public override bool HandleMovement(MoveSelectionEvent moveEvent) + { + var hitObjects = selectedMovableObjects; + + // this will potentially move the selection out of bounds... + foreach (var h in hitObjects) + h.Position += moveEvent.InstantDelta; + + // but this will be corrected. + moveSelectionInBounds(); + return true; + } /// /// During a transform, the initial origin is stored so it can be used throughout the operation. /// private Vector2? referenceOrigin; + /// + /// During a transform, the initial path types of a single selected slider are stored so they + /// can be maintained throughout the operation. + /// + private List referencePathTypes; + public override bool HandleReverse() { var hitObjects = EditorBeatmap.SelectedHitObjects; @@ -140,36 +157,11 @@ namespace osu.Game.Rulesets.Osu.Edit // the only hit object selected. with a group selection, it's likely the user // is not looking to change the duration of the slider but expand the whole pattern. if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) - { - Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); - Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height); - - foreach (var point in slider.Path.ControlPoints) - point.Position.Value *= pathRelativeDeltaScale; - } + scaleSlider(slider, scale); else - { - // move the selection before scaling if dragging from top or left anchors. - if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false; - if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false; - - Quad quad = getSurroundingQuad(hitObjects); - - foreach (var h in hitObjects) - { - var newPosition = h.Position; - - // guard against no-ops and NaN. - if (scale.X != 0 && quad.Width > 0) - newPosition.X = quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X); - - if (scale.Y != 0 && quad.Height > 0) - newPosition.Y = quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y); - - h.Position = newPosition; - } - } + scaleHitObjects(hitObjects, reference, scale); + moveSelectionInBounds(); return true; } @@ -207,28 +199,130 @@ namespace osu.Game.Rulesets.Osu.Edit return true; } - private bool moveSelection(Vector2 delta) + private void scaleSlider(Slider slider, Vector2 scale) + { + referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList(); + + Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); + + // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0. + scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size; + + Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height); + + Queue oldControlPoints = new Queue(); + + foreach (var point in slider.Path.ControlPoints) + { + oldControlPoints.Enqueue(point.Position.Value); + point.Position.Value *= pathRelativeDeltaScale; + } + + // Maintain the path types in case they were defaulted to bezier at some point during scaling + for (int i = 0; i < slider.Path.ControlPoints.Count; ++i) + slider.Path.ControlPoints[i].Type.Value = referencePathTypes[i]; + + //if sliderhead or sliderend end up outside playfield, revert scaling. + Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); + (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); + + if (xInBounds && yInBounds) + return; + + foreach (var point in slider.Path.ControlPoints) + point.Position.Value = oldControlPoints.Dequeue(); + } + + private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) + { + scale = getClampedScale(hitObjects, reference, scale); + + // move the selection before scaling if dragging from top or left anchors. + float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; + float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; + + Quad selectionQuad = getSurroundingQuad(hitObjects); + + foreach (var h in hitObjects) + { + var newPosition = h.Position; + + // guard against no-ops and NaN. + if (scale.X != 0 && selectionQuad.Width > 0) + newPosition.X = selectionQuad.TopLeft.X + xOffset + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); + + if (scale.Y != 0 && selectionQuad.Height > 0) + newPosition.Y = selectionQuad.TopLeft.Y + yOffset + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); + + h.Position = newPosition; + } + } + + private (bool X, bool Y) isQuadInBounds(Quad quad) + { + bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth); + bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight); + + return (xInBounds, yInBounds); + } + + private void moveSelectionInBounds() { var hitObjects = selectedMovableObjects; Quad quad = getSurroundingQuad(hitObjects); - Vector2 newTopLeft = quad.TopLeft + delta; - if (newTopLeft.X < 0) - delta.X -= newTopLeft.X; - if (newTopLeft.Y < 0) - delta.Y -= newTopLeft.Y; + Vector2 delta = Vector2.Zero; - Vector2 newBottomRight = quad.BottomRight + delta; - if (newBottomRight.X > DrawWidth) - delta.X -= newBottomRight.X - DrawWidth; - if (newBottomRight.Y > DrawHeight) - delta.Y -= newBottomRight.Y - DrawHeight; + if (quad.TopLeft.X < 0) + delta.X -= quad.TopLeft.X; + if (quad.TopLeft.Y < 0) + delta.Y -= quad.TopLeft.Y; + + if (quad.BottomRight.X > DrawWidth) + delta.X -= quad.BottomRight.X - DrawWidth; + if (quad.BottomRight.Y > DrawHeight) + delta.Y -= quad.BottomRight.Y - DrawHeight; foreach (var h in hitObjects) h.Position += delta; + } - return true; + /// + /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. + /// + /// The hitobjects to be scaled + /// The anchor from which the scale operation is performed + /// The scale to be clamped + /// The clamped scale vector + private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) + { + float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; + float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; + + Quad selectionQuad = getSurroundingQuad(hitObjects); + + //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. + Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); + + //max Size -> playfield bounds + if (scaledQuad.TopLeft.X < 0) + scale.X += scaledQuad.TopLeft.X; + if (scaledQuad.TopLeft.Y < 0) + scale.Y += scaledQuad.TopLeft.Y; + + if (scaledQuad.BottomRight.X > DrawWidth) + scale.X -= scaledQuad.BottomRight.X - DrawWidth; + if (scaledQuad.BottomRight.Y > DrawHeight) + scale.Y -= scaledQuad.BottomRight.Y - DrawHeight; + + //min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale. + Vector2 scaledSize = selectionQuad.Size + scale; + Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON); + + scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size; + + return scale; } /// diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 5470d0fcb4..882f848190 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")] public Bindable FixedFollowCircleHitArea { get; } = new BindableBool(true); + [SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")] + public Bindable AlwaysPlayTailSample { get; } = new BindableBool(true); + public void ApplyToHitObject(HitObject hitObject) { switch (hitObject) @@ -79,6 +82,10 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableSliderHead head: head.TrackFollowCircle = !NoSliderHeadMovement.Value; break; + + case DrawableSliderTail tail: + tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value; + break; } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 20c0818d03..683b35f282 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -9,10 +9,12 @@ using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; using osuTK; namespace osu.Game.Rulesets.Osu.Mods @@ -23,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Mods private const float default_flashlight_size = 180; + private const double default_follow_delay = 120; + private OsuFlashlight flashlight; public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(); @@ -35,8 +39,25 @@ namespace osu.Game.Rulesets.Osu.Mods } } + public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + base.ApplyToDrawableRuleset(drawableRuleset); + + flashlight.FollowDelay = FollowDelay.Value; + } + + [SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")] + public BindableNumber FollowDelay { get; } = new BindableDouble(default_follow_delay) + { + MinValue = default_follow_delay, + MaxValue = default_follow_delay * 10, + Precision = default_follow_delay, + }; + private class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition { + public double FollowDelay { private get; set; } + public OsuFlashlight() { FlashlightSize = new Vector2(0, getSizeFor(0)); @@ -50,13 +71,11 @@ namespace osu.Game.Rulesets.Osu.Mods protected override bool OnMouseMove(MouseMoveEvent e) { - const double follow_delay = 120; - var position = FlashlightPosition; var destination = e.MousePosition; FlashlightPosition = Interpolation.ValueAt( - Math.Min(Math.Abs(Clock.ElapsedFrameTime), follow_delay), position, destination, 0, follow_delay, Easing.Out); + Math.Min(Math.Abs(Clock.ElapsedFrameTime), FollowDelay), position, destination, 0, FollowDelay, Easing.Out); return base.OnMouseMove(e); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 9122f347d0..82b5492de6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osuTK; @@ -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().ToArray(); + + var slidingSamples = new List(); + + var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); + if (normalSample != null) + slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide")); + + var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE); + if (whistleSample != null) + slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle")); + + slidingSample.Samples = slidingSamples.ToArray(); } public override void StopAllSamples() @@ -280,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { // rather than doing it this way, we should probably attach the sample to the tail circle. // this can only be done after we stop using LegacyLastTick. - if (TailCircle.IsHit) + if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit) base.PlaySamples(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 6a8e02e886..87f098dd29 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -26,6 +26,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// public override bool DisplayResult => false; + /// + /// Whether the hit samples only play on successful hits. + /// If false, the hit samples will also play on misses. + /// + public bool SamplePlaysOnlyOnHit { get; set; } = true; + public bool Tracking { get; set; } private SkinnableDrawable circlePiece; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index e2b6c84896..8ba9597dc3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -81,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.Objects public List> NodeSamples { get; set; } = new List>(); + [JsonIgnore] + public IList TailSamples { get; private set; } + private int repeatCount; public int RepeatCount @@ -143,11 +146,6 @@ namespace osu.Game.Rulesets.Osu.Objects Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; - - // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. - // For now, the samples are attached to and played by the slider itself at the correct end time. - // ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live). - Samples = this.GetNodeSamples(repeatCount + 1).ToArray(); } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) @@ -238,6 +236,10 @@ namespace osu.Game.Rulesets.Osu.Objects if (HeadCircle != null) HeadCircle.Samples = this.GetNodeSamples(0); + + // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. + // For now, the samples are played by the slider itself at the correct end time. + TailSamples = this.GetNodeSamples(repeatCount + 1); } public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement(); diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json new file mode 100644 index 0000000000..8a056b3039 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json @@ -0,0 +1,36 @@ +{ + "Mappings": [{ + "StartTime": 347893, + "Objects": [{ + "StartTime": 347893, + "EndTime": 347893, + "X": 329, + "Y": 245, + "StackOffset": { + "X": 0, + "Y": 0 + } + }, + { + "StartTime": 348193, + "EndTime": 348193, + "X": 183.0447, + "Y": 245.24292, + "StackOffset": { + "X": 0, + "Y": 0 + } + }, + { + "StartTime": 348457, + "EndTime": 348457, + "X": 329, + "Y": 245, + "StackOffset": { + "X": 0, + "Y": 0 + } + } + ] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider.osu new file mode 100644 index 0000000000..843c32b8ef --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider.osu @@ -0,0 +1,17 @@ +osu file format v14 + +[General] +Mode: 0 + +[Difficulty] +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8 +SliderMultiplier:2 +SliderTickRate:1 + +[TimingPoints] +337093,300,4,2,1,40,1,0 + +[HitObjects] +329,245,347893,2,0,B|319:311|199:343|183:245|183:245,2,200,8|8|8,0:0|0:0|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj index 392442b713..4d4dabebe6 100644 --- a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj +++ b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj @@ -14,6 +14,11 @@ Properties\AndroidManifest.xml armeabi-v7a;x86;arm64-v8a + + None + cjk;mideast;other;rare;west + true + @@ -35,5 +40,10 @@ osu.Game + + + 5.0.0 + + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs index fa6c9da174..9b36b064bc 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(() => new TaikoInputManager(new RulesetInfo { ID = 1 }) + SetContents(() => new TaikoInputManager(new TaikoRuleset().RulesetInfo) { RelativeSizeAxes = Axes.Both, Child = new Container diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index eb21c02d5f..dd3c6b317a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Tests public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(3.1473940254109078d, "diffcalc-test")] - [TestCase(3.1473940254109078d, "diffcalc-test-strong")] + [TestCase(3.1704781712282624d, "diffcalc-test")] + [TestCase(3.1704781712282624d, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index fa00922706..eab144592f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index cc0738e252..769d021362 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Calculates the colour coefficient of taiko difficulty. /// - public class Colour : Skill + public class Colour : StrainSkill { protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index f2b8309ac5..a32f6ebe0d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Calculates the rhythm coefficient of taiko difficulty. /// - public class Rhythm : Skill + public class Rhythm : StrainSkill { protected override double SkillMultiplier => 10; protected override double StrainDecayBase => 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index c34cce0cd6..4cceadb23f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit). /// - public class Stamina : Skill + public class Stamina : StrainSkill { protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index fc198d2493..6b3e31c5d5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -133,11 +133,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { List peaks = new List(); - for (int i = 0; i < colour.StrainPeaks.Count; i++) + var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); + var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); + var staminaRightPeaks = staminaRight.GetCurrentStrainPeaks().ToList(); + var staminaLeftPeaks = staminaLeft.GetCurrentStrainPeaks().ToList(); + + for (int i = 0; i < colourPeaks.Count; i++) { - double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; - double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; + double colourPeak = colourPeaks[i] * colour_skill_multiplier; + double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double staminaPeak = (staminaRightPeaks[i] + staminaLeftPeaks[i]) * stamina_skill_multiplier * staminaPenalty; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj index c3d9cb5875..b45a3249ff 100644 --- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj +++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj @@ -23,6 +23,11 @@ $(NoWarn);CA2007 + + None + cjk;mideast;other;rare;west + true + %(RecursiveDir)%(Filename)%(Extension) @@ -75,6 +80,9 @@ + + 5.0.0 + diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 4b9e9dd88c..0f82492e51 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -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); } } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 0784109158..920cc36776 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -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 diff --git a/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs b/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs deleted file mode 100644 index d5ac38008e..0000000000 --- a/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -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 stack; - - [SetUp] - public void Setup() - { - stack = new LimitedCapacityStack(capacity); - } - - [Test] - public void TestEmptyStack() - { - Assert.AreEqual(0, stack.Count); - - Assert.Throws(() => - { - 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(() => - { - 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(() => - { - 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); - } - } -} diff --git a/osu.Game.Tests/NonVisual/ReverseQueueTest.cs b/osu.Game.Tests/NonVisual/ReverseQueueTest.cs new file mode 100644 index 0000000000..93cd9403ce --- /dev/null +++ b/osu.Game.Tests/NonVisual/ReverseQueueTest.cs @@ -0,0 +1,143 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Rulesets.Difficulty.Utils; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class ReverseQueueTest + { + private ReverseQueue queue; + + [SetUp] + public void Setup() + { + queue = new ReverseQueue(4); + } + + [Test] + public void TestEmptyQueue() + { + Assert.AreEqual(0, queue.Count); + + Assert.Throws(() => + { + 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(() => + { + 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(() => + { + 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++; + } + } + } +} diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index ab24a72a12..77f910c144 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Online var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); - Assert.That(deserialized.Acronym, Is.EqualTo(apiMod.Acronym)); + Assert.That(deserialized?.Acronym, Is.EqualTo(apiMod.Acronym)); } [Test] @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Online var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); - Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(2.0)); + Assert.That(deserialized?.Settings, Contains.Key("test_setting").With.ContainValue(2.0)); } [Test] @@ -44,9 +44,9 @@ namespace osu.Game.Tests.Online var apiMod = new APIMod(new TestMod { TestSetting = { Value = 2 } }); var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); - var converted = (TestMod)deserialized.ToMod(new TestRuleset()); + var converted = (TestMod)deserialized?.ToMod(new TestRuleset()); - Assert.That(converted.TestSetting.Value, Is.EqualTo(2)); + Assert.That(converted?.TestSetting.Value, Is.EqualTo(2)); } [Test] @@ -61,11 +61,11 @@ namespace osu.Game.Tests.Online }); var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); - var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset()); + var converted = (TestModTimeRamp)deserialised?.ToMod(new TestRuleset()); - Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false)); - Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25)); - Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25)); + Assert.That(converted?.AdjustPitch.Value, Is.EqualTo(false)); + Assert.That(converted?.InitialRate.Value, Is.EqualTo(1.25)); + Assert.That(converted?.FinalRate.Value, Is.EqualTo(0.25)); } [Test] @@ -78,10 +78,10 @@ namespace osu.Game.Tests.Online }); var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); - var converted = (TestModDifficultyAdjust)deserialised.ToMod(new TestRuleset()); + var converted = (TestModDifficultyAdjust)deserialised?.ToMod(new TestRuleset()); - Assert.That(converted.ExtendedLimits.Value, Is.True); - Assert.That(converted.OverallDifficulty.Value, Is.EqualTo(11)); + Assert.That(converted?.ExtendedLimits.Value, Is.True); + Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11)); } private class TestRuleset : Ruleset diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 8c30802ce3..8dab570e30 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Online private BeatmapSetInfo testBeatmapSet; private readonly Bindable selectedItem = new Bindable(); - 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 expected) { - AddAssert(description, () => availablilityTracker.Availability.Value.Equals(expected.Invoke())); + AddAssert(description, () => availabilityTracker.Availability.Value.Equals(expected.Invoke())); } private static BeatmapInfo getTestBeatmapInfo(string archiveFile) diff --git a/osu.Game.Tests/Resources/multi-segment-slider.osu b/osu.Game.Tests/Resources/multi-segment-slider.osu index 6eabe640e4..135132e35c 100644 --- a/osu.Game.Tests/Resources/multi-segment-slider.osu +++ b/osu.Game.Tests/Resources/multi-segment-slider.osu @@ -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: + diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 9f16312121..20fa0732b9 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -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) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index 390198be04..0b1617b6a6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -77,5 +77,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("start clock again", Clock.Start); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); } + + protected override void Dispose(bool isDisposing) + { + Beatmap.Disabled = false; + base.Dispose(isDisposing); + } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs new file mode 100644 index 0000000000..96ce418851 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorSeeking : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = base.CreateBeatmap(ruleset); + + beatmap.BeatmapInfo.BeatDivisor = 1; + + beatmap.ControlPointInfo = new ControlPointInfo(); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }); + beatmap.ControlPointInfo.Add(2000, new TimingControlPoint { BeatLength = 500 }); + + return beatmap; + } + + [Test] + public void TestSnappedSeeking() + { + AddStep("seek to 0", () => EditorClock.Seek(0)); + AddAssert("time is 0", () => EditorClock.CurrentTime == 0); + + pressAndCheckTime(Key.Right, 1000); + pressAndCheckTime(Key.Right, 2000); + pressAndCheckTime(Key.Right, 2500); + pressAndCheckTime(Key.Right, 3000); + + pressAndCheckTime(Key.Left, 2500); + pressAndCheckTime(Key.Left, 2000); + pressAndCheckTime(Key.Left, 1000); + } + + [Test] + public void TestSnappedSeekingAfterControlPointChange() + { + AddStep("seek to 0", () => EditorClock.Seek(0)); + AddAssert("time is 0", () => EditorClock.CurrentTime == 0); + + pressAndCheckTime(Key.Right, 1000); + pressAndCheckTime(Key.Right, 2000); + pressAndCheckTime(Key.Right, 2500); + pressAndCheckTime(Key.Right, 3000); + + AddStep("remove 2nd timing point", () => + { + EditorBeatmap.BeginChange(); + var group = EditorBeatmap.ControlPointInfo.GroupAt(2000); + EditorBeatmap.ControlPointInfo.RemoveGroup(group); + EditorBeatmap.EndChange(); + }); + + pressAndCheckTime(Key.Left, 2000); + pressAndCheckTime(Key.Left, 1000); + + pressAndCheckTime(Key.Right, 2000); + pressAndCheckTime(Key.Right, 3000); + } + + private void pressAndCheckTime(Key key, double expectedTime) + { + AddStep($"press {key}", () => InputManager.Key(key)); + AddUntilStep($"time is {expectedTime}", () => Precision.AlmostEquals(expectedTime, EditorClock.CurrentTime, 1)); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 4a0e1282c4..9d85a9995d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; @@ -29,10 +32,13 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(SpectatorStreamingClient))] private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + // used just to show beatmap card for the time being. protected override bool UseOnlineAPI => true; - private Spectator spectatorScreen; + private SoloSpectator spectatorScreen; [Resolved] private OsuGameBase game { get; set; } @@ -69,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddAssert("screen hasn't changed", () => Stack.CurrentScreen is Spectator); + AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator); start(); sendFrames(); @@ -195,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(-1234); sendFrames(); - AddAssert("screen didn't change", () => Stack.CurrentScreen is Spectator); + AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator); } private OsuFramedReplayInputHandler replayHandler => @@ -226,7 +232,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadSpectatingScreen() { - AddStep("load screen", () => LoadScreen(spectatorScreen = new Spectator(testSpectatorStreamingClient.StreamingUser))); + AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(testSpectatorStreamingClient.StreamingUser))); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } @@ -301,5 +307,14 @@ namespace osu.Game.Tests.Visual.Gameplay }); } } + + internal class TestUserLookupCache : UserLookupCache + { + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User + { + Id = lookup, + Username = $"User {lookup}" + }); + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs new file mode 100644 index 0000000000..6b03b53b4b --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.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 + }; + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8869718fd1..839118de2f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -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().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().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("click ready button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index e713cff233..7f8f04b718 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -126,6 +126,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().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() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index b44e5b1e5b..dfb4306e67 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -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 selectedItem = new Bindable(); @@ -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().Single().Enabled.Value); + AddAssert("ready button disabled", () => !button.ChildrenOfType().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().Single().Enabled.Value); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs new file mode 100644 index 0000000000..e65e4a68a7 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -0,0 +1,193 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.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 selectedItem = new Bindable(); + + private BeatmapSetInfo importedSet; + private BeatmapManager beatmaps; + private RulesetStore rulesets; + + private IDisposable readyClickOperation; + + protected override Container 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().Single().Enabled.Value == shouldBeEnabled); + + private void assertReadyButtonEnablement(bool shouldBeEnabled) + => AddAssert($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); + } +} diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index bf5338d81a..f9a991f756 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -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) { diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index f2bb518b2e..859cefe3a9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -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,43 @@ namespace osu.Game.Tests.Visual.Navigation exitViaEscapeAndConfirm(); } + /// + /// 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). + /// + [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() + { + Player player = null; + + PushAndConfirm(() => new TestSongSelect()); + + AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); + AddAssert("retry count is 0", () => player.RestartCount == 0); + + AddStep("attempt to retry", () => player.ChildrenOfType().First().Action()); + AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player); + + AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); + AddAssert("retry count is 1", () => player.RestartCount == 1); + } + [Test] public void TestRetryFromResults() { @@ -123,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().First().Children.Any(c => c.IsHovered)); AddStep("Click back button", () => InputManager.Click(MouseButton.Left)); AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index a7f6c8c0d3..a62980addf 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -45,6 +45,8 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaOffset { get; } = new Bindable(); public Bindable AreaSize { get; } = new Bindable(); + public Bindable Rotation { get; } = new Bindable(); + public IBindable Tablet => tablet; private readonly Bindable tablet = new Bindable(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs index acf037198f..06572f66bf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs @@ -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)); } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs new file mode 100644 index 0000000000..0ac65b357c --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +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); + }); + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index e36b3cdc74..0e1f6f6b0c 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs new file mode 100644 index 0000000000..4c5f5a7a1a --- /dev/null +++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Platform; +using osu.Game.Tournament.IO; +using osu.Game.Tournament.IPC; + +namespace osu.Game.Tournament.Tests.NonVisual +{ + [TestFixture] + public class IPCLocationTest + { + [Test] + public void CheckIPCLocation() + { + // don't use clean run because files are being written before osu! launches. + using (HeadlessGameHost host = new HeadlessGameHost(nameof(CheckIPCLocation))) + { + string basePath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(CheckIPCLocation)); + + // Set up a fake IPC client for the IPC Storage to switch to. + string testStableInstallDirectory = Path.Combine(basePath, "stable-ce"); + Directory.CreateDirectory(testStableInstallDirectory); + + string ipcFile = Path.Combine(testStableInstallDirectory, "ipc.txt"); + File.WriteAllText(ipcFile, string.Empty); + + try + { + var osu = loadOsu(host); + TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get(); + FileBasedIPC ipc = null; + + waitForOrAssert(() => (ipc = osu.Dependencies.Get() as FileBasedIPC) != null, @"ipc could not be populated in a reasonable amount of time"); + + Assert.True(ipc.SetIPCLocation(testStableInstallDirectory)); + Assert.True(storage.AllTournaments.Exists("stable.json")); + } + finally + { + host.Storage.DeleteDirectory(testStableInstallDirectory); + host.Storage.DeleteDirectory("tournaments"); + host.Exit(); + } + } + } + + private TournamentGameBase loadOsu(GameHost host) + { + var osu = new TournamentGameBase(); + Task.Run(() => host.Run(osu)); + waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); + return osu; + } + + private static void waitForOrAssert(Func result, string failureMessage, int timeout = 90000) + { + Task task = Task.Run(() => + { + while (!result()) Thread.Sleep(200); + }); + + Assert.IsTrue(task.Wait(timeout), failureMessage); + } + } +} diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index c1159dc000..522567584d 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -1,9 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Testing; using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens.Gameplay; +using osu.Game.Tournament.Screens.Gameplay.Components; namespace osu.Game.Tournament.Tests.Screens { @@ -18,5 +22,24 @@ namespace osu.Game.Tournament.Tests.Screens Add(new GameplayScreen()); Add(chat); } + + [Test] + public void TestWarmup() + { + checkScoreVisibility(false); + + toggleWarmup(); + checkScoreVisibility(true); + + toggleWarmup(); + checkScoreVisibility(false); + } + + private void checkScoreVisibility(bool visible) + => AddUntilStep($"scores {(visible ? "shown" : "hidden")}", + () => this.ChildrenOfType().All(score => score.Alpha == (visible ? 1 : 0))); + + private void toggleWarmup() + => AddStep("toggle warmup", () => this.ChildrenOfType().First().Click()); } } diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index cdfd19c157..1fa0ffc8e9 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -10,6 +10,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Tests.Visual; +using osu.Game.Tournament.IO; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osu.Game.Users; @@ -28,7 +29,7 @@ namespace osu.Game.Tournament.Tests protected MatchIPCInfo IPCInfo { get; private set; } = new MatchIPCInfo(); [BackgroundDependencyLoader] - private void load(Storage storage) + private void load(TournamentStorage storage) { Ladder.Ruleset.Value ??= rulesetStore.AvailableRulesets.First(); diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index b20583dd7e..a4e52f8cd4 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 5d9fed6288..044b60bbd5 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -1,12 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.IO; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IO; -using System.IO; -using System.Collections.Generic; using osu.Game.Tournament.Configuration; namespace osu.Game.Tournament.IO @@ -15,7 +15,12 @@ namespace osu.Game.Tournament.IO { private const string default_tournament = "default"; private readonly Storage storage; - private readonly Storage allTournaments; + + /// + /// The storage where all tournaments are located. + /// + public readonly Storage AllTournaments; + private readonly TournamentStorageManager storageConfig; public readonly Bindable CurrentTournament; @@ -23,16 +28,16 @@ namespace osu.Game.Tournament.IO : base(storage.GetStorageForDirectory("tournaments"), string.Empty) { this.storage = storage; - allTournaments = UnderlyingStorage; + AllTournaments = UnderlyingStorage; storageConfig = new TournamentStorageManager(storage); if (storage.Exists("tournament.ini")) { - ChangeTargetStorage(allTournaments.GetStorageForDirectory(storageConfig.Get(StorageConfig.CurrentTournament))); + ChangeTargetStorage(AllTournaments.GetStorageForDirectory(storageConfig.Get(StorageConfig.CurrentTournament))); } else - Migrate(allTournaments.GetStorageForDirectory(default_tournament)); + Migrate(AllTournaments.GetStorageForDirectory(default_tournament)); CurrentTournament = storageConfig.GetBindable(StorageConfig.CurrentTournament); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); @@ -42,11 +47,11 @@ namespace osu.Game.Tournament.IO private void updateTournament(ValueChangedEvent newTournament) { - ChangeTargetStorage(allTournaments.GetStorageForDirectory(newTournament.NewValue)); + ChangeTargetStorage(AllTournaments.GetStorageForDirectory(newTournament.NewValue)); Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty)); } - public IEnumerable ListTournaments() => allTournaments.GetDirectories(string.Empty); + public IEnumerable ListTournaments() => AllTournaments.GetDirectories(string.Empty); public override void Migrate(Storage newStorage) { diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs index 0b0050a245..1ebc81c773 100644 --- a/osu.Game.Tournament/Models/StableInfo.cs +++ b/osu.Game.Tournament/Models/StableInfo.cs @@ -5,6 +5,7 @@ using System; using System.IO; using Newtonsoft.Json; using osu.Framework.Platform; +using osu.Game.Tournament.IO; namespace osu.Game.Tournament.Models { @@ -24,18 +25,18 @@ namespace osu.Game.Tournament.Models /// public event Action OnStableInfoSaved; - private const string config_path = "tournament/stable.json"; + private const string config_path = "stable.json"; - private readonly Storage storage; + private readonly Storage configStorage; - public StableInfo(Storage storage) + public StableInfo(TournamentStorage storage) { - this.storage = storage; + configStorage = storage.AllTournaments; - if (!storage.Exists(config_path)) + if (!configStorage.Exists(config_path)) return; - using (Stream stream = storage.GetStream(config_path, FileAccess.Read, FileMode.Open)) + using (Stream stream = configStorage.GetStream(config_path, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) { JsonConvert.PopulateObject(sr.ReadToEnd(), this); @@ -44,7 +45,7 @@ namespace osu.Game.Tournament.Models public void SaveChanges() { - using (var stream = storage.GetStream(config_path, FileAccess.Write, FileMode.Create)) + using (var stream = configStorage.GetStream(config_path, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(stream)) { sw.Write(JsonConvert.SerializeObject(this, diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 582f72429b..aa1be143ea 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using Newtonsoft.Json; @@ -43,10 +44,13 @@ namespace osu.Game.Tournament.Screens.Editors private void addAllCountries() { List countries; + using (Stream stream = game.Resources.GetStream("Resources/countries.json")) using (var sr = new StreamReader(stream)) countries = JsonConvert.DeserializeObject>(sr.ReadToEnd()); + Debug.Assert(countries != null); + foreach (var c in countries) Storage.Add(c); } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index d790f4b754..8048425ce1 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -95,7 +95,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Origin = Anchor.TopRight, }, }; + } + protected override void LoadComplete() + { + base.LoadComplete(); updateDisplay(); } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 4ba86dcefc..33658115cc 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -14,9 +14,21 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private readonly TeamScore score; + private bool showScore; + public bool ShowScore { - set => score.FadeTo(value ? 1 : 0, 200); + get => showScore; + set + { + if (showScore == value) + return; + + showScore = value; + + if (IsLoaded) + updateDisplay(); + } } public TeamDisplay(TournamentTeam team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) @@ -92,5 +104,18 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateDisplay(); + FinishTransforms(true); + } + + private void updateDisplay() + { + score.FadeTo(ShowScore ? 1 : 0, 200); + } } } diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index b291edd19d..f3434c5153 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -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 /// The converted Beatmap. protected virtual Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { -#pragma warning disable 618 - return ConvertBeatmap(original); -#pragma warning restore 618 - } - - /// - /// Performs the conversion of a Beatmap using this Beatmap Converter. - /// - /// The un-converted Beatmap. - /// The converted Beatmap. - [Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318 - protected virtual Beatmap ConvertBeatmap(IBeatmap original) - { var beatmap = CreateBeatmap(); beatmap.BeatmapInfo = original.BeatmapInfo; @@ -121,21 +104,6 @@ namespace osu.Game.Beatmaps /// The un-converted Beatmap. /// The cancellation token. /// The converted hit object. - protected virtual IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) - { -#pragma warning disable 618 - return ConvertHitObject(original, beatmap); -#pragma warning restore 618 - } - - /// - /// Performs the conversion of a hit object. - /// This method is generally executed sequentially for all objects in a beatmap. - /// - /// The hit object to convert. - /// The un-converted Beatmap. - /// The converted hit object. - [Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318 - protected virtual IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) => Enumerable.Empty(); + protected virtual IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) => Enumerable.Empty(); } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index a898e10e4f..bf7906bd5c 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -147,7 +147,7 @@ namespace osu.Game.Beatmaps { string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]"; - return $"{Metadata} {version}".Trim(); + return $"{Metadata ?? BeatmapSet?.Metadata} {version}".Trim(); } public bool Equals(BeatmapInfo other) diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 367f612dc8..858da8e602 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -19,8 +19,13 @@ namespace osu.Game.Beatmaps public int ID { get; set; } public string Title { get; set; } + + [JsonProperty("title_unicode")] public string TitleUnicode { get; set; } + public string Artist { get; set; } + + [JsonProperty("artist_unicode")] public string ArtistUnicode { get; set; } [JsonIgnore] diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 9d87a20d60..7d7ba09fcf 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -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; - /// /// A function to create the icon for display purposes. Use default icons available via whenever possible for conformity. /// @@ -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 - } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index d06478b9de..acbf57d25f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -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 samples, bool banksOnly = false, bool zeroBanks = false) + private string getSampleBank(IList 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) { diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 00cf5798e7..f2e4c6d013 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -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 } } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index d12eaa10f6..cd8b486f23 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -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(); + } } } } diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 8ccdb9249e..671c3bc8bc 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -13,6 +13,7 @@ namespace osu.Game.Input.Bindings public class GlobalActionContainer : DatabasedKeyBindingContainer, 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 DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings); + protected override void LoadComplete() + { + base.LoadComplete(); + + parentInputManager = GetContainingInputManager(); + } + + public override IEnumerable DefaultKeyBindings => GlobalKeyBindings + .Concat(EditorKeyBindings) + .Concat(InGameKeyBindings) + .Concat(SongSelectKeyBindings) + .Concat(AudioControlKeyBindings); public IEnumerable GlobalKeyBindings => new[] { @@ -74,6 +86,14 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), }; + public IEnumerable 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 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 KeyBindingInputQueue => - handler == null ? base.KeyBindingInputQueue : base.KeyBindingInputQueue.Prepend(handler); + protected override IEnumerable 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, } } diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs index 93ed3ca884..fba1bee0b8 100644 --- a/osu.Game/Input/Handlers/ReplayInputHandler.cs +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -34,8 +34,6 @@ namespace osu.Game.Input.Handlers public override bool IsActive => true; - public override int Priority => 0; - public class ReplayState : IInput where T : struct { diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 5608002513..795540b65d 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -61,6 +61,9 @@ namespace osu.Game.Online.Leaderboards [Resolved(CanBeNull = true)] private SongSelect songSelect { get; set; } + [Resolved] + private ScoreManager scoreManager { get; set; } + public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true) { this.score = score; @@ -388,6 +391,9 @@ namespace osu.Game.Online.Leaderboards if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods)); + if (score.Files?.Count > 0) + items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score))); + if (score.ID != 0) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4529dfd0a7..37e11cc576 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -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); } diff --git a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs index e54c71cd85..c467ff84bb 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs @@ -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. /// Results, + + /// + /// The user is currently spectating this room. + /// + Spectating } } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 0f7050596f..2ddc10db0f 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -249,6 +249,33 @@ namespace osu.Game.Online.Multiplayer } } + /// + /// Toggles the 's spectating state. + /// + /// If a toggle of the spectating state is not valid at this time. + 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); diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs index 8868f90524..4955aa9058 100644 --- a/osu.Game/Online/OnlineViewContainer.cs +++ b/osu.Game/Online/OnlineViewContainer.cs @@ -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; } + /// + /// Construct a new instance of an online view container. + /// + /// The message to display when not logged in. If empty, no button will display. 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, diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs similarity index 97% rename from osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs rename to osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 8278162353..72ea84d4a8 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -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. /// - public class OnlinePlayBeatmapAvailablilityTracker : DownloadTrackingComposite + public class OnlinePlayBeatmapAvailabilityTracker : DownloadTrackingComposite { public readonly IBindable SelectedItem = new Bindable(); diff --git a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs index ae5ac5e26c..0d2bea1f2a 100644 --- a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.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 { 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"; } } diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index 98ba4fa052..85fa3eeb34 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -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}"; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dd1fa32ad9..809e5d3c1b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -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; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e1c7b67a8c..ca132df552 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -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); @@ -429,6 +430,9 @@ namespace osu.Game public async Task Import(params string[] paths) { + if (paths.Length == 0) + return; + var extension = Path.GetExtension(paths.First())?.ToLowerInvariant(); foreach (var importer in fileImporters) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 153aa41582..a61640a02e 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -228,8 +229,8 @@ namespace osu.Game.Overlays.BeatmapSet loading.Hide(); - title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty; - artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty; + title.Text = new RomanisableString(setInfo.NewValue.Metadata.TitleUnicode, setInfo.NewValue.Metadata.Title); + artist.Text = new RomanisableString(setInfo.NewValue.Metadata.ArtistUnicode, setInfo.NewValue.Metadata.Artist); explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 86ce724390..5f9c00b36a 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -275,7 +275,8 @@ namespace osu.Game.Overlays.Chat { if (!UserScrolling) { - ScrollToEnd(); + if (Current < ScrollableExtent) + ScrollToEnd(); lastExtent = ScrollableExtent; } }); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index c89699f2ee..336430fd9b 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -137,7 +137,7 @@ namespace osu.Game.Overlays.Dashboard Text = "Watch", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(User))), + Action = () => game?.PerformFromScreen(s => s.Push(new SoloSpectator(User))), Enabled = { Value = User.Id != api.LocalUser.Value.Id } } } diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs index 9a27c55c53..c905397e77 100644 --- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs @@ -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"; diff --git a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs new file mode 100644 index 0000000000..3e8da9f7d0 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs @@ -0,0 +1,109 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Handlers.Tablet; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + internal class RotationPresetButtons : FillFlowContainer + { + private readonly ITabletHandler tabletHandler; + + private Bindable rotation; + + private const int height = 50; + + public RotationPresetButtons(ITabletHandler tabletHandler) + { + this.tabletHandler = tabletHandler; + + RelativeSizeAxes = Axes.X; + Height = height; + + for (int i = 0; i < 360; i += 90) + { + var presetRotation = i; + + Add(new RotationButton(i) + { + RelativeSizeAxes = Axes.X, + Height = height, + Width = 0.25f, + Text = $"{presetRotation}º", + Action = () => tabletHandler.Rotation.Value = presetRotation, + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + rotation = tabletHandler.Rotation.GetBoundCopy(); + rotation.BindValueChanged(val => + { + foreach (var b in Children.OfType()) + b.IsSelected = b.Preset == val.NewValue; + }, true); + } + + public class RotationButton : TriangleButton + { + [Resolved] + private OsuColour colours { get; set; } + + public readonly int Preset; + + public RotationButton(int preset) + { + Preset = preset; + } + + private bool isSelected; + + public bool IsSelected + { + get => isSelected; + set + { + if (value == isSelected) + return; + + isSelected = value; + + if (IsLoaded) + updateColour(); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateColour(); + } + + private void updateColour() + { + if (isSelected) + { + BackgroundColour = colours.BlueDark; + Triangles.ColourDark = colours.BlueDarker; + Triangles.ColourLight = colours.Blue; + } + else + { + BackgroundColour = colours.Gray4; + Triangles.ColourDark = colours.Gray5; + Triangles.ColourLight = colours.Gray6; + } + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs index ecb8acce54..f61742093c 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs @@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly Bindable areaOffset = new Bindable(); private readonly Bindable areaSize = new Bindable(); + private readonly BindableNumber rotation = new BindableNumber(); + private readonly IBindable tablet = new Bindable(); private OsuSpriteText tabletName; @@ -124,6 +126,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}"; }, true); + rotation.BindTo(handler.Rotation); + rotation.BindValueChanged(val => + { + usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint) + .OnComplete(_ => checkBounds()); // required as we are using SSDQ. + }); + tablet.BindTo(handler.Tablet); tablet.BindValueChanged(_ => Scheduler.AddOnce(updateTabletDetails)); diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index bd0f7ddc4c..d770c18878 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber sizeX = new BindableNumber { MinValue = 10 }; private readonly BindableNumber sizeY = new BindableNumber { MinValue = 10 }; + private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; + [Resolved] private GameHost host { get; set; } @@ -110,12 +112,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input } }, new SettingsSlider - { - TransferValueOnCommit = true, - LabelText = "Aspect Ratio", - Current = aspectRatio - }, - new SettingsSlider { TransferValueOnCommit = true, LabelText = "X Offset", @@ -127,6 +123,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = "Y Offset", Current = offsetY }, + new SettingsSlider + { + TransferValueOnCommit = true, + LabelText = "Rotation", + Current = rotation + }, + new RotationPresetButtons(tabletHandler), + new SettingsSlider + { + TransferValueOnCommit = true, + LabelText = "Aspect Ratio", + Current = aspectRatio + }, new SettingsCheckbox { LabelText = "Lock aspect ratio", @@ -153,6 +162,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { base.LoadComplete(); + rotation.BindTo(tabletHandler.Rotation); + areaOffset.BindTo(tabletHandler.AreaOffset); areaOffset.BindValueChanged(val => { diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 85765bf991..0bd9750b0b 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -57,13 +57,6 @@ namespace osu.Game.Overlays.Settings } } - [Obsolete("Use Current instead")] // Can be removed 20210406 - public Bindable Bindable - { - get => Current; - set => Current = value; - } - public virtual Bindable Current { get => controlWithCurrent.Current; diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 3478f18a40..3b39b74e00 100644 --- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -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; diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index a25dc3e6db..5780fe39fa 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -16,11 +16,6 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { - /// - /// The length of each strain section. - /// - 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); - } - - 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); } diff --git a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs index ebbffb5143..5edfb2207b 100644 --- a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs +++ b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs @@ -21,10 +21,20 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing public readonly HitObject LastObject; /// - /// Amount of time elapsed between and . + /// Amount of time elapsed between and , adjusted by clockrate. /// public readonly double DeltaTime; + /// + /// Clockrate adjusted start time of . + /// + public readonly double StartTime; + + /// + /// Clockrate adjusted end time of . + /// + public readonly double EndTime; + /// /// Creates a new . /// @@ -36,6 +46,8 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing BaseObject = hitObject; LastObject = lastObject; DeltaTime = (hitObject.StartTime - lastObject.StartTime) / clockRate; + StartTime = hitObject.StartTime / clockRate; + EndTime = hitObject.GetEndTime() / clockRate; } } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 95117be073..9f0fb987a7 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -1,9 +1,7 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using 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 { /// - /// Used to processes strain values of 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. /// public abstract class Skill { - /// - /// The peak strain for each section of the beatmap. - /// - public IReadOnlyList StrainPeaks => strainPeaks; - - /// - /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other. - /// - protected abstract double SkillMultiplier { get; } - - /// - /// 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. - /// - protected abstract double StrainDecayBase { get; } - - /// - /// The weight by which each strain value decays. - /// - protected virtual double DecayWeight => 0.9; - /// /// s that were processed previously. They can affect the strain values of the following objects. /// - protected readonly LimitedCapacityStack Previous = new LimitedCapacityStack(2); // Contained objects not used yet + protected readonly ReverseQueue Previous; /// - /// The current strain level. + /// Number of previous s to keep inside the queue. /// - protected double CurrentStrain { get; private set; } = 1; + protected virtual int HistoryLength => 1; /// /// Mods for use in skill calculations. /// protected IReadOnlyList Mods => mods; - private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section. - - private readonly List strainPeaks = new List(); - private readonly Mod[] mods; protected Skill(Mod[] mods) { this.mods = mods; + Previous = new ReverseQueue(HistoryLength + 1); } - /// - /// Process a and update current strain values accordingly. - /// - 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); } /// - /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty. + /// Process a . /// - public void SaveCurrentPeak() - { - if (Previous.Count > 0) - strainPeaks.Add(currentSectionPeak); - } + /// The to process. + protected abstract void Process(DifficultyHitObject current); /// - /// Sets the initial strain level for a new section. + /// Returns the calculated difficulty value representing all s that have been processed up to this point. /// - /// The beginning of the new section in milliseconds. - 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); - } - - /// - /// Retrieves the peak strain at a point in time. - /// - /// The time to retrieve the peak strain at. - /// The peak strain. - protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].BaseObject.StartTime); - - /// - /// Returns the calculated difficulty value representing all processed s. - /// - 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; - } - - /// - /// Calculates the strain value of a . This value is affected by previously processed objects. - /// - protected abstract double StrainValueOf(DifficultyHitObject current); - - private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); + public abstract double DifficultyValue(); } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs new file mode 100644 index 0000000000..71cee36812 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -0,0 +1,135 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Difficulty.Skills +{ + /// + /// Used to processes strain values of 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. + /// + public abstract class StrainSkill : Skill + { + /// + /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other. + /// + protected abstract double SkillMultiplier { get; } + + /// + /// 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. + /// + protected abstract double StrainDecayBase { get; } + + /// + /// The weight by which each strain value decays. + /// + protected virtual double DecayWeight => 0.9; + + /// + /// The current strain level. + /// + protected double CurrentStrain { get; private set; } = 1; + + /// + /// The length of each strain section. + /// + 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 strainPeaks = new List(); + + protected StrainSkill(Mod[] mods) + : base(mods) + { + } + + /// + /// Process a and update current strain values accordingly. + /// + 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); + } + + /// + /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty. + /// + private void saveCurrentPeak() + { + strainPeaks.Add(currentSectionPeak); + } + + /// + /// Sets the initial strain level for a new section. + /// + /// The beginning of the new section in milliseconds. + 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); + } + + /// + /// Retrieves the peak strain at a point in time. + /// + /// The time to retrieve the peak strain at. + /// The peak strain. + protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime); + + /// + /// Returns a live enumerable of the peak strains for each section of the beatmap, + /// including the peak of the current section. + /// + public IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak); + + /// + /// Returns the calculated difficulty value representing all s that have been processed up to this point. + /// + 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; + } + + /// + /// Calculates the strain value of a . This value is affected by previously processed objects. + /// + protected abstract double StrainValueOf(DifficultyHitObject current); + + private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); + } +} diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs deleted file mode 100644 index 1fc5abce90..0000000000 --- a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Difficulty.Utils -{ - /// - /// An indexed stack with limited depth. Indexing starts at the top of the stack. - /// - public class LimitedCapacityStack : IEnumerable - { - /// - /// The number of elements in the stack. - /// - 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. - - /// - /// Constructs a new . - /// - /// The number of items the stack can hold. - 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. - } - - /// - /// Retrieves the item at an index in the stack. - /// - /// The index of the item to retrieve. The top of the stack is returned at index 0. - 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]; - } - } - - /// - /// Pushes an item to this . - /// - /// The item to push. - 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; - } - - /// - /// Returns an enumerator which enumerates items in the history starting from the most recently added one. - /// - public IEnumerator 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(); - } -} diff --git a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs new file mode 100644 index 0000000000..57db9df3ca --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + /// + /// 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. + /// + public class ReverseQueue : IEnumerable + { + /// + /// The number of elements in the . + /// + 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; + } + + /// + /// Retrieves the item at an index in the . + /// + /// The index of the item to retrieve. The most recently enqueued item is at index 0. + 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]; + } + } + + /// + /// Enqueues an item to this . + /// + /// The item to enqueue. + 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++; + } + + /// + /// Dequeues the least recently enqueued item from the and returns it. + /// + /// The item dequeued from the . + public T Dequeue() + { + var item = items[start]; + start = (start + 1) % capacity; + Count--; + return item; + } + + /// + /// Clears the of all items. + /// + public void Clear() + { + start = 0; + Count = 0; + } + + /// + /// Returns an enumerator which enumerates items in the starting from the most recently enqueued item. + /// + public IEnumerator GetEnumerator() => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private ReverseQueue reverseQueue; + private int currentIndex; + + internal Enumerator(ReverseQueue 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; + } + } + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index e927951d0a..736fc47dee 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -438,6 +438,8 @@ namespace osu.Game.Rulesets.Edit /// public abstract bool CursorInPlacementArea { get; } + public virtual string ConvertSelectionToString() => string.Empty; + #region IPositionSnapProvider public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 89a3a2b855..be69db5ca8 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -28,18 +28,6 @@ namespace osu.Game.Rulesets.Judgements /// protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05; - /// - /// Whether this should affect the current combo. - /// - [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; - - /// - /// Whether this should be counted as base (combo) or bonus score. - /// - [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; - /// /// The maximum that can be achieved. /// @@ -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; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e5eaf5db88..6da9f12b50 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -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( diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 826d411822..db02eafa92 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -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() { } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 8419dd66de..e8a5463cce 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -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); diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 3083fcfccb..61f5f94142 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -156,6 +156,39 @@ namespace osu.Game.Rulesets.Objects return interpolateVertices(indexOfDistance(d), d); } + /// + /// Returns the control points belonging to the same segment as the one given. + /// The first point has a PathType which all other points inherit. + /// + /// One of the control points in the segment. + /// + public List PointsInSegment(PathControlPoint controlPoint) + { + bool found = false; + List pointsInCurrentSegment = new List(); + + 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(); diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index deabea57ef..4261ee3d47 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -173,7 +173,7 @@ namespace osu.Game.Rulesets { var filename = Path.GetFileNameWithoutExtension(file); - if (loadedAssemblies.Values.Any(t => t.Namespace == filename)) + if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename)) return; try diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b81fa79345..0fb5c2f4b5 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -346,12 +346,6 @@ namespace osu.Game.Rulesets.Scoring score.HitEvents = hitEvents; } - - /// - /// Create a for this processor. - /// - [Obsolete("Method is now unused.")] // Can be removed 20210328 - public virtual HitWindows CreateHitWindows() => new HitWindows(); } public enum ScoringMode diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 79f457c050..5ab557804e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; +using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -19,6 +20,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Components.TernaryButtons; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { @@ -70,6 +72,50 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.ControlPressed) + { + switch (e.Key) + { + case Key.Left: + moveSelection(new Vector2(-1, 0)); + return true; + + case Key.Right: + moveSelection(new Vector2(1, 0)); + return true; + + case Key.Up: + moveSelection(new Vector2(0, -1)); + return true; + + case Key.Down: + moveSelection(new Vector2(0, 1)); + return true; + } + } + + return false; + } + + /// + /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). + /// + /// + private void moveSelection(Vector2 delta) + { + var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(); + + if (firstBlueprint == null) + return; + + // convert to game space coordinates + delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); + + SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, firstBlueprint.ScreenSpaceSelectionPoint + delta)); + } + private void updatePlacementNewCombo() { if (currentPlacement?.HitObject is IHasComboInformation c) diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 9e95fe4fa1..d612cf3fe0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -71,7 +71,13 @@ namespace osu.Game.Screens.Edit.Compose.Components // Put earlier blueprints towards the end of the list, so they handle input first int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); - return i == 0 ? CompareReverseChildID(x, y) : i; + + if (i != 0) return i; + + // Fall back to end time if the start time is equal. + i = yObj.HitObject.GetEndTime().CompareTo(xObj.HitObject.GetEndTime()); + + return i == 0 ? CompareReverseChildID(y, x) : i; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 0697dbb392..86a30b7e2d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -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, + }, } }, }); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 1fc529910b..be34c8d57e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -2,9 +2,13 @@ // 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.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; @@ -14,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 @@ -33,7 +38,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable 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) @@ -43,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 }); } @@ -121,14 +136,55 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } base.Update(); + + updateStacking(); + } + + private void updateStacking() + { + // because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update. + + const int stack_offset = 5; + + // after the stack gets this tall, we can presume there is space underneath to draw subsequent blueprints. + const int stack_reset_count = 3; + + Stack currentConcurrentObjects = new Stack(); + + foreach (var b in SelectionBlueprints.Reverse()) + { + // remove objects from the stack as long as their end time is in the past. + while (currentConcurrentObjects.TryPeek(out HitObject hitObject)) + { + if (Precision.AlmostBigger(hitObject.GetEndTime(), b.HitObject.StartTime, 1)) + break; + + currentConcurrentObjects.Pop(); + } + + // if the stack gets too high, we should have space below it to display the next batch of objects. + // importantly, we only do this if time has incremented, else a stack of hitobjects all at the same time value would start to overlap themselves. + if (currentConcurrentObjects.TryPeek(out HitObject h) && !Precision.AlmostEquals(h.StartTime, b.HitObject.StartTime, 1)) + { + if (currentConcurrentObjects.Count >= stack_reset_count) + currentConcurrentObjects.Clear(); + } + + b.Y = -(stack_offset * currentConcurrentObjects.Count); + + currentConcurrentObjects.Push(b.HitObject); + } } protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler(); - protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => new TimelineHitObjectBlueprint(hitObject) + protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) { - OnDragHandled = handleScrollViaDrag - }; + return new TimelineHitObjectBlueprint(hitObject) + { + OnDragHandled = handleScrollViaDrag + }; + } protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect); @@ -152,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 @@ -203,7 +286,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Box.X = Math.Min(rescaledStart, rescaledEnd); Box.Width = Math.Abs(rescaledStart - rescaledEnd); - PerformSelection?.Invoke(Box.ScreenSpaceDrawQuad.AABBFloat); + var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat; + + // we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment. + boxScreenRect.Y -= boxScreenRect.Height; + boxScreenRect.Height *= 2; + + PerformSelection?.Invoke(boxScreenRect); } public override void Hide() diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 81b1195a40..61056aeced 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -2,11 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -14,11 +19,17 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Edit.Compose { - public class ComposeScreen : EditorScreenWithTimeline + public class ComposeScreen : EditorScreenWithTimeline, IKeyBindingHandler { [Resolved] private IBindable beatmap { get; set; } + [Resolved] + private GameHost host { get; set; } + + [Resolved] + private EditorClock clock { get; set; } + private HitObjectComposer composer; public ComposeScreen() @@ -72,5 +83,34 @@ namespace osu.Game.Screens.Edit.Compose // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content)); } + + #region Input Handling + + public bool OnPressed(PlatformAction action) + { + if (action.ActionType == PlatformActionType.Copy) + host.GetClipboard().SetText(formatSelectionAsString()); + + return false; + } + + public void OnReleased(PlatformAction action) + { + } + + private string formatSelectionAsString() + { + if (composer == null) + return string.Empty; + + double displayTime = EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).FirstOrDefault()?.StartTime ?? clock.CurrentTime; + string selectionAsString = composer.ConvertSelectionToString(); + + return !string.IsNullOrEmpty(selectionAsString) + ? $"{displayTime.ToEditorFormattedString()} ({selectionAsString}) - " + : $"{displayTime.ToEditorFormattedString()} - "; + } + + #endregion } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 0c24eb6a4d..0759e21382 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -16,7 +16,6 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -103,7 +102,7 @@ namespace osu.Game.Screens.Edit private MusicController music { get; set; } [BackgroundDependencyLoader] - private void load(OsuColour colours, GameHost host, OsuConfigManager config) + private void load(OsuColour colours, OsuConfigManager config) { var loadableBeatmap = Beatmap.Value; @@ -123,22 +122,6 @@ namespace osu.Game.Screens.Edit return; } - beatDivisor.Value = loadableBeatmap.BeatmapInfo.BeatDivisor; - beatDivisor.BindValueChanged(divisor => loadableBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); - - // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - clock = new EditorClock(loadableBeatmap, beatDivisor) { IsCoupled = false }; - - UpdateClockSource(); - - dependencies.CacheAs(clock); - AddInternal(clock); - - clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState()); - - // todo: remove caching of this and consume via editorBeatmap? - dependencies.Cache(beatDivisor); - try { playableBeatmap = loadableBeatmap.GetPlayableBeatmap(loadableBeatmap.BeatmapInfo.Ruleset); @@ -155,6 +138,22 @@ namespace osu.Game.Screens.Edit return; } + beatDivisor.Value = playableBeatmap.BeatmapInfo.BeatDivisor; + beatDivisor.BindValueChanged(divisor => playableBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); + + // Todo: should probably be done at a DrawableRuleset level to share logic with Player. + clock = new EditorClock(playableBeatmap, beatDivisor) { IsCoupled = false }; + + UpdateClockSource(); + + dependencies.CacheAs(clock); + AddInternal(clock); + + clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState()); + + // todo: remove caching of this and consume via editorBeatmap? + dependencies.Cache(beatDivisor); + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.Skin)); dependencies.CacheAs(editorBeatmap); changeHandler = new EditorChangeHandler(editorBeatmap); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index d0197ce1ec..772f6ea192 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -42,12 +42,12 @@ namespace osu.Game.Screens.Edit /// public bool IsSeeking { get; private set; } - public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) - : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) + public EditorClock(IBeatmap beatmap, BindableBeatDivisor beatDivisor) + : this(beatmap.ControlPointInfo, beatDivisor) { } - public EditorClock(ControlPointInfo controlPointInfo, double trackLength, BindableBeatDivisor beatDivisor) + public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor) { this.beatDivisor = beatDivisor; @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Edit } public EditorClock() - : this(new ControlPointInfo(), 1000, new BindableBeatDivisor()) + : this(new ControlPointInfo(), new BindableBeatDivisor()) { } diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 36fb0191b0..493d3ed20c 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -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 approachRateSlider; private LabelledSliderBar overallDifficultySlider; + public override LocalisableString Title => "Difficulty"; + [BackgroundDependencyLoader] private void load() { Children = new Drawable[] { - new OsuSpriteText - { - Text = "Difficulty settings" - }, circleSizeSlider = new LabelledSliderBar { Label = "Object Size", diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index 6e2737256a..a33a70af65 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -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 /// /// A labelled textbox which reveals an inline file chooser when clicked. /// - internal class FileChooserLabelledTextBox : LabelledTextBox + internal class FileChooserLabelledTextBox : LabelledTextBox, ICanAcceptFiles { + private readonly string[] handledExtensions; + public IEnumerable HandledExtensions => handledExtensions; + + /// + /// The target container to display the file chooser in. + /// public Container Target; - private readonly IBindable currentFile = new Bindable(); + private readonly Bindable currentFile = new Bindable(); + + [Resolved] + private OsuGameBase game { get; set; } [Resolved] private SectionsContainer sectionsContainer { get; set; } - public FileChooserLabelledTextBox() + public FileChooserLabelledTextBox(params string[] handledExtensions) { - currentFile.BindValueChanged(onFileSelected); - } - - private void onFileSelected(ValueChangedEvent 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(validFileExtensions: 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 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; diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index f429164ece..889a5eab5e 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -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", diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 1b841775e2..12270f2aa4 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -1,40 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -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 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,69 +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", - Current = { Value = working.Value.Metadata.AudioFile ?? "Click to select a track" }, - 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); @@ -133,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); @@ -173,45 +149,16 @@ namespace osu.Game.Screens.Edit.Setup return true; } + private void backgroundChanged(ValueChangedEvent filePath) + { + if (!ChangeBackgroundImage(filePath.NewValue)) + backgroundTextBox.Current.Value = filePath.OldValue; + } + private void audioTrackChanged(ValueChangedEvent 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); - }); - } } } diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 1c3cbb7206..70671b487c 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -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 sections = new SectionsContainer(); + + [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 + sections = new SectionsContainer { - 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"; - } - } - } } diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs new file mode 100644 index 0000000000..06aad69afa --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.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 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 + { + 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 + { + 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 CreateTabItem(SetupSection value) => new SetupScreenTabItem(value) + { + AccentColour = AccentColour + }; + + private class SetupScreenTabItem : OverlayTabItem + { + public SetupScreenTabItem(SetupSection value) + : base(value) + { + Text.Text = value.Title; + } + } + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs new file mode 100644 index 0000000000..7304323004 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Screens.Edit.Setup +{ + public class SetupScreenHeaderBackground : CompositeDrawable + { + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + private IBindable working { get; set; } + + private readonly Container content; + + public SetupScreenHeaderBackground() + { + InternalChild = content = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true + }; + } + + [BackgroundDependencyLoader] + private void load() + { + UpdateBackground(); + } + + public void UpdateBackground() + { + LoadComponentAsync(new BeatmapBackgroundSprite(working.Value) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + }, background => + { + if (background.Texture != null) + content.Child = background; + else + { + content.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.Both + } + }; + } + + background.FadeInFromZero(500); + }); + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/SetupSection.cs b/osu.Game/Screens/Edit/Setup/SetupSection.cs index 88521a8fb0..de62c3a468 100644 --- a/osu.Game/Screens/Edit/Setup/SetupSection.cs +++ b/osu.Game/Screens/Edit/Setup/SetupSection.cs @@ -4,12 +4,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osuTK; namespace osu.Game.Screens.Edit.Setup { - internal class SetupSection : Container + internal abstract class SetupSection : Container { private readonly FillFlowContainer flow; @@ -21,19 +23,40 @@ namespace osu.Game.Screens.Edit.Setup protected override Container Content => flow; - public SetupSection() + public abstract LocalisableString Title { get; } + + protected SetupSection() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Padding = new MarginPadding(10); + Padding = new MarginPadding + { + Vertical = 10, + Horizontal = SetupScreen.HORIZONTAL_PADDING + }; - InternalChild = flow = new FillFlowContainer + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(20), Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = Title + }, + flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Direction = FillDirection.Vertical, + } + } }; } } diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs index 9d80ca0b14..97d110c502 100644 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs @@ -28,7 +28,16 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable; + var selectedPointBindable = point.NewValue.SpeedMultiplierBindable; + + // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint). + // generally that level of precision could only be set by externally editing the .osu file, so at the point + // a user is looking to update this within the editor it should be safe to obliterate this additional precision. + double expectedPrecision = new DifficultyControlPoint().SpeedMultiplierBindable.Precision; + if (selectedPointBindable.Precision < expectedPrecision) + selectedPointBindable.Precision = expectedPrecision; + + multiplierSlider.Current = selectedPointBindable; multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 6eb675976a..8f85608b29 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private IBindable availability; [BackgroundDependencyLoader] - private void load(OnlinePlayBeatmapAvailablilityTracker beatmapTracker) + private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker) { availability = beatmapTracker.Availability.GetBoundCopy(); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4a689314db..706da05d15 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -56,15 +56,15 @@ namespace osu.Game.Screens.OnlinePlay.Match private IBindable> managerUpdated; [Cached] - protected OnlinePlayBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; } + protected OnlinePlayBeatmapAvailabilityTracker BeatmapAvailabilityTracker { get; } - protected IBindable BeatmapAvailability => BeatmapAvailablilityTracker.Availability; + protected IBindable BeatmapAvailability => BeatmapAvailabilityTracker.Availability; protected RoomSubScreen() { AddRangeInternal(new Drawable[] { - BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker + BeatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = SelectedItem } }, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index fdc1ae9d3c..d4f5428bfb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -8,21 +8,28 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerMatchFooter : CompositeDrawable { public const float HEIGHT = 50; + private const float ready_button_width = 600; + private const float spectate_button_width = 200; public Action OnReadyClick { set => readyButton.OnReadyClick = value; } + public Action OnSpectateClick + { + set => spectateButton.OnSpectateClick = value; + } + private readonly Drawable background; private readonly MultiplayerReadyButton readyButton; + private readonly MultiplayerSpectateButton spectateButton; public MultiplayerMatchFooter() { @@ -32,11 +39,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both }, - readyButton = new MultiplayerReadyButton + new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(600, 50), + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + null, + spectateButton = new MultiplayerSpectateButton + { + RelativeSizeAxes = Axes.Both, + }, + null, + readyButton = new MultiplayerReadyButton + { + RelativeSizeAxes = Axes.Both, + }, + null + } + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(maxSize: spectate_button_width), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(maxSize: ready_button_width), + new Dimension() + } } }; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index c9fb234ccc..f2dd9a6f25 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -78,8 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(Room != null); int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); + int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - string countText = $"({newCountReady} / {Room.Users.Count} ready)"; + string countText = $"({newCountReady} / {newCountTotal} ready)"; switch (localUser.State) { @@ -88,6 +89,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match updateButtonColour(true); break; + case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: if (Room?.Host?.Equals(localUser) == true) { @@ -103,7 +105,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match break; } - button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; + bool enableButton = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; + + // When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. + if (localUser.State == MultiplayerUserState.Spectating) + enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0; + + button.Enabled.Value = enableButton; if (newCountReady != countReady) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs new file mode 100644 index 0000000000..4b3fb5d00f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class MultiplayerSpectateButton : MultiplayerRoomComposite + { + public Action OnSpectateClick + { + set => button.Action = value; + } + + [Resolved] + private OngoingOperationTracker ongoingOperationTracker { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + private IBindable operationInProgress; + + private readonly ButtonWithTrianglesExposed button; + + public MultiplayerSpectateButton() + { + InternalChild = button = new ButtonWithTrianglesExposed + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Enabled = { Value = true }, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); + operationInProgress.BindValueChanged(_ => updateState()); + } + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + updateState(); + } + + private void updateState() + { + var localUser = Client.LocalUser; + + if (localUser == null) + return; + + Debug.Assert(Room != null); + + switch (localUser.State) + { + default: + button.Text = "Spectate"; + button.BackgroundColour = colours.BlueDark; + button.Triangles.ColourDark = colours.BlueDarker; + button.Triangles.ColourLight = colours.Blue; + break; + + case MultiplayerUserState.Spectating: + button.Text = "Stop spectating"; + button.BackgroundColour = colours.Gray4; + button.Triangles.ColourDark = colours.Gray5; + button.Triangles.ColourLight = colours.Gray6; + break; + } + + button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; + } + + private class ButtonWithTrianglesExposed : TriangleButton + { + public new Triangles Triangles => base.Triangles; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ceeee67806..90cef0107c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -221,7 +221,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { new MultiplayerMatchFooter { - OnReadyClick = onReadyClick + OnReadyClick = onReadyClick, + OnSpectateClick = onSpectateClick } } }, @@ -363,7 +364,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(readyClickOperation == null); readyClickOperation = ongoingOperationTracker.BeginOperation(); - if (client.IsHost && client.LocalUser?.State == MultiplayerUserState.Ready) + if (client.IsHost && (client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating)) { client.StartMatch() .ContinueWith(t => @@ -390,6 +391,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } + private void onSpectateClick() + { + Debug.Assert(readyClickOperation == null); + readyClickOperation = ongoingOperationTracker.BeginOperation(); + + client.ToggleSpectate().ContinueWith(t => endOperation()); + + void endOperation() + { + readyClickOperation?.Dispose(); + readyClickOperation = null; + } + } + private void onRoomUpdated() { // user mods may have changed. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index c571b51c83..2616b07c1f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs @@ -135,6 +135,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants icon.Colour = colours.BlueLighter; break; + case MultiplayerUserState.Spectating: + text.Text = "spectating"; + icon.Icon = FontAwesome.Solid.Binoculars; + icon.Colour = colours.BlueLight; + break; + default: throw new ArgumentOutOfRangeException(nameof(state), state, null); } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 7d906cdc5b..679b3c7313 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -309,10 +309,8 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; - var restartCount = player?.RestartCount + 1 ?? 0; - player = createPlayer(); - player.RestartCount = restartCount; + player.RestartCount = restartCount++; player.RestartRequested = restartRequested; LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false); @@ -428,6 +426,8 @@ namespace osu.Game.Screens.Play private Bindable muteWarningShownOnce; + private int restartCount; + private void showMuteWarningIfNeeded() { if (!muteWarningShownOnce.Value) diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index 8585a5c309..30ca15c311 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,6 +18,8 @@ namespace osu.Game.Screens.Play private readonly GameplayClockContainer gameplayClockContainer; private Bindable isPaused; + private readonly Bindable disableSuspensionBindable = new Bindable(); + [Resolved] private GameHost host { get; set; } @@ -31,12 +32,14 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - // This is the only usage game-wide of suspension changes. - // Assert to ensure we don't accidentally forget this in the future. - Debug.Assert(host.AllowScreenSuspension.Value); - isPaused = gameplayClockContainer.IsPaused.GetBoundCopy(); - isPaused.BindValueChanged(paused => host.AllowScreenSuspension.Value = paused.NewValue, true); + isPaused.BindValueChanged(paused => + { + if (paused.NewValue) + host.AllowScreenSuspension.RemoveSource(disableSuspensionBindable); + else + host.AllowScreenSuspension.AddSource(disableSuspensionBindable); + }, true); } protected override void Dispose(bool isDisposing) @@ -44,9 +47,7 @@ namespace osu.Game.Screens.Play base.Dispose(isDisposing); isPaused?.UnbindAll(); - - if (host != null) - host.AllowScreenSuspension.Value = true; + host?.AllowScreenSuspension.RemoveSource(disableSuspensionBindable); } } } diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index ee1ccdc5b3..d0ef4131dc 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -17,7 +17,10 @@ namespace osu.Game.Screens.Play if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId)) return null; - return new CreateSoloScoreRequest(beatmapId, Game.VersionHash); + if (!(Ruleset.Value.ID is int rulesetId)) + return null; + + return new CreateSoloScoreRequest(beatmapId, rulesetId, Game.VersionHash); } protected override bool HandleTokenRetrievalFailure(Exception exception) => false; diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs similarity index 52% rename from osu.Game/Screens/Play/Spectator.cs rename to osu.Game/Screens/Play/SoloSpectator.cs index 28311f5113..820d776e63 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -1,18 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using JetBrains.Annotations; 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.Graphics.Sprites; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -24,73 +21,49 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.Settings; -using osu.Game.Replays; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Replays.Types; -using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.Spectate; using osu.Game.Users; using osuTK; namespace osu.Game.Screens.Play { [Cached(typeof(IPreviewTrackOwner))] - public class Spectator : OsuScreen, IPreviewTrackOwner + public class SoloSpectator : SpectatorScreen, IPreviewTrackOwner { + [NotNull] private readonly User targetUser; - [Resolved] - private Bindable beatmap { get; set; } - - [Resolved] - private Bindable ruleset { get; set; } - - private Ruleset rulesetInstance; - - [Resolved] - private Bindable> mods { get; set; } - [Resolved] private IAPIProvider api { get; set; } [Resolved] - private SpectatorStreamingClient spectatorStreaming { get; set; } - - [Resolved] - private BeatmapManager beatmaps { get; set; } + private PreviewTrackManager previewTrackManager { get; set; } [Resolved] private RulesetStore rulesets { get; set; } [Resolved] - private PreviewTrackManager previewTrackManager { get; set; } - - private Score score; - - private readonly object scoreLock = new object(); + private BeatmapManager beatmaps { get; set; } private Container beatmapPanelContainer; - - private SpectatorState state; - - private IBindable> managerUpdated; - private TriangleButton watchButton; - private SettingsCheckbox automaticDownload; - private BeatmapSetInfo onlineBeatmap; /// - /// Becomes true if a new state is waiting to be loaded (while this screen was not active). + /// The player's immediate online gameplay state. + /// This doesn't always reflect the gameplay state being watched. /// - private bool newStatePending; + private GameplayState immediateGameplayState; - public Spectator([NotNull] User targetUser) + private GetBeatmapSetRequest onlineBeatmapRequest; + + public SoloSpectator([NotNull] User targetUser) + : base(targetUser.Id) { - this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); + this.targetUser = targetUser; } [BackgroundDependencyLoader] @@ -173,7 +146,7 @@ namespace osu.Game.Screens.Play Width = 250, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = attemptStart, + Action = () => scheduleStart(immediateGameplayState), Enabled = { Value = false } } } @@ -185,169 +158,76 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { base.LoadComplete(); - - spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; - spectatorStreaming.OnUserFinishedPlaying += userFinishedPlaying; - spectatorStreaming.OnNewFrames += userSentFrames; - - spectatorStreaming.WatchUser(targetUser.Id); - - managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); - managerUpdated.BindValueChanged(beatmapUpdated); - automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - private void beatmapUpdated(ValueChangedEvent> beatmap) + protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) { - if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) - Schedule(attemptStart); + clearDisplay(); + showBeatmapPanel(spectatorState); } - private void userSentFrames(int userId, FrameDataBundle data) + protected override void StartGameplay(int userId, GameplayState gameplayState) { - // this is not scheduled as it handles propagation of frames even when in a child screen (at which point we are not alive). - // probably not the safest way to handle this. + immediateGameplayState = gameplayState; + watchButton.Enabled.Value = true; - if (userId != targetUser.Id) - return; - - lock (scoreLock) - { - // this should never happen as the server sends the user's state on watching, - // but is here as a safety measure. - if (score == null) - return; - - // rulesetInstance should be guaranteed to be in sync with the score via scoreLock. - Debug.Assert(rulesetInstance != null && rulesetInstance.RulesetInfo.Equals(score.ScoreInfo.Ruleset)); - - foreach (var frame in data.Frames) - { - IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap); - - var convertedFrame = (ReplayFrame)convertibleFrame; - convertedFrame.Time = frame.Time; - - score.Replay.Frames.Add(convertedFrame); - } - } + scheduleStart(gameplayState); } - private void userBeganPlaying(int userId, SpectatorState state) + protected override void EndGameplay(int userId) { - if (userId != targetUser.Id) - return; + scheduledStart?.Cancel(); + immediateGameplayState = null; + watchButton.Enabled.Value = false; - this.state = state; - - if (this.IsCurrentScreen()) - Schedule(attemptStart); - else - newStatePending = true; - } - - public override void OnResuming(IScreen last) - { - base.OnResuming(last); - - if (newStatePending) - { - attemptStart(); - newStatePending = false; - } - } - - private void userFinishedPlaying(int userId, SpectatorState state) - { - if (userId != targetUser.Id) - return; - - lock (scoreLock) - { - if (score != null) - { - score.Replay.HasReceivedAllFrames = true; - score = null; - } - } - - Schedule(clearDisplay); + clearDisplay(); } private void clearDisplay() { watchButton.Enabled.Value = false; + onlineBeatmapRequest?.Cancel(); beatmapPanelContainer.Clear(); previewTrackManager.StopAnyPlaying(this); } - private void attemptStart() + private ScheduledDelegate scheduledStart; + + private void scheduleStart(GameplayState gameplayState) { - clearDisplay(); - showBeatmapPanel(state); - - var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); - - // ruleset not available - if (resolvedRuleset == null) - return; - - if (state.BeatmapID == null) - return; - - var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID); - - if (resolvedBeatmap == null) + // This function may be called multiple times in quick succession once the screen becomes current again. + scheduledStart?.Cancel(); + scheduledStart = Schedule(() => { - return; - } + if (this.IsCurrentScreen()) + start(); + else + scheduleStart(gameplayState); + }); - lock (scoreLock) + void start() { - score = new Score - { - ScoreInfo = new ScoreInfo - { - Beatmap = resolvedBeatmap, - User = targetUser, - Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), - Ruleset = resolvedRuleset.RulesetInfo, - }, - Replay = new Replay { HasReceivedAllFrames = false }, - }; + Beatmap.Value = gameplayState.Beatmap; + Ruleset.Value = gameplayState.Ruleset.RulesetInfo; - ruleset.Value = resolvedRuleset.RulesetInfo; - rulesetInstance = resolvedRuleset; - - beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); - watchButton.Enabled.Value = true; - - this.Push(new SpectatorPlayerLoader(score)); + this.Push(new SpectatorPlayerLoader(gameplayState.Score)); } } private void showBeatmapPanel(SpectatorState state) { - if (state?.BeatmapID == null) - { - onlineBeatmap = null; - return; - } + Debug.Assert(state.BeatmapID != null); - var req = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); - req.Success += res => Schedule(() => + onlineBeatmapRequest = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); + onlineBeatmapRequest.Success += res => Schedule(() => { - if (state != this.state) - return; - onlineBeatmap = res.ToBeatmapSet(rulesets); beatmapPanelContainer.Child = new GridBeatmapPanel(onlineBeatmap); checkForAutomaticDownload(); }); - api.Queue(req); + api.Queue(onlineBeatmapRequest); } private void checkForAutomaticDownload() @@ -369,21 +249,5 @@ namespace osu.Game.Screens.Play previewTrackManager.StopAnyPlaying(this); return base.OnExiting(next); } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (spectatorStreaming != null) - { - spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; - spectatorStreaming.OnUserFinishedPlaying -= userFinishedPlaying; - spectatorStreaming.OnNewFrames -= userSentFrames; - - spectatorStreaming.StopWatchingUser(targetUser.Id); - } - - managerUpdated?.UnbindAll(); - } } } diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 8a1c291fca..26da4279f0 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using System.Linq; using osu.Game.Online.API; -using osu.Framework.Threading; using osu.Framework.Graphics.Shapes; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Screens.Select.Details; @@ -19,6 +18,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; +using osu.Game.Online; namespace osu.Game.Screens.Select { @@ -27,11 +27,8 @@ namespace osu.Game.Screens.Select private const float spacing = 10; private const float transition_duration = 250; - private readonly FillFlowContainer top, statsFlow; private readonly AdvancedStats advanced; - private readonly DetailBox ratingsContainer; private readonly UserRatings ratings; - private readonly OsuScrollContainer metadataScroll; private readonly MetadataSection description, source, tags; private readonly Container failRetryContainer; private readonly FailRetryGraph failRetryGraph; @@ -40,8 +37,6 @@ namespace osu.Game.Screens.Select [Resolved] private IAPIProvider api { get; set; } - private ScheduledDelegate pendingBeatmapSwitch; - [Resolved] private RulesetStore rulesets { get; set; } @@ -56,8 +51,7 @@ namespace osu.Game.Screens.Select beatmap = value; - pendingBeatmapSwitch?.Cancel(); - pendingBeatmapSwitch = Schedule(updateStatistics); + Scheduler.AddOnce(updateStatistics); } } @@ -76,99 +70,105 @@ namespace osu.Game.Screens.Select Padding = new MarginPadding { Horizontal = spacing }, Children = new Drawable[] { - top = new FillFlowContainer + new GridContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - statsFlow = new FillFlowContainer + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Width = 0.5f, - Spacing = new Vector2(spacing), - Padding = new MarginPadding { Right = spacing / 2 }, - Children = new[] - { - new DetailBox - { - Child = advanced = new AdvancedStats - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = spacing, Top = spacing * 2, Bottom = spacing }, - }, - }, - ratingsContainer = new DetailBox - { - Child = ratings = new UserRatings - { - RelativeSizeAxes = Axes.X, - Height = 134, - Padding = new MarginPadding { Horizontal = spacing, Top = spacing }, - }, - }, - }, - }, - metadataScroll = new OsuScrollContainer - { - RelativeSizeAxes = Axes.X, - Width = 0.5f, - ScrollbarVisible = false, - Padding = new MarginPadding { Left = spacing / 2 }, - Child = new FillFlowContainer + new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - LayoutDuration = transition_duration, - LayoutEasing = Easing.OutQuad, - Spacing = new Vector2(spacing * 2), - Margin = new MarginPadding { Top = spacing * 2 }, - Children = new[] + Direction = FillDirection.Horizontal, + Children = new Drawable[] { - description = new MetadataSection("Description"), - source = new MetadataSection("Source"), - tags = new MetadataSection("Tags"), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.5f, + Spacing = new Vector2(spacing), + Padding = new MarginPadding { Right = spacing / 2 }, + Children = new[] + { + new DetailBox().WithChild(advanced = new AdvancedStats + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = spacing, Top = spacing * 2, Bottom = spacing }, + }), + new DetailBox().WithChild(new OnlineViewContainer(string.Empty) + { + RelativeSizeAxes = Axes.X, + Height = 134, + Padding = new MarginPadding { Horizontal = spacing, Top = spacing }, + Child = ratings = new UserRatings + { + RelativeSizeAxes = Axes.Both, + }, + }), + }, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + ScrollbarVisible = false, + Padding = new MarginPadding { Left = spacing / 2 }, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + LayoutDuration = transition_duration, + LayoutEasing = Easing.OutQuad, + Spacing = new Vector2(spacing * 2), + Margin = new MarginPadding { Top = spacing * 2 }, + Children = new[] + { + description = new MetadataSection("Description"), + source = new MetadataSection("Source"), + tags = new MetadataSection("Tags"), + }, + }, + }, }, }, }, - }, - }, - failRetryContainer = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Children = new Drawable[] - { - new OsuSpriteText + new Drawable[] { - Text = "Points of Failure", - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14), - }, - failRetryGraph = new FailRetryGraph - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 14 + spacing / 2 }, - }, - }, + failRetryContainer = new OnlineViewContainer("Sign in to view more details") + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Points of Failure", + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14), + }, + failRetryGraph = new FailRetryGraph + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 14 + spacing / 2 }, + }, + }, + }, + } + } }, }, }, - loading = new LoadingLayer(true), + loading = new LoadingLayer(true) }; } - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - metadataScroll.Height = statsFlow.DrawHeight; - failRetryContainer.Height = DrawHeight - Padding.TotalVertical - (top.DrawHeight + spacing / 2); - } - private void updateStatistics() { advanced.Beatmap = Beatmap; @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Select } // for now, let's early abort if an OnlineBeatmapID is not present (should have been populated at import time). - if (Beatmap?.OnlineBeatmapID == null) + if (Beatmap?.OnlineBeatmapID == null || api.State.Value == APIState.Offline) { updateMetrics(); return; @@ -239,12 +239,13 @@ namespace osu.Game.Screens.Select if (hasRatings) { ratings.Metrics = beatmap.BeatmapSet.Metrics; - ratingsContainer.FadeIn(transition_duration); + ratings.FadeIn(transition_duration); } else { + // loading or just has no data server-side. ratings.Metrics = new BeatmapSetMetrics { Ratings = new int[10] }; - ratingsContainer.FadeTo(0.25f, transition_duration); + ratings.FadeTo(0.25f, transition_duration); } if (hasRetriesFails) @@ -259,7 +260,6 @@ namespace osu.Game.Screens.Select Fails = new int[100], Retries = new int[100], }; - failRetryContainer.FadeOut(transition_duration); } loading.Hide(); diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index 2fbf64de29..40ca3e0764 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Select.Carousel { if (sampleHover == null) return; - sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2); + sampleHover.Frequency.Value = 0.99 + RNG.NextDouble(0.02); sampleHover.Play(); } } diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index cd7c1c449f..afb3943a09 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -4,7 +4,6 @@ using System; using osuTK; using osuTK.Graphics; -using osuTK.Input; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -13,10 +12,13 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; +using osu.Framework.Input.Bindings; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Select { - public class FooterButton : OsuClickableContainer + public class FooterButton : OsuClickableContainer, IKeyBindingHandler { public const float SHEAR_WIDTH = 7.5f; @@ -64,6 +66,7 @@ namespace osu.Game.Screens.Select private readonly Box light; public FooterButton() + : base(HoverSampleSet.SongSelect) { AutoSizeAxes = Axes.Both; Shear = SHEAR; @@ -105,6 +108,7 @@ namespace osu.Game.Screens.Select AutoSizeAxes = Axes.Both, Child = SpriteText = new OsuSpriteText { + AlwaysPresent = true, Anchor = Anchor.Centre, Origin = Anchor.Centre, } @@ -118,7 +122,7 @@ namespace osu.Game.Screens.Select public Action Hovered; public Action HoverLost; - public Key? Hotkey; + public GlobalAction? Hotkey; protected override void UpdateAfterChildren() { @@ -168,15 +172,17 @@ namespace osu.Game.Screens.Select return base.OnClick(e); } - protected override bool OnKeyDown(KeyDownEvent e) + public virtual bool OnPressed(GlobalAction action) { - if (!e.Repeat && e.Key == Hotkey) + if (action == Hotkey) { Click(); return true; } - return base.OnKeyDown(e); + return false; } + + public virtual void OnReleased(GlobalAction action) { } } } diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 02333da0dc..b98b48a0c0 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; -using osuTK.Input; +using osu.Game.Input.Bindings; namespace osu.Game.Screens.Select { @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select lowMultiplierColour = colours.Red; highMultiplierColour = colours.Green; Text = @"mods"; - Hotkey = Key.F1; + Hotkey = GlobalAction.ToggleModSelection; } protected override void LoadComplete() diff --git a/osu.Game/Screens/Select/FooterButtonOptions.cs b/osu.Game/Screens/Select/FooterButtonOptions.cs index c000d8a8c8..e549656785 100644 --- a/osu.Game/Screens/Select/FooterButtonOptions.cs +++ b/osu.Game/Screens/Select/FooterButtonOptions.cs @@ -4,7 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics; -using osuTK.Input; +using osu.Game.Input.Bindings; namespace osu.Game.Screens.Select { @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select SelectedColour = colours.Blue; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"options"; - Hotkey = Key.F3; + Hotkey = GlobalAction.ToggleBeatmapOptions; } } } diff --git a/osu.Game/Screens/Select/FooterButtonRandom.cs b/osu.Game/Screens/Select/FooterButtonRandom.cs index a42e6721db..2d14111137 100644 --- a/osu.Game/Screens/Select/FooterButtonRandom.cs +++ b/osu.Game/Screens/Select/FooterButtonRandom.cs @@ -1,35 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osuTK.Input; +using osu.Game.Input.Bindings; +using osuTK; namespace osu.Game.Screens.Select { public class FooterButtonRandom : FooterButton { - private readonly SpriteText secondaryText; - private bool secondaryActive; + public Action NextRandom { get; set; } + public Action PreviousRandom { get; set; } - public FooterButtonRandom() - { - TextContainer.Add(secondaryText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = @"rewind", - Alpha = 0, - }); - - // force both text sprites to always be present to avoid width flickering while they're being swapped out - SpriteText.AlwaysPresent = secondaryText.AlwaysPresent = true; - } + private bool rewindSearch; [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -37,34 +25,57 @@ namespace osu.Game.Screens.Select SelectedColour = colours.Green; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"random"; - Hotkey = Key.F2; - } - protected override bool OnKeyDown(KeyDownEvent e) - { - secondaryActive = e.ShiftPressed; - updateText(); - return base.OnKeyDown(e); - } - - protected override void OnKeyUp(KeyUpEvent e) - { - secondaryActive = e.ShiftPressed; - updateText(); - base.OnKeyUp(e); - } - - private void updateText() - { - if (secondaryActive) + Action = () => { - SpriteText.FadeOut(120, Easing.InQuad); - secondaryText.FadeIn(120, Easing.InQuad); + if (rewindSearch) + { + const double fade_time = 500; + + OsuSpriteText rewindSpriteText; + + TextContainer.Add(rewindSpriteText = new OsuSpriteText + { + Alpha = 0, + Text = @"rewind", + AlwaysPresent = true, // make sure the button is sized large enough to always show this + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + rewindSpriteText.FadeOutFromOne(fade_time, Easing.In); + rewindSpriteText.MoveTo(Vector2.Zero).MoveTo(new Vector2(0, 10), fade_time, Easing.In); + rewindSpriteText.Expire(); + + SpriteText.FadeInFromZero(fade_time, Easing.In); + + PreviousRandom.Invoke(); + } + else + { + NextRandom.Invoke(); + } + }; + } + + public override bool OnPressed(GlobalAction action) + { + rewindSearch = action == GlobalAction.SelectPreviousRandom; + + if (action != GlobalAction.SelectNextRandom && action != GlobalAction.SelectPreviousRandom) + { + return false; } - else + + Click(); + return true; + } + + public override void OnReleased(GlobalAction action) + { + if (action == GlobalAction.SelectPreviousRandom) { - SpriteText.FadeIn(120, Easing.InQuad); - secondaryText.FadeOut(120, Easing.InQuad); + rewindSearch = false; } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b7f7c40539..215700d87c 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -307,7 +307,11 @@ namespace osu.Game.Screens.Select protected virtual IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() => new (FooterButton, OverlayContainer)[] { (new FooterButtonMods { Current = Mods }, ModSelect), - (new FooterButtonRandom { Action = triggerRandom }, null), + (new FooterButtonRandom + { + NextRandom = () => Carousel.SelectNextRandom(), + PreviousRandom = Carousel.SelectPreviousRandom + }, null), (new FooterButtonOptions(), BeatmapOptions) }; @@ -522,14 +526,6 @@ namespace osu.Game.Screens.Select } } - private void triggerRandom() - { - if (GetContainingInputManager().CurrentState.Keyboard.ShiftPressed) - Carousel.SelectPreviousRandom(); - else - Carousel.SelectNextRandom(); - } - public override void OnEntering(IScreen last) { base.OnEntering(last); diff --git a/osu.Game/Screens/Spectate/GameplayState.cs b/osu.Game/Screens/Spectate/GameplayState.cs new file mode 100644 index 0000000000..4579b9c07c --- /dev/null +++ b/osu.Game/Screens/Spectate/GameplayState.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Spectate +{ + /// + /// The gameplay state of a spectated user. This class is immutable. + /// + public class GameplayState + { + /// + /// The score which the user is playing. + /// + public readonly Score Score; + + /// + /// The ruleset which the user is playing. + /// + public readonly Ruleset Ruleset; + + /// + /// The beatmap which the user is playing. + /// + public readonly WorkingBeatmap Beatmap; + + public GameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap) + { + Score = score; + Ruleset = ruleset; + Beatmap = beatmap; + } + } +} diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs new file mode 100644 index 0000000000..6dd3144fc8 --- /dev/null +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -0,0 +1,238 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Spectator; +using osu.Game.Replays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Screens.Spectate +{ + /// + /// A which spectates one or more users. + /// + public abstract class SpectatorScreen : OsuScreen + { + private readonly int[] userIds; + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private SpectatorStreamingClient spectatorClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + // A lock is used to synchronise access to spectator/gameplay states, since this class is a screen which may become non-current and stop receiving updates at any point. + private readonly object stateLock = new object(); + + private readonly Dictionary userMap = new Dictionary(); + private readonly Dictionary spectatorStates = new Dictionary(); + private readonly Dictionary gameplayStates = new Dictionary(); + + private IBindable> managerUpdated; + + /// + /// Creates a new . + /// + /// The users to spectate. + protected SpectatorScreen(params int[] userIds) + { + this.userIds = userIds; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + spectatorClient.OnUserBeganPlaying += userBeganPlaying; + spectatorClient.OnUserFinishedPlaying += userFinishedPlaying; + spectatorClient.OnNewFrames += userSentFrames; + + foreach (var id in userIds) + { + userLookupCache.GetUserAsync(id).ContinueWith(u => Schedule(() => + { + if (u.Result == null) + return; + + lock (stateLock) + userMap[id] = u.Result; + + spectatorClient.WatchUser(id); + }), TaskContinuationOptions.OnlyOnRanToCompletion); + } + + managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); + } + + private void beatmapUpdated(ValueChangedEvent> e) + { + if (!e.NewValue.TryGetTarget(out var beatmapSet)) + return; + + lock (stateLock) + { + foreach (var (userId, state) in spectatorStates) + { + if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) + updateGameplayState(userId); + } + } + } + + private void userBeganPlaying(int userId, SpectatorState state) + { + if (state.RulesetID == null || state.BeatmapID == null) + return; + + lock (stateLock) + { + if (!userMap.ContainsKey(userId)) + return; + + spectatorStates[userId] = state; + Schedule(() => OnUserStateChanged(userId, state)); + + updateGameplayState(userId); + } + } + + private void updateGameplayState(int userId) + { + lock (stateLock) + { + Debug.Assert(userMap.ContainsKey(userId)); + + var spectatorState = spectatorStates[userId]; + var user = userMap[userId]; + + var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance(); + if (resolvedRuleset == null) + return; + + var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == spectatorState.BeatmapID); + if (resolvedBeatmap == null) + return; + + var score = new Score + { + ScoreInfo = new ScoreInfo + { + Beatmap = resolvedBeatmap, + User = user, + Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), + Ruleset = resolvedRuleset.RulesetInfo, + }, + Replay = new Replay { HasReceivedAllFrames = false }, + }; + + var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); + + gameplayStates[userId] = gameplayState; + Schedule(() => StartGameplay(userId, gameplayState)); + } + } + + private void userSentFrames(int userId, FrameDataBundle bundle) + { + lock (stateLock) + { + if (!userMap.ContainsKey(userId)) + return; + + if (!gameplayStates.TryGetValue(userId, out var gameplayState)) + return; + + // The ruleset instance should be guaranteed to be in sync with the score via ScoreLock. + Debug.Assert(gameplayState.Ruleset != null && gameplayState.Ruleset.RulesetInfo.Equals(gameplayState.Score.ScoreInfo.Ruleset)); + + foreach (var frame in bundle.Frames) + { + IConvertibleReplayFrame convertibleFrame = gameplayState.Ruleset.CreateConvertibleReplayFrame(); + convertibleFrame.FromLegacy(frame, gameplayState.Beatmap.Beatmap); + + var convertedFrame = (ReplayFrame)convertibleFrame; + convertedFrame.Time = frame.Time; + + gameplayState.Score.Replay.Frames.Add(convertedFrame); + } + } + } + + private void userFinishedPlaying(int userId, SpectatorState state) + { + lock (stateLock) + { + if (!userMap.ContainsKey(userId)) + return; + + if (!gameplayStates.TryGetValue(userId, out var gameplayState)) + return; + + gameplayState.Score.Replay.HasReceivedAllFrames = true; + + gameplayStates.Remove(userId); + Schedule(() => EndGameplay(userId)); + } + } + + /// + /// Invoked when a spectated user's state has changed. + /// + /// The user whose state has changed. + /// The new state. + protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState); + + /// + /// Starts gameplay for a user. + /// + /// The user to start gameplay for. + /// The gameplay state. + protected abstract void StartGameplay(int userId, [NotNull] GameplayState gameplayState); + + /// + /// Ends gameplay for a user. + /// + /// The user to end gameplay for. + protected abstract void EndGameplay(int userId); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorClient != null) + { + spectatorClient.OnUserBeganPlaying -= userBeganPlaying; + spectatorClient.OnUserFinishedPlaying -= userFinishedPlaying; + spectatorClient.OnNewFrames -= userSentFrames; + + lock (stateLock) + { + foreach (var (userId, _) in userMap) + spectatorClient.StopWatchingUser(userId); + } + } + + managerUpdated?.UnbindAll(); + } + } +} diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 693c9cb792..79cfee8518 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual protected EditorClockTestScene() { - Clock = new EditorClock(new ControlPointInfo(), 5000, BeatDivisor) { IsCoupled = false }; + Clock = new EditorClock(new ControlPointInfo(), BeatDivisor) { IsCoupled = false }; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 09fcc1ff47..b5cd3dad02 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -58,6 +58,12 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + public void ChangeRoomState(MultiplayerRoomState newState) + { + Debug.Assert(Room != null); + ((IMultiplayerClient)this).RoomStateChanged(newState); + } + public void ChangeUserState(int userId, MultiplayerUserState newState) { Debug.Assert(Room != null); @@ -71,6 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer case MultiplayerUserState.Loaded: if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad)) { + ChangeRoomState(MultiplayerRoomState.Playing); foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded)) ChangeUserState(u.UserID, MultiplayerUserState.Playing); @@ -82,6 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer case MultiplayerUserState.FinishedPlay: if (Room.Users.All(u => u.State != MultiplayerUserState.Playing)) { + ChangeRoomState(MultiplayerRoomState.Open); foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay)) ChangeUserState(u.UserID, MultiplayerUserState.Results); @@ -173,6 +181,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); + ChangeRoomState(MultiplayerRoomState.WaitingForLoad); foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 64f4d7b95b..01dd7a25c8 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -24,18 +24,31 @@ namespace osu.Game.Tests.Visual private readonly TriangleButton buttonTest; private readonly TriangleButton buttonLocal; + /// + /// Whether to create a nested container to handle s that result from local (manual) test input. + /// This should be disabled when instantiating an instance else actions will be lost. + /// + protected virtual bool CreateNestedActionContainer => true; + protected OsuManualInputManagerTestScene() { MenuCursorContainer cursorContainer; + CompositeDrawable mainContent = + (cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }) + .WithChild(content = new OsuTooltipContainer(cursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }); + + if (CreateNestedActionContainer) + { + mainContent = new GlobalActionContainer(null).WithChild(mainContent); + } + base.Content.AddRange(new Drawable[] { InputManager = new ManualInputManager { UseParentInput = true, - Child = new GlobalActionContainer(null) - .WithChild((cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }) - .WithChild(content = new OsuTooltipContainer(cursorContainer.Cursor) { RelativeSizeAxes = Axes.Both })) + Child = mainContent }, new Container { diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 04a358436e..5ddcd86d28 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -45,7 +45,7 @@ namespace osu.Game.Users public double Accuracy; [JsonIgnore] - public string DisplayAccuracy => Accuracy.FormatAccuracy(); + public string DisplayAccuracy => (Accuracy / 100).FormatAccuracy(); [JsonProperty(@"play_count")] public int PlayCount; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b90c938a8b..71a6f0e5cd 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,20 +18,20 @@ - + - - - + + + - - - - + + + + diff --git a/osu.iOS.props b/osu.iOS.props index ce182a3054..a389cc13dd 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -93,7 +93,7 @@ - + diff --git a/osu.sln b/osu.sln index c9453359b1..b5018db362 100644 --- a/osu.sln +++ b/osu.sln @@ -66,6 +66,34 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Benchmarks", "osu.Game.Benchmarks\osu.Game.Benchmarks.csproj", "{93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Templates", "Templates", "{70CFC05F-CF79-4A7F-81EC-B32F1E564480}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rulesets", "Rulesets", "{CA1DD4A8-FA22-48E0-860F-D57A7ED7D426}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ruleset-empty", "ruleset-empty", "{6E22BB20-901E-49B3-90A1-B0E6377FE568}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ruleset-example", "ruleset-example", "{7DBBBA73-6D84-4EBA-8711-EBC2939B04B5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ruleset-scrolling-empty", "ruleset-scrolling-empty", "{5CB72FDE-BA77-47D1-A556-FEB15AAD4523}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ruleset-scrolling-example", "ruleset-scrolling-example", "{0E0EDD4C-1E45-4E03-BC08-0102C98D34B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyFreeform", "Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj", "{9014CA66-5217-49F6-8C1E-3430FD08EF61}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyFreeform.Tests", "Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform.Tests\osu.Game.Rulesets.EmptyFreeform.Tests.csproj", "{561DFD5E-5896-40D1-9708-4D692F5BAE66}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon", "Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj", "{B325271C-85E7-4DB3-8BBB-B70F242954F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon.Tests", "Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon.Tests\osu.Game.Rulesets.Pippidon.Tests.csproj", "{4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyScrolling", "Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj", "{AD923016-F318-49B7-B08B-89DED6DC2422}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyScrolling.Tests", "Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling.Tests\osu.Game.Rulesets.EmptyScrolling.Tests.csproj", "{B9B92246-02EB-4118-9C6F-85A0D726AA70}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon", "Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj", "{B9022390-8184-4548-9DB1-50EB8878D20A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon.Tests", "Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon.Tests\osu.Game.Rulesets.Pippidon.Tests.csproj", "{1743BF7C-E6AE-4A06-BAD9-166D62894303}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -412,6 +440,102 @@ Global {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhone.Build.0 = Release|Any CPU {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|iPhone.Build.0 = Debug|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|Any CPU.Build.0 = Release|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|iPhone.ActiveCfg = Release|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|iPhone.Build.0 = Release|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|iPhone.Build.0 = Debug|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|Any CPU.Build.0 = Release|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|iPhone.ActiveCfg = Release|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|iPhone.Build.0 = Release|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|iPhone.Build.0 = Debug|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|Any CPU.Build.0 = Release|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|iPhone.ActiveCfg = Release|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|iPhone.Build.0 = Release|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|iPhone.Build.0 = Debug|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|Any CPU.Build.0 = Release|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|iPhone.ActiveCfg = Release|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|iPhone.Build.0 = Release|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|iPhone.Build.0 = Debug|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|Any CPU.Build.0 = Release|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|iPhone.ActiveCfg = Release|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|iPhone.Build.0 = Release|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|iPhone.Build.0 = Debug|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|Any CPU.Build.0 = Release|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|iPhone.ActiveCfg = Release|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|iPhone.Build.0 = Release|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|iPhone.Build.0 = Debug|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|Any CPU.Build.0 = Release|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|iPhone.ActiveCfg = Release|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|iPhone.Build.0 = Release|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|iPhone.Build.0 = Debug|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|Any CPU.Build.0 = Release|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|iPhone.ActiveCfg = Release|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|iPhone.Build.0 = Release|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -448,4 +572,19 @@ Global $2.inheritsScope = text/x-csharp $2.scope = text/x-csharp EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CA1DD4A8-FA22-48E0-860F-D57A7ED7D426} = {70CFC05F-CF79-4A7F-81EC-B32F1E564480} + {6E22BB20-901E-49B3-90A1-B0E6377FE568} = {CA1DD4A8-FA22-48E0-860F-D57A7ED7D426} + {9014CA66-5217-49F6-8C1E-3430FD08EF61} = {6E22BB20-901E-49B3-90A1-B0E6377FE568} + {561DFD5E-5896-40D1-9708-4D692F5BAE66} = {6E22BB20-901E-49B3-90A1-B0E6377FE568} + {7DBBBA73-6D84-4EBA-8711-EBC2939B04B5} = {CA1DD4A8-FA22-48E0-860F-D57A7ED7D426} + {B325271C-85E7-4DB3-8BBB-B70F242954F8} = {7DBBBA73-6D84-4EBA-8711-EBC2939B04B5} + {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738} = {7DBBBA73-6D84-4EBA-8711-EBC2939B04B5} + {5CB72FDE-BA77-47D1-A556-FEB15AAD4523} = {CA1DD4A8-FA22-48E0-860F-D57A7ED7D426} + {0E0EDD4C-1E45-4E03-BC08-0102C98D34B3} = {CA1DD4A8-FA22-48E0-860F-D57A7ED7D426} + {AD923016-F318-49B7-B08B-89DED6DC2422} = {5CB72FDE-BA77-47D1-A556-FEB15AAD4523} + {B9B92246-02EB-4118-9C6F-85A0D726AA70} = {5CB72FDE-BA77-47D1-A556-FEB15AAD4523} + {B9022390-8184-4548-9DB1-50EB8878D20A} = {0E0EDD4C-1E45-4E03-BC08-0102C98D34B3} + {1743BF7C-E6AE-4A06-BAD9-166D62894303} = {0E0EDD4C-1E45-4E03-BC08-0102C98D34B3} + EndGlobalSection EndGlobal