diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 6ba6ae82c8..1dca8b3859 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -2,17 +2,35 @@
"version": 1,
"isRoot": true,
"tools": {
- "cake.tool": {
- "version": "0.35.0",
- "commands": [
- "dotnet-cake"
- ]
- },
"dotnet-format": {
"version": "3.1.37601",
"commands": [
"dotnet-format"
]
+ },
+ "jetbrains.resharper.globaltools": {
+ "version": "2020.3.2",
+ "commands": [
+ "jb"
+ ]
+ },
+ "nvika": {
+ "version": "2.0.0",
+ "commands": [
+ "nvika"
+ ]
+ },
+ "codefilesanity": {
+ "version": "0.0.36",
+ "commands": [
+ "CodeFileSanity"
+ ]
+ },
+ "ppy.localisationanalyser.tools": {
+ "version": "2021.608.0",
+ "commands": [
+ "localisation"
+ ]
}
}
}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index 67f98f94eb..f4d7e08d08 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,14 +10,6 @@ trim_trailing_whitespace = true
#Roslyn naming styles
-#PascalCase for public and protected members
-dotnet_naming_style.pascalcase.capitalization = pascal_case
-dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
-dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
-dotnet_naming_rule.public_members_pascalcase.severity = error
-dotnet_naming_rule.public_members_pascalcase.symbols = public_members
-dotnet_naming_rule.public_members_pascalcase.style = pascalcase
-
#camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case
@@ -191,4 +183,10 @@ dotnet_diagnostic.IDE0052.severity = silent
#Rules for disposable
dotnet_diagnostic.IDE0067.severity = none
dotnet_diagnostic.IDE0068.severity = none
-dotnet_diagnostic.IDE0069.severity = none
\ No newline at end of file
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
index 0b80ce44dd..7026179259 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md
@@ -1,7 +1,18 @@
---
name: Bug Report
-about: Issues regarding encountered bugs.
+about: Report a bug or crash to desktop
---
+
+
+
+
**Describe the bug:**
**Screenshots or videos showing encountered issue:**
@@ -9,8 +20,11 @@ about: Issues regarding encountered bugs.
**osu!lazer version:**
**Logs:**
+
diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
deleted file mode 100644
index ada8de73c0..0000000000
--- a/.github/ISSUE_TEMPLATE/02-crash-issues.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-name: Crash Report
-about: Issues regarding crashes or permanent freezes.
----
-**Describe the crash:**
-
-**Screenshots or videos showing encountered issue:**
-
-**osu!lazer version:**
-
-**Logs:**
-
-
-**Computer Specifications:**
diff --git a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md b/.github/ISSUE_TEMPLATE/03-feature-request-issues.md
deleted file mode 100644
index 54c4ff94e5..0000000000
--- a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Feature Request
-about: Features you would like to see in the game!
----
-**Describe the new feature:**
-
-**Proposal designs of the feature:**
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 69baeee60c..c62231e8e0 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,12 @@
blank_issues_enabled: false
contact_links:
+ - name: Suggestions or feature request
+ url: https://github.com/ppy/osu/discussions/categories/ideas
+ about: Got something you think should change or be added? Search for or start a new discussion!
+ - name: Help
+ url: https://github.com/ppy/osu/discussions/categories/q-a
+ about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues
- about: For issues regarding osu!stable (not osu!lazer), open them here.
+ about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..9e9af23b27
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,46 @@
+version: 2
+updates:
+- package-ecosystem: nuget
+ directory: "/"
+ schedule:
+ interval: monthly
+ time: "17:00"
+ open-pull-requests-limit: 99
+ ignore:
+ - dependency-name: Microsoft.EntityFrameworkCore.Design
+ versions:
+ - "> 2.2.6"
+ - dependency-name: Microsoft.EntityFrameworkCore.Sqlite
+ versions:
+ - "> 2.2.6"
+ - dependency-name: Microsoft.EntityFrameworkCore.Sqlite.Core
+ versions:
+ - "> 2.2.6"
+ - dependency-name: Microsoft.Extensions.DependencyInjection
+ versions:
+ - ">= 5.a, < 6"
+ - dependency-name: NUnit3TestAdapter
+ versions:
+ - ">= 3.16.a, < 3.17"
+ - dependency-name: Microsoft.NET.Test.Sdk
+ versions:
+ - 16.9.1
+ - dependency-name: Microsoft.Extensions.DependencyInjection
+ versions:
+ - 3.1.11
+ - 3.1.12
+ - dependency-name: Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson
+ versions:
+ - 3.1.11
+ - dependency-name: Microsoft.NETCore.Targets
+ versions:
+ - 5.0.0
+ - dependency-name: Microsoft.AspNetCore.SignalR.Protocols.MessagePack
+ versions:
+ - 5.0.2
+ - dependency-name: NUnit
+ versions:
+ - 3.13.1
+ - dependency-name: Microsoft.AspNetCore.SignalR.Client
+ versions:
+ - 3.1.11
diff --git a/.gitignore b/.gitignore
index 732b171f69..d122d25054 100644
--- a/.gitignore
+++ b/.gitignore
@@ -334,3 +334,5 @@ inspectcode
# BenchmarkDotNet
/BenchmarkDotNet.Artifacts
+
+*.GeneratedMSBuildEditorConfig.editorconfig
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 fe63f5faf3..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
index 7515e76054..4bb9f4d2a0 100644
--- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
+++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
index 1815c271b4..8fa7608b8e 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
index a4154623b6..657b885df1 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,10 +12,10 @@
-
+
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
index 080dc04001..847dcf822c 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,10 +12,10 @@
-
+
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
index 3de6a7e609..5dc1168e35 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,10 +12,10 @@
-
+
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
index da14c2a29e..ab4ce5a9cc 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,10 +12,10 @@
-
+
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
index 45d1ce25e9..61331944a8 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,9 +12,9 @@
-
+
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
index ba80f7c100..9a00f58c3b 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,10 +12,10 @@
-
+
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
index 911c3ed9b7..4ae656b6d8 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,9 +12,9 @@
-
+
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
deleted file mode 100644
index d85a0ae44c..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
index ec3c81f4cd..e58c602962 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,9 +12,9 @@
-
+
-
+
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 4e8af405a2..1b590008cd 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1/osu!.dll"
+ "${workspaceRoot}/osu.Desktop/bin/Debug/net5.0/osu!.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
@@ -19,7 +19,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1/osu!.dll"
+ "${workspaceRoot}/osu.Desktop/bin/Release/net5.0/osu!.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Release)",
@@ -31,7 +31,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp3.1/osu.Game.Tests.dll"
+ "${workspaceRoot}/osu.Game.Tests/bin/Debug/net5.0/osu.Game.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tests (Debug)",
@@ -43,7 +43,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp3.1/osu.Game.Tests.dll"
+ "${workspaceRoot}/osu.Game.Tests/bin/Release/net5.0/osu.Game.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tests (Release)",
@@ -55,7 +55,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1/osu!.dll",
+ "${workspaceRoot}/osu.Desktop/bin/Debug/net5.0/osu!.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
@@ -68,7 +68,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1/osu!.dll",
+ "${workspaceRoot}/osu.Desktop/bin/Release/net5.0/osu!.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
@@ -81,7 +81,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1/osu.Game.Tournament.Tests.dll",
+ "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net5.0/osu.Game.Tournament.Tests.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
@@ -94,7 +94,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1/osu.Game.Tournament.Tests.dll",
+ "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net5.0/osu.Game.Tournament.Tests.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
@@ -105,7 +105,7 @@
"name": "Benchmark",
"type": "coreclr",
"request": "launch",
- "program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/netcoreapp3.1/osu.Game.Benchmarks.dll",
+ "program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/net5.0/osu.Game.Benchmarks.dll",
"args": [
"--filter",
"*"
@@ -113,20 +113,6 @@
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build benchmarks",
"console": "internalConsole"
- },
- {
- "name": "Cake: Debug Script",
- "type": "coreclr",
- "request": "launch",
- "program": "${workspaceRoot}/build/tools/Cake.CoreCLR/0.30.0/Cake.dll",
- "args": [
- "${workspaceRoot}/build/build.cake",
- "--debug",
- "--verbosity=diagnostic"
- ],
- "cwd": "${workspaceRoot}/build",
- "stopAtEntry": true,
- "externalConsole": false
}
]
}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index e638dec767..a70e5ac3a9 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -9,11 +9,10 @@
"command": "dotnet",
"args": [
"build",
- "--no-restore",
"osu.Desktop",
- "/p:GenerateFullPaths=true",
- "/m",
- "/verbosity:m"
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
@@ -24,12 +23,11 @@
"command": "dotnet",
"args": [
"build",
- "--no-restore",
"osu.Desktop",
- "/p:Configuration=Release",
- "/p:GenerateFullPaths=true",
- "/m",
- "/verbosity:m"
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
@@ -40,11 +38,10 @@
"command": "dotnet",
"args": [
"build",
- "--no-restore",
"osu.Game.Tests",
- "/p:GenerateFullPaths=true",
- "/m",
- "/verbosity:m"
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
@@ -55,12 +52,11 @@
"command": "dotnet",
"args": [
"build",
- "--no-restore",
"osu.Game.Tests",
- "/p:Configuration=Release",
- "/p:GenerateFullPaths=true",
- "/m",
- "/verbosity:m"
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
@@ -71,11 +67,10 @@
"command": "dotnet",
"args": [
"build",
- "--no-restore",
"osu.Game.Tournament.Tests",
- "/p:GenerateFullPaths=true",
- "/m",
- "/verbosity:m"
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
@@ -86,12 +81,11 @@
"command": "dotnet",
"args": [
"build",
- "--no-restore",
"osu.Game.Tournament.Tests",
- "/p:Configuration=Release",
- "/p:GenerateFullPaths=true",
- "/m",
- "/verbosity:m"
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
@@ -102,25 +96,14 @@
"command": "dotnet",
"args": [
"build",
- "--no-restore",
"osu.Game.Benchmarks",
- "/p:Configuration=Release",
- "/p:GenerateFullPaths=true",
- "/m",
- "/verbosity:m"
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
- },
- {
- "label": "Restore (netcoreapp3.1)",
- "type": "shell",
- "command": "dotnet",
- "args": [
- "restore",
- "build/Desktop.proj"
- ],
- "problemMatcher": []
}
]
}
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..e14be20642
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,137 @@
+# Contributing Guidelines
+
+Thank you for showing interest in the development of osu!lazer! We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
+
+These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner.
+
+## Table of contents
+
+1. [I would like to submit an issue!](#i-would-like-to-submit-an-issue)
+2. [I would like to submit a pull request!](#i-would-like-to-submit-a-pull-request)
+
+## I would like to submit an issue!
+
+Issues, bug reports and feature suggestions are welcomed, though please keep in mind that at any point in time, hundreds of issues are open, which vary in severity and the amount of time needed to address them. As such it's not uncommon for issues to remain unresolved for a long time or even closed outright if they are deemed not important enough to fix in the foreseeable future. Issues that are required to "go live" or otherwise achieve parity with stable are prioritised the most.
+
+* **Before submitting an issue, try searching existing issues first.**
+
+ For housekeeping purposes, we close issues that overlap with or duplicate other pre-existing issues - you can help us not to have to do that by searching existing issues yourself first. The issue search box, as well as the issue tag system, are tools you can use to check if an issue has been reported before.
+
+* **When submitting a bug report, please try to include as much detail as possible.**
+
+ Bugs are not equal - some of them will be reproducible every time on pretty much all hardware, while others will be hard to track down due to being specific to particular hardware or even somewhat random in nature. As such, providing as much detail as possible when reporting a bug is hugely appreciated. A good starting set of information consists of:
+
+ * the in-game logs, which are located at:
+ * `%AppData%/osu/logs` (on Windows),
+ * `~/.local/share/osu/logs` (on Linux and macOS),
+ * `Android/data/sh.ppy.osulazer/files/logs` (on Android),
+ * on iOS they can be obtained by connecting your device to your desktop and [copying the `logs` directory from the app's own document storage using iTunes](https://support.apple.com/en-us/HT201301#copy-to-computer),
+ * your system specifications (including the operating system and platform you are playing on),
+ * a reproduction scenario (list of steps you have performed leading up to the occurrence of the bug),
+ * a video or picture of the bug, if at all possible.
+
+* **Provide more information when asked to do so.**
+
+ Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local lazer database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
+
+* **When submitting a feature proposal, please describe it in the most understandable way you can.**
+
+ Communicating your idea for a feature can often be hard, and we would like to avoid any misunderstandings. As such, please try to explain your idea in a short, but understandable manner - it's best to avoid jargon or terms and references that could be considered obscure. A mock-up picture (doesn't have to be good!) of the feature can also go a long way in explaining.
+
+* **Refrain from posting "+1" comments.**
+
+ If an issue has already been created, saying that you also experience it without providing any additional details doesn't really help us in any way. To express support for a proposal or indicate that you are also affected by a particular bug, you can use comment reactions instead.
+
+* **Refrain from asking if an issue has been resolved yet.**
+
+ As mentioned above, the issue tracker has hundreds of issues open at any given time. Currently the game is being worked on by two members of the core team, and a handful of outside contributors who offer their free time to help out. As such, it can happen that an issue gets placed on the backburner due to being less important; generally posting a comment demanding its resolution some months or years after it is reported is not very likely to increase its priority.
+
+* **Avoid long discussions about non-development topics.**
+
+ GitHub is mostly a developer space, and as such isn't really fit for lengthened discussions about gameplay mechanics (which might not even be in any way confirmed for the final release) and similar non-technical matters. Such matters are probably best addressed at the osu! forums.
+
+## I would like to submit a pull request!
+
+We also welcome pull requests from unaffiliated contributors. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label.
+
+However, do keep in mind that the core team is committed to bringing osu!lazer up to par with stable first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
+
+Here are some key things to note before jumping in:
+
+* **Make sure you are comfortable with C\# and your development environment.**
+
+ While we are accepting of all kinds of contributions, we also have a certain quality standard we'd like to uphold and limited time to review your code. Therefore, we would like to avoid providing entry-level advice, and as such if you're not very familiar with C\# as a programming language, we'd recommend that you start off with a few personal projects to get acquainted with the language's syntax, toolchain and principles of object-oriented programming first.
+
+ In addition, please take the time to take a look at and get acquainted with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up.
+
+* **Make sure you are familiar with git and the pull request workflow.**
+
+ [git](https://git-scm.com/) is a distributed version control system that might not be very intuitive at the beginning if you're not familiar with version control. In particular, projects using git have a particular workflow for submitting code changes, which is called the pull request workflow.
+
+ To make things run more smoothly, we recommend that you look up some online resources to familiarise yourself with the git vocabulary and commands, and practice working with forks and submitting pull requests at your own pace. A high-level overview of the process can be found in [this article by GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests).
+
+* **Double-check designs before starting work on new functionality.**
+
+ When implementing new features, keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted.
+
+* **Make sure to submit pull requests off of a topic branch.**
+
+ As described in the article linked in the previous point, topic branches help you parallelise your work and separate it from the main `master` branch, and additionally are easier for maintainers to work with. Working with multiple `master` branches across many remotes is difficult to keep track of, and it's easy to make a mistake and push to the wrong `master` branch by accident.
+
+* **Refrain from making changes through the GitHub web interface.**
+
+ Even though GitHub provides an option to edit code or replace files in the repository using the web interface, we strongly discourage using it in most scenarios. Editing files this way is inefficient and likely to introduce whitespace or file encoding changes that make it more difficult to review the code.
+
+ Code written through the web interface will also very likely be questioned outright by the reviewers, as it is likely that it has not been properly tested or that it will fail continuous integration checks. We strongly encourage using an IDE like [Visual Studio](https://visualstudio.microsoft.com/), [Visual Studio Code](https://code.visualstudio.com/) or [JetBrains Rider](https://www.jetbrains.com/rider/) instead.
+
+* **Add tests for your code whenever possible.**
+
+ Automated tests are an essential part of a quality and reliable codebase. They help to make the code more maintainable by ensuring it is safe to reorganise (or refactor) the code in various ways, and also prevent regressions - bugs that resurface after having been fixed at some point in the past. If it is viable, please put in the time to add tests, so that the changes you make can last for a (hopefully) very long time.
+
+* **Run tests before opening a pull request.**
+
+ Tying into the previous point, sometimes changes in one part of the codebase can result in unpredictable changes in behaviour in other pieces of the code. This is why it is best to always try to run tests before opening a PR.
+
+ Continuous integration will always run the tests for you (and us), too, but it is best not to rely on it, as there might be many builds queued at any time. Running tests on your own will help you be more certain that at the point of clicking the "Create pull request" button, your changes are as ready as can be.
+
+* **Run code style analysis before opening a pull request.**
+
+ As part of continuous integration, we also run code style analysis, which is supposed to make sure that your code is formatted the same way as all the pre-existing code in the repository. The reason we enforce a particular code style everywhere is to make sure the codebase is consistent in that regard - having one whitespace convention in one place and another one elsewhere causes disorganisation.
+
+* **Make sure that the pull request is complete before opening it.**
+
+ Whether it's fixing a bug or implementing new functionality, it's best that you make sure that the change you want to submit as a pull request is as complete as it can be before clicking the *Create pull request* button. Having to track if a pull request is ready for review or not places additional burden on reviewers.
+
+ Draft pull requests are an option, but use them sparingly and within reason. They are best suited to discuss code changes that cannot be easily described in natural language or have a potential large impact on the future direction of the project. When in doubt, don't open drafts unless a maintainer asks you to do so.
+
+* **Only push code when it's ready.**
+
+ As an extension of the above, when making changes to an already-open PR, please try to only push changes you are reasonably certain of. Pushing after every commit causes the continuous integration build queue to grow in size, slowing down work and taking up time that could be spent verifying other changes.
+
+* **Make sure to keep the *Allow edits from maintainers* check box checked.**
+
+ To speed up the merging process, collaborators and team members will sometimes want to push changes to your branch themselves, to make minor code style adjustments or to otherwise refactor the code without having to describe how they'd like the code to look like in painstaking detail. Having the *Allow edits from maintainers* check box checked lets them do that; without it they are forced to report issues back to you and wait for you to address them.
+
+* **Refrain from continually merging the master branch back to the PR.**
+
+ Unless there are merge conflicts that need resolution, there is no need to keep merging `master` back to a branch over and over again. One of the maintainers will merge `master` themselves before merging the PR itself anyway, and continual merge commits can cause CI to get overwhelmed due to queueing up too many builds.
+
+* **Refrain from force-pushing to the PR branch.**
+
+ Force-pushing should be avoided, as it can lead to accidentally overwriting a maintainer's changes or CI building wrong commits. We value all history in the project, so there is no need to squash or amend commits in most cases.
+
+ The cases in which force-pushing is warranted are very rare (such as accidentally leaking sensitive info in one of the files committed, adding unrelated files, or mis-merging a dependent PR).
+
+* **Be patient when waiting for the code to be reviewed and merged.**
+
+ As much as we'd like to review all contributions as fast as possible, our time is limited, as team members have to work on their own tasks in addition to reviewing code. As such, work needs to be prioritised, and it can unfortunately take weeks or months for your PR to be merged, depending on how important it is deemed to be.
+
+* **Don't mistake criticism of code for criticism of your person.**
+
+ As mentioned before, we are highly committed to quality when it comes to the lazer project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
+
+* **Feel free to reach out for help.**
+
+ If you're uncertain about some part of the codebase or some inner workings of the game and framework, please reach out either by leaving a comment in the relevant issue or PR thread, or by posting a message in the [development Discord server](https://discord.gg/ppy). We will try to help you as much as we can.
+
+ When it comes to which form of communication is best, GitHub generally lends better to longer-form discussions, while Discord is better for snappy call-and-response answers. Use your best discretion when deciding, and try to keep a single discussion in one place instead of moving back and forth.
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index a92191a439..46c50dbfa2 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -4,3 +4,7 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
+M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
+T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
+T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
+M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead.
\ No newline at end of file
diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset
index d497365f87..6a99e230d1 100644
--- a/CodeAnalysis/osu.ruleset
+++ b/CodeAnalysis/osu.ruleset
@@ -30,7 +30,7 @@
-
+
diff --git a/Directory.Build.props b/Directory.Build.props
index 21b8b402e0..53ad973e47 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,9 +16,9 @@
-
+
-
+
$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset
@@ -28,9 +28,17 @@
$(NoWarn);CS1591
-
- $(NoWarn);NU1701
+
+ $(NoWarn);NU1701;CA9998
false
@@ -40,7 +48,7 @@
https://github.com/ppy/osu
Automated release.
ppy Pty Ltd
- Copyright (c) 2020 ppy Pty Ltd
+ Copyright (c) 2021 ppy Pty Ltd
osu game
-
\ No newline at end of file
+
diff --git a/Gemfile.lock b/Gemfile.lock
index e3954c2681..8ac863c9a8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,58 +1,75 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.2)
+ CFPropertyList (3.0.3)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
- babosa (1.0.3)
+ aws-eventstream (1.1.0)
+ aws-partitions (1.413.0)
+ aws-sdk-core (3.110.0)
+ aws-eventstream (~> 1, >= 1.0.2)
+ aws-partitions (~> 1, >= 1.239.0)
+ aws-sigv4 (~> 1.1)
+ jmespath (~> 1.0)
+ aws-sdk-kms (1.40.0)
+ aws-sdk-core (~> 3, >= 3.109.0)
+ aws-sigv4 (~> 1.1)
+ aws-sdk-s3 (1.87.0)
+ aws-sdk-core (~> 3, >= 3.109.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.1)
+ aws-sigv4 (1.2.2)
+ aws-eventstream (~> 1, >= 1.0.2)
+ babosa (1.0.4)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
- declarative (0.0.10)
+ declarative (0.0.20)
declarative-option (0.1.0)
- digest-crc (0.4.1)
+ digest-crc (0.6.3)
+ rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
- dotenv (2.7.5)
- emoji_regex (1.0.1)
- excon (0.71.1)
- faraday (0.17.3)
+ dotenv (2.7.6)
+ emoji_regex (3.2.1)
+ excon (0.78.1)
+ faraday (1.2.0)
multipart-post (>= 1.2, < 3)
- faraday-cookie_jar (0.0.6)
- faraday (>= 0.7.4)
+ ruby2_keywords
+ faraday-cookie_jar (0.0.7)
+ faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
- faraday_middleware (0.13.1)
- faraday (>= 0.7.4, < 1.0)
- fastimage (2.1.7)
- fastlane (2.140.0)
+ faraday_middleware (1.0.0)
+ faraday (~> 1.0)
+ fastimage (2.2.1)
+ fastlane (2.170.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
- babosa (>= 1.0.2, < 2.0.0)
+ aws-sdk-s3 (~> 1.0)
+ babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
- emoji_regex (>= 0.1, < 2.0)
+ emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
- faraday (~> 0.17)
+ faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (~> 0.13.1)
+ faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
- google-api-client (>= 0.29.2, < 0.37.0)
+ google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
- jwt (~> 2.1.0)
+ jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
- multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
- public_suffix (~> 2.0.0)
- rubyzip (>= 1.3.0, < 2.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
@@ -69,7 +86,7 @@ GEM
souyuz (= 0.9.1)
fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3)
- google-api-client (0.36.4)
+ google-api-client (0.38.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
@@ -80,57 +97,59 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.3.0)
- faraday (~> 0.11)
- google-cloud-errors (1.0.0)
- google-cloud-storage (1.25.1)
+ google-cloud-env (1.4.0)
+ faraday (>= 0.17.3, < 2.0)
+ google-cloud-errors (1.0.1)
+ google-cloud-storage (1.29.2)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
- googleauth (0.10.0)
- faraday (~> 0.12)
+ googleauth (0.14.0)
+ faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
- signet (~> 0.12)
+ signet (~> 0.14)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
- json (2.3.0)
- jwt (2.1.0)
+ jmespath (1.4.0)
+ json (2.5.1)
+ jwt (2.2.2)
memoist (0.16.2)
- mini_magick (4.10.1)
+ mini_magick (4.11.0)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
- multi_json (1.14.1)
- multi_xml (0.6.0)
+ multi_json (1.15.0)
multipart-post (2.0.0)
- nanaimo (0.2.6)
+ nanaimo (0.3.0)
naturally (2.2.0)
- nokogiri (1.10.7)
+ nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
- os (1.0.1)
+ os (1.1.1)
plist (3.5.0)
- public_suffix (2.0.5)
+ public_suffix (4.0.6)
+ rake (13.0.3)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
- rubyzip (1.3.0)
+ ruby2_keywords (0.0.2)
+ rubyzip (2.3.0)
security (0.1.3)
- signet (0.12.0)
+ signet (0.14.0)
addressable (~> 2.3)
- faraday (~> 0.9)
+ faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
- simctl (1.6.7)
+ simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
@@ -141,22 +160,22 @@ GEM
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
- tty-cursor (0.7.0)
- tty-screen (0.7.0)
- tty-spinner (0.9.2)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.1)
+ tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.6)
- unicode-display_width (1.6.1)
+ unf_ext (0.0.7.7)
+ unicode-display_width (1.7.0)
word_wrap (1.0.0)
- xcodeproj (1.14.0)
+ xcodeproj (1.19.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
- nanaimo (~> 0.2.6)
+ nanaimo (~> 0.3.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
diff --git a/InspectCode.ps1 b/InspectCode.ps1
index 6ed935fdbb..8316f48ff3 100644
--- a/InspectCode.ps1
+++ b/InspectCode.ps1
@@ -1,27 +1,11 @@
-[CmdletBinding()]
-Param(
- [string]$Target,
- [string]$Configuration,
- [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
- [string]$Verbosity,
- [switch]$ShowDescription,
- [Alias("WhatIf", "Noop")]
- [switch]$DryRun,
- [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)]
- [string[]]$ScriptArgs
-)
-
-# Build Cake arguments
-$cakeArguments = "";
-if ($Target) { $cakeArguments += "-target=$Target" }
-if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
-if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
-if ($ShowDescription) { $cakeArguments += "-showdescription" }
-if ($DryRun) { $cakeArguments += "-dryrun" }
-if ($Experimental) { $cakeArguments += "-experimental" }
-$cakeArguments += $ScriptArgs
-
dotnet tool restore
-dotnet cake ./build/InspectCode.cake --bootstrap
-dotnet cake ./build/InspectCode.cake $cakeArguments
-exit $LASTEXITCODE
\ No newline at end of file
+
+# Temporarily disabled until the tool is upgraded to 5.0.
+ # The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7.
+ # - cmd: dotnet format --dry-run --check
+
+dotnet CodeFileSanity
+dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
+
+exit $LASTEXITCODE
diff --git a/InspectCode.sh b/InspectCode.sh
new file mode 100755
index 0000000000..cf2bc18175
--- /dev/null
+++ b/InspectCode.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+dotnet tool restore
+dotnet CodeFileSanity
+dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
diff --git a/LICENCE b/LICENCE
index 2435c23545..b5962ad3b2 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,4 +1,4 @@
-Copyright (c) 2020 ppy Pty Ltd .
+Copyright (c) 2021 ppy Pty Ltd .
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 77c7eb9d2d..3054f19e79 100644
--- a/README.md
+++ b/README.md
@@ -5,16 +5,20 @@
# osu!
[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu)
-[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)]()
+[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest)
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
-Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
+A free-to-win rhythm game. Rhythm is just a *click* away!
+
+The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
## Status
This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
+**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passes come at the end of development, preceded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
+
We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
@@ -30,15 +34,22 @@ If you are looking to install or test osu! without setting up a development envi
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
-- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
+- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
+- When running on Windows 7 or 8.1, *[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net50&pivots=os-windows#dependencies)** may be required to correctly run .NET 5 applications if your operating system is not up-to-date with the latest service packs.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
-## Developing or debugging
+## 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/tree/master/Templates).
+
+You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
+
+## Developing osu!
Please make sure you have the following prerequisites:
-- A desktop platform with the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) or higher installed.
+- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) or higher installed.
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
@@ -63,7 +74,6 @@ git pull
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations.
-- Visual Studio Code users must run the `Restore` task before any build attempt.
You can also build and run *osu!* from the command-line with a single command:
@@ -87,15 +97,11 @@ Before committing your code, please run a code formatter. This can be achieved b
We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
-JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`, which is [only supported on Windows](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
+JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`. Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## Contributing
-We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted.
-
-If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) label).
-
-Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**.
+When it comes to contributing to the project, the two main things you can do to help out are reporting issues and submitting pull requests. Based on past experiences, we have prepared a [list of contributing guidelines](CONTRIBUTING.md) that should hopefully ease you into our collaboration process and answer the most frequently-asked questions.
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible.
diff --git a/Templates/Directory.Build.props b/Templates/Directory.Build.props
new file mode 100644
index 0000000000..0e470106e8
--- /dev/null
+++ b/Templates/Directory.Build.props
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Templates/README.md b/Templates/README.md
new file mode 100644
index 0000000000..cf25a89273
--- /dev/null
+++ b/Templates/README.md
@@ -0,0 +1,21 @@
+# Templates
+
+Templates for use when creating osu! dependent projects. Create a fully-testable (and ready for git) custom ruleset in just two lines.
+
+## Usage
+
+```bash
+# install (or update) templates package.
+# this only needs to be done once
+dotnet new -i ppy.osu.Game.Templates
+
+# create an empty freeform ruleset
+dotnet new ruleset -n MyCoolRuleset
+# create an empty scrolling ruleset (which provides the basics for a scrolling ←↑→↓ ruleset)
+dotnet new ruleset-scrolling -n MyCoolRuleset
+
+# ..or start with a working sample freeform game
+dotnet new ruleset-example -n MyCoolWorkingRuleset
+# ..or a working sample scrolling game
+dotnet new ruleset-scrolling-example -n MyCoolWorkingRuleset
+```
diff --git a/Templates/Rulesets/ruleset-empty/.editorconfig b/Templates/Rulesets/ruleset-empty/.editorconfig
new file mode 100644
index 0000000000..f3badda9b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/.editorconfig
@@ -0,0 +1,200 @@
+# EditorConfig is awesome: http://editorconfig.org
+root = true
+
+[*.cs]
+end_of_line = crlf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+#Roslyn naming styles
+
+#PascalCase for public and protected members
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.public_members_pascalcase.severity = error
+dotnet_naming_rule.public_members_pascalcase.symbols = public_members
+dotnet_naming_rule.public_members_pascalcase.style = pascalcase
+
+#camelCase for private members
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_symbols.private_members.applicable_accessibilities = private
+dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.private_members_camelcase.severity = warning
+dotnet_naming_rule.private_members_camelcase.symbols = private_members
+dotnet_naming_rule.private_members_camelcase.style = camelcase
+
+dotnet_naming_symbols.local_function.applicable_kinds = local_function
+dotnet_naming_rule.local_function_camelcase.severity = warning
+dotnet_naming_rule.local_function_camelcase.symbols = local_function
+dotnet_naming_rule.local_function_camelcase.style = camelcase
+
+#all_lower for private and local constants/static readonlys
+dotnet_naming_style.all_lower.capitalization = all_lower
+dotnet_naming_style.all_lower.word_separator = _
+
+dotnet_naming_symbols.private_constants.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants.required_modifiers = const
+dotnet_naming_symbols.private_constants.applicable_kinds = field
+dotnet_naming_rule.private_const_all_lower.severity = warning
+dotnet_naming_rule.private_const_all_lower.symbols = private_constants
+dotnet_naming_rule.private_const_all_lower.style = all_lower
+
+dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly.applicable_kinds = field
+dotnet_naming_rule.private_static_readonly_all_lower.severity = warning
+dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly
+dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+dotnet_naming_rule.local_const_all_lower.severity = warning
+dotnet_naming_rule.local_const_all_lower.symbols = local_constants
+dotnet_naming_rule.local_const_all_lower.style = all_lower
+
+#ALL_UPPER for non private constants/static readonlys
+dotnet_naming_style.all_upper.capitalization = all_upper
+dotnet_naming_style.all_upper.word_separator = _
+
+dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_constants.required_modifiers = const
+dotnet_naming_symbols.public_constants.applicable_kinds = field
+dotnet_naming_rule.public_const_all_upper.severity = warning
+dotnet_naming_rule.public_const_all_upper.symbols = public_constants
+dotnet_naming_rule.public_const_all_upper.style = all_upper
+
+dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.public_static_readonly.applicable_kinds = field
+dotnet_naming_rule.public_static_readonly_all_upper.severity = warning
+dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly
+dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper
+
+#Roslyn formating options
+
+#Formatting - indentation options
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+#Formatting - new line options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_open_brace = all
+#csharp_new_line_before_members_in_anonymous_types = true
+#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing
+csharp_new_line_between_query_expression_clauses = true
+
+#Formatting - organize using options
+dotnet_sort_system_directives_first = true
+
+#Formatting - spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+
+#Formatting - wrapping options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#Roslyn language styles
+
+#Style - this. qualification
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+#Style - type names
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+csharp_style_var_when_type_is_apparent = true:none
+csharp_style_var_for_built_in_types = true:none
+csharp_style_var_elsewhere = true:silent
+
+#Style - modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning
+
+#Style - parentheses
+# Skipped because roslyn cannot separate +-*/ with << >>
+
+#Style - expression bodies
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
+
+#Style - expression preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:warning
+
+#Style - null/type checks
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:warning
+
+#Style - unused
+dotnet_style_readonly_field = true:silent
+dotnet_code_quality_unused_parameters = non_public:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_unused_value_assignment_preference = discard_variable:warning
+
+#Style - variable declaration
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+
+#Style - other C# 7.x features
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+
+#Style - C# 8 features
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:silent
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_switch_expression = false:none
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Private method is unused
+dotnet_diagnostic.IDE0051.severity = silent
+#Private member is unused
+dotnet_diagnostic.IDE0052.severity = silent
+
+#Rules for disposable
+dotnet_diagnostic.IDE0067.severity = none
+dotnet_diagnostic.IDE0068.severity = none
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/.gitignore b/Templates/Rulesets/ruleset-empty/.gitignore
new file mode 100644
index 0000000000..940794e60f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/.gitignore
@@ -0,0 +1,288 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
diff --git a/Templates/Rulesets/ruleset-empty/.template.config/template.json b/Templates/Rulesets/ruleset-empty/.template.config/template.json
new file mode 100644
index 0000000000..6bfe2e19dc
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/.template.config/template.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "ppy Pty Ltd",
+ "classifications": [
+ "Console"
+ ],
+ "name": "osu! ruleset",
+ "identity": "ppy.osu.Game.Templates.Rulesets",
+ "shortName": "ruleset",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "sourceName": "EmptyFreeform",
+ "preferNameDirectory": true
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json
new file mode 100644
index 0000000000..fd03878699
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "VisualTests (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Debug)",
+ "env": {},
+ "console": "internalConsole"
+ },
+ {
+ "name": "VisualTests (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Release)",
+ "env": {},
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/tasks.json
new file mode 100644
index 0000000000..509df6a510
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/tasks.json
@@ -0,0 +1,47 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.EmptyFreeformRuleset.Tests.csproj",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.EmptyFreeformRuleset.Tests.csproj",
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "restore"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
new file mode 100644
index 0000000000..9c512a01ea
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Tests
+{
+ public class TestSceneOsuGame : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuGameBase gameBase)
+ {
+ OsuGame game = new OsuGame();
+ game.SetHost(host);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ },
+ game
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuPlayer.cs
new file mode 100644
index 0000000000..0f2ddf82a5
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuPlayer.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Tests
+{
+ [TestFixture]
+ public class TestSceneOsuPlayer : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new EmptyFreeformRuleset();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs
new file mode 100644
index 0000000000..4f810ce17f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework;
+using osu.Framework.Platform;
+using osu.Game.Tests;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Tests
+{
+ public static class VisualTestRunner
+ {
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ {
+ host.Run(new OsuTestBrowser());
+ return 0;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
new file mode 100644
index 0000000000..5eb5efa54c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+ osu.Game.Rulesets.EmptyFreeform.Tests.VisualTestRunner
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ WinExe
+ net5.0
+ osu.Game.Rulesets.EmptyFreeform.Tests
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln
new file mode 100644
index 0000000000..706df08472
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln
@@ -0,0 +1,96 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29123.88
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.EmptyFreeform", "osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyFreeform.Tests", "osu.Game.Rulesets.EmptyFreeform.Tests\osu.Game.Rulesets.EmptyFreeform.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ VisualTests|Any CPU = VisualTests|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668}
+ EndGlobalSection
+ GlobalSection(MonoDevelopProperties) = preSolution
+ Policies = $0
+ $0.TextStylePolicy = $1
+ $1.EolMarker = Windows
+ $1.inheritsSet = VisualStudio
+ $1.inheritsScope = text/plain
+ $1.scope = text/x-csharp
+ $0.CSharpFormattingPolicy = $2
+ $2.IndentSwitchSection = True
+ $2.NewLinesForBracesInProperties = True
+ $2.NewLinesForBracesInAccessors = True
+ $2.NewLinesForBracesInAnonymousMethods = True
+ $2.NewLinesForBracesInControlBlocks = True
+ $2.NewLinesForBracesInAnonymousTypes = True
+ $2.NewLinesForBracesInObjectCollectionArrayInitializers = True
+ $2.NewLinesForBracesInLambdaExpressionBody = True
+ $2.NewLineForElse = True
+ $2.NewLineForCatch = True
+ $2.NewLineForFinally = True
+ $2.NewLineForMembersInObjectInit = True
+ $2.NewLineForMembersInAnonymousTypes = True
+ $2.NewLineForClausesInQuery = True
+ $2.SpacingAfterMethodDeclarationName = False
+ $2.SpaceAfterMethodCallName = False
+ $2.SpaceBeforeOpenSquareBracket = False
+ $2.inheritsSet = Mono
+ $2.inheritsScope = text/x-csharp
+ $2.scope = text/x-csharp
+ EndGlobalSection
+ GlobalSection(MonoDevelopProperties) = preSolution
+ Policies = $0
+ $0.TextStylePolicy = $1
+ $1.EolMarker = Windows
+ $1.inheritsSet = VisualStudio
+ $1.inheritsScope = text/plain
+ $1.scope = text/x-csharp
+ $0.CSharpFormattingPolicy = $2
+ $2.IndentSwitchSection = True
+ $2.NewLinesForBracesInProperties = True
+ $2.NewLinesForBracesInAccessors = True
+ $2.NewLinesForBracesInAnonymousMethods = True
+ $2.NewLinesForBracesInControlBlocks = True
+ $2.NewLinesForBracesInAnonymousTypes = True
+ $2.NewLinesForBracesInObjectCollectionArrayInitializers = True
+ $2.NewLinesForBracesInLambdaExpressionBody = True
+ $2.NewLineForElse = True
+ $2.NewLineForCatch = True
+ $2.NewLineForFinally = True
+ $2.NewLineForMembersInObjectInit = True
+ $2.NewLineForMembersInAnonymousTypes = True
+ $2.NewLineForClausesInQuery = True
+ $2.SpacingAfterMethodDeclarationName = False
+ $2.SpaceAfterMethodCallName = False
+ $2.SpaceBeforeOpenSquareBracket = False
+ $2.inheritsSet = Mono
+ $2.inheritsScope = text/x-csharp
+ $2.scope = text/x-csharp
+ EndGlobalSection
+EndGlobal
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings
new file mode 100644
index 0000000000..aa8f8739c1
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings
@@ -0,0 +1,934 @@
+
+ True
+ True
+ True
+ True
+ ExplicitlyExcluded
+ ExplicitlyExcluded
+ SOLUTION
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ True
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ SUGGESTION
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ ERROR
+ WARNING
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ ERROR
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+
+ True
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile>
+ Code Cleanup (peppy)
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ Explicit
+ ExpressionBody
+ BlockBody
+ True
+ NEXT_LINE
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ NEXT_LINE
+ MULTILINE
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ True
+ NEXT_LINE
+ NEVER
+ NEVER
+ True
+ False
+ True
+ NEVER
+ False
+ False
+ True
+ False
+ False
+ True
+ True
+ False
+ False
+ CHOP_IF_LONG
+ True
+ 200
+ CHOP_IF_LONG
+ False
+ False
+ AABB
+ API
+ BPM
+ GC
+ GL
+ GLSL
+ HID
+ HTML
+ HUD
+ ID
+ IL
+ IOS
+ IP
+ IPC
+ JIT
+ LTRB
+ MD5
+ NS
+ OS
+ PM
+ RGB
+ RNG
+ SHA
+ SRGB
+ TK
+ SS
+ PP
+ GMT
+ QAT
+ BNG
+ UI
+ False
+ HINT
+ <?xml version="1.0" encoding="utf-16"?>
+<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
+ <TypePattern DisplayName="COM interfaces or structs">
+ <TypePattern.Match>
+ <Or>
+ <And>
+ <Kind Is="Interface" />
+ <Or>
+ <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" />
+ <HasAttribute Name="System.Runtime.InteropServices.ComImport" />
+ </Or>
+ </And>
+ <Kind Is="Struct" />
+ </Or>
+ </TypePattern.Match>
+ </TypePattern>
+ <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All">
+ <TypePattern.Match>
+ <And>
+ <Kind Is="Class" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" />
+ </And>
+ </TypePattern.Match>
+ <Entry DisplayName="Setup/Teardown Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <Or>
+ <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" />
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="All other members" />
+ <Entry Priority="100" DisplayName="Test Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="NUnit.Framework.TestAttribute" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ </TypePattern>
+ <TypePattern DisplayName="Default Pattern">
+ <Group DisplayName="Fields/Properties">
+ <Group DisplayName="Public Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Public Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Internal Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Internal Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Protected Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Protected Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Private Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Private Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Constructor/Destructor">
+ <Entry DisplayName="Ctor">
+ <Entry.Match>
+ <Kind Is="Constructor" />
+ </Entry.Match>
+ </Entry>
+ <Region Name="Disposal">
+ <Entry DisplayName="Dtor">
+ <Entry.Match>
+ <Kind Is="Destructor" />
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Dispose()">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Method" />
+ <Name Is="Dispose" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Dispose(true)">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Or>
+ <Virtual />
+ <Override />
+ </Or>
+ <Kind Is="Method" />
+ <Name Is="Dispose" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Region>
+ </Group>
+ <Group DisplayName="Methods">
+ <Group DisplayName="Public">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Internal">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Protected">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Private">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ </Group>
+ </TypePattern>
+</Patterns>
+ Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+See the LICENCE file in the repository root for full licence text.
+
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
+ <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ TestFolder
+ True
+ True
+ o!f – Object Initializer: Anchor&Origin
+ True
+ constant("Centre")
+ 0
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofao
+ True
+ Anchor = Anchor.$anchor$,
+Origin = Anchor.$anchor$,
+ True
+ True
+ o!f – InternalChildren = []
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofic
+ True
+ InternalChildren = new Drawable[]
+{
+ $END$
+};
+ True
+ True
+ o!f – new GridContainer { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofgc
+ True
+ new GridContainer
+{
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
+ {
+ new Drawable[] { $END$ },
+ new Drawable[] { }
+ }
+};
+ True
+ True
+ o!f – new FillFlowContainer { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ offf
+ True
+ new FillFlowContainer
+{
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ $END$
+ }
+},
+ True
+ True
+ o!f – new Container { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofcont
+ True
+ new Container
+{
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ $END$
+ }
+},
+ True
+ True
+ o!f – BackgroundDependencyLoader load()
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofbdl
+ True
+ [BackgroundDependencyLoader]
+private void load()
+{
+ $END$
+}
+ True
+ True
+ o!f – new Box { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofbox
+ True
+ new Box
+{
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+},
+ True
+ True
+ o!f – Children = []
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofc
+ True
+ Children = new Drawable[]
+{
+ $END$
+};
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Beatmaps/EmptyFreeformBeatmapConverter.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Beatmaps/EmptyFreeformBeatmapConverter.cs
new file mode 100644
index 0000000000..a441438d80
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Beatmaps/EmptyFreeformBeatmapConverter.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.EmptyFreeform.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Beatmaps
+{
+ public class EmptyFreeformBeatmapConverter : BeatmapConverter
+ {
+ public EmptyFreeformBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
+ {
+ }
+
+ // todo: Check for conversion types that should be supported (ie. Beatmap.HitObjects.Any(h => h is IHasXPosition))
+ // https://github.com/ppy/osu/tree/master/osu.Game/Rulesets/Objects/Types
+ public override bool CanConvert() => true;
+
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
+ {
+ yield return new EmptyFreeformHitObject
+ {
+ Samples = original.Samples,
+ StartTime = original.StartTime,
+ Position = (original as IHasPosition)?.Position ?? Vector2.Zero,
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
new file mode 100644
index 0000000000..a80f1178b6
--- /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, double clockRate) => 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..f705009d18
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.EmptyFreeform.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Mods
+{
+ public class EmptyFreeformModAutoplay : ModAutoplay
+ {
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
+ {
+ ScoreInfo = new ScoreInfo
+ {
+ User = new User { Username = "sample" },
+ },
+ Replay = new EmptyFreeformAutoGenerator(beatmap).Generate(),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs
new file mode 100644
index 0000000000..0f38e9fdf8
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Objects.Drawables
+{
+ public class DrawableEmptyFreeformHitObject : DrawableHitObject
+ {
+ public DrawableEmptyFreeformHitObject(EmptyFreeformHitObject hitObject)
+ : base(hitObject)
+ {
+ Size = new Vector2(40);
+ Origin = Anchor.Centre;
+
+ Position = hitObject.Position;
+
+ // todo: add visuals.
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ if (timeOffset >= 0)
+ // todo: implement judgement logic
+ ApplyResult(r => r.Type = HitResult.Perfect);
+ }
+
+ protected override void UpdateHitStateTransforms(ArmedState state)
+ {
+ const double duration = 1000;
+
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.FadeOut(duration, Easing.OutQuint).Expire();
+ break;
+
+ case ArmedState.Miss:
+ this.FadeColour(Color4.Red, duration);
+ this.FadeOut(duration, Easing.InQuint).Expire();
+ break;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs
new file mode 100644
index 0000000000..9cd18d2d9f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Objects
+{
+ public class EmptyFreeformHitObject : HitObject, IHasPosition
+ {
+ public override Judgement CreateJudgement() => new Judgement();
+
+ public Vector2 Position { get; set; }
+
+ public float X => Position.X;
+ public float Y => Position.Y;
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs
new file mode 100644
index 0000000000..62f394d1ce
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.EmptyFreeform.Objects;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Replays
+{
+ public class EmptyFreeformAutoGenerator : AutoGenerator
+ {
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public EmptyFreeformAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ protected override void GenerateFrames()
+ {
+ Frames.Add(new EmptyFreeformReplayFrame());
+
+ foreach (EmptyFreeformHitObject hitObject in Beatmap.HitObjects)
+ {
+ Frames.Add(new EmptyFreeformReplayFrame
+ {
+ Time = hitObject.StartTime,
+ Position = hitObject.Position,
+ // todo: add required inputs and extra frames.
+ });
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs
new file mode 100644
index 0000000000..cc4483de31
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Input.StateChanges;
+using osu.Framework.Utils;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Replays
+{
+ public class EmptyFreeformFramedReplayInputHandler : FramedReplayInputHandler
+ {
+ public EmptyFreeformFramedReplayInputHandler(Replay replay)
+ : base(replay)
+ {
+ }
+
+ protected override bool IsImportant(EmptyFreeformReplayFrame frame) => frame.Actions.Any();
+
+ public override void CollectPendingInputs(List inputs)
+ {
+ var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
+
+ inputs.Add(new MousePositionAbsoluteInput
+ {
+ Position = GamefieldToScreenSpace(position),
+ });
+ inputs.Add(new ReplayState
+ {
+ PressedActions = CurrentFrame?.Actions ?? new List(),
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs
new file mode 100644
index 0000000000..c84101ca70
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Replays;
+using osuTK;
+
+namespace osu.Game.Rulesets.EmptyFreeform.Replays
+{
+ public class EmptyFreeformReplayFrame : ReplayFrame
+ {
+ public List Actions = new List();
+ public Vector2 Position;
+
+ public EmptyFreeformReplayFrame(EmptyFreeformAction? button = null)
+ {
+ if (button.HasValue)
+ Actions.Add(button.Value);
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/DrawableEmptyFreeformRuleset.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/DrawableEmptyFreeformRuleset.cs
new file mode 100644
index 0000000000..290f35f516
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/DrawableEmptyFreeformRuleset.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Input;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Handlers;
+using osu.Game.Replays;
+using osu.Game.Rulesets.EmptyFreeform.Objects;
+using osu.Game.Rulesets.EmptyFreeform.Objects.Drawables;
+using osu.Game.Rulesets.EmptyFreeform.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.EmptyFreeform.UI
+{
+ [Cached]
+ public class DrawableEmptyFreeformRuleset : DrawableRuleset
+ {
+ public DrawableEmptyFreeformRuleset(EmptyFreeformRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ }
+
+ protected override Playfield CreatePlayfield() => new EmptyFreeformPlayfield();
+
+ protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new EmptyFreeformFramedReplayInputHandler(replay);
+
+ public override DrawableHitObject CreateDrawableRepresentation(EmptyFreeformHitObject h) => new DrawableEmptyFreeformHitObject(h);
+
+ protected override PassThroughInputManager CreateInputManager() => new EmptyFreeformInputManager(Ruleset?.RulesetInfo);
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/EmptyFreeformPlayfield.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/EmptyFreeformPlayfield.cs
new file mode 100644
index 0000000000..9df5935c45
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/UI/EmptyFreeformPlayfield.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.EmptyFreeform.UI
+{
+ [Cached]
+ public class EmptyFreeformPlayfield : Playfield
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ HitObjectContainer,
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
new file mode 100644
index 0000000000..cfe2bd1cb2
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.1
+ osu.Game.Rulesets.Sample
+ Library
+ AnyCPU
+ osu.Game.Rulesets.EmptyFreeform
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/.editorconfig b/Templates/Rulesets/ruleset-example/.editorconfig
new file mode 100644
index 0000000000..f3badda9b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/.editorconfig
@@ -0,0 +1,200 @@
+# EditorConfig is awesome: http://editorconfig.org
+root = true
+
+[*.cs]
+end_of_line = crlf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+#Roslyn naming styles
+
+#PascalCase for public and protected members
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.public_members_pascalcase.severity = error
+dotnet_naming_rule.public_members_pascalcase.symbols = public_members
+dotnet_naming_rule.public_members_pascalcase.style = pascalcase
+
+#camelCase for private members
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_symbols.private_members.applicable_accessibilities = private
+dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.private_members_camelcase.severity = warning
+dotnet_naming_rule.private_members_camelcase.symbols = private_members
+dotnet_naming_rule.private_members_camelcase.style = camelcase
+
+dotnet_naming_symbols.local_function.applicable_kinds = local_function
+dotnet_naming_rule.local_function_camelcase.severity = warning
+dotnet_naming_rule.local_function_camelcase.symbols = local_function
+dotnet_naming_rule.local_function_camelcase.style = camelcase
+
+#all_lower for private and local constants/static readonlys
+dotnet_naming_style.all_lower.capitalization = all_lower
+dotnet_naming_style.all_lower.word_separator = _
+
+dotnet_naming_symbols.private_constants.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants.required_modifiers = const
+dotnet_naming_symbols.private_constants.applicable_kinds = field
+dotnet_naming_rule.private_const_all_lower.severity = warning
+dotnet_naming_rule.private_const_all_lower.symbols = private_constants
+dotnet_naming_rule.private_const_all_lower.style = all_lower
+
+dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly.applicable_kinds = field
+dotnet_naming_rule.private_static_readonly_all_lower.severity = warning
+dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly
+dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+dotnet_naming_rule.local_const_all_lower.severity = warning
+dotnet_naming_rule.local_const_all_lower.symbols = local_constants
+dotnet_naming_rule.local_const_all_lower.style = all_lower
+
+#ALL_UPPER for non private constants/static readonlys
+dotnet_naming_style.all_upper.capitalization = all_upper
+dotnet_naming_style.all_upper.word_separator = _
+
+dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_constants.required_modifiers = const
+dotnet_naming_symbols.public_constants.applicable_kinds = field
+dotnet_naming_rule.public_const_all_upper.severity = warning
+dotnet_naming_rule.public_const_all_upper.symbols = public_constants
+dotnet_naming_rule.public_const_all_upper.style = all_upper
+
+dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.public_static_readonly.applicable_kinds = field
+dotnet_naming_rule.public_static_readonly_all_upper.severity = warning
+dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly
+dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper
+
+#Roslyn formating options
+
+#Formatting - indentation options
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+#Formatting - new line options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_open_brace = all
+#csharp_new_line_before_members_in_anonymous_types = true
+#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing
+csharp_new_line_between_query_expression_clauses = true
+
+#Formatting - organize using options
+dotnet_sort_system_directives_first = true
+
+#Formatting - spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+
+#Formatting - wrapping options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#Roslyn language styles
+
+#Style - this. qualification
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+#Style - type names
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+csharp_style_var_when_type_is_apparent = true:none
+csharp_style_var_for_built_in_types = true:none
+csharp_style_var_elsewhere = true:silent
+
+#Style - modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning
+
+#Style - parentheses
+# Skipped because roslyn cannot separate +-*/ with << >>
+
+#Style - expression bodies
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
+
+#Style - expression preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:warning
+
+#Style - null/type checks
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:warning
+
+#Style - unused
+dotnet_style_readonly_field = true:silent
+dotnet_code_quality_unused_parameters = non_public:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_unused_value_assignment_preference = discard_variable:warning
+
+#Style - variable declaration
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+
+#Style - other C# 7.x features
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+
+#Style - C# 8 features
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:silent
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_switch_expression = false:none
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Private method is unused
+dotnet_diagnostic.IDE0051.severity = silent
+#Private member is unused
+dotnet_diagnostic.IDE0052.severity = silent
+
+#Rules for disposable
+dotnet_diagnostic.IDE0067.severity = none
+dotnet_diagnostic.IDE0068.severity = none
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/.gitignore b/Templates/Rulesets/ruleset-example/.gitignore
new file mode 100644
index 0000000000..940794e60f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/.gitignore
@@ -0,0 +1,288 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
diff --git a/Templates/Rulesets/ruleset-example/.template.config/template.json b/Templates/Rulesets/ruleset-example/.template.config/template.json
new file mode 100644
index 0000000000..5d2f6f1ebd
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/.template.config/template.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "ppy Pty Ltd",
+ "classifications": [
+ "Console"
+ ],
+ "name": "osu! ruleset (pippidon example)",
+ "identity": "ppy.osu.Game.Templates.Rulesets.Pippidon",
+ "shortName": "ruleset-example",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "sourceName": "Pippidon",
+ "preferNameDirectory": true
+}
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
new file mode 100644
index 0000000000..bd9db14259
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "VisualTests (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Debug)",
+ "env": {},
+ "console": "internalConsole"
+ },
+ {
+ "name": "VisualTests (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Release)",
+ "env": {},
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json
new file mode 100644
index 0000000000..0ee07c1036
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json
@@ -0,0 +1,47 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "restore"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
new file mode 100644
index 0000000000..270d906b01
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ public class TestSceneOsuGame : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuGameBase gameBase)
+ {
+ OsuGame game = new OsuGame();
+ game.SetHost(host);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ },
+ game
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs
new file mode 100644
index 0000000000..f00528900c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ [TestFixture]
+ public class TestSceneOsuPlayer : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new PippidonRuleset();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
new file mode 100644
index 0000000000..fd6bd9b714
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework;
+using osu.Framework.Platform;
+using osu.Game.Tests;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ public static class VisualTestRunner
+ {
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ {
+ host.Run(new OsuTestBrowser());
+ return 0;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
new file mode 100644
index 0000000000..d7c116411a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+ osu.Game.Rulesets.Pippidon.Tests.VisualTestRunner
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ WinExe
+ net5.0
+ osu.Game.Rulesets.Pippidon.Tests
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln
new file mode 100644
index 0000000000..bccffcd7ff
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln
@@ -0,0 +1,96 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29123.88
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Pippidon", "osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon.Tests", "osu.Game.Rulesets.Pippidon.Tests\osu.Game.Rulesets.Pippidon.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ VisualTests|Any CPU = VisualTests|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668}
+ EndGlobalSection
+ GlobalSection(MonoDevelopProperties) = preSolution
+ Policies = $0
+ $0.TextStylePolicy = $1
+ $1.EolMarker = Windows
+ $1.inheritsSet = VisualStudio
+ $1.inheritsScope = text/plain
+ $1.scope = text/x-csharp
+ $0.CSharpFormattingPolicy = $2
+ $2.IndentSwitchSection = True
+ $2.NewLinesForBracesInProperties = True
+ $2.NewLinesForBracesInAccessors = True
+ $2.NewLinesForBracesInAnonymousMethods = True
+ $2.NewLinesForBracesInControlBlocks = True
+ $2.NewLinesForBracesInAnonymousTypes = True
+ $2.NewLinesForBracesInObjectCollectionArrayInitializers = True
+ $2.NewLinesForBracesInLambdaExpressionBody = True
+ $2.NewLineForElse = True
+ $2.NewLineForCatch = True
+ $2.NewLineForFinally = True
+ $2.NewLineForMembersInObjectInit = True
+ $2.NewLineForMembersInAnonymousTypes = True
+ $2.NewLineForClausesInQuery = True
+ $2.SpacingAfterMethodDeclarationName = False
+ $2.SpaceAfterMethodCallName = False
+ $2.SpaceBeforeOpenSquareBracket = False
+ $2.inheritsSet = Mono
+ $2.inheritsScope = text/x-csharp
+ $2.scope = text/x-csharp
+ EndGlobalSection
+ GlobalSection(MonoDevelopProperties) = preSolution
+ Policies = $0
+ $0.TextStylePolicy = $1
+ $1.EolMarker = Windows
+ $1.inheritsSet = VisualStudio
+ $1.inheritsScope = text/plain
+ $1.scope = text/x-csharp
+ $0.CSharpFormattingPolicy = $2
+ $2.IndentSwitchSection = True
+ $2.NewLinesForBracesInProperties = True
+ $2.NewLinesForBracesInAccessors = True
+ $2.NewLinesForBracesInAnonymousMethods = True
+ $2.NewLinesForBracesInControlBlocks = True
+ $2.NewLinesForBracesInAnonymousTypes = True
+ $2.NewLinesForBracesInObjectCollectionArrayInitializers = True
+ $2.NewLinesForBracesInLambdaExpressionBody = True
+ $2.NewLineForElse = True
+ $2.NewLineForCatch = True
+ $2.NewLineForFinally = True
+ $2.NewLineForMembersInObjectInit = True
+ $2.NewLineForMembersInAnonymousTypes = True
+ $2.NewLineForClausesInQuery = True
+ $2.SpacingAfterMethodDeclarationName = False
+ $2.SpaceAfterMethodCallName = False
+ $2.SpaceBeforeOpenSquareBracket = False
+ $2.inheritsSet = Mono
+ $2.inheritsScope = text/x-csharp
+ $2.scope = text/x-csharp
+ EndGlobalSection
+EndGlobal
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings
new file mode 100644
index 0000000000..aa8f8739c1
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings
@@ -0,0 +1,934 @@
+
+ True
+ True
+ True
+ True
+ ExplicitlyExcluded
+ ExplicitlyExcluded
+ SOLUTION
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ True
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ SUGGESTION
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ ERROR
+ WARNING
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ ERROR
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+
+ True
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile>
+ Code Cleanup (peppy)
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ Explicit
+ ExpressionBody
+ BlockBody
+ True
+ NEXT_LINE
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ NEXT_LINE
+ MULTILINE
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ True
+ NEXT_LINE
+ NEVER
+ NEVER
+ True
+ False
+ True
+ NEVER
+ False
+ False
+ True
+ False
+ False
+ True
+ True
+ False
+ False
+ CHOP_IF_LONG
+ True
+ 200
+ CHOP_IF_LONG
+ False
+ False
+ AABB
+ API
+ BPM
+ GC
+ GL
+ GLSL
+ HID
+ HTML
+ HUD
+ ID
+ IL
+ IOS
+ IP
+ IPC
+ JIT
+ LTRB
+ MD5
+ NS
+ OS
+ PM
+ RGB
+ RNG
+ SHA
+ SRGB
+ TK
+ SS
+ PP
+ GMT
+ QAT
+ BNG
+ UI
+ False
+ HINT
+ <?xml version="1.0" encoding="utf-16"?>
+<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
+ <TypePattern DisplayName="COM interfaces or structs">
+ <TypePattern.Match>
+ <Or>
+ <And>
+ <Kind Is="Interface" />
+ <Or>
+ <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" />
+ <HasAttribute Name="System.Runtime.InteropServices.ComImport" />
+ </Or>
+ </And>
+ <Kind Is="Struct" />
+ </Or>
+ </TypePattern.Match>
+ </TypePattern>
+ <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All">
+ <TypePattern.Match>
+ <And>
+ <Kind Is="Class" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" />
+ </And>
+ </TypePattern.Match>
+ <Entry DisplayName="Setup/Teardown Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <Or>
+ <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" />
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="All other members" />
+ <Entry Priority="100" DisplayName="Test Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="NUnit.Framework.TestAttribute" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ </TypePattern>
+ <TypePattern DisplayName="Default Pattern">
+ <Group DisplayName="Fields/Properties">
+ <Group DisplayName="Public Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Public Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Internal Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Internal Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Protected Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Protected Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Private Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Private Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Constructor/Destructor">
+ <Entry DisplayName="Ctor">
+ <Entry.Match>
+ <Kind Is="Constructor" />
+ </Entry.Match>
+ </Entry>
+ <Region Name="Disposal">
+ <Entry DisplayName="Dtor">
+ <Entry.Match>
+ <Kind Is="Destructor" />
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Dispose()">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Method" />
+ <Name Is="Dispose" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Dispose(true)">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Or>
+ <Virtual />
+ <Override />
+ </Or>
+ <Kind Is="Method" />
+ <Name Is="Dispose" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Region>
+ </Group>
+ <Group DisplayName="Methods">
+ <Group DisplayName="Public">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Internal">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Protected">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Private">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ </Group>
+ </TypePattern>
+</Patterns>
+ Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+See the LICENCE file in the repository root for full licence text.
+
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
+ <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ TestFolder
+ True
+ True
+ o!f – Object Initializer: Anchor&Origin
+ True
+ constant("Centre")
+ 0
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofao
+ True
+ Anchor = Anchor.$anchor$,
+Origin = Anchor.$anchor$,
+ True
+ True
+ o!f – InternalChildren = []
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofic
+ True
+ InternalChildren = new Drawable[]
+{
+ $END$
+};
+ True
+ True
+ o!f – new GridContainer { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofgc
+ True
+ new GridContainer
+{
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
+ {
+ new Drawable[] { $END$ },
+ new Drawable[] { }
+ }
+};
+ True
+ True
+ o!f – new FillFlowContainer { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ offf
+ True
+ new FillFlowContainer
+{
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ $END$
+ }
+},
+ True
+ True
+ o!f – new Container { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofcont
+ True
+ new Container
+{
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ $END$
+ }
+},
+ True
+ True
+ o!f – BackgroundDependencyLoader load()
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofbdl
+ True
+ [BackgroundDependencyLoader]
+private void load()
+{
+ $END$
+}
+ True
+ True
+ o!f – new Box { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofbox
+ True
+ new Box
+{
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+},
+ True
+ True
+ o!f – Children = []
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofc
+ True
+ Children = new Drawable[]
+{
+ $END$
+};
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
new file mode 100644
index 0000000000..a2a4784603
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.Beatmaps
+{
+ public class PippidonBeatmapConverter : BeatmapConverter
+ {
+ public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
+ {
+ }
+
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition);
+
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
+ {
+ yield return new PippidonHitObject
+ {
+ Samples = original.Samples,
+ StartTime = original.StartTime,
+ Position = (original as IHasPosition)?.Position ?? Vector2.Zero,
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
new file mode 100644
index 0000000000..4565c97d1a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+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..290148d14b
--- /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, double clockRate) => new Skill[0];
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs
new file mode 100644
index 0000000000..aa7fa3188b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.ComponentModel;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonInputManager : RulesetInputManager
+ {
+ public PippidonInputManager(RulesetInfo ruleset)
+ : base(ruleset, 0, SimultaneousBindingMode.Unique)
+ {
+ }
+ }
+
+ public enum PippidonAction
+ {
+ [Description("Button 1")]
+ Button1,
+
+ [Description("Button 2")]
+ Button2,
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs
new file mode 100644
index 0000000000..89fed791cd
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs
@@ -0,0 +1,57 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Pippidon.Beatmaps;
+using osu.Game.Rulesets.Pippidon.Mods;
+using osu.Game.Rulesets.Pippidon.UI;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonRuleset : Ruleset
+ {
+ public override string Description => "gather the osu!coins";
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) =>
+ new DrawablePippidonRuleset(this, beatmap, mods);
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) =>
+ new PippidonBeatmapConverter(beatmap, this);
+
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) =>
+ new PippidonDifficultyCalculator(this, beatmap);
+
+ public override IEnumerable GetModsFor(ModType type)
+ {
+ switch (type)
+ {
+ case ModType.Automation:
+ return new[] { new PippidonModAutoplay() };
+
+ default:
+ return new Mod[] { null };
+ }
+ }
+
+ public override string ShortName => "pippidon";
+
+ public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[]
+ {
+ new KeyBinding(InputKey.Z, PippidonAction.Button1),
+ new KeyBinding(InputKey.X, PippidonAction.Button2),
+ };
+
+ public override Drawable CreateIcon() => new Sprite
+ {
+ Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
new file mode 100644
index 0000000000..612288257d
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonAutoGenerator : AutoGenerator
+ {
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public PippidonAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ protected override void GenerateFrames()
+ {
+ Frames.Add(new PippidonReplayFrame());
+
+ foreach (PippidonHitObject hitObject in Beatmap.HitObjects)
+ {
+ Frames.Add(new PippidonReplayFrame
+ {
+ Time = hitObject.StartTime,
+ Position = hitObject.Position,
+ });
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs
new file mode 100644
index 0000000000..e005346e1e
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Input.StateChanges;
+using osu.Framework.Utils;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonFramedReplayInputHandler : FramedReplayInputHandler
+ {
+ public PippidonFramedReplayInputHandler(Replay replay)
+ : base(replay)
+ {
+ }
+
+ protected override bool IsImportant(PippidonReplayFrame frame) => true;
+
+ public override void CollectPendingInputs(List inputs)
+ {
+ var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
+
+ inputs.Add(new MousePositionAbsoluteInput
+ {
+ Position = GamefieldToScreenSpace(position)
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs
new file mode 100644
index 0000000000..949ca160be
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Replays;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonReplayFrame : ReplayFrame
+ {
+ public Vector2 Position;
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3
new file mode 100644
index 0000000000..90b13d1f73
Binary files /dev/null and b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 differ
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png
new file mode 100644
index 0000000000..e79d2528ec
Binary files /dev/null and b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png differ
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png
new file mode 100644
index 0000000000..3cd89c6ce6
Binary files /dev/null and b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png differ
diff --git a/osu.Game/Rulesets/Replays/IAutoGenerator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs
similarity index 55%
rename from osu.Game/Rulesets/Replays/IAutoGenerator.cs
rename to Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs
index b1905e2b6f..1c4fe698c2 100644
--- a/osu.Game/Rulesets/Replays/IAutoGenerator.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs
@@ -1,12 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Replays;
+using osu.Game.Rulesets.Scoring;
-namespace osu.Game.Rulesets.Replays
+namespace osu.Game.Rulesets.Pippidon.Scoring
{
- public interface IAutoGenerator
+ public class PippidonScoreProcessor : ScoreProcessor
{
- Replay Generate();
}
}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs
new file mode 100644
index 0000000000..d923963bef
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Input;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Handlers;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.Objects.Drawables;
+using osu.Game.Rulesets.Pippidon.Replays;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ [Cached]
+ public class DrawablePippidonRuleset : DrawableRuleset
+ {
+ public DrawablePippidonRuleset(PippidonRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ }
+
+ public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new PippidonPlayfieldAdjustmentContainer();
+
+ protected override Playfield CreatePlayfield() => new PippidonPlayfield();
+
+ protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new PippidonFramedReplayInputHandler(replay);
+
+ public override DrawableHitObject CreateDrawableRepresentation(PippidonHitObject h) => new DrawablePippidonHitObject(h);
+
+ protected override PassThroughInputManager CreateInputManager() => new PippidonInputManager(Ruleset?.RulesetInfo);
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonCursorContainer.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonCursorContainer.cs
new file mode 100644
index 0000000000..9de3f4ba14
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonCursorContainer.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ public class PippidonCursorContainer : GameplayCursorContainer
+ {
+ private Sprite cursorSprite;
+ private Texture cursorTexture;
+
+ protected override Drawable CreateCursor() => cursorSprite = new Sprite
+ {
+ Scale = new Vector2(0.5f),
+ Origin = Anchor.Centre,
+ Texture = cursorTexture,
+ };
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ cursorTexture = textures.Get("character");
+
+ if (cursorSprite != null)
+ cursorSprite.Texture = cursorTexture;
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
new file mode 100644
index 0000000000..b5a97c5ea3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ [Cached]
+ public class PippidonPlayfield : Playfield
+ {
+ protected override GameplayCursorContainer CreateCursor() => new PippidonCursorContainer();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ HitObjectContainer,
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfieldAdjustmentContainer.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfieldAdjustmentContainer.cs
new file mode 100644
index 0000000000..9236683827
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfieldAdjustmentContainer.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ public class PippidonPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
+ {
+ public PippidonPlayfieldAdjustmentContainer()
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Size = new Vector2(0.8f);
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
new file mode 100644
index 0000000000..61b859f45b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.1
+ osu.Game.Rulesets.Sample
+ Library
+ AnyCPU
+ osu.Game.Rulesets.Pippidon
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig
new file mode 100644
index 0000000000..f3badda9b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig
@@ -0,0 +1,200 @@
+# EditorConfig is awesome: http://editorconfig.org
+root = true
+
+[*.cs]
+end_of_line = crlf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+#Roslyn naming styles
+
+#PascalCase for public and protected members
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.public_members_pascalcase.severity = error
+dotnet_naming_rule.public_members_pascalcase.symbols = public_members
+dotnet_naming_rule.public_members_pascalcase.style = pascalcase
+
+#camelCase for private members
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_symbols.private_members.applicable_accessibilities = private
+dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.private_members_camelcase.severity = warning
+dotnet_naming_rule.private_members_camelcase.symbols = private_members
+dotnet_naming_rule.private_members_camelcase.style = camelcase
+
+dotnet_naming_symbols.local_function.applicable_kinds = local_function
+dotnet_naming_rule.local_function_camelcase.severity = warning
+dotnet_naming_rule.local_function_camelcase.symbols = local_function
+dotnet_naming_rule.local_function_camelcase.style = camelcase
+
+#all_lower for private and local constants/static readonlys
+dotnet_naming_style.all_lower.capitalization = all_lower
+dotnet_naming_style.all_lower.word_separator = _
+
+dotnet_naming_symbols.private_constants.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants.required_modifiers = const
+dotnet_naming_symbols.private_constants.applicable_kinds = field
+dotnet_naming_rule.private_const_all_lower.severity = warning
+dotnet_naming_rule.private_const_all_lower.symbols = private_constants
+dotnet_naming_rule.private_const_all_lower.style = all_lower
+
+dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly.applicable_kinds = field
+dotnet_naming_rule.private_static_readonly_all_lower.severity = warning
+dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly
+dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+dotnet_naming_rule.local_const_all_lower.severity = warning
+dotnet_naming_rule.local_const_all_lower.symbols = local_constants
+dotnet_naming_rule.local_const_all_lower.style = all_lower
+
+#ALL_UPPER for non private constants/static readonlys
+dotnet_naming_style.all_upper.capitalization = all_upper
+dotnet_naming_style.all_upper.word_separator = _
+
+dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_constants.required_modifiers = const
+dotnet_naming_symbols.public_constants.applicable_kinds = field
+dotnet_naming_rule.public_const_all_upper.severity = warning
+dotnet_naming_rule.public_const_all_upper.symbols = public_constants
+dotnet_naming_rule.public_const_all_upper.style = all_upper
+
+dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.public_static_readonly.applicable_kinds = field
+dotnet_naming_rule.public_static_readonly_all_upper.severity = warning
+dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly
+dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper
+
+#Roslyn formating options
+
+#Formatting - indentation options
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+#Formatting - new line options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_open_brace = all
+#csharp_new_line_before_members_in_anonymous_types = true
+#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing
+csharp_new_line_between_query_expression_clauses = true
+
+#Formatting - organize using options
+dotnet_sort_system_directives_first = true
+
+#Formatting - spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+
+#Formatting - wrapping options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#Roslyn language styles
+
+#Style - this. qualification
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+#Style - type names
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+csharp_style_var_when_type_is_apparent = true:none
+csharp_style_var_for_built_in_types = true:none
+csharp_style_var_elsewhere = true:silent
+
+#Style - modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning
+
+#Style - parentheses
+# Skipped because roslyn cannot separate +-*/ with << >>
+
+#Style - expression bodies
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
+
+#Style - expression preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:warning
+
+#Style - null/type checks
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:warning
+
+#Style - unused
+dotnet_style_readonly_field = true:silent
+dotnet_code_quality_unused_parameters = non_public:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_unused_value_assignment_preference = discard_variable:warning
+
+#Style - variable declaration
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+
+#Style - other C# 7.x features
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+
+#Style - C# 8 features
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:silent
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_switch_expression = false:none
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Private method is unused
+dotnet_diagnostic.IDE0051.severity = silent
+#Private member is unused
+dotnet_diagnostic.IDE0052.severity = silent
+
+#Rules for disposable
+dotnet_diagnostic.IDE0067.severity = none
+dotnet_diagnostic.IDE0068.severity = none
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.gitignore b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore
new file mode 100644
index 0000000000..940794e60f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore
@@ -0,0 +1,288 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.template.config/template.json b/Templates/Rulesets/ruleset-scrolling-empty/.template.config/template.json
new file mode 100644
index 0000000000..3eb99a1f9d
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/.template.config/template.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "ppy Pty Ltd",
+ "classifications": [
+ "Console"
+ ],
+ "name": "osu! ruleset (scrolling)",
+ "identity": "ppy.osu.Game.Templates.Rulesets.Scrolling",
+ "shortName": "ruleset-scrolling",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "sourceName": "EmptyScrolling",
+ "preferNameDirectory": true
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json
new file mode 100644
index 0000000000..24e4873ed6
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "VisualTests (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Debug)",
+ "env": {},
+ "console": "internalConsole"
+ },
+ {
+ "name": "VisualTests (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Release)",
+ "env": {},
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/tasks.json
new file mode 100644
index 0000000000..00d0dc7d9b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/tasks.json
@@ -0,0 +1,47 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "restore"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
new file mode 100644
index 0000000000..aed6abb6bf
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Tests
+{
+ public class TestSceneOsuGame : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuGameBase gameBase)
+ {
+ OsuGame game = new OsuGame();
+ game.SetHost(host);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ },
+ game
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuPlayer.cs
new file mode 100644
index 0000000000..9460576196
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuPlayer.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Tests
+{
+ [TestFixture]
+ public class TestSceneOsuPlayer : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new EmptyScrollingRuleset();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs
new file mode 100644
index 0000000000..65cfb2bff4
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework;
+using osu.Framework.Platform;
+using osu.Game.Tests;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Tests
+{
+ public static class VisualTestRunner
+ {
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ {
+ host.Run(new OsuTestBrowser());
+ return 0;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
new file mode 100644
index 0000000000..89b551286b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+ osu.Game.Rulesets.EmptyScrolling.Tests.VisualTestRunner
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ WinExe
+ net5.0
+ osu.Game.Rulesets.EmptyScrolling.Tests
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln
new file mode 100644
index 0000000000..97361e1a7b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln
@@ -0,0 +1,96 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29123.88
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.EmptyScrolling", "osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.EmptyScrolling.Tests", "osu.Game.Rulesets.EmptyScrolling.Tests\osu.Game.Rulesets.EmptyScrolling.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ VisualTests|Any CPU = VisualTests|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668}
+ EndGlobalSection
+ GlobalSection(MonoDevelopProperties) = preSolution
+ Policies = $0
+ $0.TextStylePolicy = $1
+ $1.EolMarker = Windows
+ $1.inheritsSet = VisualStudio
+ $1.inheritsScope = text/plain
+ $1.scope = text/x-csharp
+ $0.CSharpFormattingPolicy = $2
+ $2.IndentSwitchSection = True
+ $2.NewLinesForBracesInProperties = True
+ $2.NewLinesForBracesInAccessors = True
+ $2.NewLinesForBracesInAnonymousMethods = True
+ $2.NewLinesForBracesInControlBlocks = True
+ $2.NewLinesForBracesInAnonymousTypes = True
+ $2.NewLinesForBracesInObjectCollectionArrayInitializers = True
+ $2.NewLinesForBracesInLambdaExpressionBody = True
+ $2.NewLineForElse = True
+ $2.NewLineForCatch = True
+ $2.NewLineForFinally = True
+ $2.NewLineForMembersInObjectInit = True
+ $2.NewLineForMembersInAnonymousTypes = True
+ $2.NewLineForClausesInQuery = True
+ $2.SpacingAfterMethodDeclarationName = False
+ $2.SpaceAfterMethodCallName = False
+ $2.SpaceBeforeOpenSquareBracket = False
+ $2.inheritsSet = Mono
+ $2.inheritsScope = text/x-csharp
+ $2.scope = text/x-csharp
+ EndGlobalSection
+ GlobalSection(MonoDevelopProperties) = preSolution
+ Policies = $0
+ $0.TextStylePolicy = $1
+ $1.EolMarker = Windows
+ $1.inheritsSet = VisualStudio
+ $1.inheritsScope = text/plain
+ $1.scope = text/x-csharp
+ $0.CSharpFormattingPolicy = $2
+ $2.IndentSwitchSection = True
+ $2.NewLinesForBracesInProperties = True
+ $2.NewLinesForBracesInAccessors = True
+ $2.NewLinesForBracesInAnonymousMethods = True
+ $2.NewLinesForBracesInControlBlocks = True
+ $2.NewLinesForBracesInAnonymousTypes = True
+ $2.NewLinesForBracesInObjectCollectionArrayInitializers = True
+ $2.NewLinesForBracesInLambdaExpressionBody = True
+ $2.NewLineForElse = True
+ $2.NewLineForCatch = True
+ $2.NewLineForFinally = True
+ $2.NewLineForMembersInObjectInit = True
+ $2.NewLineForMembersInAnonymousTypes = True
+ $2.NewLineForClausesInQuery = True
+ $2.SpacingAfterMethodDeclarationName = False
+ $2.SpaceAfterMethodCallName = False
+ $2.SpaceBeforeOpenSquareBracket = False
+ $2.inheritsSet = Mono
+ $2.inheritsScope = text/x-csharp
+ $2.scope = text/x-csharp
+ EndGlobalSection
+EndGlobal
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings
new file mode 100644
index 0000000000..aa8f8739c1
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings
@@ -0,0 +1,934 @@
+
+ True
+ True
+ True
+ True
+ ExplicitlyExcluded
+ ExplicitlyExcluded
+ SOLUTION
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ True
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ SUGGESTION
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ ERROR
+ WARNING
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ ERROR
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+
+ True
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile>
+ Code Cleanup (peppy)
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ Explicit
+ ExpressionBody
+ BlockBody
+ True
+ NEXT_LINE
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ NEXT_LINE
+ MULTILINE
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ True
+ NEXT_LINE
+ NEVER
+ NEVER
+ True
+ False
+ True
+ NEVER
+ False
+ False
+ True
+ False
+ False
+ True
+ True
+ False
+ False
+ CHOP_IF_LONG
+ True
+ 200
+ CHOP_IF_LONG
+ False
+ False
+ AABB
+ API
+ BPM
+ GC
+ GL
+ GLSL
+ HID
+ HTML
+ HUD
+ ID
+ IL
+ IOS
+ IP
+ IPC
+ JIT
+ LTRB
+ MD5
+ NS
+ OS
+ PM
+ RGB
+ RNG
+ SHA
+ SRGB
+ TK
+ SS
+ PP
+ GMT
+ QAT
+ BNG
+ UI
+ False
+ HINT
+ <?xml version="1.0" encoding="utf-16"?>
+<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
+ <TypePattern DisplayName="COM interfaces or structs">
+ <TypePattern.Match>
+ <Or>
+ <And>
+ <Kind Is="Interface" />
+ <Or>
+ <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" />
+ <HasAttribute Name="System.Runtime.InteropServices.ComImport" />
+ </Or>
+ </And>
+ <Kind Is="Struct" />
+ </Or>
+ </TypePattern.Match>
+ </TypePattern>
+ <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All">
+ <TypePattern.Match>
+ <And>
+ <Kind Is="Class" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" />
+ </And>
+ </TypePattern.Match>
+ <Entry DisplayName="Setup/Teardown Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <Or>
+ <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" />
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="All other members" />
+ <Entry Priority="100" DisplayName="Test Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="NUnit.Framework.TestAttribute" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ </TypePattern>
+ <TypePattern DisplayName="Default Pattern">
+ <Group DisplayName="Fields/Properties">
+ <Group DisplayName="Public Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Public Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Internal Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Internal Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Protected Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Protected Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Private Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Private Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Constructor/Destructor">
+ <Entry DisplayName="Ctor">
+ <Entry.Match>
+ <Kind Is="Constructor" />
+ </Entry.Match>
+ </Entry>
+ <Region Name="Disposal">
+ <Entry DisplayName="Dtor">
+ <Entry.Match>
+ <Kind Is="Destructor" />
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Dispose()">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Method" />
+ <Name Is="Dispose" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Dispose(true)">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Or>
+ <Virtual />
+ <Override />
+ </Or>
+ <Kind Is="Method" />
+ <Name Is="Dispose" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Region>
+ </Group>
+ <Group DisplayName="Methods">
+ <Group DisplayName="Public">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Internal">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Protected">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Private">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ </Group>
+ </TypePattern>
+</Patterns>
+ Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+See the LICENCE file in the repository root for full licence text.
+
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
+ <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ TestFolder
+ True
+ True
+ o!f – Object Initializer: Anchor&Origin
+ True
+ constant("Centre")
+ 0
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofao
+ True
+ Anchor = Anchor.$anchor$,
+Origin = Anchor.$anchor$,
+ True
+ True
+ o!f – InternalChildren = []
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofic
+ True
+ InternalChildren = new Drawable[]
+{
+ $END$
+};
+ True
+ True
+ o!f – new GridContainer { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofgc
+ True
+ new GridContainer
+{
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
+ {
+ new Drawable[] { $END$ },
+ new Drawable[] { }
+ }
+};
+ True
+ True
+ o!f – new FillFlowContainer { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ offf
+ True
+ new FillFlowContainer
+{
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ $END$
+ }
+},
+ True
+ True
+ o!f – new Container { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofcont
+ True
+ new Container
+{
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ $END$
+ }
+},
+ True
+ True
+ o!f – BackgroundDependencyLoader load()
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofbdl
+ True
+ [BackgroundDependencyLoader]
+private void load()
+{
+ $END$
+}
+ True
+ True
+ o!f – new Box { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofbox
+ True
+ new Box
+{
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+},
+ True
+ True
+ o!f – Children = []
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofc
+ True
+ Children = new Drawable[]
+{
+ $END$
+};
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Beatmaps/EmptyScrollingBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Beatmaps/EmptyScrollingBeatmapConverter.cs
new file mode 100644
index 0000000000..02fb9a9dd5
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Beatmaps/EmptyScrollingBeatmapConverter.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.EmptyScrolling.Objects;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Beatmaps
+{
+ public class EmptyScrollingBeatmapConverter : BeatmapConverter
+ {
+ public EmptyScrollingBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
+ {
+ }
+
+ // todo: Check for conversion types that should be supported (ie. Beatmap.HitObjects.Any(h => h is IHasXPosition))
+ // https://github.com/ppy/osu/tree/master/osu.Game/Rulesets/Objects/Types
+ public override bool CanConvert() => true;
+
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
+ {
+ yield return new EmptyScrollingHitObject
+ {
+ Samples = original.Samples,
+ StartTime = original.StartTime,
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
new file mode 100644
index 0000000000..f557a4c754
--- /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, double clockRate) => 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..431994e098
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.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.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.EmptyScrolling.Replays;
+using osu.Game.Scoring;
+using osu.Game.Users;
+using System.Collections.Generic;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Mods
+{
+ public class EmptyScrollingModAutoplay : ModAutoplay
+ {
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
+ {
+ ScoreInfo = new ScoreInfo
+ {
+ User = new User { Username = "sample" },
+ },
+ Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs
new file mode 100644
index 0000000000..b5ff0cde7c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Objects.Drawables
+{
+ public class DrawableEmptyScrollingHitObject : DrawableHitObject
+ {
+ public DrawableEmptyScrollingHitObject(EmptyScrollingHitObject hitObject)
+ : base(hitObject)
+ {
+ Size = new Vector2(40);
+ Origin = Anchor.Centre;
+
+ // todo: add visuals.
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ if (timeOffset >= 0)
+ // todo: implement judgement logic
+ ApplyResult(r => r.Type = HitResult.Perfect);
+ }
+
+ protected override void UpdateHitStateTransforms(ArmedState state)
+ {
+ const double duration = 1000;
+
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.FadeOut(duration, Easing.OutQuint).Expire();
+ break;
+
+ case ArmedState.Miss:
+
+ this.FadeColour(Color4.Red, duration);
+ this.FadeOut(duration, Easing.InQuint).Expire();
+ break;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/EmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/EmptyScrollingHitObject.cs
new file mode 100644
index 0000000000..9b469be496
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/EmptyScrollingHitObject.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Objects
+{
+ public class EmptyScrollingHitObject : HitObject
+ {
+ public override Judgement CreateJudgement() => new Judgement();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs
new file mode 100644
index 0000000000..1058f756f3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.EmptyScrolling.Objects;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Replays
+{
+ public class EmptyScrollingAutoGenerator : AutoGenerator
+ {
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public EmptyScrollingAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ protected override void GenerateFrames()
+ {
+ Frames.Add(new EmptyScrollingReplayFrame());
+
+ foreach (EmptyScrollingHitObject hitObject in Beatmap.HitObjects)
+ {
+ Frames.Add(new EmptyScrollingReplayFrame
+ {
+ Time = hitObject.StartTime
+ // todo: add required inputs and extra frames.
+ });
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs
new file mode 100644
index 0000000000..4b998cfca3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Input.StateChanges;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Replays
+{
+ public class EmptyScrollingFramedReplayInputHandler : FramedReplayInputHandler
+ {
+ public EmptyScrollingFramedReplayInputHandler(Replay replay)
+ : base(replay)
+ {
+ }
+
+ protected override bool IsImportant(EmptyScrollingReplayFrame frame) => frame.Actions.Any();
+
+ public override void CollectPendingInputs(List inputs)
+ {
+ inputs.Add(new ReplayState
+ {
+ PressedActions = CurrentFrame?.Actions ?? new List(),
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs
new file mode 100644
index 0000000000..2f19cffd2a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.EmptyScrolling.Replays
+{
+ public class EmptyScrollingReplayFrame : ReplayFrame
+ {
+ public List Actions = new List();
+
+ public EmptyScrollingReplayFrame(EmptyScrollingAction? button = null)
+ {
+ if (button.HasValue)
+ Actions.Add(button.Value);
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/DrawableEmptyScrollingRuleset.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/DrawableEmptyScrollingRuleset.cs
new file mode 100644
index 0000000000..620a4abc51
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/DrawableEmptyScrollingRuleset.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Input;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Handlers;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.EmptyScrolling.Objects;
+using osu.Game.Rulesets.EmptyScrolling.Objects.Drawables;
+using osu.Game.Rulesets.EmptyScrolling.Replays;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.EmptyScrolling.UI
+{
+ [Cached]
+ public class DrawableEmptyScrollingRuleset : DrawableScrollingRuleset
+ {
+ public DrawableEmptyScrollingRuleset(EmptyScrollingRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ Direction.Value = ScrollingDirection.Left;
+ TimeRange.Value = 6000;
+ }
+
+ protected override Playfield CreatePlayfield() => new EmptyScrollingPlayfield();
+
+ protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new EmptyScrollingFramedReplayInputHandler(replay);
+
+ public override DrawableHitObject CreateDrawableRepresentation(EmptyScrollingHitObject h) => new DrawableEmptyScrollingHitObject(h);
+
+ protected override PassThroughInputManager CreateInputManager() => new EmptyScrollingInputManager(Ruleset?.RulesetInfo);
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/EmptyScrollingPlayfield.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/EmptyScrollingPlayfield.cs
new file mode 100644
index 0000000000..56620e44b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/UI/EmptyScrollingPlayfield.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.EmptyScrolling.UI
+{
+ [Cached]
+ public class EmptyScrollingPlayfield : ScrollingPlayfield
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ HitObjectContainer,
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
new file mode 100644
index 0000000000..9dce3c9a0a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.1
+ osu.Game.Rulesets.Sample
+ Library
+ AnyCPU
+ osu.Game.Rulesets.EmptyScrolling
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig
new file mode 100644
index 0000000000..f3badda9b3
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig
@@ -0,0 +1,200 @@
+# EditorConfig is awesome: http://editorconfig.org
+root = true
+
+[*.cs]
+end_of_line = crlf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+#Roslyn naming styles
+
+#PascalCase for public and protected members
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.public_members_pascalcase.severity = error
+dotnet_naming_rule.public_members_pascalcase.symbols = public_members
+dotnet_naming_rule.public_members_pascalcase.style = pascalcase
+
+#camelCase for private members
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_symbols.private_members.applicable_accessibilities = private
+dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event
+dotnet_naming_rule.private_members_camelcase.severity = warning
+dotnet_naming_rule.private_members_camelcase.symbols = private_members
+dotnet_naming_rule.private_members_camelcase.style = camelcase
+
+dotnet_naming_symbols.local_function.applicable_kinds = local_function
+dotnet_naming_rule.local_function_camelcase.severity = warning
+dotnet_naming_rule.local_function_camelcase.symbols = local_function
+dotnet_naming_rule.local_function_camelcase.style = camelcase
+
+#all_lower for private and local constants/static readonlys
+dotnet_naming_style.all_lower.capitalization = all_lower
+dotnet_naming_style.all_lower.word_separator = _
+
+dotnet_naming_symbols.private_constants.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants.required_modifiers = const
+dotnet_naming_symbols.private_constants.applicable_kinds = field
+dotnet_naming_rule.private_const_all_lower.severity = warning
+dotnet_naming_rule.private_const_all_lower.symbols = private_constants
+dotnet_naming_rule.private_const_all_lower.style = all_lower
+
+dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly.applicable_kinds = field
+dotnet_naming_rule.private_static_readonly_all_lower.severity = warning
+dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly
+dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+dotnet_naming_rule.local_const_all_lower.severity = warning
+dotnet_naming_rule.local_const_all_lower.symbols = local_constants
+dotnet_naming_rule.local_const_all_lower.style = all_lower
+
+#ALL_UPPER for non private constants/static readonlys
+dotnet_naming_style.all_upper.capitalization = all_upper
+dotnet_naming_style.all_upper.word_separator = _
+
+dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_constants.required_modifiers = const
+dotnet_naming_symbols.public_constants.applicable_kinds = field
+dotnet_naming_rule.public_const_all_upper.severity = warning
+dotnet_naming_rule.public_const_all_upper.symbols = public_constants
+dotnet_naming_rule.public_const_all_upper.style = all_upper
+
+dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly
+dotnet_naming_symbols.public_static_readonly.applicable_kinds = field
+dotnet_naming_rule.public_static_readonly_all_upper.severity = warning
+dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly
+dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper
+
+#Roslyn formating options
+
+#Formatting - indentation options
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+#Formatting - new line options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_open_brace = all
+#csharp_new_line_before_members_in_anonymous_types = true
+#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing
+csharp_new_line_between_query_expression_clauses = true
+
+#Formatting - organize using options
+dotnet_sort_system_directives_first = true
+
+#Formatting - spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+
+#Formatting - wrapping options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#Roslyn language styles
+
+#Style - this. qualification
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+#Style - type names
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+csharp_style_var_when_type_is_apparent = true:none
+csharp_style_var_for_built_in_types = true:none
+csharp_style_var_elsewhere = true:silent
+
+#Style - modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning
+
+#Style - parentheses
+# Skipped because roslyn cannot separate +-*/ with << >>
+
+#Style - expression bodies
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
+
+#Style - expression preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_compound_assignment = true:warning
+
+#Style - null/type checks
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_throw_expression = true:silent
+csharp_style_conditional_delegate_call = true:warning
+
+#Style - unused
+dotnet_style_readonly_field = true:silent
+dotnet_code_quality_unused_parameters = non_public:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_unused_value_assignment_preference = discard_variable:warning
+
+#Style - variable declaration
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+
+#Style - other C# 7.x features
+dotnet_style_prefer_inferred_tuple_names = true:warning
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+
+#Style - C# 8 features
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:silent
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_switch_expression = false:none
+
+#Supressing roslyn built-in analyzers
+# Suppress: EC112
+
+#Private method is unused
+dotnet_diagnostic.IDE0051.severity = silent
+#Private member is unused
+dotnet_diagnostic.IDE0052.severity = silent
+
+#Rules for disposable
+dotnet_diagnostic.IDE0067.severity = none
+dotnet_diagnostic.IDE0068.severity = none
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/.gitignore b/Templates/Rulesets/ruleset-scrolling-example/.gitignore
new file mode 100644
index 0000000000..940794e60f
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/.gitignore
@@ -0,0 +1,288 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
diff --git a/Templates/Rulesets/ruleset-scrolling-example/.template.config/template.json b/Templates/Rulesets/ruleset-scrolling-example/.template.config/template.json
new file mode 100644
index 0000000000..a1c097f1c8
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/.template.config/template.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "ppy Pty Ltd",
+ "classifications": [
+ "Console"
+ ],
+ "name": "osu! ruleset (scrolling pippidon example)",
+ "identity": "ppy.osu.Game.Templates.Rulesets.Scrolling.Pippidon",
+ "shortName": "ruleset-scrolling-example",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "sourceName": "Pippidon",
+ "preferNameDirectory": true
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
new file mode 100644
index 0000000000..bd9db14259
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "VisualTests (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Debug)",
+ "env": {},
+ "console": "internalConsole"
+ },
+ {
+ "name": "VisualTests (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Release)",
+ "env": {},
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json
new file mode 100644
index 0000000000..0ee07c1036
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/tasks.json
@@ -0,0 +1,47 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "-p:Configuration=Release",
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "restore"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
new file mode 100644
index 0000000000..270d906b01
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ public class TestSceneOsuGame : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuGameBase gameBase)
+ {
+ OsuGame game = new OsuGame();
+ game.SetHost(host);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ },
+ game
+ };
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs
new file mode 100644
index 0000000000..f00528900c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuPlayer.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ [TestFixture]
+ public class TestSceneOsuPlayer : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new PippidonRuleset();
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
new file mode 100644
index 0000000000..fd6bd9b714
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework;
+using osu.Framework.Platform;
+using osu.Game.Tests;
+
+namespace osu.Game.Rulesets.Pippidon.Tests
+{
+ public static class VisualTestRunner
+ {
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ {
+ host.Run(new OsuTestBrowser());
+ return 0;
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
new file mode 100644
index 0000000000..d7c116411a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+ osu.Game.Rulesets.Pippidon.Tests.VisualTestRunner
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ WinExe
+ net5.0
+ osu.Game.Rulesets.Pippidon.Tests
+
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln
new file mode 100644
index 0000000000..bccffcd7ff
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln
@@ -0,0 +1,96 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29123.88
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Pippidon", "osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon.Tests", "osu.Game.Rulesets.Pippidon.Tests\osu.Game.Rulesets.Pippidon.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ VisualTests|Any CPU = VisualTests|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU
+ {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668}
+ EndGlobalSection
+ GlobalSection(MonoDevelopProperties) = preSolution
+ Policies = $0
+ $0.TextStylePolicy = $1
+ $1.EolMarker = Windows
+ $1.inheritsSet = VisualStudio
+ $1.inheritsScope = text/plain
+ $1.scope = text/x-csharp
+ $0.CSharpFormattingPolicy = $2
+ $2.IndentSwitchSection = True
+ $2.NewLinesForBracesInProperties = True
+ $2.NewLinesForBracesInAccessors = True
+ $2.NewLinesForBracesInAnonymousMethods = True
+ $2.NewLinesForBracesInControlBlocks = True
+ $2.NewLinesForBracesInAnonymousTypes = True
+ $2.NewLinesForBracesInObjectCollectionArrayInitializers = True
+ $2.NewLinesForBracesInLambdaExpressionBody = True
+ $2.NewLineForElse = True
+ $2.NewLineForCatch = True
+ $2.NewLineForFinally = True
+ $2.NewLineForMembersInObjectInit = True
+ $2.NewLineForMembersInAnonymousTypes = True
+ $2.NewLineForClausesInQuery = True
+ $2.SpacingAfterMethodDeclarationName = False
+ $2.SpaceAfterMethodCallName = False
+ $2.SpaceBeforeOpenSquareBracket = False
+ $2.inheritsSet = Mono
+ $2.inheritsScope = text/x-csharp
+ $2.scope = text/x-csharp
+ EndGlobalSection
+ GlobalSection(MonoDevelopProperties) = preSolution
+ Policies = $0
+ $0.TextStylePolicy = $1
+ $1.EolMarker = Windows
+ $1.inheritsSet = VisualStudio
+ $1.inheritsScope = text/plain
+ $1.scope = text/x-csharp
+ $0.CSharpFormattingPolicy = $2
+ $2.IndentSwitchSection = True
+ $2.NewLinesForBracesInProperties = True
+ $2.NewLinesForBracesInAccessors = True
+ $2.NewLinesForBracesInAnonymousMethods = True
+ $2.NewLinesForBracesInControlBlocks = True
+ $2.NewLinesForBracesInAnonymousTypes = True
+ $2.NewLinesForBracesInObjectCollectionArrayInitializers = True
+ $2.NewLinesForBracesInLambdaExpressionBody = True
+ $2.NewLineForElse = True
+ $2.NewLineForCatch = True
+ $2.NewLineForFinally = True
+ $2.NewLineForMembersInObjectInit = True
+ $2.NewLineForMembersInAnonymousTypes = True
+ $2.NewLineForClausesInQuery = True
+ $2.SpacingAfterMethodDeclarationName = False
+ $2.SpaceAfterMethodCallName = False
+ $2.SpaceBeforeOpenSquareBracket = False
+ $2.inheritsSet = Mono
+ $2.inheritsScope = text/x-csharp
+ $2.scope = text/x-csharp
+ EndGlobalSection
+EndGlobal
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings
new file mode 100644
index 0000000000..aa8f8739c1
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings
@@ -0,0 +1,934 @@
+
+ True
+ True
+ True
+ True
+ ExplicitlyExcluded
+ ExplicitlyExcluded
+ SOLUTION
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ True
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ SUGGESTION
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ ERROR
+ WARNING
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ HINT
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ HINT
+ HINT
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ ERROR
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ WARNING
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ HINT
+ DO_NOT_SHOW
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+
+ True
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ HINT
+ HINT
+ WARNING
+ WARNING
+ <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile>
+ Code Cleanup (peppy)
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ RequiredForMultiline
+ Explicit
+ ExpressionBody
+ BlockBody
+ True
+ NEXT_LINE
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ NEXT_LINE
+ MULTILINE
+ True
+ True
+ True
+ True
+ NEXT_LINE
+ 1
+ 1
+ True
+ NEXT_LINE
+ NEVER
+ NEVER
+ True
+ False
+ True
+ NEVER
+ False
+ False
+ True
+ False
+ False
+ True
+ True
+ False
+ False
+ CHOP_IF_LONG
+ True
+ 200
+ CHOP_IF_LONG
+ False
+ False
+ AABB
+ API
+ BPM
+ GC
+ GL
+ GLSL
+ HID
+ HTML
+ HUD
+ ID
+ IL
+ IOS
+ IP
+ IPC
+ JIT
+ LTRB
+ MD5
+ NS
+ OS
+ PM
+ RGB
+ RNG
+ SHA
+ SRGB
+ TK
+ SS
+ PP
+ GMT
+ QAT
+ BNG
+ UI
+ False
+ HINT
+ <?xml version="1.0" encoding="utf-16"?>
+<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
+ <TypePattern DisplayName="COM interfaces or structs">
+ <TypePattern.Match>
+ <Or>
+ <And>
+ <Kind Is="Interface" />
+ <Or>
+ <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" />
+ <HasAttribute Name="System.Runtime.InteropServices.ComImport" />
+ </Or>
+ </And>
+ <Kind Is="Struct" />
+ </Or>
+ </TypePattern.Match>
+ </TypePattern>
+ <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All">
+ <TypePattern.Match>
+ <And>
+ <Kind Is="Class" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" />
+ </And>
+ </TypePattern.Match>
+ <Entry DisplayName="Setup/Teardown Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <Or>
+ <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" />
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="All other members" />
+ <Entry Priority="100" DisplayName="Test Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="NUnit.Framework.TestAttribute" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ </TypePattern>
+ <TypePattern DisplayName="Default Pattern">
+ <Group DisplayName="Fields/Properties">
+ <Group DisplayName="Public Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Public Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Internal Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Internal Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Protected Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Protected Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Group DisplayName="Private Fields">
+ <Entry DisplayName="Constant Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Or>
+ <Kind Is="Constant" />
+ <Readonly />
+ <And>
+ <Static />
+ <Readonly />
+ </And>
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Static Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Static />
+ <Not>
+ <Readonly />
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Normal Fields">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Not>
+ <Or>
+ <Static />
+ <Readonly />
+ </Or>
+ </Not>
+ <Kind Is="Field" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Entry DisplayName="Private Properties">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Kind Is="Property" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Constructor/Destructor">
+ <Entry DisplayName="Ctor">
+ <Entry.Match>
+ <Kind Is="Constructor" />
+ </Entry.Match>
+ </Entry>
+ <Region Name="Disposal">
+ <Entry DisplayName="Dtor">
+ <Entry.Match>
+ <Kind Is="Destructor" />
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Dispose()">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Method" />
+ <Name Is="Dispose" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Dispose(true)">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Or>
+ <Virtual />
+ <Override />
+ </Or>
+ <Kind Is="Method" />
+ <Name Is="Dispose" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Region>
+ </Group>
+ <Group DisplayName="Methods">
+ <Group DisplayName="Public">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Internal">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Internal" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Protected">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Protected" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ <Group DisplayName="Private">
+ <Entry DisplayName="Static Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Static />
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <And>
+ <Access Is="Private" />
+ <Not>
+ <Static />
+ </Not>
+ <Kind Is="Method" />
+ </And>
+ </Entry.Match>
+ </Entry>
+ </Group>
+ </Group>
+ </TypePattern>
+</Patterns>
+ Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+See the LICENCE file in the repository root for full licence text.
+
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
+ <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ TestFolder
+ True
+ True
+ o!f – Object Initializer: Anchor&Origin
+ True
+ constant("Centre")
+ 0
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofao
+ True
+ Anchor = Anchor.$anchor$,
+Origin = Anchor.$anchor$,
+ True
+ True
+ o!f – InternalChildren = []
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofic
+ True
+ InternalChildren = new Drawable[]
+{
+ $END$
+};
+ True
+ True
+ o!f – new GridContainer { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofgc
+ True
+ new GridContainer
+{
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
+ {
+ new Drawable[] { $END$ },
+ new Drawable[] { }
+ }
+};
+ True
+ True
+ o!f – new FillFlowContainer { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ offf
+ True
+ new FillFlowContainer
+{
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ $END$
+ }
+},
+ True
+ True
+ o!f – new Container { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofcont
+ True
+ new Container
+{
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ $END$
+ }
+},
+ True
+ True
+ o!f – BackgroundDependencyLoader load()
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofbdl
+ True
+ [BackgroundDependencyLoader]
+private void load()
+{
+ $END$
+}
+ True
+ True
+ o!f – new Box { .. }
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofbox
+ True
+ new Box
+{
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+},
+ True
+ True
+ o!f – Children = []
+ True
+ True
+ 2.0
+ InCSharpFile
+ ofc
+ True
+ Children = new Drawable[]
+{
+ $END$
+};
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
new file mode 100644
index 0000000000..8f0b31ef1b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.Beatmaps
+{
+ public class PippidonBeatmapConverter : BeatmapConverter
+ {
+ private readonly float minPosition;
+ private readonly float maxPosition;
+
+ public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
+ : base(beatmap, ruleset)
+ {
+ minPosition = beatmap.HitObjects.Min(getUsablePosition);
+ maxPosition = beatmap.HitObjects.Max(getUsablePosition);
+ }
+
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition);
+
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
+ {
+ yield return new PippidonHitObject
+ {
+ Samples = original.Samples,
+ StartTime = original.StartTime,
+ Lane = getLane(original)
+ };
+ }
+
+ private int getLane(HitObject hitObject) => (int)MathHelper.Clamp(
+ (getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1);
+
+ private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X;
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
new file mode 100644
index 0000000000..4565c97d1a
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+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..290148d14b
--- /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, double clockRate) => new Skill[0];
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs
new file mode 100644
index 0000000000..c9e6e6faaa
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonInputManager.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.ComponentModel;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonInputManager : RulesetInputManager
+ {
+ public PippidonInputManager(RulesetInfo ruleset)
+ : base(ruleset, 0, SimultaneousBindingMode.Unique)
+ {
+ }
+ }
+
+ public enum PippidonAction
+ {
+ [Description("Move up")]
+ MoveUp,
+
+ [Description("Move down")]
+ MoveDown,
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs
new file mode 100644
index 0000000000..ede00f1510
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Pippidon.Beatmaps;
+using osu.Game.Rulesets.Pippidon.Mods;
+using osu.Game.Rulesets.Pippidon.UI;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Pippidon
+{
+ public class PippidonRuleset : Ruleset
+ {
+ public override string Description => "gather the osu!coins";
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawablePippidonRuleset(this, beatmap, mods);
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new PippidonBeatmapConverter(beatmap, this);
+
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new PippidonDifficultyCalculator(this, beatmap);
+
+ public override IEnumerable GetModsFor(ModType type)
+ {
+ switch (type)
+ {
+ case ModType.Automation:
+ return new[] { new PippidonModAutoplay() };
+
+ default:
+ return new Mod[] { null };
+ }
+ }
+
+ public override string ShortName => "pippidon";
+
+ public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[]
+ {
+ new KeyBinding(InputKey.W, PippidonAction.MoveUp),
+ new KeyBinding(InputKey.S, PippidonAction.MoveDown),
+ };
+
+ public override Drawable CreateIcon() => new Sprite
+ {
+ Margin = new MarginPadding { Top = 3 },
+ Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
+ };
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
new file mode 100644
index 0000000000..724026273d
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
@@ -0,0 +1,60 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.UI;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonAutoGenerator : AutoGenerator
+ {
+ public new Beatmap Beatmap => (Beatmap)base.Beatmap;
+
+ public PippidonAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ protected override void GenerateFrames()
+ {
+ int currentLane = 0;
+
+ Frames.Add(new PippidonReplayFrame());
+
+ foreach (PippidonHitObject hitObject in Beatmap.HitObjects)
+ {
+ if (currentLane == hitObject.Lane)
+ continue;
+
+ int totalTravel = Math.Abs(hitObject.Lane - currentLane);
+ var direction = hitObject.Lane > currentLane ? PippidonAction.MoveDown : PippidonAction.MoveUp;
+
+ double time = hitObject.StartTime - 5;
+
+ if (totalTravel == PippidonPlayfield.LANE_COUNT - 1)
+ addFrame(time, direction == PippidonAction.MoveDown ? PippidonAction.MoveUp : PippidonAction.MoveDown);
+ else
+ {
+ time -= totalTravel * KEY_UP_DELAY;
+
+ for (int i = 0; i < totalTravel; i++)
+ {
+ addFrame(time, direction);
+ time += KEY_UP_DELAY;
+ }
+ }
+
+ currentLane = hitObject.Lane;
+ }
+ }
+
+ private void addFrame(double time, PippidonAction direction)
+ {
+ Frames.Add(new PippidonReplayFrame(direction) { Time = time });
+ Frames.Add(new PippidonReplayFrame { Time = time + KEY_UP_DELAY }); //Release the keys as well
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs
new file mode 100644
index 0000000000..7652357b4d
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Input.StateChanges;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonFramedReplayInputHandler : FramedReplayInputHandler
+ {
+ public PippidonFramedReplayInputHandler(Replay replay)
+ : base(replay)
+ {
+ }
+
+ protected override bool IsImportant(PippidonReplayFrame frame) => frame.Actions.Any();
+
+ public override void CollectPendingInputs(List inputs)
+ {
+ inputs.Add(new ReplayState
+ {
+ PressedActions = CurrentFrame?.Actions ?? new List(),
+ });
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs
new file mode 100644
index 0000000000..468ac9c725
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Pippidon.Replays
+{
+ public class PippidonReplayFrame : ReplayFrame
+ {
+ public List Actions = new List();
+
+ public PippidonReplayFrame(PippidonAction? button = null)
+ {
+ if (button.HasValue)
+ Actions.Add(button.Value);
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3
new file mode 100644
index 0000000000..90b13d1f73
Binary files /dev/null and b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Samples/Gameplay/normal-hitnormal.mp3 differ
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png
new file mode 100644
index 0000000000..e79d2528ec
Binary files /dev/null and b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/character.png differ
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png
new file mode 100644
index 0000000000..3cd89c6ce6
Binary files /dev/null and b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Resources/Textures/coin.png differ
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs
new file mode 100644
index 0000000000..9a73dd7790
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/DrawablePippidonRuleset.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Input;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Handlers;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Pippidon.Objects;
+using osu.Game.Rulesets.Pippidon.Objects.Drawables;
+using osu.Game.Rulesets.Pippidon.Replays;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ [Cached]
+ public class DrawablePippidonRuleset : DrawableScrollingRuleset
+ {
+ public DrawablePippidonRuleset(PippidonRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ Direction.Value = ScrollingDirection.Left;
+ TimeRange.Value = 6000;
+ }
+
+ protected override Playfield CreatePlayfield() => new PippidonPlayfield();
+
+ protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new PippidonFramedReplayInputHandler(replay);
+
+ public override DrawableHitObject CreateDrawableRepresentation(PippidonHitObject h) => new DrawablePippidonHitObject(h);
+
+ protected override PassThroughInputManager CreateInputManager() => new PippidonInputManager(Ruleset?.RulesetInfo);
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs
new file mode 100644
index 0000000000..dd0a20f1b4
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs
@@ -0,0 +1,87 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osuTK;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ public class PippidonCharacter : BeatSyncedContainer, IKeyBindingHandler
+ {
+ public readonly BindableInt LanePosition = new BindableInt
+ {
+ Value = 0,
+ MinValue = 0,
+ MaxValue = PippidonPlayfield.LANE_COUNT - 1,
+ };
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ Size = new Vector2(PippidonPlayfield.LANE_HEIGHT);
+
+ Child = new Sprite
+ {
+ FillMode = FillMode.Fit,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(1.2f),
+ RelativeSizeAxes = Axes.Both,
+ Texture = textures.Get("character")
+ };
+
+ LanePosition.BindValueChanged(e => { this.MoveToY(e.NewValue * PippidonPlayfield.LANE_HEIGHT); });
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
+ {
+ if (effectPoint.KiaiMode)
+ {
+ bool direction = beatIndex % 2 == 1;
+ double duration = timingPoint.BeatLength / 2;
+
+ Child.RotateTo(direction ? 10 : -10, duration * 2, Easing.InOutSine);
+
+ Child.Animate(i => i.MoveToY(-10, duration, Easing.Out))
+ .Then(i => i.MoveToY(0, duration, Easing.In));
+ }
+ else
+ {
+ Child.ClearTransforms();
+ Child.RotateTo(0, 500, Easing.Out);
+ Child.MoveTo(Vector2.Zero, 500, Easing.Out);
+ }
+ }
+
+ public bool OnPressed(PippidonAction action)
+ {
+ switch (action)
+ {
+ case PippidonAction.MoveUp:
+ changeLane(-1);
+ return true;
+
+ case PippidonAction.MoveDown:
+ changeLane(1);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ public void OnReleased(PippidonAction action)
+ {
+ }
+
+ private void changeLane(int change) => LanePosition.Value = (LanePosition.Value + change + PippidonPlayfield.LANE_COUNT) % PippidonPlayfield.LANE_COUNT;
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
new file mode 100644
index 0000000000..0e50030162
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
@@ -0,0 +1,128 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Pippidon.UI
+{
+ [Cached]
+ public class PippidonPlayfield : ScrollingPlayfield
+ {
+ public const float LANE_HEIGHT = 70;
+
+ public const int LANE_COUNT = 6;
+
+ public BindableInt CurrentLane => pippidon.LanePosition;
+
+ private PippidonCharacter pippidon;
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ new LaneContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding
+ {
+ Left = 200,
+ Top = LANE_HEIGHT / 2,
+ Bottom = LANE_HEIGHT / 2
+ },
+ Children = new Drawable[]
+ {
+ HitObjectContainer,
+ pippidon = new PippidonCharacter
+ {
+ Origin = Anchor.Centre,
+ },
+ }
+ },
+ },
+ });
+ }
+
+ private class LaneContainer : BeatSyncedContainer
+ {
+ private OsuColour colours;
+ private FillFlowContainer fill;
+
+ private readonly Container content = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ };
+
+ protected override Container Content => content;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ this.colours = colours;
+
+ InternalChildren = new Drawable[]
+ {
+ fill = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Colour = colours.BlueLight,
+ Direction = FillDirection.Vertical,
+ },
+ content,
+ };
+
+ for (int i = 0; i < LANE_COUNT; i++)
+ {
+ fill.Add(new Lane
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = LANE_HEIGHT,
+ });
+ }
+ }
+
+ private class Lane : CompositeDrawable
+ {
+ public Lane()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.White,
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Height = 0.95f,
+ },
+ };
+ }
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
+ {
+ if (effectPoint.KiaiMode)
+ fill.FlashColour(colours.PinkLight, 800, Easing.In);
+ }
+ }
+ }
+}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
new file mode 100644
index 0000000000..61b859f45b
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.1
+ osu.Game.Rulesets.Sample
+ Library
+ AnyCPU
+ osu.Game.Rulesets.Pippidon
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj
new file mode 100644
index 0000000000..31a24a301f
--- /dev/null
+++ b/Templates/osu.Game.Templates.csproj
@@ -0,0 +1,24 @@
+
+
+ Template
+ ppy.osu.Game.Templates
+ osu! templates
+ ppy Pty Ltd
+ https://github.com/ppy/osu/blob/master/LICENCE
+ https://github.com/ppy/osu/blob/master/Templates
+ https://github.com/ppy/osu
+ Automated release.
+ Copyright (c) 2021 ppy Pty Ltd
+ Templates to use when creating a ruleset for consumption in osu!.
+ dotnet-new;templates;osu
+ netstandard2.1
+ true
+ false
+ content
+
+
+
+
+
+
+
diff --git a/appveyor.yml b/appveyor.yml
index a4a0cedc66..5be73f9875 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,24 +1,27 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2019
+cache:
+ - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
+
dotnet_csproj:
patch: true
file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
version: '0.0.{build}'
-cache:
- - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
+
before_build:
- - ps: dotnet --info # Useful when version mismatch between CI and local
- - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
+ - cmd: dotnet --info # Useful when version mismatch between CI and local
+ - cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
+
build:
project: osu.sln
parallel: true
verbosity: minimal
publish_nuget: true
+
after_build:
- - ps: dotnet tool restore
- - ps: dotnet format --dry-run --check
- ps: .\InspectCode.ps1
+
test:
assemblies:
except:
diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml
index bb4482f501..adf98848bc 100644
--- a/appveyor_deploy.yml
+++ b/appveyor_deploy.yml
@@ -1,21 +1,86 @@
clone_depth: 1
version: '{build}'
image: Visual Studio 2019
-dotnet_csproj:
- patch: true
- file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
- version: $(APPVEYOR_REPO_TAG_NAME)
-before_build:
- - ps: dotnet --info # Useful when version mismatch between CI and local
- - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
test: off
skip_non_tags: true
configuration: Release
-build:
- project: build\Desktop.proj # Skipping Xamarin Release that's slow and covered by fastlane
- parallel: true
- verbosity: minimal
- publish_nuget: true
+
+environment:
+ matrix:
+ - job_name: osu-game
+ - job_name: osu-ruleset
+ job_depends_on: osu-game
+ - job_name: taiko-ruleset
+ job_depends_on: osu-game
+ - job_name: catch-ruleset
+ 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
+
+for:
+ -
+ matrix:
+ only:
+ - job_name: osu-game
+ build_script:
+ - cmd: dotnet pack osu.Game\osu.Game.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
+ -
+ matrix:
+ only:
+ - job_name: osu-ruleset
+ build_script:
+ - cmd: dotnet remove osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj reference osu.Game\osu.Game.csproj
+ - cmd: dotnet add osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
+ - cmd: dotnet pack osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
+ -
+ matrix:
+ only:
+ - job_name: taiko-ruleset
+ build_script:
+ - cmd: dotnet remove osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj reference osu.Game\osu.Game.csproj
+ - cmd: dotnet add osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
+ - cmd: dotnet pack osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
+ -
+ matrix:
+ only:
+ - job_name: catch-ruleset
+ build_script:
+ - cmd: dotnet remove osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj reference osu.Game\osu.Game.csproj
+ - cmd: dotnet add osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
+ - cmd: dotnet pack osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
+ -
+ matrix:
+ only:
+ - job_name: mania-ruleset
+ build_script:
+ - 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'
+
deploy:
- provider: Environment
- name: nuget
+ name: nuget
\ No newline at end of file
diff --git a/build/Desktop.proj b/build/Desktop.proj
deleted file mode 100644
index b1c6b065e8..0000000000
--- a/build/Desktop.proj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build/InspectCode.cake b/build/InspectCode.cake
deleted file mode 100644
index 2e7a1d1b28..0000000000
--- a/build/InspectCode.cake
+++ /dev/null
@@ -1,52 +0,0 @@
-#addin "nuget:?package=CodeFileSanity&version=0.0.33"
-#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.3.2"
-#tool "nuget:?package=NVika.MSBuild&version=1.0.1"
-var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
-
-///////////////////////////////////////////////////////////////////////////////
-// ARGUMENTS
-///////////////////////////////////////////////////////////////////////////////
-
-var target = Argument("target", "CodeAnalysis");
-var configuration = Argument("configuration", "Release");
-
-var rootDirectory = new DirectoryPath("..");
-var sln = rootDirectory.CombineWithFilePath("osu.sln");
-var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
-
-///////////////////////////////////////////////////////////////////////////////
-// TASKS
-///////////////////////////////////////////////////////////////////////////////
-
-// windows only because both inspectcode and nvika depend on net45
-Task("InspectCode")
- .WithCriteria(IsRunningOnWindows())
- .Does(() => {
- InspectCode(desktopSlnf, new InspectCodeSettings {
- CachesHome = "inspectcode",
- OutputFile = "inspectcodereport.xml",
- ArgumentCustomization = arg => {
- if (AppVeyor.IsRunningOnAppVeyor) // Don't flood CI output
- arg.Append("--verbosity:WARN");
- return arg;
- },
- });
-
- int returnCode = StartProcess(nVikaToolPath, $@"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors");
- if (returnCode != 0)
- throw new Exception($"inspectcode failed with return code {returnCode}");
- });
-
-Task("CodeFileSanity")
- .Does(() => {
- ValidateCodeSanity(new ValidateCodeSanitySettings {
- RootDirectory = rootDirectory.FullPath,
- IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor
- });
- });
-
-Task("CodeAnalysis")
- .IsDependentOn("CodeFileSanity")
- .IsDependentOn("InspectCode");
-
-RunTarget(target);
\ No newline at end of file
diff --git a/cake.config b/cake.config
deleted file mode 100644
index 187d825591..0000000000
--- a/cake.config
+++ /dev/null
@@ -1,5 +0,0 @@
-
-[Nuget]
-Source=https://api.nuget.org/v3/index.json
-UseInProcessClient=true
-LoadDependencies=true
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 510b53054b..cc5abf5b03 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -48,9 +48,12 @@ desc 'Deploy to play store'
desc 'Compile the project'
lane :build do |options|
- nuget_restore(
- project_path: 'osu.sln'
- )
+ nuget_restore(project_path: 'osu.Android/osu.Android.csproj')
+ nuget_restore(project_path: 'osu.Game/osu.Game.csproj')
+ nuget_restore(project_path: 'osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj')
+ nuget_restore(project_path: 'osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj')
+ nuget_restore(project_path: 'osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj')
+ nuget_restore(project_path: 'osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj')
souyuz(
build_configuration: 'Release',
@@ -97,21 +100,26 @@ platform :ios do
changelog.gsub!('$BUILD_ID', options[:build])
pilot(
- wait_processing_interval: 1800,
+ wait_processing_interval: 900,
changelog: changelog,
+ groups: ['osu! supporters', 'public'],
+ distribute_external: true,
ipa: './osu.iOS/bin/iPhone/Release/osu.iOS.ipa'
)
end
desc 'Compile the project'
lane :build do
- nuget_restore(
- project_path: 'osu.sln'
- )
+ nuget_restore(project_path: 'osu.iOS/osu.iOS.csproj')
+ nuget_restore(project_path: 'osu.Game/osu.Game.csproj')
+ nuget_restore(project_path: 'osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj')
+ nuget_restore(project_path: 'osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj')
+ nuget_restore(project_path: 'osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj')
+ nuget_restore(project_path: 'osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj')
souyuz(
platform: "ios",
- plist_path: "../osu.iOS/Info.plist"
+ plist_path: "osu.iOS/Info.plist"
)
end
@@ -125,7 +133,7 @@ platform :ios do
end
lane :update_version do |options|
- options[:plist_path] = '../osu.iOS/Info.plist'
+ options[:plist_path] = 'osu.iOS/Info.plist'
app_version(options)
end
diff --git a/global.json b/global.json
deleted file mode 100644
index 6858d4044d..0000000000
--- a/global.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "sdk": {
- "allowPrerelease": false,
- "rollForward": "minor",
- "version": "3.1.100"
- },
- "msbuild-sdks": {
- "Microsoft.Build.Traversal": "2.0.24"
- }
-}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 1a63b893a1..c020b1d783 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs
new file mode 100644
index 0000000000..25bd659a5d
--- /dev/null
+++ b/osu.Android/GameplayScreenRotationLocker.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 Android.Content.PM;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game;
+
+namespace osu.Android
+{
+ public class GameplayScreenRotationLocker : Component
+ {
+ private Bindable localUserPlaying;
+
+ [Resolved]
+ private OsuGameActivity gameActivity { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGame game)
+ {
+ localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
+ localUserPlaying.BindValueChanged(updateLock, true);
+ }
+
+ private void updateLock(ValueChangedEvent userPlaying)
+ {
+ gameActivity.RunOnUiThread(() =>
+ {
+ gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser;
+ });
+ }
+ }
+}
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 2e5fa59d20..063e02d349 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -1,18 +1,35 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
using Android.App;
+using Android.Content;
using Android.Content.PM;
+using Android.Net;
using Android.OS;
+using Android.Provider;
using Android.Views;
using osu.Framework.Android;
+using osu.Game.Database;
namespace osu.Android
{
- [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = true)]
+ [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-archive")]
+ [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed", "application/x-osu-archive" })]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
- protected override Framework.Game CreateGame() => new OsuGameAndroid();
+ private static readonly string[] osu_url_schemes = { "osu", "osump" };
+
+ private OsuGameAndroid game;
+
+ protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
protected override void OnCreate(Bundle savedInstanceState)
{
@@ -23,8 +40,76 @@ namespace osu.Android
base.OnCreate(savedInstanceState);
+ // OnNewIntent() only fires for an activity if it's *re-launched* while it's on top of the activity stack.
+ // on first launch we still have to fire manually.
+ // reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)
+ handleIntent(Intent);
+
Window.AddFlags(WindowManagerFlags.Fullscreen);
Window.AddFlags(WindowManagerFlags.KeepScreenOn);
}
+
+ protected override void OnNewIntent(Intent intent) => handleIntent(intent);
+
+ private void handleIntent(Intent intent)
+ {
+ switch (intent.Action)
+ {
+ case Intent.ActionDefault:
+ if (intent.Scheme == ContentResolver.SchemeContent)
+ handleImportFromUris(intent.Data);
+ else if (osu_url_schemes.Contains(intent.Scheme))
+ game.HandleLink(intent.DataString);
+ break;
+
+ case Intent.ActionSend:
+ case Intent.ActionSendMultiple:
+ {
+ var uris = new List();
+ for (int i = 0; i < intent.ClipData?.ItemCount; i++)
+ {
+ var content = intent.ClipData?.GetItemAt(i);
+ if (content != null)
+ uris.Add(content.Uri);
+ }
+ handleImportFromUris(uris.ToArray());
+ break;
+ }
+ }
+ }
+
+ private void handleImportFromUris(params Uri[] uris) => Task.Factory.StartNew(async () =>
+ {
+ var tasks = new List();
+
+ await Task.WhenAll(uris.Select(async uri =>
+ {
+ // there are more performant overloads of this method, but this one is the most backwards-compatible
+ // (dates back to API 1).
+ var cursor = ContentResolver?.Query(uri, null, null, null, null);
+
+ if (cursor == null)
+ return;
+
+ cursor.MoveToFirst();
+
+ var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
+ string filename = cursor.GetString(filenameColumn);
+
+ // SharpCompress requires archive streams to be seekable, which the stream opened by
+ // OpenInputStream() seems to not necessarily be.
+ // copy to an arbitrary-access memory stream to be able to proceed with the import.
+ var copy = new MemoryStream();
+ using (var stream = ContentResolver.OpenInputStream(uri))
+ await stream.CopyToAsync(copy).ConfigureAwait(false);
+
+ lock (tasks)
+ {
+ tasks.Add(new ImportTask(copy, filename));
+ }
+ })).ConfigureAwait(false);
+
+ await game.Import(tasks.ToArray()).ConfigureAwait(false);
+ }, TaskCreationOptions.LongRunning);
}
}
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 84f215f930..050bf2b787 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -3,13 +3,26 @@
using System;
using Android.App;
+using Android.OS;
+using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
+using osu.Game.Utils;
+using Xamarin.Essentials;
namespace osu.Android
{
public class OsuGameAndroid : OsuGame
{
+ [Cached]
+ private readonly OsuGameActivity gameActivity;
+
+ public OsuGameAndroid(OsuGameActivity activity)
+ : base(null)
+ {
+ gameActivity = activity;
+ }
+
public override Version AssemblyVersion
{
get
@@ -18,8 +31,32 @@ namespace osu.Android
try
{
- string versionName = packageInfo.VersionCode.ToString();
- // undo play store version garbling
+ // We store the osu! build number in the "VersionCode" field to better support google play releases.
+ // If we were to use the main build number, it would require a new submission each time (similar to TestFlight).
+ // In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time.
+ //
+ // We also need to be aware that older SDK versions store this as a 32bit int.
+ //
+ // Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060
+
+ // https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
+ string versionName = string.Empty;
+
+ if (Build.VERSION.SdkInt >= BuildVersionCodes.P)
+ {
+ versionName = packageInfo.LongVersionCode.ToString();
+ // ensure we only read the trailing portion of long (the part we are interested in).
+ versionName = versionName.Substring(versionName.Length - 9);
+ }
+ else
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ // this is required else older SDKs will report missing method exception.
+ versionName = packageInfo.VersionCode.ToString();
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ // undo play store version garbling (as mentioned above).
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
catch
@@ -30,6 +67,21 @@ namespace osu.Android
}
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
+ }
+
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
+
+ protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
+
+ private class AndroidBatteryInfo : BatteryInfo
+ {
+ public override double ChargeLevel => Battery.ChargeLevel;
+
+ public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/osu.Android/Properties/AndroidManifest.xml b/osu.Android/Properties/AndroidManifest.xml
index 770eaf2222..e717bab310 100644
--- a/osu.Android/Properties/AndroidManifest.xml
+++ b/osu.Android/Properties/AndroidManifest.xml
@@ -6,5 +6,6 @@
+
\ No newline at end of file
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 0598a50530..582c856a47 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -20,7 +20,13 @@
d8
r8
+
+ None
+ cjk;mideast;other;rare;west
+ true
+
+
@@ -52,5 +58,13 @@
+
+
+ 5.0.0
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf
index d2c14d321a..503e5935f5 100644
--- a/osu.Desktop.slnf
+++ b/osu.Desktop.slnf
@@ -15,7 +15,16 @@
"osu.Game.Tests\\osu.Game.Tests.csproj",
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
"osu.Game.Tournament\\osu.Game.Tournament.csproj",
- "osu.Game\\osu.Game.csproj"
+ "osu.Game\\osu.Game.csproj",
+
+ "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
+ "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj",
+ "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
+ "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj"
]
}
-}
\ No newline at end of file
+}
diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
index 08cc0e7f5f..832d26b0ef 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
+using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Users;
@@ -26,18 +27,20 @@ namespace osu.Desktop
[Resolved]
private IBindable ruleset { get; set; }
- private Bindable user;
+ private IBindable user;
private readonly IBindable status = new Bindable();
private readonly IBindable activity = new Bindable();
+ private readonly Bindable privacyMode = new Bindable();
+
private readonly RichPresence presence = new RichPresence
{
Assets = new Assets { LargeImageKey = "osu_logo_lazer", }
};
[BackgroundDependencyLoader]
- private void load(IAPIProvider provider)
+ private void load(IAPIProvider provider, OsuConfigManager config)
{
client = new DiscordRpcClient(client_id)
{
@@ -51,6 +54,8 @@ namespace osu.Desktop
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
+ config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
+
(user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u =>
{
status.UnbindBindings();
@@ -63,6 +68,7 @@ namespace osu.Desktop
ruleset.BindValueChanged(_ => updateStatus());
status.BindValueChanged(_ => updateStatus());
activity.BindValueChanged(_ => updateStatus());
+ privacyMode.BindValueChanged(_ => updateStatus());
client.Initialize();
}
@@ -78,7 +84,7 @@ namespace osu.Desktop
if (!client.IsInitialized)
return;
- if (status.Value is UserStatusOffline)
+ if (status.Value is UserStatusOffline || privacyMode.Value == DiscordRichPresenceMode.Off)
{
client.ClearPresence();
return;
@@ -96,7 +102,10 @@ namespace osu.Desktop
}
// update user information
- presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty);
+ if (privacyMode.Value == DiscordRichPresenceMode.Limited)
+ presence.Assets.LargeImageText = string.Empty;
+ else
+ presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
// update ruleset
presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom";
@@ -135,6 +144,9 @@ namespace osu.Desktop
case UserActivity.Editing edit:
return edit.Beatmap.ToString();
+
+ case UserActivity.InLobby lobby:
+ return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
}
return string.Empty;
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index f05ee48914..4de1e84fbf 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -2,22 +2,26 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
+using Microsoft.Win32;
+using osu.Desktop.Security;
using osu.Desktop.Overlays;
using osu.Framework.Platform;
using osu.Game;
-using osuTK.Input;
-using Microsoft.Win32;
using osu.Desktop.Updater;
using osu.Framework;
using osu.Framework.Logging;
-using osu.Framework.Platform.Windows;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
+using osu.Desktop.Windows;
+using osu.Framework.Threading;
+using osu.Game.IO;
namespace osu.Desktop
{
@@ -32,12 +36,16 @@ namespace osu.Desktop
noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false;
}
- public override Storage GetStorageForStableInstall()
+ public override StableStorage GetStorageForStableInstall()
{
try
{
if (Host is DesktopGameHost desktopHost)
- return new StableStorage(desktopHost);
+ {
+ string stablePath = getStableInstallPath();
+ if (!string.IsNullOrEmpty(stablePath))
+ return new StableStorage(stablePath, desktopHost);
+ }
}
catch (Exception)
{
@@ -47,6 +55,42 @@ namespace osu.Desktop
return null;
}
+ private string getStableInstallPath()
+ {
+ static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")) || File.Exists(Path.Combine(p, "osu!.cfg"));
+
+ string stableInstallPath;
+
+ if (OperatingSystem.IsWindows())
+ {
+ try
+ {
+ stableInstallPath = getStableInstallPathFromRegistry();
+
+ if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
+ return stableInstallPath;
+ }
+ catch { }
+ }
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ return null;
+ }
+
+ [SupportedOSPlatform("windows")]
+ private string getStableInstallPathFromRegistry()
+ {
+ using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
+ return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
+ }
+
protected override UpdateManager CreateUpdateManager()
{
switch (RuntimeInfo.OS)
@@ -67,6 +111,11 @@ namespace osu.Desktop
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
LoadComponentAsync(new DiscordRichPresence(), Add);
+
+ if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
+ LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
+
+ LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
}
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)
@@ -90,65 +139,48 @@ namespace osu.Desktop
{
base.SetHost(host);
- if (host.Window is DesktopGameWindow desktopWindow)
+ var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
+
+ var desktopWindow = (SDL2DesktopWindow)host.Window;
+
+ desktopWindow.CursorState |= CursorState.Hidden;
+ desktopWindow.SetIconFromStream(iconStream);
+ desktopWindow.Title = Name;
+ desktopWindow.DragDrop += f => fileDrop(new[] { f });
+ }
+
+ private readonly List importableFiles = new List();
+ private ScheduledDelegate importSchedule;
+
+ private void fileDrop(string[] filePaths)
+ {
+ lock (importableFiles)
{
- desktopWindow.CursorState |= CursorState.Hidden;
+ var firstExtension = Path.GetExtension(filePaths.First());
- desktopWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
- desktopWindow.Title = Name;
+ if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
- desktopWindow.FileDrop += fileDrop;
+ 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 fileDrop(object sender, FileDropEventArgs e)
+ private void handlePendingImports()
{
- var filePaths = e.FileNames;
-
- var firstExtension = Path.GetExtension(filePaths.First());
-
- if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
-
- Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning);
- }
-
- ///
- /// A method of accessing an osu-stable install in a controlled fashion.
- ///
- private class StableStorage : WindowsStorage
- {
- protected override string LocateBasePath()
+ lock (importableFiles)
{
- static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
+ Logger.Log($"Handling batch import of {importableFiles.Count} files");
- string stableInstallPath;
+ var paths = importableFiles.ToArray();
+ importableFiles.Clear();
- try
- {
- using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
-
- if (checkExists(stableInstallPath))
- return stableInstallPath;
- }
- catch
- {
- }
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- return null;
- }
-
- public StableStorage(DesktopGameHost host)
- : base(string.Empty, host)
- {
+ Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
}
}
}
diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs
index 8c759f8487..e4a3451651 100644
--- a/osu.Desktop/Overlays/VersionManager.cs
+++ b/osu.Desktop/Overlays/VersionManager.cs
@@ -26,9 +26,11 @@ namespace osu.Desktop.Overlays
Alpha = 0;
+ FillFlowContainer mainFill;
+
Children = new Drawable[]
{
- new FillFlowContainer
+ mainFill = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
@@ -55,23 +57,30 @@ namespace osu.Desktop.Overlays
},
}
},
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Font = OsuFont.Numeric.With(size: 12),
- Colour = colours.Yellow,
- Text = @"Development Build"
- },
- new Sprite
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Texture = textures.Get(@"Menu/dev-build-footer"),
- },
}
}
};
+
+ if (!game.IsDeployedBuild)
+ {
+ mainFill.AddRange(new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Font = OsuFont.Numeric.With(size: 12),
+ Colour = colours.Yellow,
+ Text = @"Development Build"
+ },
+ new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Texture = textures.Get(@"Menu/dev-build-footer"),
+ },
+ });
+ }
}
protected override void PopIn()
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index bd91bcc933..5fb09c0cef 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -22,9 +22,8 @@ namespace osu.Desktop
{
// Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory;
- bool useSdl = args.Contains("--sdl");
- using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useSdl: useSdl))
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
{
host.ExceptionThrown += handleException;
@@ -33,13 +32,11 @@ namespace osu.Desktop
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
{
var importer = new ArchiveImportIPCChannel(host);
- // Restore the cwd so relative paths given at the command line work correctly
- Directory.SetCurrentDirectory(cwd);
foreach (var file in args)
{
Console.WriteLine(@"Importing {0}", file);
- if (!importer.ImportAsync(Path.GetFullPath(file)).Wait(3000))
+ if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
throw new TimeoutException(@"IPC took too long to send");
}
@@ -72,7 +69,6 @@ namespace osu.Desktop
/// Allow a maximum of one unhandled exception, per second of execution.
///
///
- ///
private static bool handleException(Exception arg)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
new file mode 100644
index 0000000000..01458b4c37
--- /dev/null
+++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
@@ -0,0 +1,83 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Security.Principal;
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Notifications;
+
+namespace osu.Desktop.Security
+{
+ ///
+ /// Checks if the game is running with elevated privileges (as admin in Windows, root in Unix) and displays a warning notification if so.
+ ///
+ public class ElevatedPrivilegesChecker : Component
+ {
+ [Resolved]
+ private NotificationOverlay notifications { get; set; }
+
+ private bool elevated;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ elevated = checkElevated();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (elevated)
+ notifications.Post(new ElevatedPrivilegesNotification());
+ }
+
+ private bool checkElevated()
+ {
+ try
+ {
+ switch (RuntimeInfo.OS)
+ {
+ case RuntimeInfo.Platform.Windows:
+ if (!OperatingSystem.IsWindows()) return false;
+
+ var windowsIdentity = WindowsIdentity.GetCurrent();
+ var windowsPrincipal = new WindowsPrincipal(windowsIdentity);
+
+ return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
+
+ case RuntimeInfo.Platform.macOS:
+ case RuntimeInfo.Platform.Linux:
+ return Mono.Unix.Native.Syscall.geteuid() == 0;
+ }
+ }
+ catch
+ {
+ }
+
+ return false;
+ }
+
+ private class ElevatedPrivilegesNotification : SimpleNotification
+ {
+ public override bool IsImportant => true;
+
+ public ElevatedPrivilegesNotification()
+ {
+ Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, NotificationOverlay notificationOverlay)
+ {
+ Icon = FontAwesome.Solid.ShieldAlt;
+ IconBackgound.Colour = colours.YellowDark;
+ }
+ }
+ }
+}
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 60b47a8b3a..47cd39dc5a 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -29,31 +29,44 @@ namespace osu.Desktop.Updater
private static readonly Logger logger = Logger.GetLogger("updater");
+ ///
+ /// Whether an update has been downloaded but not yet applied.
+ ///
+ private bool updatePending;
+
[BackgroundDependencyLoader]
- private void load(NotificationOverlay notification, OsuGameBase game)
+ private void load(NotificationOverlay notification)
{
notificationOverlay = notification;
- if (game.IsDeployedBuild)
- {
- Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
- Schedule(() => Task.Run(() => checkForUpdateAsync()));
- }
+ Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
}
- private async void checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
+ protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
+
+ private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
- //should we schedule a retry on completion of this check?
+ // should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
try
{
- if (updateManager == null) updateManager = await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
+ updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false);
+
+ var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
- var info = await updateManager.CheckForUpdate(!useDeltaPatching);
if (info.ReleasesToApply.Count == 0)
- //no updates available. bail and retry later.
- return;
+ {
+ if (updatePending)
+ {
+ // the user may have dismissed the completion notice, so show it again.
+ notificationOverlay.Post(new UpdateCompleteNotification(this));
+ return true;
+ }
+
+ // no updates available. bail and retry later.
+ return false;
+ }
if (notification == null)
{
@@ -66,14 +79,15 @@ namespace osu.Desktop.Updater
try
{
- await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f);
+ await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.Progress = 0;
notification.Text = @"Installing update...";
- await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
+ await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.State = ProgressNotificationState.Completed;
+ updatePending = true;
}
catch (Exception e)
{
@@ -81,9 +95,9 @@ namespace osu.Desktop.Updater
{
logger.Add(@"delta patching failed; will attempt full download!");
- //could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
- //try again without deltas.
- checkForUpdateAsync(false, notification);
+ // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
+ // try again without deltas.
+ await checkForUpdateAsync(false, notification).ConfigureAwait(false);
scheduleRecheck = false;
}
else
@@ -101,10 +115,12 @@ namespace osu.Desktop.Updater
{
if (scheduleRecheck)
{
- //check again in 30 minutes.
- Scheduler.AddDelayed(() => checkForUpdateAsync(), 60000 * 30);
+ // check again in 30 minutes.
+ Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
}
}
+
+ return true;
}
protected override void Dispose(bool isDisposing)
@@ -113,10 +129,27 @@ namespace osu.Desktop.Updater
updateManager?.Dispose();
}
+ private class UpdateCompleteNotification : ProgressCompletionNotification
+ {
+ [Resolved]
+ private OsuGame game { get; set; }
+
+ public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
+ {
+ Text = @"Update ready to install. Click to restart!";
+
+ Activated = () =>
+ {
+ updateManager.PrepareUpdateAsync()
+ .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
+ return true;
+ };
+ }
+ }
+
private class UpdateProgressNotification : ProgressNotification
{
private readonly SquirrelUpdateManager updateManager;
- private OsuGame game;
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
{
@@ -125,23 +158,12 @@ namespace osu.Desktop.Updater
protected override Notification CreateCompletionNotification()
{
- return new ProgressCompletionNotification
- {
- Text = @"Update ready to install. Click to restart!",
- Activated = () =>
- {
- updateManager.PrepareUpdateAsync()
- .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
- return true;
- }
- };
+ return new UpdateCompleteNotification(updateManager);
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours, OsuGame game)
+ private void load(OsuColour colours)
{
- this.game = game;
-
IconContent.AddRange(new Drawable[]
{
new Box
diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
new file mode 100644
index 0000000000..efc3f21149
--- /dev/null
+++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Platform;
+using osu.Game;
+using osu.Game.Configuration;
+
+namespace osu.Desktop.Windows
+{
+ public class GameplayWinKeyBlocker : Component
+ {
+ private Bindable disableWinKey;
+ private Bindable localUserPlaying;
+
+ [Resolved]
+ private GameHost host { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGame game, OsuConfigManager config)
+ {
+ localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
+ localUserPlaying.BindValueChanged(_ => updateBlocking());
+
+ disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey);
+ disableWinKey.BindValueChanged(_ => updateBlocking(), true);
+ }
+
+ private void updateBlocking()
+ {
+ bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
+
+ if (shouldDisable)
+ host.InputThread.Scheduler.Add(WindowsKey.Disable);
+ else
+ host.InputThread.Scheduler.Add(WindowsKey.Enable);
+ }
+ }
+}
diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs
new file mode 100644
index 0000000000..f19d741107
--- /dev/null
+++ b/osu.Desktop/Windows/WindowsKey.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;
+using System.Runtime.InteropServices;
+
+namespace osu.Desktop.Windows
+{
+ internal class WindowsKey
+ {
+ private delegate int LowLevelKeyboardProcDelegate(int nCode, int wParam, ref KdDllHookStruct lParam);
+
+ private static bool isBlocked;
+
+ private const int wh_keyboard_ll = 13;
+ private const int wm_keydown = 256;
+ private const int wm_syskeyup = 261;
+
+ //Resharper disable once NotAccessedField.Local
+ private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC
+ private static IntPtr keyHook;
+
+ [StructLayout(LayoutKind.Explicit)]
+ private readonly struct KdDllHookStruct
+ {
+ [FieldOffset(0)]
+ public readonly int VkCode;
+
+ [FieldOffset(8)]
+ public readonly int Flags;
+ }
+
+ private static int lowLevelKeyboardProc(int nCode, int wParam, ref KdDllHookStruct lParam)
+ {
+ if (wParam >= wm_keydown && wParam <= wm_syskeyup)
+ {
+ switch (lParam.VkCode)
+ {
+ case 0x5B: // left windows key
+ case 0x5C: // right windows key
+ return 1;
+ }
+ }
+
+ return callNextHookEx(0, nCode, wParam, ref lParam);
+ }
+
+ internal static void Disable()
+ {
+ if (keyHook != IntPtr.Zero || isBlocked)
+ return;
+
+ keyHook = setWindowsHookEx(wh_keyboard_ll, (keyboardHookDelegate = lowLevelKeyboardProc), Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);
+
+ isBlocked = true;
+ }
+
+ internal static void Enable()
+ {
+ if (keyHook == IntPtr.Zero || !isBlocked)
+ return;
+
+ keyHook = unhookWindowsHookEx(keyHook);
+ keyboardHookDelegate = null;
+
+ keyHook = IntPtr.Zero;
+
+ isBlocked = false;
+ }
+
+ [DllImport(@"user32.dll", EntryPoint = @"SetWindowsHookExA")]
+ private static extern IntPtr setWindowsHookEx(int idHook, LowLevelKeyboardProcDelegate lpfn, IntPtr hMod, int dwThreadId);
+
+ [DllImport(@"user32.dll", EntryPoint = @"UnhookWindowsHookEx")]
+ private static extern IntPtr unhookWindowsHookEx(IntPtr hHook);
+
+ [DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")]
+ private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam);
+ }
+}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index c34e1e1221..ad5c323e9b 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -1,9 +1,9 @@
- netcoreapp3.1
+ net5.0
WinExe
true
- click the circles. to the beat.
+ A free-to-win rhythm game. Rhythm is just a *click* away!
osu!
osu!lazer
osu!lazer
@@ -24,12 +24,14 @@
-
-
+
+
+
+
-
-
+
+
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index a919d54f38..fa182f8e70 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -9,10 +9,9 @@
https://osu.ppy.sh/
https://puu.sh/tYyXZ/9a01a5d1b0.ico
false
- click the circles. to the beat.
- click the circles.
+ A free-to-win rhythm game. Rhythm is just a *click* away!
testing
- Copyright (c) 2020 ppy Pty Ltd
+ Copyright (c) 2021 ppy Pty Ltd
en-AU
diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
index 394fd75488..1d207d04c7 100644
--- a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
+++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
@@ -8,7 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.IO.Archives;
-using osu.Game.Resources;
+using osu.Game.Tests.Resources;
namespace osu.Game.Benchmarks
{
@@ -18,8 +18,8 @@ namespace osu.Game.Benchmarks
public override void SetUp()
{
- using (var resources = new DllResourceStore(OsuResources.ResourceAssembly))
- using (var archive = resources.GetStream("Beatmaps/241526 Soleily - Renatus.osz"))
+ using (var resources = new DllResourceStore(typeof(TestResources).Assembly))
+ using (var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz"))
using (var reader = new ZipArchiveReader(archive))
reader.GetStream("Soleily - Renatus (Gamu) [Insane].osu").CopyTo(beatmapStream);
}
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index f2e1c0ec3b..7a74563b2b 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -1,18 +1,19 @@
- netcoreapp3.1
+ net5.0
Exe
false
-
-
-
+
+
+
+
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/.vscode/launch.json b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
index 67d27c33eb..9aaaf418c2 100644
--- a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
+++ b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Debug/netcoreapp3.1/osu.Game.Rulesets.Catch.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/netcoreapp3.1/osu.Game.Rulesets.Catch.Tests.dll"
+ "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json
index 18a6f8ca70..d8feacc8a7 100644
--- a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json
+++ b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json
@@ -9,11 +9,10 @@
"command": "dotnet",
"args": [
"build",
- "--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj",
- "/p:GenerateFullPaths=true",
- "/m",
- "/verbosity:m"
+ "-p:GenerateFullPaths=true",
+ "-m",
+ "-verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
@@ -24,24 +23,14 @@
"command": "dotnet",
"args": [
"build",
- "--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj",
- "/p:Configuration=Release",
- "/p:GenerateFullPaths=true",
- "/m",
- "/verbosity:m"
+ "-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/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index f4749be370..33fdcdaf1e 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -8,13 +8,13 @@ using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
+ [Timeout(10000)]
public class CatchBeatmapConversionTest : BeatmapConversionTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
@@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("hardrock-stream", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })]
+ [TestCase("right-bound-hr-offset", new[] { typeof(CatchModHardRock) })]
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
@@ -83,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public float Position
{
- get => HitObject?.X * CatchPlayfield.BASE_WIDTH ?? position;
+ get => HitObject?.EffectiveX ?? position;
set => position = value;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index 51fe0b035d..5580358f89 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
+using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Tests.Beatmaps;
@@ -13,10 +14,14 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
- [TestCase(4.2058561036909863d, "diffcalc-test")]
+ [TestCase(4.050601681491468d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
+ [TestCase(5.169743871843191d, "diffcalc-test")]
+ public void TestClockRateAdjusted(double expected, string name)
+ => Test(expected, name, new CatchModDoubleTime());
+
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new CatchRuleset();
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs
index 04e6dea376..eae07daa3d 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs
@@ -12,17 +12,32 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestFixture]
public class CatchLegacyModConversionTest : LegacyModConversionTest
{
- [TestCase(LegacyMods.Easy, new[] { typeof(CatchModEasy) })]
- [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) })]
- [TestCase(LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) })]
- [TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })]
+ private static readonly object[][] catch_mod_mapping =
+ {
+ new object[] { LegacyMods.NoFail, new[] { typeof(CatchModNoFail) } },
+ new object[] { LegacyMods.Easy, new[] { typeof(CatchModEasy) } },
+ new object[] { LegacyMods.Hidden, new[] { typeof(CatchModHidden) } },
+ new object[] { LegacyMods.HardRock, new[] { typeof(CatchModHardRock) } },
+ new object[] { LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) } },
+ new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } },
+ new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } },
+ new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
+ new object[] { LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) } },
+ new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
+ new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
+ new object[] { LegacyMods.Perfect, new[] { typeof(CatchModPerfect) } },
+ new object[] { LegacyMods.Cinema, new[] { typeof(CatchModCinema) } },
+ new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
+ };
+
+ [TestCaseSource(nameof(catch_mod_mapping))]
+ [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
- [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModFlashlight), typeof(CatchModNightcore) })]
- [TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })]
- [TestCase(LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
- [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime), typeof(CatchModPerfect) })]
- public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
+ public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
+
+ [TestCaseSource(nameof(catch_mod_mapping))]
+ public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new CatchRuleset();
}
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
new file mode 100644
index 0000000000..e70def7f8b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.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 NUnit.Framework;
+using osu.Framework.IO.Stores;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Rulesets.Catch.Skinning.Legacy;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ [TestFixture]
+ public class CatchSkinColourDecodingTest
+ {
+ [Test]
+ public void TestCatchSkinColourDecoding()
+ {
+ var store = new NamespacedResourceStore(new DllResourceStore(GetType().Assembly), "Resources/special-skin");
+ var rawSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, store);
+ var skinSource = new SkinProvidingContainer(rawSkin);
+ var skin = new CatchLegacySkinTransformer(skinSource);
+
+ Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig(CatchSkinColour.HyperDash)?.Value);
+ Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value);
+ Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value);
+ }
+
+ private class TestLegacySkin : LegacySkin
+ {
+ public TestLegacySkin(SkinInfo skin, IResourceStore storage)
+ // Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
+ : base(skin, storage, null, "skin.ini")
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs
new file mode 100644
index 0000000000..378772fea3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public abstract class CatchSkinnableTestScene : SkinnableTestScene
+ {
+ protected override Ruleset CreateRulesetForSkinProvider() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
index 47e91e50d4..3e06e78dba 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
@@ -13,8 +13,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public class TestSceneCatchModPerfect : ModPerfectTestScene
{
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
public TestSceneCatchModPerfect()
- : base(new CatchRuleset(), new CatchModPerfect())
+ : base(new CatchModPerfect())
{
}
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
new file mode 100644
index 0000000000..c01aff0aa0
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
@@ -0,0 +1,84 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests.Mods
+{
+ public class TestSceneCatchModRelax : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
+ [Test]
+ public void TestModRelax() => CreateModTest(new ModTestData
+ {
+ Mod = new CatchModRelax(),
+ Autoplay = false,
+ PassCondition = passCondition,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Fruit
+ {
+ X = CatchPlayfield.CENTER_X,
+ StartTime = 0
+ },
+ new Fruit
+ {
+ X = 0,
+ StartTime = 1000
+ },
+ new Fruit
+ {
+ X = CatchPlayfield.WIDTH,
+ StartTime = 2000
+ },
+ new JuiceStream
+ {
+ X = CatchPlayfield.CENTER_X,
+ StartTime = 3000,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 })
+ }
+ }
+ }
+ });
+
+ private bool passCondition()
+ {
+ var playfield = this.ChildrenOfType().Single();
+
+ switch (Player.ScoreProcessor.Combo.Value)
+ {
+ case 0:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre);
+ break;
+
+ case 1:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomLeft);
+ break;
+
+ case 2:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomRight);
+ break;
+
+ case 3:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre);
+ break;
+ }
+
+ return Player.ScoreProcessor.Combo.Value >= 6;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png
new file mode 100644
index 0000000000..8304617d8c
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png
new file mode 100644
index 0000000000..c3b85eb873
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png
new file mode 100644
index 0000000000..7f65eb7ca7
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png
new file mode 100644
index 0000000000..82bec3babe
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png
new file mode 100644
index 0000000000..5e38c75a9d
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png
new file mode 100644
index 0000000000..a562d9f2ac
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png
new file mode 100644
index 0000000000..b4cf81f26e
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png
new file mode 100644
index 0000000000..a23f5379b2
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png
new file mode 100644
index 0000000000..430b18509d
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png
new file mode 100644
index 0000000000..add1202c31
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini
new file mode 100644
index 0000000000..94c6b5b58d
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini
@@ -0,0 +1,2 @@
+[General]
+// no version specified means v1
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png
similarity index 100%
rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png
rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png
similarity index 100%
rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png
rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png
new file mode 100644
index 0000000000..508cc85e4a
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png
new file mode 100644
index 0000000000..84f74e1ec9
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png
new file mode 100644
index 0000000000..49625c6623
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png
new file mode 100644
index 0000000000..623b24612f
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png
new file mode 100644
index 0000000000..a33286dc8f
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png
new file mode 100644
index 0000000000..d8250b0c63
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png
new file mode 100644
index 0000000000..75d3cbd3bd
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png
new file mode 100644
index 0000000000..cfe2021df4
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png
new file mode 100644
index 0000000000..ba9492c7f8
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png
new file mode 100644
index 0000000000..a7b6b81570
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini
new file mode 100644
index 0000000000..96d50f1451
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini
@@ -0,0 +1,4 @@
+[CatchTheBeat]
+HyperDash: 232,185,35
+HyperDashFruit: 0,255,255
+HyperDashAfterImage: 232,74,35
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index ed7bfb9a44..f552c3c27b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@@ -12,13 +14,8 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
- public class TestSceneAutoJuiceStream : PlayerTestScene
+ public class TestSceneAutoJuiceStream : TestSceneCatchPlayer
{
- public TestSceneAutoJuiceStream()
- : base(new CatchRuleset())
- {
- }
-
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
@@ -32,18 +29,22 @@ namespace osu.Game.Rulesets.Catch.Tests
for (int i = 0; i < 100; i++)
{
- float width = (i % 10 + 1) / 20f;
+ float width = (i % 10 + 1) / 20f * CatchPlayfield.WIDTH;
beatmap.HitObjects.Add(new JuiceStream
{
- X = 0.5f - width / 2,
+ X = CatchPlayfield.CENTER_X - width / 2,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
- new Vector2(width * CatchPlayfield.BASE_WIDTH, 0)
+ new Vector2(width, 0)
}),
StartTime = i * 2000,
- NewCombo = i % 8 == 0
+ NewCombo = i % 8 == 0,
+ Samples = new List(new[]
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 100)
+ })
});
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
index 024c4cefb0..e89a95ae37 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
@@ -1,36 +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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawables;
-using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneBananaShower : PlayerTestScene
+ public class TestSceneBananaShower : TestSceneCatchPlayer
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(BananaShower),
- typeof(Banana),
- typeof(DrawableBananaShower),
- typeof(DrawableBanana),
-
- typeof(CatchRuleset),
- typeof(DrawableCatchRuleset),
- };
-
- public TestSceneBananaShower()
- : base(new CatchRuleset())
- {
- }
-
[Test]
public void TestBananaShower()
{
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
new file mode 100644
index 0000000000..09362929d2
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.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 System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneCatchModHidden : ModTestScene
+ {
+ [Test]
+ public void TestJuiceStream()
+ {
+ CreateModTest(new ModTestData
+ {
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new JuiceStream
+ {
+ StartTime = 1000,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }),
+ X = CatchPlayfield.WIDTH / 2
+ }
+ }
+ },
+ Mod = new CatchModHidden(),
+ PassCondition = () => Player.Results.Count > 0
+ && Player.ChildrenOfType().Single().Alpha > 0
+ && Player.ChildrenOfType().Last().Alpha > 0
+ });
+ }
+
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs
index 9836a7811a..31d0831fae 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs
@@ -9,9 +9,6 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestFixture]
public class TestSceneCatchPlayer : PlayerTestScene
{
- public TestSceneCatchPlayer()
- : base(new CatchRuleset())
- {
- }
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
new file mode 100644
index 0000000000..b7cd6737b1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ [TestFixture]
+ public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
+ [Test]
+ public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin)
+ {
+ if (withModifiedSkin)
+ {
+ AddStep("change component scale", () => Player.ChildrenOfType().First().Scale = new Vector2(2f));
+ AddStep("update target", () => Player.ChildrenOfType().ForEach(LegacySkin.UpdateDrawableTarget));
+ AddStep("exit player", () => Player.Exit());
+ CreateTest(null);
+ }
+
+ AddAssert("legacy HUD combo counter hidden", () =>
+ {
+ return Player.ChildrenOfType().All(c => c.ChildrenOfType().Single().Alpha == 0f);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs
new file mode 100644
index 0000000000..a10371b0f7
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneCatchReplay : TestSceneCatchPlayer
+ {
+ protected override bool Autoplay => true;
+
+ private const int object_count = 10;
+
+ [Test]
+ public void TestReplayCatcherPositionIsFramePerfect()
+ {
+ AddUntilStep("caught all fruits", () => Player.ScoreProcessor.Combo.Value == object_count);
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ var beatmap = new Beatmap
+ {
+ BeatmapInfo =
+ {
+ Ruleset = ruleset,
+ }
+ };
+
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
+
+ for (int i = 0; i < object_count / 2; i++)
+ {
+ beatmap.HitObjects.Add(new Fruit
+ {
+ StartTime = (i + 1) * 1000,
+ X = 0
+ });
+ beatmap.HitObjects.Add(new Fruit
+ {
+ StartTime = (i + 1) * 1000 + 1,
+ X = CatchPlayfield.WIDTH
+ });
+ }
+
+ return beatmap;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
index 9ce46ad6ba..1ff31697b8 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
@@ -4,18 +4,13 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Tests.Visual;
+using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatchStacker : PlayerTestScene
+ public class TestSceneCatchStacker : TestSceneCatchPlayer
{
- public TestSceneCatchStacker()
- : base(new CatchRuleset())
- {
- }
-
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
@@ -28,7 +23,14 @@ namespace osu.Game.Rulesets.Catch.Tests
};
for (int i = 0; i < 512; i++)
- beatmap.HitObjects.Add(new Fruit { X = 0.5f + i / 2048f * (i % 10 - 5), StartTime = i * 100, NewCombo = i % 8 == 0 });
+ {
+ beatmap.HitObjects.Add(new Fruit
+ {
+ X = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH,
+ StartTime = i * 100,
+ NewCombo = i % 8 == 0
+ });
+ }
return beatmap;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index fe0d512166..900691ecae 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -1,34 +1,311 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Tests.Visual;
-using System;
-using System.Collections.Generic;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Judgements;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcher : SkinnableTestScene
+ public class TestSceneCatcher : OsuTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(CatcherArea),
- typeof(CatcherSprite)
- };
+ [Resolved]
+ private OsuConfigManager config { get; set; }
- [BackgroundDependencyLoader]
- private void load()
+ private Container droppedObjectContainer;
+
+ private TestCatcher catcher;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
- SetContents(() => new Catcher
+ var difficulty = new BeatmapDifficulty
+ {
+ CircleSize = 0,
+ };
+
+ var trailContainer = new Container();
+ droppedObjectContainer = new Container();
+ catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
+
+ Child = new Container
{
- RelativePositionAxes = Axes.None,
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ trailContainer,
+ droppedObjectContainer,
+ catcher
+ }
+ };
+ });
+
+ [Test]
+ public void TestCatcherHyperStateReverted()
+ {
+ DrawableCatchHitObject drawableObject1 = null;
+ DrawableCatchHitObject drawableObject2 = null;
+ JudgementResult result1 = null;
+ JudgementResult result2 = null;
+ AddStep("catch hyper fruit", () =>
+ {
+ attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1);
});
+ AddStep("catch normal fruit", () =>
+ {
+ attemptCatch(new Fruit(), out drawableObject2, out result2);
+ });
+ AddStep("revert second result", () =>
+ {
+ catcher.OnRevertResult(drawableObject2, result2);
+ });
+ checkHyperDash(true);
+ AddStep("revert first result", () =>
+ {
+ catcher.OnRevertResult(drawableObject1, result1);
+ });
+ checkHyperDash(false);
+ }
+
+ [Test]
+ public void TestCatcherAnimationStateReverted()
+ {
+ DrawableCatchHitObject drawableObject = null;
+ JudgementResult result = null;
+ AddStep("catch kiai fruit", () =>
+ {
+ attemptCatch(new TestKiaiFruit(), out drawableObject, out result);
+ });
+ checkState(CatcherAnimationState.Kiai);
+ AddStep("revert result", () =>
+ {
+ catcher.OnRevertResult(drawableObject, result);
+ });
+ checkState(CatcherAnimationState.Idle);
+ }
+
+ [Test]
+ public void TestCatcherCatchWidth()
+ {
+ var halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
+ AddStep("catch fruit", () =>
+ {
+ attemptCatch(new Fruit { X = -halfWidth + 1 });
+ attemptCatch(new Fruit { X = halfWidth - 1 });
+ });
+ checkPlate(2);
+ AddStep("miss fruit", () =>
+ {
+ attemptCatch(new Fruit { X = -halfWidth - 1 });
+ attemptCatch(new Fruit { X = halfWidth + 1 });
+ });
+ checkPlate(2);
+ }
+
+ [Test]
+ public void TestFruitChangesCatcherState()
+ {
+ AddStep("miss fruit", () => attemptCatch(new Fruit { X = 100 }));
+ checkState(CatcherAnimationState.Fail);
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ checkState(CatcherAnimationState.Idle);
+ AddStep("catch kiai fruit", () => attemptCatch(new TestKiaiFruit()));
+ checkState(CatcherAnimationState.Kiai);
+ }
+
+ [Test]
+ public void TestNormalFruitResetsHyperDashState()
+ {
+ AddStep("catch hyper fruit", () => attemptCatch(new Fruit
+ {
+ HyperDashTarget = new Fruit { X = 100 }
+ }));
+ checkHyperDash(true);
+ AddStep("catch normal fruit", () => attemptCatch(new Fruit()));
+ checkHyperDash(false);
+ }
+
+ [Test]
+ public void TestTinyDropletMissPreservesCatcherState()
+ {
+ AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
+ {
+ HyperDashTarget = new Fruit { X = 100 }
+ }));
+ AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
+ AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
+ // catcher state and hyper dash state is preserved
+ checkState(CatcherAnimationState.Kiai);
+ checkHyperDash(true);
+ }
+
+ [Test]
+ public void TestBananaMissPreservesCatcherState()
+ {
+ AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
+ {
+ HyperDashTarget = new Fruit { X = 100 }
+ }));
+ AddStep("miss banana", () => attemptCatch(new Banana { X = 100 }));
+ // catcher state is preserved but hyper dash state is reset
+ checkState(CatcherAnimationState.Kiai);
+ checkHyperDash(false);
+ }
+
+ [Test]
+ public void TestCatcherRandomStacking()
+ {
+ AddStep("catch more fruits", () => attemptCatch(() => new Fruit
+ {
+ X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(Vector2.One)
+ }, 50));
+ }
+
+ [Test]
+ public void TestCatcherStackingSameCaughtPosition()
+ {
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ checkPlate(1);
+ AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
+ checkPlate(10);
+ AddAssert("caught objects are stacked", () =>
+ catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
+ catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
+ catcher.CaughtObjects.Any(obj => obj.Y < -25));
+ }
+
+ [Test]
+ public void TestCatcherExplosionAndDropping()
+ {
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
+ AddAssert("tiny droplet is exploded", () => catcher.CaughtObjects.Count() == 1 && droppedObjectContainer.Count == 1);
+ AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
+ AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
+ AddStep("explode", () => catcher.Explode());
+ AddAssert("fruits are exploded", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
+ AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
+ AddStep("catch fruits", () => attemptCatch(() => new Fruit(), 10));
+ AddStep("drop", () => catcher.Drop());
+ AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
+ }
+
+ [Test]
+ public void TestHitLightingColour()
+ {
+ var fruitColour = SkinConfiguration.DefaultComboColours[1];
+ AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ AddAssert("correct hit lighting colour", () =>
+ catcher.ChildrenOfType().First()?.Entry?.ObjectColour == fruitColour);
+ }
+
+ [Test]
+ public void TestHitLightingDisabled()
+ {
+ AddStep("disable hit lighting", () => config.SetValue(OsuSetting.HitLighting, false));
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ AddAssert("no hit lighting", () => !catcher.ChildrenOfType().Any());
+ }
+
+ private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
+
+ private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state);
+
+ private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
+
+ private void attemptCatch(CatchHitObject hitObject)
+ {
+ attemptCatch(() => hitObject, 1);
+ }
+
+ private void attemptCatch(Func hitObject, int count)
+ {
+ for (var i = 0; i < count; i++)
+ attemptCatch(hitObject(), out _, out _);
+ }
+
+ private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result)
+ {
+ hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ drawableObject = createDrawableObject(hitObject);
+ result = createResult(hitObject);
+ applyResult(drawableObject, result);
+ }
+
+ private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result)
+ {
+ // Load DHO to set colour of hit explosion correctly
+ Add(drawableObject);
+ drawableObject.OnLoadComplete += _ =>
+ {
+ catcher.OnNewResult(drawableObject, result);
+ drawableObject.Expire();
+ };
+ }
+
+ private JudgementResult createResult(CatchHitObject hitObject)
+ {
+ return new CatchJudgementResult(hitObject, hitObject.CreateJudgement())
+ {
+ Type = catcher.CanCatch(hitObject) ? HitResult.Great : HitResult.Miss
+ };
+ }
+
+ private DrawableCatchHitObject createDrawableObject(CatchHitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Banana banana:
+ return new DrawableBanana(banana);
+
+ case Droplet droplet:
+ return new DrawableDroplet(droplet);
+
+ case Fruit fruit:
+ return new DrawableFruit(fruit);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(hitObject));
+ }
+ }
+
+ public class TestCatcher : Catcher
+ {
+ public IEnumerable CaughtObjects => this.ChildrenOfType();
+
+ public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty)
+ : base(trailsTarget, droppedObjectTarget, difficulty)
+ {
+ }
+ }
+
+ public class TestKiaiFruit : Fruit
+ {
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index cf68c5424d..4af5098451 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -6,81 +6,115 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Threading;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.UI;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcherArea : SkinnableTestScene
+ public class TestSceneCatcherArea : CatchSkinnableTestScene
{
private RulesetInfo catchRuleset;
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ private Catcher catcher => this.ChildrenOfType().First();
+
+ private float circleSize;
+
+ private ScheduledDelegate addManyFruit;
+
+ private BeatmapDifficulty beatmapDifficulty;
+
public TestSceneCatcherArea()
{
- AddSliderStep("CircleSize", 0, 8, 5, createCatcher);
- AddToggleStep("Hyperdash", t =>
- CreatedDrawables.OfType().Select(i => i.Child)
- .OfType().ForEach(c => c.ToggleHyperDash(t)));
+ AddSliderStep("circle size", 0, 8, 5, createCatcher);
+ AddToggleStep("hyper dash", t => this.ChildrenOfType().ForEach(area => area.ToggleHyperDash(t)));
- AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false)
+ AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
+ AddStep("catch many random fruit", () =>
{
- X = this.ChildrenOfType().First().MovableCatcher.X
- }), 20);
- AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
- {
- X = this.ChildrenOfType().First().MovableCatcher.X,
- LastInCombo = true,
- }), 20);
- AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
- {
- X = this.ChildrenOfType().First().MovableCatcher.X,
- }), 20);
- AddRepeatStep("miss fruit", () => catchFruit(new Fruit
- {
- X = this.ChildrenOfType().First().MovableCatcher.X + 100,
- LastInCombo = true,
- }, true), 20);
+ int count = 50;
+
+ addManyFruit?.Cancel();
+ addManyFruit = Scheduler.AddDelayed(() =>
+ {
+ attemptCatch(new Fruit
+ {
+ X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty) * 0.6f,
+ });
+
+ if (count-- == 0)
+ addManyFruit?.Cancel();
+ }, 50, true);
+ });
+ AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true }));
+ AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit()));
+ AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true }));
}
- private void catchFruit(Fruit fruit, bool miss = false)
+ private void attemptCatch(Fruit fruit)
{
- this.ChildrenOfType().ForEach(area =>
+ fruit.X = fruit.OriginalX + catcher.X;
+ fruit.ApplyDefaults(new ControlPointInfo(), beatmapDifficulty);
+
+ foreach (var area in this.ChildrenOfType())
{
DrawableFruit drawable = new DrawableFruit(fruit);
area.Add(drawable);
Schedule(() =>
{
- area.AttemptCatch(fruit);
- area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
+ area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement())
+ {
+ Type = area.MovableCatcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
+ });
drawable.Expire();
});
- });
+ }
}
private void createCatcher(float size)
{
- SetContents(() => new CatchInputManager(catchRuleset)
+ circleSize = size;
+
+ beatmapDifficulty = new BeatmapDifficulty
{
- RelativeSizeAxes = Axes.Both,
- Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
+ CircleSize = circleSize
+ };
+
+ SetContents(_ =>
+ {
+ var droppedObjectContainer = new Container
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.TopLeft,
- CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation
- },
+ RelativeSizeAxes = Axes.Both
+ };
+
+ return new CatchInputManager(catchRuleset)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ droppedObjectContainer,
+ new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.TopCentre,
+ }
+ }
+ };
});
}
@@ -90,26 +124,13 @@ namespace osu.Game.Rulesets.Catch.Tests
catchRuleset = rulesets.GetRuleset(2);
}
- public class TestFruit : Fruit
- {
- public TestFruit(bool kiai)
- {
- var kiaiCpi = new ControlPointInfo();
- kiaiCpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
-
- ApplyDefaultsToSelf(kiaiCpi, new BeatmapDifficulty());
- }
- }
-
private class TestCatcherArea : CatcherArea
{
- public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
- : base(beatmapDifficulty)
+ public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
+ : base(droppedObjectContainer, beatmapDifficulty)
{
}
- public new Catcher MovableCatcher => base.MovableCatcher;
-
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
new file mode 100644
index 0000000000..064a84cb98
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneComboCounter : CatchSkinnableTestScene
+ {
+ private ScoreProcessor scoreProcessor;
+
+ private Color4 judgedObjectColour = Color4.White;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ scoreProcessor = new ScoreProcessor();
+
+ SetContents(_ => new CatchComboDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(2.5f),
+ });
+ });
+
+ [Test]
+ public void TestCatchComboCounter()
+ {
+ AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 20);
+ AddStep("perform miss", () => performJudgement(HitResult.Miss));
+
+ AddStep("randomize judged object colour", () =>
+ {
+ judgedObjectColour = new Color4(
+ RNG.NextSingle(1f),
+ RNG.NextSingle(1f),
+ RNG.NextSingle(1f),
+ 1f
+ );
+ });
+ }
+
+ private void performJudgement(HitResult type, Judgement judgement = null)
+ {
+ var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };
+
+ var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type };
+ scoreProcessor.ApplyResult(result);
+
+ foreach (var counter in CreatedDrawables.Cast())
+ counter.OnNewResult(judgedObject, result);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index df5494aab0..3e4995482d 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -22,15 +21,6 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneDrawableHitObjects : OsuTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(Catcher),
- typeof(DrawableCatchRuleset),
- typeof(DrawableFruit),
- typeof(DrawableJuiceStream),
- typeof(DrawableBanana)
- };
-
private DrawableCatchRuleset drawableRuleset;
private double playfieldTime => drawableRuleset.Playfield.Time.Current;
@@ -146,7 +136,7 @@ namespace osu.Game.Rulesets.Catch.Tests
if (juice.NestedHitObjects.Last() is CatchHitObject tail)
tail.LastInCombo = true; // usually the (Catch)BeatmapProcessor would do this for us when necessary
- addToPlayfield(new DrawableJuiceStream(juice, drawableRuleset.CreateDrawableRepresentation));
+ addToPlayfield(new DrawableJuiceStream(juice));
}
private void spawnBananas(bool hit = false)
@@ -168,8 +158,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private float getXCoords(bool hit)
{
- const float x_offset = 0.2f;
- float xCoords = drawableRuleset.Playfield.Width / 2;
+ const float x_offset = 0.2f * CatchPlayfield.WIDTH;
+ float xCoords = CatchPlayfield.CENTER_X;
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset;
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs
index 8c3dfef39c..62fe5dca2c 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using System.Collections.Generic;
-using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods;
@@ -11,8 +8,6 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneDrawableHitObjectsHidden : TestSceneDrawableHitObjects
{
- public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(CatchModHidden) }).ToList();
-
[SetUp]
public void SetUp() => Schedule(() =>
{
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index 82d5aa936f..943adbef52 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -1,118 +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 System;
-using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
-using osu.Game.Tests.Visual;
-using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneFruitObjects : SkinnableTestScene
+ public class TestSceneFruitObjects : CatchSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(CatchHitObject),
- typeof(Fruit),
- typeof(FruitPiece),
- typeof(Droplet),
- typeof(Banana),
- typeof(BananaShower),
- typeof(DrawableCatchHitObject),
- typeof(DrawableFruit),
- typeof(DrawableDroplet),
- typeof(DrawableBanana),
- typeof(DrawableBananaShower),
- typeof(Pulp),
- };
-
protected override void LoadComplete()
{
base.LoadComplete();
- foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
- AddStep($"show {rep}", () => SetContents(() => createDrawable(rep)));
+ AddStep("show pear", () => SetContents(_ => createDrawableFruit(0)));
+ AddStep("show grape", () => SetContents(_ => createDrawableFruit(1)));
+ AddStep("show pineapple / apple", () => SetContents(_ => createDrawableFruit(2)));
+ AddStep("show raspberry / orange", () => SetContents(_ => createDrawableFruit(3)));
- AddStep("show droplet", () => SetContents(createDrawableDroplet));
+ AddStep("show banana", () => SetContents(_ => createDrawableBanana()));
- AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
+ AddStep("show droplet", () => SetContents(_ => createDrawableDroplet()));
+ AddStep("show tiny droplet", () => SetContents(_ => createDrawableTinyDroplet()));
- foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
- AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true)));
+ AddStep("show hyperdash pear", () => SetContents(_ => createDrawableFruit(0, true)));
+ AddStep("show hyperdash grape", () => SetContents(_ => createDrawableFruit(1, true)));
+ AddStep("show hyperdash pineapple / apple", () => SetContents(_ => createDrawableFruit(2, true)));
+ AddStep("show hyperdash raspberry / orange", () => SetContents(_ => createDrawableFruit(3, true)));
+
+ AddStep("show hyperdash droplet", () => SetContents(_ => createDrawableDroplet(true)));
}
- private Drawable createDrawableTinyDroplet()
+ private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>
+ new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
+ {
+ IndexInBeatmap = indexInBeatmap,
+ HyperDashBindable = { Value = hyperdash }
+ }));
+
+ private Drawable createDrawableBanana() =>
+ new TestDrawableCatchHitObjectSpecimen(new DrawableBanana(new Banana()));
+
+ private Drawable createDrawableDroplet(bool hyperdash = false) =>
+ new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
+ {
+ HyperDashBindable = { Value = hyperdash }
+ }));
+
+ private Drawable createDrawableTinyDroplet() => new TestDrawableCatchHitObjectSpecimen(new DrawableTinyDroplet(new TinyDroplet()));
+ }
+
+ public class TestDrawableCatchHitObjectSpecimen : CompositeDrawable
+ {
+ public readonly ManualClock ManualClock;
+
+ public TestDrawableCatchHitObjectSpecimen(DrawableCatchHitObject d)
{
- var droplet = new TinyDroplet
+ AutoSizeAxes = Axes.Both;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ ManualClock = new ManualClock();
+ Clock = new FramedClock(ManualClock);
+
+ var hitObject = d.HitObject;
+ hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ hitObject.Scale = 1.5f;
+ hitObject.StartTime = 500;
+
+ d.Anchor = Anchor.Centre;
+ d.HitObjectApplied += _ =>
{
- StartTime = Clock.CurrentTime,
- Scale = 1.5f,
+ d.LifetimeStart = double.NegativeInfinity;
+ d.LifetimeEnd = double.PositiveInfinity;
};
- return new DrawableTinyDroplet(droplet)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
-
- private Drawable createDrawableDroplet()
- {
- var droplet = new Droplet
- {
- StartTime = Clock.CurrentTime,
- Scale = 1.5f,
- };
-
- return new DrawableDroplet(droplet)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
-
- private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false)
- {
- Fruit fruit = new TestCatchFruit(rep)
- {
- Scale = 1.5f,
- HyperDashTarget = hyperdash ? new Banana() : null
- };
-
- return new DrawableFruit(fruit)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
-
- public class TestCatchFruit : Fruit
- {
- public TestCatchFruit(FruitVisualRepresentation rep)
- {
- VisualRepresentation = rep;
- StartTime = 1000000000000;
- }
-
- public override FruitVisualRepresentation VisualRepresentation { get; }
+ InternalChild = d;
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs
new file mode 100644
index 0000000000..c888dc0a65
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.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 NUnit.Framework;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneFruitRandomness : OsuTestScene
+ {
+ private readonly DrawableFruit drawableFruit;
+ private readonly DrawableBanana drawableBanana;
+
+ public TestSceneFruitRandomness()
+ {
+ drawableFruit = new DrawableFruit(new Fruit());
+ drawableBanana = new DrawableBanana(new Banana());
+
+ Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 });
+ Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana));
+
+ AddSliderStep("start time", 500, 600, 0, x =>
+ {
+ drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x;
+ });
+ }
+
+ [Test]
+ public void TestFruitRandomness()
+ {
+ // Use values such that the banana colour changes (2/3 of the integers are okay)
+ const int initial_start_time = 500;
+ const int another_start_time = 501;
+
+ float fruitRotation = 0;
+ float bananaRotation = 0;
+ Vector2 bananaSize = new Vector2();
+ Color4 bananaColour = new Color4();
+
+ AddStep("Initialize start time", () =>
+ {
+ drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
+
+ fruitRotation = drawableFruit.DisplayRotation;
+ bananaRotation = drawableBanana.DisplayRotation;
+ bananaSize = drawableBanana.DisplaySize;
+ bananaColour = drawableBanana.AccentColour.Value;
+ });
+
+ AddStep("change start time", () =>
+ {
+ drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time;
+ });
+
+ AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation);
+ AddAssert("banana rotation is changed", () => drawableBanana.DisplayRotation != bananaRotation);
+ AddAssert("banana size is changed", () => drawableBanana.DisplaySize != bananaSize);
+ AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour);
+
+ AddStep("reset start time", () =>
+ {
+ drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
+ });
+
+ AddAssert("rotation and size restored", () =>
+ drawableFruit.DisplayRotation == fruitRotation &&
+ drawableBanana.DisplayRotation == bananaRotation &&
+ drawableBanana.DisplaySize == bananaSize &&
+ drawableBanana.AccentColour.Value == bananaColour);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
new file mode 100644
index 0000000000..9446e864a1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.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.Bindables;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneFruitVisualChange : TestSceneFruitObjects
+ {
+ private readonly Bindable indexInBeatmap = new Bindable();
+ private readonly Bindable hyperDash = new Bindable();
+
+ protected override void LoadComplete()
+ {
+ AddStep("fruit changes visual and hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
+ {
+ IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
+ HyperDashBindable = { BindTarget = hyperDash },
+ }))));
+
+ AddStep("droplet changes hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
+ {
+ HyperDashBindable = { BindTarget = hyperDash },
+ }))));
+
+ Scheduler.AddDelayed(() => indexInBeatmap.Value++, 250, true);
+ Scheduler.AddDelayed(() => hyperDash.Value = !hyperDash.Value, 1000, true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index 49ff9df4d7..db09b2bc6b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -2,51 +2,60 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
-using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneHyperDash : PlayerTestScene
+ public class TestSceneHyperDash : TestSceneCatchPlayer
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(CatcherArea),
- };
-
- public TestSceneHyperDash()
- : base(new CatchRuleset())
- {
- }
-
protected override bool Autoplay => true;
+ private int hyperDashCount;
+ private bool inHyperDash;
+
[Test]
public void TestHyperDash()
{
- AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
- AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast.
-
- AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0);
-
- for (int i = 0; i < 3; i++)
+ AddStep("reset count", () =>
{
- AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing);
- AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing);
+ inHyperDash = false;
+ hyperDashCount = 0;
+
+ // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
+ Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
+ {
+ var catcher = Player.ChildrenOfType().FirstOrDefault()?.MovableCatcher;
+
+ if (catcher == null)
+ return;
+
+ if (catcher.HyperDashing != inHyperDash)
+ {
+ inHyperDash = catcher.HyperDashing;
+ if (catcher.HyperDashing)
+ hyperDashCount++;
+ }
+ };
+ });
+
+ AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
+
+ for (int i = 0; i < 9; i++)
+ {
+ int count = i + 1;
+ AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count);
}
}
- private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher;
-
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
@@ -58,14 +67,16 @@ namespace osu.Game.Rulesets.Catch.Tests
}
};
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
+
// Should produce a hyper-dash (edge case test)
- beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56 / 512f, NewCombo = true });
- beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308 / 512f, NewCombo = true });
+ beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true });
+ beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true });
double startTime = 3000;
- const float left_x = 0.02f;
- const float right_x = 0.98f;
+ const float left_x = 0.02f * CatchPlayfield.WIDTH;
+ const float right_x = 0.98f * CatchPlayfield.WIDTH;
createObjects(() => new Fruit { X = left_x });
createObjects(() => new TestJuiceStream(right_x), 1);
@@ -75,6 +86,20 @@ namespace osu.Game.Rulesets.Catch.Tests
createObjects(() => new Fruit { X = right_x });
createObjects(() => new TestJuiceStream(left_x), 1);
+ beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint
+ {
+ BeatLength = 50
+ });
+
+ createObjects(() => new TestJuiceStream(left_x)
+ {
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(Vector2.Zero),
+ new PathControlPoint(new Vector2(512, 0))
+ })
+ }, 1);
+
return beatmap;
void createObjects(Func createObject, int count = 3)
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
new file mode 100644
index 0000000000..683a776dcc
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -0,0 +1,210 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Rulesets.Catch.Skinning.Legacy;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneHyperDashColouring : OsuTestScene
+ {
+ [Resolved]
+ private SkinManager skins { get; set; }
+
+ [Test]
+ public void TestDefaultCatcherColour()
+ {
+ var skin = new TestSkin();
+
+ checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
+ }
+
+ [Test]
+ public void TestCustomCatcherColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod
+ };
+
+ checkHyperDashCatcherColour(skin, skin.HyperDashColour);
+ }
+
+ [Test]
+ public void TestCustomEndGlowColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashAfterImageColour = Color4.Lime
+ };
+
+ checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR, skin.HyperDashAfterImageColour);
+ }
+
+ [Test]
+ public void TestCustomEndGlowColourPriority()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod,
+ HyperDashAfterImageColour = Color4.Lime
+ };
+
+ checkHyperDashCatcherColour(skin, skin.HyperDashColour, skin.HyperDashAfterImageColour);
+ }
+
+ [Test]
+ public void TestDefaultFruitColour()
+ {
+ var skin = new TestSkin();
+
+ checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
+ }
+
+ [Test]
+ public void TestCustomFruitColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashFruitColour = Color4.Cyan
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
+ }
+
+ [Test]
+ public void TestCustomFruitColourPriority()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod,
+ HyperDashFruitColour = Color4.Cyan
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
+ }
+
+ [Test]
+ public void TestFruitColourFallback()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashColour);
+ }
+
+ private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
+ {
+ CatcherArea catcherArea = null;
+ CatcherTrailDisplay trails = null;
+
+ AddStep("create hyper-dashing catcher", () =>
+ {
+ Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(4f),
+ }, skin);
+ });
+
+ AddStep("get trails container", () =>
+ {
+ trails = catcherArea.OfType().Single();
+ catcherArea.MovableCatcher.SetHyperDashState(2);
+ });
+
+ AddUntilStep("catcher colour is correct", () => catcherArea.MovableCatcher.Colour == expectedCatcherColour);
+
+ AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
+ AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour));
+
+ AddStep("finish hyper-dashing", () =>
+ {
+ catcherArea.MovableCatcher.SetHyperDashState(1);
+ catcherArea.MovableCatcher.FinishTransforms();
+ });
+
+ AddAssert("catcher colour returned to white", () => catcherArea.MovableCatcher.Colour == Color4.White);
+ }
+
+ private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
+ {
+ DrawableFruit drawableFruit = null;
+
+ AddStep("create hyper-dash fruit", () =>
+ {
+ var fruit = new Fruit { HyperDashTarget = new Banana() };
+ fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(4f),
+ }, skin);
+ });
+
+ AddAssert("hyper-dash colour is correct", () => checkLegacyFruitHyperDashColour(drawableFruit, expectedColour));
+ }
+
+ private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
+ {
+ var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
+ var testSkinProvider = new SkinProvidingContainer(skin);
+ var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
+
+ return legacySkinProvider
+ .WithChild(testSkinProvider
+ .WithChild(legacySkinTransformer
+ .WithChild(child)));
+ }
+
+ private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) =>
+ fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour);
+
+ private class TestSkin : LegacySkin
+ {
+ public Color4 HyperDashColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
+ }
+
+ public Color4 HyperDashAfterImageColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
+ }
+
+ public Color4 HyperDashFruitColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
+ }
+
+ public TestSkin()
+ : base(new SkinInfo(), null, null, string.Empty)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
index cbc87459e1..269e783899 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
@@ -5,20 +5,15 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
-using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
- public class TestSceneJuiceStream : PlayerTestScene
+ public class TestSceneJuiceStream : TestSceneCatchPlayer
{
- public TestSceneJuiceStream()
- : base(new CatchRuleset())
- {
- }
-
[Test]
public void TestJuiceStreamEndingCombo()
{
@@ -36,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
new JuiceStream
{
- X = 0.5f,
+ X = CatchPlayfield.CENTER_X,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
@@ -46,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
},
new Banana
{
- X = 0.5f,
+ X = CatchPlayfield.CENTER_X,
StartTime = 1000,
NewCombo = true
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs
new file mode 100644
index 0000000000..bc3daca16f
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs
@@ -0,0 +1,151 @@
+// Copyright (c) ppy Pty Ltd