diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 6ba6ae82c8..58c24181d3 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -13,6 +13,24 @@
"commands": [
"dotnet-format"
]
+ },
+ "jetbrains.resharper.globaltools": {
+ "version": "2020.3.2",
+ "commands": [
+ "jb"
+ ]
+ },
+ "nvika": {
+ "version": "2.0.0",
+ "commands": [
+ "nvika"
+ ]
+ },
+ "codefilesanity": {
+ "version": "15.0.0",
+ "commands": [
+ "CodeFileSanity"
+ ]
}
}
}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index 67f98f94eb..0cdf3b92d3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -191,4 +191,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
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
index 0b80ce44dd..6050036cbf 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md
@@ -13,4 +13,6 @@ about: Issues regarding encountered bugs.
*please attach logs here, which are located at:*
- `%AppData%/osu/logs` *(on Windows),*
- `~/.local/share/osu/logs` *(on Linux & macOS).*
+- `Android/Data/sh.ppy.osulazer/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)
-->
diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
index ada8de73c0..04170312d1 100644
--- a/.github/ISSUE_TEMPLATE/02-crash-issues.md
+++ b/.github/ISSUE_TEMPLATE/02-crash-issues.md
@@ -13,6 +13,8 @@ about: Issues regarding crashes or permanent freezes.
*please attach logs here, which are located at:*
- `%AppData%/osu/logs` *(on Windows),*
- `~/.local/share/osu/logs` *(on Linux & macOS).*
+- `Android/Data/sh.ppy.osulazer/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)
-->
**Computer Specifications:**
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/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml
index 366f172c30..680312ad27 100644
--- a/.idea/.idea.osu.Desktop/.idea/modules.xml
+++ b/.idea/.idea.osu.Desktop/.idea/modules.xml
@@ -2,8 +2,7 @@
-
-
+
\ 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___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/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml
similarity index 68%
rename from .idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml
index d85a0ae44c..9ece926b34 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.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..afd997f91d 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",
"*"
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..6c327f01b3
--- /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/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/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 59d72247f5..e09b4d86a5 100644
--- a/README.md
+++ b/README.md
@@ -5,16 +5,20 @@
# osu!
[](https://ci.appveyor.com/project/peppy/osu)
-[]()
+[](https://github.com/ppy/osu/releases/latest)
[](https://www.codefactor.io/repository/github/ppy/osu)
[](https://discord.gg/ppy)
-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 passses come at the end of development, preceeded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
+
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-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:
@@ -91,11 +101,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it
## 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%3Aopen+label%3Agood-first-issue+sort%3Aupdated-desc) 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/appveyor_deploy.yml b/appveyor_deploy.yml
index bb4482f501..737e5c43ab 100644
--- a/appveyor_deploy.yml
+++ b/appveyor_deploy.yml
@@ -1,21 +1,68 @@
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
+
+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%
+
+artifacts:
+ - path: '**\*.nupkg'
+
deploy:
- provider: Environment
- name: nuget
+ name: nuget
\ No newline at end of file
diff --git a/build/InspectCode.cake b/build/InspectCode.cake
index 2e7a1d1b28..6836d9071b 100644
--- a/build/InspectCode.cake
+++ b/build/InspectCode.cake
@@ -1,7 +1,4 @@
-#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();
+#addin "nuget:?package=CodeFileSanity&version=0.0.36"
///////////////////////////////////////////////////////////////////////////////
// ARGUMENTS
@@ -18,23 +15,15 @@ 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;
- },
- });
+ var inspectcodereport = "inspectcodereport.xml";
+ var cacheDir = "inspectcode";
+ var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output
- int returnCode = StartProcess(nVikaToolPath, $@"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors");
- if (returnCode != 0)
- throw new Exception($"inspectcode failed with return code {returnCode}");
+ DotNetCoreTool(rootDirectory.FullPath,
+ "jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}");
+ DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors");
});
Task("CodeFileSanity")
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 4fd0e5e8c7..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',
@@ -107,13 +110,16 @@ platform :ios do
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
@@ -127,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 7e17f9da16..5b700224db 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..cffcea22c2 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -1,18 +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.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.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
- 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 +39,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..21d6336b2c 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -3,6 +3,8 @@
using System;
using Android.App;
+using Android.OS;
+using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
@@ -10,6 +12,15 @@ 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 +29,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 +65,12 @@ namespace osu.Android
}
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
+ }
+
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
}
-}
\ No newline at end of file
+}
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 0598a50530..a2638e95c8 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -21,6 +21,7 @@
r8
+
@@ -53,4 +54,4 @@
-
+
\ 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..5909b82c8f 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -5,19 +5,20 @@ using System;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
+using Microsoft.Win32;
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.Game.IO;
namespace osu.Desktop
{
@@ -32,12 +33,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 +52,42 @@ namespace osu.Desktop
return null;
}
+ private string getStableInstallPath()
+ {
+ static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
+
+ 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 +108,9 @@ 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);
}
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)
@@ -90,66 +134,35 @@ namespace osu.Desktop
{
base.SetHost(host);
- if (host.Window is DesktopGameWindow desktopWindow)
+ var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
+
+ switch (host.Window)
{
- desktopWindow.CursorState |= CursorState.Hidden;
+ // Legacy osuTK DesktopGameWindow
+ case OsuTKDesktopWindow desktopGameWindow:
+ desktopGameWindow.CursorState |= CursorState.Hidden;
+ desktopGameWindow.SetIconFromStream(iconStream);
+ desktopGameWindow.Title = Name;
+ desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
+ break;
- desktopWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
- desktopWindow.Title = Name;
-
- desktopWindow.FileDrop += fileDrop;
+ // SDL2 DesktopWindow
+ case SDL2DesktopWindow desktopWindow:
+ desktopWindow.CursorState |= CursorState.Hidden;
+ desktopWindow.SetIconFromStream(iconStream);
+ desktopWindow.Title = Name;
+ desktopWindow.DragDrop += f => fileDrop(new[] { f });
+ break;
}
}
- private void fileDrop(object sender, FileDropEventArgs e)
+ private void fileDrop(string[] filePaths)
{
- 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()
- {
- static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
-
- string stableInstallPath;
-
- 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)
- {
- }
- }
}
}
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..6ca7079654 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -22,9 +22,9 @@ namespace osu.Desktop
{
// Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory;
- bool useSdl = args.Contains("--sdl");
+ bool useOsuTK = args.Contains("--tk");
- using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useSdl: useSdl))
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useOsuTK: useOsuTK))
{
host.ExceptionThrown += handleException;
@@ -33,13 +33,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");
}
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..d9d23dea6b 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,13 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
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..ea43d9a54c 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/.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..f4ee3f5a42 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.0565038923984691d, "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/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..f15da29993
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
@@ -0,0 +1,56 @@
+// 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.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+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
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LocalConfig.Set(OsuSetting.IncreaseFirstObjectVisibility, false);
+ }
+
+ [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..64695153b5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.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.Catch.Tests
+{
+ [TestFixture]
+ public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+ }
+}
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..e8bb57cdf3 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -1,34 +1,295 @@
// 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.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;
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 TestCatcherStacking()
+ {
+ 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 <= 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y < -20));
+ }
+
+ [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.Set(OsuSetting.HitLighting, true));
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ AddAssert("correct hit lighting colour", () =>
+ catcher.ChildrenOfType().First()?.ObjectColour == fruitColour);
+ }
+
+ [Test]
+ public void TestHitLightingDisabled()
+ {
+ AddStep("disable hit lighting", () => config.Set(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, int count = 1)
+ {
+ 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..1cbfa6338e 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -6,81 +6,91 @@ 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.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;
+
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)
- {
- 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);
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ 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(), new BeatmapDifficulty
+ {
+ CircleSize = circleSize
+ });
+
+ 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;
+
+ SetContents(() =>
{
- RelativeSizeAxes = Axes.Both,
- Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
+ 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, new BeatmapDifficulty { CircleSize = size })
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.TopCentre,
+ }
+ }
+ };
});
}
@@ -90,26 +100,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..c7b322c8a0
--- /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..3a651605d3 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 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..125e0c674c
--- /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..eea83ef7c1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs
@@ -0,0 +1,151 @@
+// 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.Audio;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Skinning;
+using osu.Game.Tests.Beatmaps;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneLegacyBeatmapSkin : LegacyBeatmapSkinColourTest
+ {
+ [Resolved]
+ private AudioManager audio { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.BeatmapSkins, BeatmapSkins);
+ config.BindWith(OsuSetting.BeatmapColours, BeatmapColours);
+ }
+
+ [TestCase(true, true)]
+ [TestCase(true, false)]
+ [TestCase(false, true)]
+ [TestCase(false, false)]
+ public override void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
+ {
+ TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
+ base.TestBeatmapComboColours(userHasCustomColours, useBeatmapSkin);
+ AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours));
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public override void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
+ {
+ TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
+ base.TestBeatmapComboColoursOverride(useBeatmapSkin);
+ AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public override void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
+ {
+ TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
+ base.TestBeatmapComboColoursOverrideWithDefaultColours(useBeatmapSkin);
+ AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
+ }
+
+ [TestCase(true, true)]
+ [TestCase(false, true)]
+ [TestCase(true, false)]
+ [TestCase(false, false)]
+ public override void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
+ {
+ TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false);
+ base.TestBeatmapNoComboColours(useBeatmapSkin, useBeatmapColour);
+ AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
+ }
+
+ [TestCase(true, true)]
+ [TestCase(false, true)]
+ [TestCase(true, false)]
+ [TestCase(false, false)]
+ public override void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
+ {
+ TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false);
+ base.TestBeatmapNoComboColoursSkinOverride(useBeatmapSkin, useBeatmapColour);
+ AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestBeatmapHyperDashColours(bool useBeatmapSkin)
+ {
+ TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
+ ConfigureTest(useBeatmapSkin, true, true);
+ AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestBeatmapSkin.HYPER_DASH_COLOUR);
+ AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);
+ AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == TestBeatmapSkin.HYPER_DASH_FRUIT_COLOUR);
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestBeatmapHyperDashColoursOverride(bool useBeatmapSkin)
+ {
+ TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
+ ConfigureTest(useBeatmapSkin, false, true);
+ AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestSkin.HYPER_DASH_COLOUR);
+ AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);
+ AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == TestSkin.HYPER_DASH_FRUIT_COLOUR);
+ }
+
+ protected override ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new CatchExposedPlayer(userHasCustomColours);
+
+ private class CatchExposedPlayer : ExposedPlayer
+ {
+ public CatchExposedPlayer(bool userHasCustomColours)
+ : base(userHasCustomColours)
+ {
+ }
+
+ public Color4 UsableHyperDashColour =>
+ GameplayClockContainer.ChildrenOfType()
+ .First()
+ .GetConfig(new SkinCustomColourLookup(CatchSkinColour.HyperDash))?
+ .Value ?? Color4.Red;
+
+ public Color4 UsableHyperDashAfterImageColour =>
+ GameplayClockContainer.ChildrenOfType()
+ .First()
+ .GetConfig(new SkinCustomColourLookup(CatchSkinColour.HyperDashAfterImage))?
+ .Value ?? Color4.Red;
+
+ public Color4 UsableHyperDashFruitColour =>
+ GameplayClockContainer.ChildrenOfType()
+ .First()
+ .GetConfig(new SkinCustomColourLookup(CatchSkinColour.HyperDashFruit))?
+ .Value ?? Color4.Red;
+ }
+
+ private class CatchCustomSkinWorkingBeatmap : CustomSkinWorkingBeatmap
+ {
+ public CatchCustomSkinWorkingBeatmap(AudioManager audio, bool hasColours)
+ : base(createBeatmap(), audio, hasColours)
+ {
+ }
+
+ private static IBeatmap createBeatmap() =>
+ new Beatmap
+ {
+ BeatmapInfo =
+ {
+ BeatmapSet = new BeatmapSetInfo(),
+ Ruleset = new CatchRuleset().RulesetInfo
+ },
+ HitObjects = { new Fruit { StartTime = 1816, X = 56, NewCombo = true } }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 8c371db257..42f70151ac 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,14 +2,14 @@
-
-
-
-
+
+
+
+
WinExe
- netcoreapp3.1
+ net5.0
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
index 18cc300ff9..f009c10a9c 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
Name = @"Fruit Count",
Content = fruits.ToString(),
- Icon = FontAwesome.Regular.Circle
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
},
new BeatmapStatistic
{
Name = @"Juice Stream Count",
Content = juiceStreams.ToString(),
- Icon = FontAwesome.Regular.Circle
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
},
new BeatmapStatistic
{
Name = @"Banana Shower Count",
Content = bananaShowers.ToString(),
- Icon = FontAwesome.Regular.Circle
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
}
};
}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 90a6e609f0..34964fc4ae 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -5,7 +5,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
using System.Linq;
-using osu.Game.Rulesets.Catch.UI;
+using System.Threading;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -21,14 +21,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
- protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap)
+ protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
{
var positionData = obj as IHasXPosition;
var comboData = obj as IHasCombo;
switch (obj)
{
- case IHasCurve curveData:
+ case IHasPathWithRepeats curveData:
return new JuiceStream
{
StartTime = obj.StartTime,
@@ -36,13 +36,13 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
- X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
+ X = positionData?.X ?? 0,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
}.Yield();
- case IHasEndTime endTime:
+ case IHasDuration endTime:
return new BananaShower
{
StartTime = obj.StartTime,
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
- X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
+ X = positionData?.X ?? 0
}.Yield();
}
}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index 7c81bcdf0c..fac5d03833 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -5,11 +5,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Rulesets.Objects.Types;
-using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case BananaShower bananaShower:
foreach (var banana in bananaShower.NestedHitObjects.OfType())
{
- banana.XOffset = (float)rng.NextDouble();
+ banana.XOffset = (float)(rng.NextDouble() * CatchPlayfield.WIDTH);
rng.Next(); // osu!stable retrieved a random banana type
rng.Next(); // osu!stable retrieved a random banana rotation
rng.Next(); // osu!stable retrieved a random banana colour
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case JuiceStream juiceStream:
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
- lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X / CatchPlayfield.BASE_WIDTH;
+ lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.Value.X;
// Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
lastStartTime = juiceStream.StartTime;
@@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
catchObject.XOffset = 0;
if (catchObject is TinyDroplet)
- catchObject.XOffset = Math.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X);
+ catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.OriginalX, CatchPlayfield.WIDTH - catchObject.OriginalX);
else if (catchObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation
}
@@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng)
{
- float offsetPosition = hitObject.X;
+ float offsetPosition = hitObject.OriginalX;
double startTime = hitObject.StartTime;
if (lastPosition == null)
@@ -126,15 +126,15 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
if (positionDiff == 0)
{
applyRandomOffset(ref offsetPosition, timeDiff / 4d, rng);
- hitObject.XOffset = offsetPosition - hitObject.X;
+ hitObject.XOffset = offsetPosition - hitObject.OriginalX;
return;
}
// ReSharper disable once PossibleLossOfFraction
- if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3)
+ if (Math.Abs(positionDiff) < timeDiff / 3)
applyOffset(ref offsetPosition, positionDiff);
- hitObject.XOffset = offsetPosition - hitObject.X;
+ hitObject.XOffset = offsetPosition - hitObject.OriginalX;
lastPosition = offsetPosition;
lastStartTime = startTime;
@@ -149,12 +149,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng)
{
bool right = rng.NextBool();
- float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH;
+ float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset)));
if (right)
{
// Clamp to the right bound
- if (position + rand <= 1)
+ if (position + rand <= CatchPlayfield.WIDTH)
position += rand;
else
position -= rand;
@@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
if (amount > 0)
{
// Clamp to the right bound
- if (position + amount < 1)
+ if (position + amount < CatchPlayfield.WIDTH)
position += amount;
}
else
@@ -192,41 +192,47 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private static void initialiseHyperDash(IBeatmap beatmap)
{
- List objectWithDroplets = new List();
+ List palpableObjects = new List();
foreach (var currentObject in beatmap.HitObjects)
{
if (currentObject is Fruit fruitObject)
- objectWithDroplets.Add(fruitObject);
+ palpableObjects.Add(fruitObject);
if (currentObject is JuiceStream)
{
- foreach (var currentJuiceElement in currentObject.NestedHitObjects)
+ foreach (var juice in currentObject.NestedHitObjects)
{
- if (!(currentJuiceElement is TinyDroplet))
- objectWithDroplets.Add((CatchHitObject)currentJuiceElement);
+ if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet))
+ palpableObjects.Add(palpableObject);
}
}
}
- objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
+ palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
+
+ double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
+
+ // Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
+ // This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
+ // For now, to bring gameplay (and diffcalc!) completely in-line with stable, this code also uses the full catcher size.
+ halfCatcherWidth /= Catcher.ALLOWED_CATCH_RANGE;
- double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2;
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
- for (int i = 0; i < objectWithDroplets.Count - 1; i++)
+ for (int i = 0; i < palpableObjects.Count - 1; i++)
{
- CatchHitObject currentObject = objectWithDroplets[i];
- CatchHitObject nextObject = objectWithDroplets[i + 1];
+ var currentObject = palpableObjects[i];
+ var nextObject = palpableObjects[i + 1];
// Reset variables in-case values have changed (e.g. after applying HR)
currentObject.HyperDashTarget = null;
currentObject.DistanceToHyperDash = 0;
- int thisDirection = nextObject.X > currentObject.X ? 1 : -1;
+ int thisDirection = nextObject.EffectiveX > currentObject.EffectiveX ? 1 : -1;
double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable
- double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
+ double distanceToNext = Math.Abs(nextObject.EffectiveX - currentObject.EffectiveX) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext);
if (distanceToHyper < 0)
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index b9d791fdb1..f4ddbd3021 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,7 +21,8 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
-using osu.Game.Rulesets.Catch.Skinning;
+using osu.Framework.Extensions.EnumExtensions;
+using osu.Game.Rulesets.Catch.Skinning.Legacy;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
@@ -48,42 +49,42 @@ namespace osu.Game.Rulesets.Catch
new KeyBinding(InputKey.Shift, CatchAction.Dash),
};
- public override IEnumerable ConvertLegacyMods(LegacyMods mods)
+ public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
- if (mods.HasFlag(LegacyMods.Nightcore))
+ if (mods.HasFlagFast(LegacyMods.Nightcore))
yield return new CatchModNightcore();
- else if (mods.HasFlag(LegacyMods.DoubleTime))
+ else if (mods.HasFlagFast(LegacyMods.DoubleTime))
yield return new CatchModDoubleTime();
- if (mods.HasFlag(LegacyMods.Perfect))
+ if (mods.HasFlagFast(LegacyMods.Perfect))
yield return new CatchModPerfect();
- else if (mods.HasFlag(LegacyMods.SuddenDeath))
+ else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
yield return new CatchModSuddenDeath();
- if (mods.HasFlag(LegacyMods.Cinema))
+ if (mods.HasFlagFast(LegacyMods.Cinema))
yield return new CatchModCinema();
- else if (mods.HasFlag(LegacyMods.Autoplay))
+ else if (mods.HasFlagFast(LegacyMods.Autoplay))
yield return new CatchModAutoplay();
- if (mods.HasFlag(LegacyMods.Easy))
+ if (mods.HasFlagFast(LegacyMods.Easy))
yield return new CatchModEasy();
- if (mods.HasFlag(LegacyMods.Flashlight))
+ if (mods.HasFlagFast(LegacyMods.Flashlight))
yield return new CatchModFlashlight();
- if (mods.HasFlag(LegacyMods.HalfTime))
+ if (mods.HasFlagFast(LegacyMods.HalfTime))
yield return new CatchModHalfTime();
- if (mods.HasFlag(LegacyMods.HardRock))
+ if (mods.HasFlagFast(LegacyMods.HardRock))
yield return new CatchModHardRock();
- if (mods.HasFlag(LegacyMods.Hidden))
+ if (mods.HasFlagFast(LegacyMods.Hidden))
yield return new CatchModHidden();
- if (mods.HasFlag(LegacyMods.NoFail))
+ if (mods.HasFlagFast(LegacyMods.NoFail))
yield return new CatchModNoFail();
- if (mods.HasFlag(LegacyMods.Relax))
+ if (mods.HasFlagFast(LegacyMods.Relax))
yield return new CatchModRelax();
}
@@ -141,11 +142,40 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
+ protected override IEnumerable GetValidHitResults()
+ {
+ return new[]
+ {
+ HitResult.Great,
+
+ HitResult.LargeTickHit,
+ HitResult.SmallTickHit,
+ HitResult.LargeBonus,
+ };
+ }
+
+ public override string GetDisplayNameForHitResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ return "large droplet";
+
+ case HitResult.SmallTickHit:
+ return "small droplet";
+
+ case HitResult.LargeBonus:
+ return "banana";
+ }
+
+ return base.GetDisplayNameForHitResult(result);
+ }
+
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
- public override ISkin CreateLegacySkinProvider(ISkinSource source) => new CatchLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
- public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);
+ public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
index 80390705fe..668f7197be 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
@@ -5,14 +5,12 @@ namespace osu.Game.Rulesets.Catch
{
public enum CatchSkinComponents
{
- FruitBananas,
- FruitApple,
- FruitGrapes,
- FruitOrange,
- FruitPear,
+ Fruit,
+ Banana,
Droplet,
CatcherIdle,
CatcherFail,
- CatcherKiai
+ CatcherKiai,
+ CatchComboCounter
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
index 75f5b18607..fa9011d826 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Difficulty
public class CatchDifficultyAttributes : DifficultyAttributes
{
public double ApproachRate;
- public int MaxCombo;
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 5880a227c2..10aae70722 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyCalculator : DifficultyCalculator
{
- private const double star_scaling_factor = 0.145;
+ private const double star_scaling_factor = 0.153;
protected override int SectionLength => 750;
@@ -69,17 +69,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty
}
}
- protected override Skill[] CreateSkills(IBeatmap beatmap)
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
{
- using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty))
- {
- halfCatcherWidth = catcher.CatchWidth * 0.5f;
- halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay.
- }
+ halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
+
+ // For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
+ halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f);
return new Skill[]
{
- new Movement(halfCatcherWidth),
+ new Movement(mods, halfCatcherWidth),
};
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index a6283eb7c4..6a3a16ed33 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -4,12 +4,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Game.Beatmaps;
+using osu.Framework.Extensions;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
-using osu.Game.Scoring.Legacy;
namespace osu.Game.Rulesets.Catch.Difficulty
{
@@ -25,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private int tinyTicksMissed;
private int misses;
- public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
- : base(ruleset, beatmap, score)
+ public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
+ : base(ruleset, attributes, score)
{
}
@@ -34,11 +33,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
mods = Score.Mods;
- fruitsHit = Score?.GetCount300() ?? Score.Statistics[HitResult.Perfect];
- ticksHit = Score?.GetCount100() ?? 0;
- tinyTicksHit = Score?.GetCount50() ?? 0;
- tinyTicksMissed = Score?.GetCountKatu() ?? 0;
- misses = Score.Statistics[HitResult.Miss];
+ fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great);
+ ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit);
+ tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit);
+ tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);
+ misses = Score.Statistics.GetOrDefault(HitResult.Miss);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
@@ -52,8 +51,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Longer maps are worth more
double lengthBonus =
- 0.95 + 0.4 * Math.Min(1.0, numTotalHits / 3000.0) +
- (numTotalHits > 3000 ? Math.Log10(numTotalHits / 3000.0) * 0.5 : 0.0);
+ 0.95 + 0.3 * Math.Min(1.0, numTotalHits / 2500.0) +
+ (numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
// Longer maps are worth more
value *= lengthBonus;
@@ -63,19 +62,27 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Combo scaling
if (Attributes.MaxCombo > 0)
- value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
+ value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
+ double approachRate = Attributes.ApproachRate;
double approachRateFactor = 1.0;
- if (Attributes.ApproachRate > 9.0)
- approachRateFactor += 0.1 * (Attributes.ApproachRate - 9.0); // 10% for each AR above 9
- else if (Attributes.ApproachRate < 8.0)
- approachRateFactor += 0.025 * (8.0 - Attributes.ApproachRate); // 2.5% for each AR below 8
+ if (approachRate > 9.0)
+ approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
+ if (approachRate > 10.0)
+ approachRateFactor += 0.1 * (approachRate - 10.0); // Additional 10% at AR 11, 30% total
+ else if (approachRate < 8.0)
+ approachRateFactor += 0.025 * (8.0 - approachRate); // 2.5% for each AR below 8
value *= approachRateFactor;
if (mods.Any(m => m is ModHidden))
- // Hiddens gives nothing on max approach rate, and more the lower it is
- value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
+ {
+ // Hiddens gives almost nothing on max approach rate, and more the lower it is
+ if (approachRate <= 10.0)
+ value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10
+ else if (approachRate > 10.0)
+ value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11
+ }
if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
@@ -91,7 +98,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return value;
}
- private float accuracy() => totalHits() == 0 ? 0 : Math.Clamp((float)totalSuccessfulHits() / totalHits(), 0, 1);
+ private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
private int totalComboHits() => misses + ticksHit + fruitsHit;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
index 24e526ed19..d936ef97ac 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
@@ -3,7 +3,6 @@
using System;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
@@ -13,29 +12,32 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
{
private const float normalized_hitobject_radius = 41.0f;
- public new CatchHitObject BaseObject => (CatchHitObject)base.BaseObject;
+ public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
- public new CatchHitObject LastObject => (CatchHitObject)base.LastObject;
+ public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
public readonly float NormalizedPosition;
public readonly float LastNormalizedPosition;
///
- /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms.
+ /// Milliseconds elapsed since the start time of the previous , with a minimum of 40ms.
///
public readonly double StrainTime;
+ public readonly double ClockRate;
+
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: base(hitObject, lastObject, clockRate)
{
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
- NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
- LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
+ NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
+ LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
- // Every strain interval is hard capped at the equivalent of 600 BPM streaming speed as a safety measure
- StrainTime = Math.Max(25, DeltaTime);
+ // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
+ StrainTime = Math.Max(40, DeltaTime);
+ ClockRate = clockRate;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index fd164907e0..9ad719be1a 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -3,9 +3,9 @@
using System;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
@@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
- private const double direction_change_bonus = 12.5;
+ private const double direction_change_bonus = 21.0;
- protected override double SkillMultiplier => 850;
+ protected override double SkillMultiplier => 900;
protected override double StrainDecayBase => 0.2;
protected override double DecayWeight => 0.94;
@@ -24,8 +24,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float? lastPlayerPosition;
private float lastDistanceMoved;
+ private double lastStrainTime;
- public Movement(float halfCatcherWidth)
+ public Movement(Mod[] mods, float halfCatcherWidth)
+ : base(mods)
{
HalfCatcherWidth = halfCatcherWidth;
}
@@ -34,8 +36,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
var catchCurrent = (CatchDifficultyHitObject)current;
- if (lastPlayerPosition == null)
- lastPlayerPosition = catchCurrent.LastNormalizedPosition;
+ lastPlayerPosition ??= catchCurrent.LastNormalizedPosition;
float playerPosition = Math.Clamp(
lastPlayerPosition.Value,
@@ -45,47 +46,47 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value;
- double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.3) / 500;
- double sqrtStrain = Math.Sqrt(catchCurrent.StrainTime);
+ double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate);
- double bonus = 0;
+ double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
+ double sqrtStrain = Math.Sqrt(weightedStrainTime);
- // Direction changes give an extra point!
+ double edgeDashBonus = 0;
+
+ // Direction change bonus.
if (Math.Abs(distanceMoved) > 0.1)
{
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
{
- double bonusFactor = Math.Min(absolute_player_positioning_error, Math.Abs(distanceMoved)) / absolute_player_positioning_error;
+ double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
+ double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
- distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor;
-
- // Bonus for tougher direction switches and "almost" hyperdashes at this point
- if (catchCurrent.LastObject.DistanceToHyperDash <= 10 / CatchPlayfield.BASE_WIDTH)
- bonus = 0.3 * bonusFactor;
+ distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
}
// Base bonus for every movement, giving some weight to streams.
- distanceAddition += 7.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
+ distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
}
- // Bonus for "almost" hyperdashes at corner points
- if (catchCurrent.LastObject.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
+ // Bonus for edge dashes.
+ if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
{
if (!catchCurrent.LastObject.HyperDash)
- bonus += 1.0;
+ edgeDashBonus += 5.7;
else
{
// After a hyperdash we ARE in the correct position. Always!
playerPosition = catchCurrent.NormalizedPosition;
}
- distanceAddition *= 1.0 + bonus * ((10 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10);
+ distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
}
lastPlayerPosition = playerPosition;
lastDistanceMoved = distanceMoved;
+ lastStrainTime = catchCurrent.StrainTime;
- return distanceAddition / catchCurrent.StrainTime;
+ return distanceAddition / weightedStrainTime;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
index fc030877f1..b919102215 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
@@ -8,31 +8,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchBananaJudgement : CatchJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 1100;
- }
- }
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 0.01;
- }
- }
+ public override HitResult MaxResult => HitResult.LargeBonus;
public override bool ShouldExplodeFor(JudgementResult result) => true;
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
index e87ecba749..8fd7b93e4c 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
@@ -7,16 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchDropletJudgement : CatchJudgement
{
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 30;
- }
- }
+ public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
index 2149ed9712..ccafe0abc4 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
@@ -9,19 +9,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchJudgement : Judgement
{
- public override HitResult MaxResult => HitResult.Perfect;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 300;
- }
- }
+ public override HitResult MaxResult => HitResult.Great;
///
/// Whether fruit on the platter should explode or drop.
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs
new file mode 100644
index 0000000000..c09355d59c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Catch.Judgements
+{
+ public class CatchJudgementResult : JudgementResult
+ {
+ ///
+ /// The catcher animation state prior to this judgement.
+ ///
+ public CatcherAnimationState CatcherAnimationState;
+
+ ///
+ /// Whether the catcher was hyper dashing prior to this judgement.
+ ///
+ public bool CatcherHyperDash;
+
+ public CatchJudgementResult([NotNull] HitObject hitObject, [NotNull] Judgement judgement)
+ : base(hitObject, judgement)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
index d607b49ea4..d957d4171b 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
@@ -7,30 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchTinyDropletJudgement : CatchJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 10;
- }
- }
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 0.02;
- }
- }
+ public override HitResult MaxResult => HitResult.SmallTickHit;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
index 692e63fa69..e1eceea606 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
index 3bc1ee5bf5..d53d019e90 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModCinema : ModCinema
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index e2465d727e..5f1736450a 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
- public BindableNumber CircleSize { get; } = new BindableFloat
+ public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
- public BindableNumber ApproachRate { get; } = new BindableFloat
+ public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@@ -30,6 +31,30 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
+ protected override void ApplyLimits(bool extended)
+ {
+ base.ApplyLimits(extended);
+
+ CircleSize.MaxValue = extended ? 11 : 10;
+ ApproachRate.MaxValue = extended ? 11 : 10;
+ }
+
+ public override string SettingDescription
+ {
+ get
+ {
+ string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
+ string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
+
+ return string.Join(", ", new[]
+ {
+ circleSize,
+ base.SettingDescription,
+ approachRate
+ }.Where(s => !string.IsNullOrEmpty(s)));
+ }
+ }
+
protected override void TransferSettings(BeatmapDifficulty difficulty)
{
base.TransferSettings(difficulty);
@@ -42,8 +67,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
base.ApplySettings(difficulty);
- difficulty.CircleSize = CircleSize.Value;
- difficulty.ApproachRate = ApproachRate.Value;
+ ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
+ ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs
index a82d0af102..16ef56d845 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs
@@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModEasy : ModEasy
+ public class CatchModEasy : ModEasyWithExtraLives
{
public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
index ee88edbea1..4b008d2734 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
@@ -17,9 +17,11 @@ namespace osu.Game.Rulesets.Catch.Mods
private const double fade_out_offset_multiplier = 0.6;
private const double fade_out_duration_multiplier = 0.44;
- protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
+ protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
- if (!(drawable is DrawableCatchHitObject catchDrawable))
+ base.ApplyNormalVisibilityState(hitObject, state);
+
+ if (!(hitObject is DrawableCatchHitObject catchDrawable))
return;
if (catchDrawable.NestedHitObjects.Any())
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
index e3391c47f1..fb92399102 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
@@ -1,17 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Rulesets.Catch.Judgements;
-using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModPerfect : ModPerfect
{
- protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
- => !(result.Judgement is CatchBananaJudgement)
- && base.FailCondition(healthProcessor, result);
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 1ef235f764..1e42c6a240 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -9,17 +9,26 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset
+ public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override string Description => @"Use the mouse to control the catcher.";
+ private DrawableRuleset drawableRuleset;
+
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
+ this.drawableRuleset = drawableRuleset;
+ }
+
+ public void ApplyToPlayer(Player player)
+ {
+ if (!drawableRuleset.HasReplayLoaded.Value)
+ drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
}
private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition
@@ -34,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Mods
RelativeSizeAxes = Axes.Both;
}
- //disable keyboard controls
+ // disable keyboard controls
public bool OnPressed(CatchAction action) => true;
public void OnReleased(CatchAction action)
@@ -43,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods
protected override bool OnMouseMove(MouseMoveEvent e)
{
- catcher.UpdatePosition(e.MousePosition.X / DrawSize.X);
+ catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH);
return base.OnMouseMove(e);
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs
index 0b3d1d23e0..178306b3bc 100644
--- a/osu.Game.Rulesets.Catch/Objects/Banana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs
@@ -1,15 +1,74 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Utils;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class Banana : Fruit
+ public class Banana : PalpableCatchHitObject, IHasComboInformation
{
- public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
+ ///
+ /// Index of banana in current shower.
+ ///
+ public int BananaIndex;
public override Judgement CreateJudgement() => new CatchBananaJudgement();
+
+ private static readonly List samples = new List { new BananaHitSampleInfo() };
+
+ public Banana()
+ {
+ Samples = samples;
+ }
+
+ // override any external colour changes with banananana
+ Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => getBananaColour();
+
+ private Color4 getBananaColour()
+ {
+ switch (StatelessRNG.NextInt(3, RandomSeed))
+ {
+ default:
+ return new Color4(255, 240, 0, 255);
+
+ case 1:
+ return new Color4(255, 192, 0, 255);
+
+ case 2:
+ return new Color4(214, 221, 28, 255);
+ }
+ }
+
+ private class BananaHitSampleInfo : HitSampleInfo, IEquatable
+ {
+ private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" };
+
+ public override IEnumerable LookupNames => lookup_names;
+
+ public BananaHitSampleInfo(int volume = 0)
+ : base(string.Empty, volume: volume)
+ {
+ }
+
+ public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default)
+ => new BananaHitSampleInfo(newVolume.GetOr(Volume));
+
+ public bool Equals(BananaHitSampleInfo? other)
+ => other != null;
+
+ public override bool Equals(object? obj)
+ => obj is BananaHitSampleInfo other && Equals(other);
+
+ public override int GetHashCode() => lookup_names.GetHashCode();
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index c3488aec11..b45f95a8e6 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -1,26 +1,25 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Threading;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class BananaShower : CatchHitObject, IHasEndTime
+ public class BananaShower : CatchHitObject, IHasDuration
{
- public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
-
public override bool LastInCombo => true;
public override Judgement CreateJudgement() => new IgnoreJudgement();
- protected override void CreateNestedHitObjects()
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
- createBananas();
+ base.CreateNestedHitObjects(cancellationToken);
+ createBananas(cancellationToken);
}
- private void createBananas()
+ private void createBananas(CancellationToken cancellationToken)
{
double spacing = Duration;
while (spacing > 100)
@@ -29,13 +28,21 @@ namespace osu.Game.Rulesets.Catch.Objects
if (spacing <= 0)
return;
- for (double i = StartTime; i <= EndTime; i += spacing)
+ double time = StartTime;
+ int i = 0;
+
+ while (time <= EndTime)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
AddNested(new Banana
{
- Samples = Samples,
- StartTime = i
+ StartTime = time,
+ BananaIndex = i,
});
+
+ time += spacing;
+ i++;
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index f3b566f340..ae45182960 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -4,7 +4,7 @@
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -15,24 +15,55 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public const float OBJECT_RADIUS = 64;
- private float x;
+ public readonly Bindable OriginalXBindable = new Bindable();
+ ///
+ /// The horizontal position of the hit object between 0 and .
+ ///
public float X
{
- get => x + XOffset;
- set => x = value;
+ set => OriginalXBindable.Value = value;
+ }
+
+ float IHasXPosition.X => OriginalXBindable.Value;
+
+ public readonly Bindable XOffsetBindable = new Bindable();
+
+ ///
+ /// A random offset applied to the horizontal position, set by the beatmap processing.
+ ///
+ public float XOffset
+ {
+ set => XOffsetBindable.Value = value;
}
///
- /// A random offset applied to , set by the .
+ /// The horizontal position of the hit object between 0 and .
///
- internal float XOffset { get; set; }
+ ///
+ /// This value is the original value specified in the beatmap, not affected by the beatmap processing.
+ /// Use for a gameplay.
+ ///
+ public float OriginalX => OriginalXBindable.Value;
+
+ ///
+ /// The effective horizontal position of the hit object between 0 and .
+ ///
+ ///
+ /// This value is the original value plus the offset applied by the beatmap processing.
+ /// Use if a value not affected by the offset is desired.
+ ///
+ public float EffectiveX => OriginalXBindable.Value + XOffsetBindable.Value;
public double TimePreempt = 1000;
- public int IndexInBeatmap { get; set; }
+ public readonly Bindable IndexInBeatmapBindable = new Bindable();
- public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4);
+ public int IndexInBeatmap
+ {
+ get => IndexInBeatmapBindable.Value;
+ set => IndexInBeatmapBindable.Value = value;
+ }
public virtual bool NewCombo { get; set; }
@@ -54,13 +85,6 @@ namespace osu.Game.Rulesets.Catch.Objects
set => ComboIndexBindable.Value = value;
}
- ///
- /// Difference between the distance to the next object
- /// and the distance that would have triggered a hyper dash.
- /// A value close to 0 indicates a difficult jump (for difficulty calculation).
- ///
- public float DistanceToHyperDash { get; set; }
-
public Bindable LastInComboBindable { get; } = new Bindable();
///
@@ -72,17 +96,19 @@ namespace osu.Game.Rulesets.Catch.Objects
set => LastInComboBindable.Value = value;
}
- public float Scale { get; set; } = 1;
+ public readonly Bindable ScaleBindable = new Bindable(1);
+
+ public float Scale
+ {
+ get => ScaleBindable.Value;
+ set => ScaleBindable.Value = value;
+ }
///
- /// Whether this fruit can initiate a hyperdash.
+ /// The seed value used for visual randomness such as fruit rotation.
+ /// The value is truncated to an integer.
///
- public bool HyperDash => HyperDashTarget != null;
-
- ///
- /// The target fruit if we are to initiate a hyperdash.
- ///
- public CatchHitObject HyperDashTarget;
+ public int RandomSeed => (int)StartTime;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
@@ -95,13 +121,4 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
-
- public enum FruitVisualRepresentation
- {
- Pear,
- Grape,
- Pineapple,
- Raspberry,
- Banana // banananananannaanana
- }
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs
deleted file mode 100644
index ebb0bf0f2c..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawables
-{
- public class BananaPiece : PulpFormation
- {
- public BananaPiece()
- {
- InternalChildren = new Drawable[]
- {
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(SMALL_PULP),
- Y = -0.3f
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f),
- Y = 0.05f,
- },
- };
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs
new file mode 100644
index 0000000000..8a91f82437
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.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.Catch.Skinning.Default;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ ///
+ /// Represents a caught by the catcher.
+ ///
+ public class CaughtBanana : CaughtObject
+ {
+ public CaughtBanana()
+ : base(CatchSkinComponents.Banana, _ => new BananaPiece())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.cs
new file mode 100644
index 0000000000..4a3397feff
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.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.Catch.Skinning.Default;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ ///
+ /// Represents a caught by the catcher.
+ ///
+ public class CaughtDroplet : CaughtObject
+ {
+ public override bool StaysOnPlate => false;
+
+ public CaughtDroplet()
+ : base(CatchSkinComponents.Droplet, _ => new DropletPiece())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs
new file mode 100644
index 0000000000..140b411c88
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ ///
+ /// Represents a caught by the catcher.
+ ///
+ public class CaughtFruit : CaughtObject, IHasFruitState
+ {
+ public Bindable VisualRepresentation { get; } = new Bindable();
+
+ public CaughtFruit()
+ : base(CatchSkinComponents.Fruit, _ => new FruitPiece())
+ {
+ }
+
+ public override void CopyStateFrom(IHasCatchObjectState objectState)
+ {
+ base.CopyStateFrom(objectState);
+
+ var fruitState = (IHasFruitState)objectState;
+ VisualRepresentation.Value = fruitState.VisualRepresentation.Value;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
new file mode 100644
index 0000000000..524505d588
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ ///
+ /// Represents a caught by the catcher.
+ ///
+ [Cached(typeof(IHasCatchObjectState))]
+ public abstract class CaughtObject : SkinnableDrawable, IHasCatchObjectState
+ {
+ public PalpableCatchHitObject HitObject { get; private set; }
+ public Bindable AccentColour { get; } = new Bindable();
+ public Bindable HyperDash { get; } = new Bindable();
+
+ public Vector2 DisplaySize => Size * Scale;
+
+ public float DisplayRotation => Rotation;
+
+ ///
+ /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
+ ///
+ public virtual bool StaysOnPlate => true;
+
+ public override bool RemoveWhenNotAlive => true;
+
+ protected CaughtObject(CatchSkinComponents skinComponent, Func defaultImplementation)
+ : base(new CatchSkinComponent(skinComponent), defaultImplementation)
+ {
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.None;
+ Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
+ }
+
+ ///
+ /// Copies the hit object visual state from another object.
+ ///
+ public virtual void CopyStateFrom(IHasCatchObjectState objectState)
+ {
+ HitObject = objectState.HitObject;
+ Scale = Vector2.Divide(objectState.DisplaySize, Size);
+ Rotation = objectState.DisplayRotation;
+ AccentColour.Value = objectState.AccentColour.Value;
+ HyperDash.Value = objectState.HyperDash.Value;
+ }
+
+ protected override void FreeAfterUse()
+ {
+ ClearTransforms();
+ Alpha = 1;
+
+ base.FreeAfterUse();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
index 01b76ceed9..c1b41a7afc 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
@@ -1,26 +1,40 @@
// 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 JetBrains.Annotations;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
-using osuTK.Graphics;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableBanana : DrawableFruit
+ public class DrawableBanana : DrawablePalpableCatchHitObject
{
- public DrawableBanana(Banana h)
+ public DrawableBanana()
+ : this(null)
+ {
+ }
+
+ public DrawableBanana([CanBeNull] Banana h)
: base(h)
{
}
- private Color4? colour;
-
- protected override Color4 GetComboColour(IReadOnlyList comboColours)
+ [BackgroundDependencyLoader]
+ private void load()
{
- // override any external colour changes with banananana
- return colour ??= getBananaColour();
+ ScalingContainer.Child = new SkinnableDrawable(
+ new CatchSkinComponent(CatchSkinComponents.Banana),
+ _ => new BananaPiece());
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // start time affects the random seed which is used to determine the banana colour
+ StartTimeBindable.BindValueChanged(_ => UpdateComboColour());
}
protected override void UpdateInitialTransforms()
@@ -30,29 +44,21 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
const float end_scale = 0.6f;
const float random_scale_range = 1.6f;
- ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RNG.NextSingle()))
- .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
+ ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3)))
+ .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
- ScaleContainer.RotateTo(getRandomAngle())
- .Then()
- .RotateTo(getRandomAngle(), HitObject.TimePreempt);
+ ScalingContainer.RotateTo(getRandomAngle(1))
+ .Then()
+ .RotateTo(getRandomAngle(2), HitObject.TimePreempt);
- float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1);
+ float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1);
}
- private Color4 getBananaColour()
+ public override void PlaySamples()
{
- switch (RNG.Next(0, 3))
- {
- default:
- return new Color4(255, 240, 0, 255);
-
- case 1:
- return new Color4(255, 192, 0, 255);
-
- case 2:
- return new Color4(214, 221, 28, 255);
- }
+ base.PlaySamples();
+ if (Samples != null)
+ Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
index 4ce80aceb8..9b2f95e221 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
@@ -1,26 +1,27 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
+using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableBananaShower : DrawableCatchHitObject
+ public class DrawableBananaShower : DrawableCatchHitObject
{
- private readonly Func> createDrawableRepresentation;
private readonly Container bananaContainer;
- public DrawableBananaShower(BananaShower s, Func> createDrawableRepresentation = null)
+ public DrawableBananaShower()
+ : this(null)
+ {
+ }
+
+ public DrawableBananaShower([CanBeNull] BananaShower s)
: base(s)
{
- this.createDrawableRepresentation = createDrawableRepresentation;
RelativeSizeAxes = Axes.X;
Origin = Anchor.BottomLeft;
- X = 0;
AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both });
}
@@ -34,18 +35,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
- bananaContainer.Clear();
- }
-
- protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
- {
- switch (hitObject)
- {
- case Banana banana:
- return createDrawableRepresentation?.Invoke(banana)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
- }
-
- return base.CreateNestedHitObject(hitObject);
+ bananaContainer.Clear(false);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index 6844be5941..0c065948ef 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -2,111 +2,78 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
-using osu.Framework.Allocation;
+using JetBrains.Annotations;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Catch.Judgements;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
-using osuTK;
-using osuTK.Graphics;
+using osu.Game.Utils;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public abstract class PalpableCatchHitObject : DrawableCatchHitObject
- where TObject : CatchHitObject
- {
- public override bool CanBePlated => true;
-
- protected Container ScaleContainer { get; private set; }
-
- protected PalpableCatchHitObject(TObject hitObject)
- : base(hitObject)
- {
- Origin = Anchor.Centre;
- Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
- Masking = false;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- AddRangeInternal(new Drawable[]
- {
- ScaleContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- }
- });
-
- ScaleContainer.Scale = new Vector2(HitObject.Scale);
- }
-
- protected override Color4 GetComboColour(IReadOnlyList comboColours) =>
- comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count];
- }
-
- public abstract class DrawableCatchHitObject : DrawableCatchHitObject
- where TObject : CatchHitObject
- {
- public new TObject HitObject;
-
- protected DrawableCatchHitObject(TObject hitObject)
- : base(hitObject)
- {
- HitObject = hitObject;
- Anchor = Anchor.BottomLeft;
- }
- }
-
public abstract class DrawableCatchHitObject : DrawableHitObject
{
- public virtual bool CanBePlated => false;
+ public readonly Bindable OriginalXBindable = new Bindable();
+ public readonly Bindable XOffsetBindable = new Bindable();
- public virtual bool StaysOnPlate => CanBePlated;
+ protected override double InitialLifetimeOffset => HitObject.TimePreempt;
- public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
+ protected override float SamplePlaybackPosition => HitObject.EffectiveX / CatchPlayfield.WIDTH;
- protected DrawableCatchHitObject(CatchHitObject hitObject)
+ public int RandomSeed => HitObject?.RandomSeed ?? 0;
+
+ protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject)
: base(hitObject)
{
- RelativePositionAxes = Axes.X;
- X = hitObject.X;
+ Anchor = Anchor.BottomLeft;
+ }
+
+ ///
+ /// Get a random number in range [0,1) based on seed .
+ ///
+ public float RandomSingle(int series) => StatelessRNG.NextSingle(RandomSeed, series);
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ OriginalXBindable.BindTo(HitObject.OriginalXBindable);
+ XOffsetBindable.BindTo(HitObject.XOffsetBindable);
+ }
+
+ protected override void OnFree()
+ {
+ base.OnFree();
+
+ OriginalXBindable.UnbindFrom(HitObject.OriginalXBindable);
+ XOffsetBindable.UnbindFrom(HitObject.XOffsetBindable);
}
public Func CheckPosition;
- public bool IsOnPlate;
-
- public override bool RemoveWhenNotAlive => IsOnPlate;
+ protected override JudgementResult CreateResult(Judgement judgement) => new CatchJudgementResult(HitObject, judgement);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (CheckPosition == null) return;
if (timeOffset >= 0 && Result != null)
- ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss);
+ ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
- protected override void UpdateStateTransforms(ArmedState state)
+ protected override void UpdateHitStateTransforms(ArmedState state)
{
- var endTime = HitObject.GetEndTime();
-
- using (BeginAbsoluteSequence(endTime, true))
+ switch (state)
{
- switch (state)
- {
- case ArmedState.Miss:
- this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out);
- break;
+ case ArmedState.Miss:
+ this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out);
+ break;
- case ArmedState.Hit:
- this.FadeOut();
- break;
- }
+ case ArmedState.Hit:
+ this.FadeOut();
+ break;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
index cad8892283..2dce9507a5 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
@@ -1,19 +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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableDroplet : PalpableCatchHitObject
+ public class DrawableDroplet : DrawablePalpableCatchHitObject
{
- public override bool StaysOnPlate => false;
+ public DrawableDroplet()
+ : this(null)
+ {
+ }
- public DrawableDroplet(Droplet h)
+ public DrawableDroplet([CanBeNull] CatchHitObject h)
: base(h)
{
}
@@ -21,11 +24,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new Pulp
- {
- Size = Size / 4,
- AccentColour = { BindTarget = AccentColour }
- });
+ ScalingContainer.Child = new SkinnableDrawable(
+ new CatchSkinComponent(CatchSkinComponents.Droplet),
+ _ => new DropletPiece());
}
protected override void UpdateInitialTransforms()
@@ -33,10 +34,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
base.UpdateInitialTransforms();
// roughly matches osu-stable
- float startRotation = RNG.NextSingle() * 20;
+ float startRotation = RandomSingle(1) * 20;
double duration = HitObject.TimePreempt + 2000;
- ScaleContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
+ ScalingContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
index fae5a10d04..0b89c46480 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
@@ -1,16 +1,25 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
-using osu.Framework.Utils;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableFruit : PalpableCatchHitObject
+ public class DrawableFruit : DrawablePalpableCatchHitObject, IHasFruitState
{
- public DrawableFruit(Fruit h)
+ public Bindable VisualRepresentation { get; } = new Bindable();
+
+ public DrawableFruit()
+ : this(null)
+ {
+ }
+
+ public DrawableFruit([CanBeNull] Fruit h)
: base(h)
{
}
@@ -18,34 +27,29 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- ScaleContainer.Child = new SkinnableDrawable(
- new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece());
+ IndexInBeatmap.BindValueChanged(change =>
+ {
+ VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4);
+ }, true);
- ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
+ ScalingContainer.Child = new SkinnableDrawable(
+ new CatchSkinComponent(CatchSkinComponents.Fruit),
+ _ => new FruitPiece());
}
- private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation)
+ protected override void UpdateInitialTransforms()
{
- switch (hitObjectVisualRepresentation)
- {
- case FruitVisualRepresentation.Pear:
- return CatchSkinComponents.FruitPear;
+ base.UpdateInitialTransforms();
- case FruitVisualRepresentation.Grape:
- return CatchSkinComponents.FruitGrapes;
-
- case FruitVisualRepresentation.Pineapple:
- return CatchSkinComponents.FruitApple;
-
- case FruitVisualRepresentation.Raspberry:
- return CatchSkinComponents.FruitOrange;
-
- case FruitVisualRepresentation.Banana:
- return CatchSkinComponents.FruitBananas;
-
- default:
- throw new ArgumentOutOfRangeException(nameof(hitObjectVisualRepresentation), hitObjectVisualRepresentation, null);
- }
+ ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
}
}
+
+ public enum FruitVisualRepresentation
+ {
+ Pear,
+ Grape,
+ Pineapple,
+ Raspberry,
+ }
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
index 7bc016d94f..a496a35842 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
@@ -1,37 +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 System;
+using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableJuiceStream : DrawableCatchHitObject
+ public class DrawableJuiceStream : DrawableCatchHitObject
{
- private readonly Func> createDrawableRepresentation;
private readonly Container dropletContainer;
- public override Vector2 OriginPosition => base.OriginPosition - new Vector2(0, CatchHitObject.OBJECT_RADIUS);
+ public DrawableJuiceStream()
+ : this(null)
+ {
+ }
- public DrawableJuiceStream(JuiceStream s, Func> createDrawableRepresentation = null)
+ public DrawableJuiceStream([CanBeNull] JuiceStream s)
: base(s)
{
- this.createDrawableRepresentation = createDrawableRepresentation;
RelativeSizeAxes = Axes.X;
Origin = Anchor.BottomLeft;
- X = 0;
AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, });
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
- hitObject.Origin = Anchor.BottomCentre;
-
base.AddNestedHitObject(hitObject);
dropletContainer.Add(hitObject);
}
@@ -39,19 +35,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
- dropletContainer.Clear();
- }
-
- protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
- {
- switch (hitObject)
- {
- case CatchHitObject catchObject:
- return createDrawableRepresentation?.Invoke(catchObject)?.With(o =>
- ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
- }
-
- throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}.");
+ dropletContainer.Clear(false);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
new file mode 100644
index 0000000000..27cd7ed2bc
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
@@ -0,0 +1,93 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ [Cached(typeof(IHasCatchObjectState))]
+ public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject, IHasCatchObjectState
+ {
+ public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
+
+ Bindable IHasCatchObjectState.AccentColour => AccentColour;
+
+ public Bindable HyperDash { get; } = new Bindable();
+
+ public Bindable ScaleBindable { get; } = new Bindable(1);
+
+ public Bindable IndexInBeatmap { get; } = new Bindable();
+
+ ///
+ /// The multiplicative factor applied to relative to scale.
+ ///
+ protected virtual float ScaleFactor => 1;
+
+ ///
+ /// The container internal transforms (such as scaling based on the circle size) are applied to.
+ ///
+ protected readonly Container ScalingContainer;
+
+ public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale;
+
+ public float DisplayRotation => ScalingContainer.Rotation;
+
+ protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h)
+ : base(h)
+ {
+ Origin = Anchor.Centre;
+ Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
+
+ AddInternal(ScalingContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2)
+ });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ OriginalXBindable.BindValueChanged(updateXPosition);
+ XOffsetBindable.BindValueChanged(updateXPosition, true);
+
+ ScaleBindable.BindValueChanged(scale =>
+ {
+ ScalingContainer.Scale = new Vector2(scale.NewValue * ScaleFactor);
+ Size = DisplaySize;
+ }, true);
+
+ IndexInBeatmap.BindValueChanged(_ => UpdateComboColour());
+ }
+
+ private void updateXPosition(ValueChangedEvent _)
+ {
+ X = OriginalXBindable.Value + XOffsetBindable.Value;
+ }
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ HyperDash.BindTo(HitObject.HyperDashBindable);
+ ScaleBindable.BindTo(HitObject.ScaleBindable);
+ IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable);
+ }
+
+ protected override void OnFree()
+ {
+ HyperDash.UnbindFrom(HitObject.HyperDashBindable);
+ ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
+ IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable);
+
+ base.OnFree();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
index ae775684d8..8f5a04dfda 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
@@ -1,21 +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 JetBrains.Annotations;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableTinyDroplet : DrawableDroplet
{
- public DrawableTinyDroplet(TinyDroplet h)
- : base(h)
+ protected override float ScaleFactor => base.ScaleFactor / 2;
+
+ public DrawableTinyDroplet()
+ : this(null)
{
}
- [BackgroundDependencyLoader]
- private void load()
+ public DrawableTinyDroplet([CanBeNull] TinyDroplet h)
+ : base(h)
{
- ScaleContainer.Scale /= 2;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
deleted file mode 100644
index 5797588ded..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Rulesets.Objects.Drawables;
-using osuTK.Graphics;
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawables
-{
- internal class FruitPiece : CompositeDrawable
- {
- ///
- /// Because we're adding a border around the fruit, we need to scale down some.
- ///
- public const float RADIUS_ADJUST = 1.1f;
-
- private Circle border;
-
- private CatchHitObject hitObject;
-
- private readonly IBindable accentColour = new Bindable();
-
- public FruitPiece()
- {
- RelativeSizeAxes = Axes.Both;
- }
-
- [BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject)
- {
- DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
- hitObject = drawableCatchObject.HitObject;
-
- accentColour.BindTo(drawableCatchObject.AccentColour);
-
- AddRangeInternal(new[]
- {
- getFruitFor(drawableCatchObject.HitObject.VisualRepresentation),
- border = new Circle
- {
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- BorderColour = Color4.White,
- BorderThickness = 6f * RADIUS_ADJUST,
- Children = new Drawable[]
- {
- new Box
- {
- AlwaysPresent = true,
- Alpha = 0,
- RelativeSizeAxes = Axes.Both
- }
- }
- },
- });
-
- if (hitObject.HyperDash)
- {
- AddInternal(new Circle
- {
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- BorderColour = Color4.Red,
- BorderThickness = 12f * RADIUS_ADJUST,
- Children = new Drawable[]
- {
- new Box
- {
- AlwaysPresent = true,
- Alpha = 0.3f,
- Blending = BlendingParameters.Additive,
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Red,
- }
- }
- });
- }
- }
-
- protected override void Update()
- {
- base.Update();
- border.Alpha = (float)Math.Clamp((hitObject.StartTime - Time.Current) / 500, 0, 1);
- }
-
- private Drawable getFruitFor(FruitVisualRepresentation representation)
- {
- switch (representation)
- {
- case FruitVisualRepresentation.Pear:
- return new PearPiece();
-
- case FruitVisualRepresentation.Grape:
- return new GrapePiece();
-
- case FruitVisualRepresentation.Pineapple:
- return new PineapplePiece();
-
- case FruitVisualRepresentation.Banana:
- return new BananaPiece();
-
- case FruitVisualRepresentation.Raspberry:
- return new RaspberryPiece();
- }
-
- return Empty();
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs
deleted file mode 100644
index 1d1faf893b..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawables
-{
- public class GrapePiece : PulpFormation
- {
- public GrapePiece()
- {
- InternalChildren = new Drawable[]
- {
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(SMALL_PULP),
- Y = -0.25f,
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_3),
- Position = PositionAt(0, DISTANCE_FROM_CENTRE_3),
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_3),
- Position = PositionAt(120, DISTANCE_FROM_CENTRE_3),
- },
- new Pulp
- {
- Size = new Vector2(LARGE_PULP_3),
- AccentColour = { BindTarget = AccentColour },
- Position = PositionAt(240, DISTANCE_FROM_CENTRE_3),
- },
- };
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
new file mode 100644
index 0000000000..81b61f0959
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ ///
+ /// Provides a visual state of a .
+ ///
+ public interface IHasCatchObjectState
+ {
+ PalpableCatchHitObject HitObject { get; }
+
+ Bindable AccentColour { get; }
+
+ Bindable HyperDash { get; }
+
+ Vector2 DisplaySize { get; }
+
+ float DisplayRotation { get; }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs
new file mode 100644
index 0000000000..2d4de543c3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs
@@ -0,0 +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 osu.Framework.Bindables;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ ///
+ /// Provides a visual state of a .
+ ///
+ public interface IHasFruitState : IHasCatchObjectState
+ {
+ Bindable VisualRepresentation { get; }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs
deleted file mode 100644
index 7f14217cda..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawables
-{
- public class PearPiece : PulpFormation
- {
- public PearPiece()
- {
- InternalChildren = new Drawable[]
- {
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(SMALL_PULP),
- Y = -0.33f,
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_3),
- Position = PositionAt(60, DISTANCE_FROM_CENTRE_3),
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_3),
- Position = PositionAt(180, DISTANCE_FROM_CENTRE_3),
- },
- new Pulp
- {
- Size = new Vector2(LARGE_PULP_3),
- AccentColour = { BindTarget = AccentColour },
- Position = PositionAt(300, DISTANCE_FROM_CENTRE_3),
- },
- };
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs
deleted file mode 100644
index c328ba1837..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawables
-{
- public class PineapplePiece : PulpFormation
- {
- public PineapplePiece()
- {
- InternalChildren = new Drawable[]
- {
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(SMALL_PULP),
- Y = -0.3f,
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_4),
- Position = PositionAt(45, DISTANCE_FROM_CENTRE_4),
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_4),
- Position = PositionAt(135, DISTANCE_FROM_CENTRE_4),
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_4),
- Position = PositionAt(225, DISTANCE_FROM_CENTRE_4),
- },
- new Pulp
- {
- Size = new Vector2(LARGE_PULP_4),
- AccentColour = { BindTarget = AccentColour },
- Position = PositionAt(315, DISTANCE_FROM_CENTRE_4),
- },
- };
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs
deleted file mode 100644
index 22ce3ba5b3..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawables
-{
- public class RaspberryPiece : PulpFormation
- {
- public RaspberryPiece()
- {
- InternalChildren = new Drawable[]
- {
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(SMALL_PULP),
- Y = -0.34f,
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_4),
- Position = PositionAt(0, DISTANCE_FROM_CENTRE_4),
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_4),
- Position = PositionAt(90, DISTANCE_FROM_CENTRE_4),
- },
- new Pulp
- {
- AccentColour = { BindTarget = AccentColour },
- Size = new Vector2(LARGE_PULP_4),
- Position = PositionAt(180, DISTANCE_FROM_CENTRE_4),
- },
- new Pulp
- {
- Size = new Vector2(LARGE_PULP_4),
- AccentColour = { BindTarget = AccentColour },
- Position = PositionAt(270, DISTANCE_FROM_CENTRE_4),
- },
- };
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs
index 7b0bb3f0ae..9c1004a04b 100644
--- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs
@@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class Droplet : CatchHitObject
+ public class Droplet : PalpableCatchHitObject
{
public override Judgement CreateJudgement() => new CatchDropletJudgement();
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
index 6f0423b420..43486796ad 100644
--- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
@@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class Fruit : CatchHitObject
+ public class Fruit : PalpableCatchHitObject
{
public override Judgement CreateJudgement() => new CatchJudgement();
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 01011645bd..35fd58826e 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -1,19 +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 System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class JuiceStream : CatchHitObject, IHasCurve
+ public class JuiceStream : CatchHitObject, IHasPathWithRepeats
{
///
/// Positional distance that results in a duration of one second, before any speed adjustments.
@@ -45,20 +46,16 @@ namespace osu.Game.Rulesets.Catch.Objects
TickDistance = scoringDistance / difficulty.SliderTickRate;
}
- protected override void CreateNestedHitObjects()
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
+ base.CreateNestedHitObjects(cancellationToken);
- var dropletSamples = Samples.Select(s => new HitSampleInfo
- {
- Bank = s.Bank,
- Name = @"slidertick",
- Volume = s.Volume
- }).ToList();
+ var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();
+ int nodeIndex = 0;
SliderEventDescriptor? lastEvent = null;
- foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
+ foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
{
// generate tiny droplets since the last point
if (lastEvent != null)
@@ -73,11 +70,13 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
AddNested(new TinyDroplet
{
StartTime = t + lastEvent.Value.Time,
- X = X + Path.PositionAt(
- lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
+ X = OriginalX + Path.PositionAt(
+ lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
});
}
}
@@ -94,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = dropletSamples,
StartTime = e.Time,
- X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
+ X = OriginalX + Path.PositionAt(e.PathProgress).X,
});
break;
@@ -103,24 +102,24 @@ namespace osu.Game.Rulesets.Catch.Objects
case SliderEventType.Repeat:
AddNested(new Fruit
{
- Samples = Samples,
+ Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
- X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
+ X = OriginalX + Path.PositionAt(e.PathProgress).X,
});
break;
}
}
}
- public double EndTime
+ public float EndX => OriginalX + this.CurvePositionAt(1).X;
+
+ public double Duration
{
- get => StartTime + this.SpanCount() * Path.Distance / Velocity;
- set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
+ get => this.SpanCount() * Path.Distance / Velocity;
+ set => throw new NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
- public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
-
- public double Duration => EndTime - StartTime;
+ public double EndTime => StartTime + Duration;
private readonly SliderPath path = new SliderPath();
diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
new file mode 100644
index 0000000000..0cd3af01df
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.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 osu.Framework.Bindables;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Objects
+{
+ ///
+ /// Represents a single object that can be caught by the catcher.
+ /// This includes normal fruits, droplets, and bananas but excludes objects that act only as a container of nested hit objects.
+ ///
+ public abstract class PalpableCatchHitObject : CatchHitObject, IHasComboInformation
+ {
+ ///
+ /// Difference between the distance to the next object
+ /// and the distance that would have triggered a hyper dash.
+ /// A value close to 0 indicates a difficult jump (for difficulty calculation).
+ ///
+ public float DistanceToHyperDash { get; set; }
+
+ public readonly Bindable HyperDashBindable = new Bindable();
+
+ ///
+ /// Whether this fruit can initiate a hyperdash.
+ ///
+ public bool HyperDash => HyperDashBindable.Value;
+
+ private CatchHitObject hyperDashTarget;
+
+ ///
+ /// The target fruit if we are to initiate a hyperdash.
+ ///
+ public CatchHitObject HyperDashTarget
+ {
+ get => hyperDashTarget;
+ set
+ {
+ hyperDashTarget = value;
+ HyperDashBindable.Value = value != null;
+ }
+ }
+
+ Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count];
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index b90b5812a6..64ded8e94f 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -31,17 +31,25 @@ namespace osu.Game.Rulesets.Catch.Replays
public override Replay Generate()
{
+ if (Beatmap.HitObjects.Count == 0)
+ return Replay;
+
// todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED;
const double movement_speed = dash_speed / 2;
- float lastPosition = 0.5f;
+ float lastPosition = CatchPlayfield.CENTER_X;
double lastTime = 0;
- void moveToNext(CatchHitObject h)
+ void moveToNext(PalpableCatchHitObject h)
{
- float positionChange = Math.Abs(lastPosition - h.X);
+ float positionChange = Math.Abs(lastPosition - h.EffectiveX);
double timeAvailable = h.StartTime - lastTime;
+ if (timeAvailable < 0)
+ {
+ return;
+ }
+
// So we can either make it there without a dash or not.
// If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too)
// The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour.
@@ -51,11 +59,11 @@ namespace osu.Game.Rulesets.Catch.Replays
bool impossibleJump = speedRequired > movement_speed * 2;
// todo: get correct catcher size, based on difficulty CS.
- const float catcher_width_half = CatcherArea.CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * 0.3f * 0.5f;
+ const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f;
- if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X)
+ if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
{
- //we are already in the correct range.
+ // we are already in the correct range.
lastTime = h.StartTime;
addFrame(h.StartTime, lastPosition);
return;
@@ -63,58 +71,51 @@ namespace osu.Game.Rulesets.Catch.Replays
if (impossibleJump)
{
- addFrame(h.StartTime, h.X);
+ addFrame(h.StartTime, h.EffectiveX);
}
else if (h.HyperDash)
{
addFrame(h.StartTime - timeAvailable, lastPosition);
- addFrame(h.StartTime, h.X);
+ addFrame(h.StartTime, h.EffectiveX);
}
else if (dashRequired)
{
- //we do a movement in two parts - the dash part then the normal part...
+ // we do a movement in two parts - the dash part then the normal part...
double timeAtNormalSpeed = positionChange / movement_speed;
double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable;
double timeAtDashSpeed = timeWeNeedToSave / 2;
- float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable);
+ float midPosition = (float)Interpolation.Lerp(lastPosition, h.EffectiveX, (float)timeAtDashSpeed / timeAvailable);
- //dash movement
+ // dash movement
addFrame(h.StartTime - timeAvailable + 1, lastPosition, true);
addFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition);
- addFrame(h.StartTime, h.X);
+ addFrame(h.StartTime, h.EffectiveX);
}
else
{
double timeBefore = positionChange / movement_speed;
addFrame(h.StartTime - timeBefore, lastPosition);
- addFrame(h.StartTime, h.X);
+ addFrame(h.StartTime, h.EffectiveX);
}
lastTime = h.StartTime;
- lastPosition = h.X;
+ lastPosition = h.EffectiveX;
}
foreach (var obj in Beatmap.HitObjects)
{
- switch (obj)
+ if (obj is PalpableCatchHitObject palpableObject)
{
- case Fruit _:
- moveToNext(obj);
- break;
+ moveToNext(palpableObject);
}
foreach (var nestedObj in obj.NestedHitObjects.Cast())
{
- switch (nestedObj)
+ if (nestedObj is PalpableCatchHitObject palpableNestedObject)
{
- case Banana _:
- case TinyDroplet _:
- case Droplet _:
- case Fruit _:
- moveToNext(nestedObj);
- break;
+ moveToNext(palpableNestedObject);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
index f122588a2b..99d899db80 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
@@ -35,18 +35,15 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
- public override List GetPendingInputs()
+ public override void CollectPendingInputs(List inputs)
{
- if (!Position.HasValue) return new List();
+ if (!Position.HasValue) return;
- return new List
+ inputs.Add(new CatchReplayState
{
- new CatchReplayState
- {
- PressedActions = CurrentFrame?.Actions ?? new List(),
- CatcherX = Position.Value
- },
- };
+ PressedActions = CurrentFrame?.Actions ?? new List(),
+ CatcherX = Position.Value
+ });
}
public class CatchReplayState : ReplayState
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
index b41a5e0612..1a80adb584 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -39,9 +38,9 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
- public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
+ public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
- Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH;
+ Position = currentFrame.Position.X;
Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;
if (Dashing)
@@ -53,8 +52,17 @@ namespace osu.Game.Rulesets.Catch.Replays
if (Position > lastCatchFrame.Position)
lastCatchFrame.Actions.Add(CatchAction.MoveRight);
else if (Position < lastCatchFrame.Position)
- Actions.Add(CatchAction.MoveLeft);
+ lastCatchFrame.Actions.Add(CatchAction.MoveLeft);
}
}
+
+ public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
+ {
+ ReplayButtonState state = ReplayButtonState.None;
+
+ if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1;
+
+ return new LegacyReplayFrame(Time, Position, null, state);
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json
new file mode 100644
index 0000000000..3bde97070c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json
@@ -0,0 +1,17 @@
+{
+ "Mappings": [{
+ "StartTime": 3368,
+ "Objects": [{
+ "StartTime": 3368,
+ "Position": 374
+ }]
+ },
+ {
+ "StartTime": 3501,
+ "Objects": [{
+ "StartTime": 3501,
+ "Position": 446
+ }]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu
new file mode 100644
index 0000000000..6630f369d5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu
@@ -0,0 +1,20 @@
+osu file format v14
+
+[General]
+StackLeniency: 0.7
+Mode: 2
+
+[Difficulty]
+HPDrainRate:6
+CircleSize:4
+OverallDifficulty:9.6
+ApproachRate:9.6
+SliderMultiplier:1.9
+SliderTickRate:1
+
+[TimingPoints]
+2169,266.666666666667,4,2,1,70,1,0
+
+[HitObjects]
+374,60,3368,1,0,0:0:0:0:
+410,146,3501,1,2,0:1:0:0:
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
index ff793a372e..0a444d923e 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
switch (result)
{
- case HitResult.Perfect:
+ case HitResult.Great:
case HitResult.Miss:
return true;
}
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index 4c7bc4ab73..2cc05826b4 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreProcessor : ScoreProcessor
{
- public override HitWindows CreateHitWindows() => new CatchHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
deleted file mode 100644
index 65e6e6f209..0000000000
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using Humanizer;
-using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Textures;
-using osu.Game.Audio;
-using osu.Game.Skinning;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.Skinning
-{
- public class CatchLegacySkinTransformer : ISkin
- {
- private readonly ISkin source;
-
- public CatchLegacySkinTransformer(ISkinSource source)
- {
- this.source = source;
- }
-
- public Drawable GetDrawableComponent(ISkinComponent component)
- {
- if (!(component is CatchSkinComponent catchSkinComponent))
- return null;
-
- switch (catchSkinComponent.Component)
- {
- case CatchSkinComponents.FruitApple:
- case CatchSkinComponents.FruitBananas:
- case CatchSkinComponents.FruitOrange:
- case CatchSkinComponents.FruitGrapes:
- case CatchSkinComponents.FruitPear:
- var lookupName = catchSkinComponent.Component.ToString().Kebaberize();
- if (GetTexture(lookupName) != null)
- return new LegacyFruitPiece(lookupName);
-
- break;
-
- case CatchSkinComponents.Droplet:
- if (GetTexture("fruit-drop") != null)
- return new LegacyFruitPiece("fruit-drop") { Scale = new Vector2(0.8f) };
-
- break;
-
- case CatchSkinComponents.CatcherIdle:
- return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
- this.GetAnimation("fruit-ryuuta", true, true, true);
-
- case CatchSkinComponents.CatcherFail:
- return this.GetAnimation("fruit-catcher-fail", true, true, true) ??
- this.GetAnimation("fruit-ryuuta", true, true, true);
-
- case CatchSkinComponents.CatcherKiai:
- return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
- this.GetAnimation("fruit-ryuuta", true, true, true);
- }
-
- return null;
- }
-
- public Texture GetTexture(string componentName) => source.GetTexture(componentName);
-
- public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
-
- public IBindable GetConfig(TLookup lookup) => source.GetConfig(lookup);
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
new file mode 100644
index 0000000000..4506111498
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.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.
+
+namespace osu.Game.Rulesets.Catch.Skinning
+{
+ public enum CatchSkinColour
+ {
+ ///
+ /// The colour to be used for the catcher while in hyper-dashing state.
+ ///
+ HyperDash,
+
+ ///
+ /// The colour to be used for fruits that grant the catcher the ability to hyper-dash.
+ ///
+ HyperDashFruit,
+
+ ///
+ /// The colour to be used for the "exploding" catcher sprite on beginning of hyper-dashing.
+ ///
+ HyperDashAfterImage,
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs
new file mode 100644
index 0000000000..8da18a668a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.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 osu.Framework.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+ public class BananaPiece : CatchHitObjectPiece
+ {
+ protected override BorderPiece BorderPiece { get; }
+
+ public BananaPiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new BananaPulpFormation
+ {
+ AccentColour = { BindTarget = AccentColour },
+ },
+ BorderPiece = new BorderPiece(),
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPulpFormation.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPulpFormation.cs
new file mode 100644
index 0000000000..ee1cc68f7d
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPulpFormation.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+ public class BananaPulpFormation : PulpFormation
+ {
+ public BananaPulpFormation()
+ {
+ AddPulp(new Vector2(0, -0.3f), new Vector2(SMALL_PULP));
+ AddPulp(new Vector2(0, 0.05f), new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs
new file mode 100644
index 0000000000..8d8ee49af7
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Catch.Objects;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+ public class BorderPiece : Circle
+ {
+ public BorderPiece()
+ {
+ Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ BorderColour = Color4.White;
+ BorderThickness = 6f * FruitPiece.RADIUS_ADJUST;
+
+ // Border is drawn only when there is a child drawable.
+ Child = new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both,
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs
new file mode 100644
index 0000000000..51c06c8e37
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+ public abstract class CatchHitObjectPiece : CompositeDrawable
+ {
+ public readonly Bindable AccentColour = new Bindable();
+ public readonly Bindable HyperDash = new Bindable();
+
+ [Resolved]
+ protected IHasCatchObjectState ObjectState { get; private set; }
+
+ ///
+ /// A part of this piece that will be faded out while falling in the playfield.
+ ///
+ [CanBeNull]
+ protected virtual BorderPiece BorderPiece => null;
+
+ ///
+ /// A part of this piece that will be only visible when is true.
+ ///
+ [CanBeNull]
+ protected virtual HyperBorderPiece HyperBorderPiece => null;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AccentColour.BindTo(ObjectState.AccentColour);
+ HyperDash.BindTo(ObjectState.HyperDash);
+
+ HyperDash.BindValueChanged(hyper =>
+ {
+ if (HyperBorderPiece != null)
+ HyperBorderPiece.Alpha = hyper.NewValue ? 1 : 0;
+ }, true);
+ }
+
+ protected override void Update()
+ {
+ if (BorderPiece != null)
+ BorderPiece.Alpha = (float)Math.Clamp((ObjectState.HitObject.StartTime - Time.Current) / 500, 0, 1);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs
new file mode 100644
index 0000000000..8b1052dfe2
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+ public class DropletPiece : CatchHitObjectPiece
+ {
+ protected override HyperBorderPiece HyperBorderPiece { get; }
+
+ public DropletPiece()
+ {
+ Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
+
+ InternalChildren = new Drawable[]
+ {
+ new Pulp
+ {
+ RelativeSizeAxes = Axes.Both,
+ AccentColour = { BindTarget = AccentColour }
+ },
+ HyperBorderPiece = new HyperDropletBorderPiece()
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs
new file mode 100644
index 0000000000..49f128c960
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+ internal class FruitPiece : CatchHitObjectPiece
+ {
+ ///
+ /// Because we're adding a border around the fruit, we need to scale down some.
+ ///
+ public const float RADIUS_ADJUST = 1.1f;
+
+ public readonly Bindable VisualRepresentation = new Bindable();
+
+ protected override BorderPiece BorderPiece { get; }
+ protected override HyperBorderPiece HyperBorderPiece { get; }
+
+ public FruitPiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new FruitPulpFormation
+ {
+ AccentColour = { BindTarget = AccentColour },
+ VisualRepresentation = { BindTarget = VisualRepresentation }
+ },
+ BorderPiece = new BorderPiece(),
+ HyperBorderPiece = new HyperBorderPiece()
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ var fruitState = (IHasFruitState)ObjectState;
+ VisualRepresentation.BindTo(fruitState.VisualRepresentation);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs
new file mode 100644
index 0000000000..88e0b5133a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs
@@ -0,0 +1,59 @@
+// 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.Drawables;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+ public class FruitPulpFormation : PulpFormation
+ {
+ public readonly Bindable VisualRepresentation = new Bindable();
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ VisualRepresentation.BindValueChanged(setFormation, true);
+ }
+
+ private void setFormation(ValueChangedEvent visualRepresentation)
+ {
+ Clear();
+
+ switch (visualRepresentation.NewValue)
+ {
+ case FruitVisualRepresentation.Pear:
+ AddPulp(new Vector2(0, -0.33f), new Vector2(SMALL_PULP));
+ AddPulp(PositionAt(60, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
+ AddPulp(PositionAt(180, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
+ AddPulp(PositionAt(300, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
+ break;
+
+ case FruitVisualRepresentation.Grape:
+ AddPulp(new Vector2(0, -0.25f), new Vector2(SMALL_PULP));
+ AddPulp(PositionAt(0, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
+ AddPulp(PositionAt(120, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
+ AddPulp(PositionAt(240, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
+ break;
+
+ case FruitVisualRepresentation.Pineapple:
+ AddPulp(new Vector2(0, -0.3f), new Vector2(SMALL_PULP));
+ AddPulp(PositionAt(45, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
+ AddPulp(PositionAt(135, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
+ AddPulp(PositionAt(225, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
+ AddPulp(PositionAt(315, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
+ break;
+
+ case FruitVisualRepresentation.Raspberry:
+ AddPulp(new Vector2(0, -0.34f), new Vector2(SMALL_PULP));
+ AddPulp(PositionAt(0, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
+ AddPulp(PositionAt(90, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
+ AddPulp(PositionAt(180, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
+ AddPulp(PositionAt(270, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs
new file mode 100644
index 0000000000..c8895f32f4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd