diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml
index 366f172c30..fe63f5faf3 100644
--- a/.idea/.idea.osu.Desktop/.idea/modules.xml
+++ b/.idea/.idea.osu.Desktop/.idea/modules.xml
@@ -2,7 +2,6 @@
-
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
index a4154623b6..512ac4393a 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 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ 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..dec1ef717f 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 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ 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..d9370d5440 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 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ 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..def4940bb1 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 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ 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..1ffa73c257 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -14,7 +14,7 @@
-
+
\ 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..e64da796b7 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 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ 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..22105e1de2 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -14,7 +14,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
index d85a0ae44c..31f1fda09d 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.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..cc243f6901 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 @@
-
+
-
+
@@ -14,7 +14,7 @@
-
+
\ 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/Gemfile.lock b/Gemfile.lock
index e3954c2681..bf971d2c22 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -5,6 +5,22 @@ GEM
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
+ aws-eventstream (1.1.0)
+ aws-partitions (1.329.0)
+ aws-sdk-core (3.99.2)
+ aws-eventstream (~> 1, >= 1.0.2)
+ aws-partitions (~> 1, >= 1.239.0)
+ aws-sigv4 (~> 1.1)
+ jmespath (~> 1.0)
+ aws-sdk-kms (1.34.1)
+ aws-sdk-core (~> 3, >= 3.99.0)
+ aws-sigv4 (~> 1.1)
+ aws-sdk-s3 (1.68.1)
+ aws-sdk-core (~> 3, >= 3.99.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.1)
+ aws-sigv4 (1.1.4)
+ aws-eventstream (~> 1.0, >= 1.0.2)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
@@ -13,23 +29,24 @@ GEM
highline (~> 1.7.2)
declarative (0.0.10)
declarative-option (0.1.0)
- digest-crc (0.4.1)
+ digest-crc (0.5.1)
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)
+ excon (0.74.0)
+ faraday (1.0.1)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
- faraday_middleware (0.13.1)
- faraday (>= 0.7.4, < 1.0)
+ faraday_middleware (1.0.0)
+ faraday (~> 1.0)
fastimage (2.1.7)
- fastlane (2.140.0)
+ fastlane (2.149.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
+ aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
@@ -37,12 +54,12 @@ GEM
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.71.0, < 1.0.0)
- faraday (~> 0.17)
+ faraday (>= 0.17, < 2.0)
faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (~> 0.13.1)
+ faraday_middleware (>= 0.13.1, < 2.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)
@@ -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,27 +97,28 @@ 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.3.2)
+ faraday (>= 0.17.3, < 2.0)
+ google-cloud-errors (1.0.1)
+ google-cloud-storage (1.26.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.12.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)
+ jmespath (1.4.0)
json (2.3.0)
jwt (2.1.0)
memoist (0.16.2)
@@ -114,7 +132,7 @@ GEM
naturally (2.2.0)
nokogiri (1.10.7)
mini_portile2 (~> 2.4.0)
- os (1.0.1)
+ os (1.1.0)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
@@ -125,12 +143,12 @@ GEM
rouge (2.0.7)
rubyzip (1.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,17 +159,17 @@ 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.0)
+ 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.16.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
diff --git a/README.md b/README.md
index 336bf33f7e..dc3ee63844 100644
--- a/README.md
+++ b/README.md
@@ -93,11 +93,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/build/InspectCode.cake b/build/InspectCode.cake
index 2e7a1d1b28..c8f4f37c94 100644
--- a/build/InspectCode.cake
+++ b/build/InspectCode.cake
@@ -1,5 +1,5 @@
-#addin "nuget:?package=CodeFileSanity&version=0.0.33"
-#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.3.2"
+#addin "nuget:?package=CodeFileSanity&version=0.0.36"
+#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2020.1.3"
#tool "nuget:?package=NVika.MSBuild&version=1.0.1"
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
diff --git a/global.json b/global.json
index 6c793a3f1d..9aa5b6192b 100644
--- a/global.json
+++ b/global.json
@@ -5,6 +5,6 @@
"version": "3.1.100"
},
"msbuild-sdks": {
- "Microsoft.Build.Traversal": "2.0.34"
+ "Microsoft.Build.Traversal": "2.0.50"
}
}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 07be3ab0d2..0881861bdc 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 2e5fa59d20..9839d16030 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -9,7 +9,7 @@ using osu.Framework.Android;
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.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid();
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 19ed7ffcf5..7542a2b997 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -3,6 +3,7 @@
using System;
using Android.App;
+using Android.OS;
using osu.Game;
using osu.Game.Updater;
@@ -18,9 +19,32 @@ namespace osu.Android
try
{
- // todo: needs checking before play store redeploy.
- string versionName = packageInfo.VersionName;
- // 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
@@ -33,4 +57,4 @@ namespace osu.Android
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
}
-}
\ No newline at end of file
+}
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 9351e17419..cd31df316a 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -10,7 +10,6 @@ using Microsoft.Win32;
using osu.Desktop.Overlays;
using osu.Framework.Platform;
using osu.Game;
-using osuTK.Input;
using osu.Desktop.Updater;
using osu.Framework;
using osu.Framework.Logging;
@@ -59,7 +58,7 @@ namespace osu.Desktop
try
{
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString()?.Split('"')[1].Replace("osu!.exe", "");
if (checkExists(stableInstallPath))
return stableInstallPath;
@@ -122,21 +121,27 @@ namespace osu.Desktop
{
base.SetHost(host);
- if (host.Window is DesktopGameWindow desktopWindow)
+ switch (host.Window)
{
- desktopWindow.CursorState |= CursorState.Hidden;
+ // Legacy osuTK DesktopGameWindow
+ case DesktopGameWindow desktopGameWindow:
+ desktopGameWindow.CursorState |= CursorState.Hidden;
+ desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
+ 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 DesktopWindow desktopWindow:
+ desktopWindow.CursorState.Value |= CursorState.Hidden;
+ 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;
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index ade8460dd7..05c8e835ac 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -30,25 +30,23 @@ namespace osu.Desktop.Updater
private static readonly Logger logger = Logger.GetLogger("updater");
[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();
+
+ private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
// 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);
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
if (info.ReleasesToApply.Count == 0)
@@ -83,7 +81,7 @@ namespace osu.Desktop.Updater
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
- checkForUpdateAsync(false, notification);
+ await checkForUpdateAsync(false, notification);
scheduleRecheck = false;
}
else
@@ -102,7 +100,7 @@ namespace osu.Desktop.Updater
if (scheduleRecheck)
{
// check again in 30 minutes.
- Scheduler.AddDelayed(() => checkForUpdateAsync(), 60000 * 30);
+ Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
}
}
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index c34e1e1221..7a99c70999 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -30,6 +30,10 @@
+
+
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index f4749be370..df54df7b01 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -8,7 +8,6 @@ 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;
@@ -83,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public float Position
{
- get => HitObject?.X * CatchPlayfield.BASE_WIDTH ?? position;
+ get => HitObject?.X ?? position;
set => position = value;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
index 7deeec527f..b570f090ca 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
@@ -17,7 +17,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
var store = new NamespacedResourceStore(new DllResourceStore(GetType().Assembly), "Resources/special-skin");
var rawSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, store);
- var skin = new CatchLegacySkinTransformer(rawSkin);
+ 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);
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/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index ed7bfb9a44..d6bba3d55e 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -12,13 +12,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,15 +27,15 @@ 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
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
index 27a2d5bd0a..e89a95ae37 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
@@ -4,18 +4,12 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneBananaShower : PlayerTestScene
+ public class TestSceneBananaShower : TestSceneCatchPlayer
{
- public TestSceneBananaShower()
- : base(new CatchRuleset())
- {
- }
-
[Test]
public void TestBananaShower()
{
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/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/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index 2b30edb70b..fbb22a8498 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -76,8 +76,8 @@ namespace osu.Game.Rulesets.Catch.Tests
RelativeSizeAxes = Axes.Both,
Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.TopLeft,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.TopCentre,
CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation
},
});
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index a7094c00be..d35f828e28 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -158,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/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index 0a142a52f8..ad24adf352 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -9,19 +9,13 @@ using osu.Game.Beatmaps;
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 TestSceneHyperDash()
- : base(new CatchRuleset())
- {
- }
-
protected override bool Autoplay => true;
[Test]
@@ -53,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Tests
};
// 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);
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/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 0de2060e2d..145a40f5f5 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -5,7 +5,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
using System.Linq;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -36,7 +35,7 @@ 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
@@ -59,7 +58,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..bb14988414 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -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.X + 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.X, CatchPlayfield.WIDTH - catchObject.X);
else if (catchObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation
}
@@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
// 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;
@@ -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;
@@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
- double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2;
+ double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index ca75a816f1..9437023c70 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,11 +21,13 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
+ [ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
index 360af1a8c9..3e21b8fbaf 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;
@@ -33,8 +32,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
// 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.X * scalingFactor;
+ LastNormalizedPosition = LastObject.X * scalingFactor;
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(40, DeltaTime);
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 5cd2f1f581..e679231638 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -3,7 +3,6 @@
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;
@@ -35,8 +34,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,
@@ -69,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
}
// Bonus for edge dashes.
- if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH)
+ if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
{
if (!catchCurrent.LastObject.HyperDash)
edgeDashBonus += 5.7;
@@ -79,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition;
}
- distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
+ 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;
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index f3b566f340..04932ecdbb 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -5,6 +5,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;
@@ -17,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Objects
private float x;
+ ///
+ /// The horizontal position of the fruit between 0 and .
+ ///
public float X
{
get => x + XOffset;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index b12cdd4ccb..c6345a9df7 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Catch.UI;
using osuTK;
using osuTK.Graphics;
@@ -70,12 +71,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
- protected override float SamplePlaybackPosition => HitObject.X;
+ protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
protected DrawableCatchHitObject(CatchHitObject hitObject)
: base(hitObject)
{
- RelativePositionAxes = Axes.X;
X = hitObject.X;
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 2c96ee2b19..6b8b70ed54 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -7,7 +7,6 @@ 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;
@@ -80,7 +79,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
StartTime = t + lastEvent.Value.Time,
X = X + Path.PositionAt(
- lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
+ lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
});
}
}
@@ -97,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = dropletSamples,
StartTime = e.Time,
- X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
+ X = X + Path.PositionAt(e.PathProgress).X,
});
break;
@@ -108,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = Samples,
StartTime = e.Time,
- X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
+ X = X + Path.PositionAt(e.PathProgress).X,
});
break;
}
}
}
- public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
+ public float EndX => X + this.CurvePositionAt(1).X;
public double Duration
{
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 7a33cb0577..5d11c574b1 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Replays
// 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)
@@ -51,7 +51,7 @@ 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)
{
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
index 9dab3ed630..7efd832f62 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;
@@ -41,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays
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)
@@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Replays
if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1;
- return new LegacyReplayFrame(Time, Position * CatchPlayfield.BASE_WIDTH, null, state);
+ return new LegacyReplayFrame(Time, Position, null, state);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
index 954f2dfc5f..d929da1a29 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -2,26 +2,21 @@
// 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
+ public class CatchLegacySkinTransformer : LegacySkinTransformer
{
- private readonly ISkin source;
-
- public CatchLegacySkinTransformer(ISkin source)
+ public CatchLegacySkinTransformer(ISkinSource source)
+ : base(source)
{
- this.source = source;
}
- public Drawable GetDrawableComponent(ISkinComponent component)
+ public override Drawable GetDrawableComponent(ISkinComponent component)
{
if (!(component is CatchSkinComponent catchSkinComponent))
return null;
@@ -61,19 +56,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
return null;
}
- public Texture GetTexture(string componentName) => source.GetTexture(componentName);
-
- public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
-
- public IBindable GetConfig(TLookup lookup)
+ public override IBindable GetConfig(TLookup lookup)
{
switch (lookup)
{
case CatchSkinColour colour:
- return source.GetConfig(new SkinCustomColourLookup(colour));
+ return Source.GetConfig(new SkinCustomColourLookup(colour));
}
- return source.GetConfig(lookup);
+ return Source.GetConfig(lookup);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 2319c5ac1f..d034f3c7d4 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatchPlayfield : ScrollingPlayfield
{
- public const float BASE_WIDTH = 512;
+ ///
+ /// The width of the playfield.
+ /// The horizontal movement of the catcher is confined in the area of this width.
+ ///
+ public const float WIDTH = 512;
+
+ ///
+ /// The center position of the playfield.
+ ///
+ public const float CENTER_X = WIDTH / 2;
internal readonly CatcherArea CatcherArea;
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
index b8d3dc9017..8ee23461ba 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
- Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.BASE_WIDTH);
+ Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
Size = Vector2.Divide(Vector2.One, Scale);
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 9cce46d730..82cbbefcca 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
///
- public const double BASE_SPEED = 1.0 / 512;
+ public const double BASE_SPEED = 1.0;
public Container ExplodingFruitTarget;
@@ -104,9 +104,6 @@ namespace osu.Game.Rulesets.Catch.UI
{
this.trailsTarget = trailsTarget;
- RelativePositionAxes = Axes.X;
- X = 0.5f;
-
Origin = Anchor.TopCentre;
Size = new Vector2(CatcherArea.CATCHER_SIZE);
@@ -209,8 +206,8 @@ namespace osu.Game.Rulesets.Catch.UI
var halfCatchWidth = catchWidth * 0.5f;
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
- var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
- var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH;
+ var catchObjectPosition = fruit.X;
+ var catcherPosition = Position.X;
var validCatch =
catchObjectPosition >= catcherPosition - halfCatchWidth &&
@@ -224,7 +221,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
var target = fruit.HyperDashTarget;
var timeDifference = target.StartTime - fruit.StartTime;
- double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition;
+ double positionDifference = target.X - catcherPosition;
var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
SetHyperDashState(Math.Abs(velocity), target.X);
@@ -331,7 +328,7 @@ namespace osu.Game.Rulesets.Catch.UI
public void UpdatePosition(float position)
{
- position = Math.Clamp(position, 0, 1);
+ position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
if (position == X)
return;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 37d177b936..bf1ac5bc0e 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -31,14 +31,8 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherArea(BeatmapDifficulty difficulty = null)
{
- RelativeSizeAxes = Axes.X;
- Height = CATCHER_SIZE;
- Child = MovableCatcher = new Catcher(this, difficulty);
- }
-
- public static float GetCatcherSize(BeatmapDifficulty difficulty)
- {
- return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+ Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
+ Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
}
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs
new file mode 100644
index 0000000000..c8feb4ae24
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs
@@ -0,0 +1,85 @@
+// 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.Utils;
+using osu.Game.Audio;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaBeatmapSampleConversionTest : BeatmapConversionTest, SampleConvertValue>
+ {
+ protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
+
+ [TestCase("convert-samples")]
+ [TestCase("mania-samples")]
+ [TestCase("slider-convert-samples")]
+ public void Test(string name) => base.Test(name);
+
+ protected override IEnumerable CreateConvertValue(HitObject hitObject)
+ {
+ yield return new SampleConvertValue
+ {
+ StartTime = hitObject.StartTime,
+ EndTime = hitObject.GetEndTime(),
+ Column = ((ManiaHitObject)hitObject).Column,
+ Samples = getSampleNames(hitObject.Samples),
+ NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples)
+ };
+ }
+
+ private IList getSampleNames(IList hitSampleInfo)
+ => hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList();
+
+ private IList> getNodeSampleNames(List> hitSampleInfo)
+ => hitSampleInfo?.Select(getSampleNames)
+ .ToList();
+
+ protected override Ruleset CreateRuleset() => new ManiaRuleset();
+ }
+
+ public struct SampleConvertValue : IEquatable
+ {
+ ///
+ /// A sane value to account for osu!stable using ints everywhere.
+ ///
+ private const float conversion_lenience = 2;
+
+ public double StartTime;
+ public double EndTime;
+ public int Column;
+ public IList Samples;
+ public IList> NodeSamples;
+
+ public bool Equals(SampleConvertValue other)
+ => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
+ && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
+ && samplesEqual(Samples, other.Samples)
+ && nodeSamplesEqual(NodeSamples, other.NodeSamples);
+
+ private static bool samplesEqual(ICollection firstSampleList, ICollection secondSampleList)
+ => firstSampleList.SequenceEqual(secondSampleList);
+
+ private static bool nodeSamplesEqual(ICollection> firstSampleList, ICollection> secondSampleList)
+ {
+ if (firstSampleList == null && secondSampleList == null)
+ return true;
+
+ // both items can't be null now, so if any single one is, then they're not equal
+ if (firstSampleList == null || secondSampleList == null)
+ return false;
+
+ return firstSampleList.Count == secondSampleList.Count
+ // cannot use .Zip() without the selector function as it doesn't compile in android test project
+ && firstSampleList.Zip(secondSampleList, (first, second) => (first, second))
+ .All(samples => samples.first.SequenceEqual(samples.second));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs
index 607d42a1bb..2e3b21aed7 100644
--- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs
@@ -10,8 +10,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public class TestSceneManiaModPerfect : ModPerfectTestScene
{
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
public TestSceneManiaModPerfect()
- : base(new ManiaRuleset(), new ManiaModPerfect())
+ : base(new ManiaModPerfect())
{
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu
new file mode 100644
index 0000000000..4f8e1b68dd
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu
@@ -0,0 +1,10 @@
+osu file format v14
+
+[General]
+Mode: 3
+
+[TimingPoints]
+0,300,4,0,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,2,0:0:0:0:
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu
new file mode 100644
index 0000000000..f22901e304
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu
@@ -0,0 +1,10 @@
+osu file format v14
+
+[General]
+Mode: 3
+
+[TimingPoints]
+0,300,4,0,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,1,0:0:0:0:
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit0@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit0@2x.png
new file mode 100644
index 0000000000..2e7b9bc34f
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit0@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit100@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit100@2x.png
new file mode 100644
index 0000000000..27ca7f8b42
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit100@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit200@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit200@2x.png
new file mode 100644
index 0000000000..24ad926375
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit200@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300@2x.png
new file mode 100644
index 0000000000..098561f980
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-0@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-0@2x.png
new file mode 100644
index 0000000000..7e6501d1be
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-0@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-1@2x.png
new file mode 100644
index 0000000000..f17b2b1e73
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-1@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit50@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit50@2x.png
new file mode 100644
index 0000000000..1afec2f4a9
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit50@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-left.png
similarity index 100%
rename from osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png
rename to osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-left.png
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-right.png
similarity index 100%
rename from osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png
rename to osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-right.png
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
index 56564776b3..36765d61bf 100644
--- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
@@ -1,6 +1,14 @@
[General]
-Version: 2.4
+Version: 2.5
[Mania]
Keys: 4
-ColumnLineWidth: 3,1,3,1,1
\ No newline at end of file
+ColumnLineWidth: 3,1,3,1,1
+Hit0: mania/hit0
+Hit50: mania/hit50
+Hit100: mania/hit100
+Hit200: mania/hit200
+Hit300: mania/hit300
+Hit300g: mania/hit300g
+StageLeft: mania/stage-left
+StageRight: mania/stage-right
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
index 497b80950a..a4d4ec50f8 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@@ -16,14 +17,19 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public TestSceneDrawableJudgement()
{
+ var hitWindows = new ManiaHitWindows();
+
foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1))
{
- AddStep("Show " + result.GetDescription(), () => SetContents(() =>
- new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }));
+ if (hitWindows.IsHitResultAllowed(result))
+ {
+ AddStep("Show " + result.GetDescription(), () => SetContents(() =>
+ new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs
index 7ed886be49..3b9c03b86a 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs
@@ -15,8 +15,9 @@ namespace osu.Game.Rulesets.Mania.Tests
{
private readonly Bindable direction = new Bindable();
+ protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
+
public TestSceneEditor()
- : base(new ManiaRuleset())
{
AddStep("upwards scroll", () => direction.Value = ManiaScrollingDirection.Up);
AddStep("downwards scroll", () => direction.Value = ManiaScrollingDirection.Down);
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs
new file mode 100644
index 0000000000..0d726e1a50
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Reflection;
+using NUnit.Framework;
+using osu.Framework.IO.Stores;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneManiaHitObjectSamples : HitObjectSampleTest
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+ protected override IResourceStore Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples)));
+
+ ///
+ /// Tests that when a normal sample bank is used, the normal hitsound will be looked up.
+ ///
+ [Test]
+ public void TestManiaHitObjectNormalSampleBank()
+ {
+ const string expected_sample = "normal-hitnormal2";
+
+ SetupSkins(expected_sample, expected_sample);
+
+ CreateTestWithBeatmap("mania-hitobject-beatmap-normal-sample-bank.osu");
+
+ AssertBeatmapLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that when a custom sample bank is used, layered hitsounds are not played
+ /// (only the sample from the custom bank is looked up).
+ ///
+ [Test]
+ public void TestManiaHitObjectCustomSampleBank()
+ {
+ const string expected_sample = "normal-hitwhistle2";
+ const string unwanted_sample = "normal-hitnormal2";
+
+ SetupSkins(expected_sample, unwanted_sample);
+
+ CreateTestWithBeatmap("mania-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertBeatmapLookup(expected_sample);
+ AssertNoLookup(unwanted_sample);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs
similarity index 62%
rename from osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs
rename to osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs
index cd25d162d0..a399b90585 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs
@@ -5,11 +5,8 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
- public class TestScenePlayer : PlayerTestScene
+ public class TestSceneManiaPlayer : PlayerTestScene
{
- public TestScenePlayer()
- : base(new ManiaRuleset())
- {
- }
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 32abf5e7f9..b025ac7992 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -6,6 +6,7 @@ using System;
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Utils;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -239,7 +240,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
Duration = endTimeData.Duration,
Column = column,
Samples = HitObject.Samples,
- NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
+ NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? defaultNodeSamples
});
}
else if (HitObject is IHasXPosition)
@@ -254,6 +255,16 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return pattern;
}
+
+ ///
+ /// osu!mania-specific beatmaps in stable only play samples at the start of the hold note.
+ ///
+ private List> defaultNodeSamples
+ => new List>
+ {
+ HitObject.Samples,
+ new List()
+ };
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 1bd796511b..d03eb0b3c9 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -472,15 +472,22 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The time to retrieve the sample info list from.
///
- private IList sampleInfoListAt(double time)
+ private IList sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
+
+ ///
+ /// Retrieves the list of node samples that occur at time greater than or equal to .
+ ///
+ /// The time to retrieve node samples at.
+ private List> nodeSamplesAt(double time)
{
if (!(HitObject is IHasPathWithRepeats curveData))
- return HitObject.Samples;
+ return null;
- double segmentTime = (EndTime - HitObject.StartTime) / spanCount;
+ // mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
+ var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
- int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
- return curveData.NodeSamples[index];
+ // avoid slicing the list & creating copies, if at all possible.
+ return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
}
///
@@ -511,7 +518,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Duration = endTime - startTime,
Column = column,
Samples = HitObject.Samples,
- NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
+ NodeSamples = nodeSamplesAt(startTime)
};
}
diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
index 00b839f8ec..294aab1e4e 100644
--- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
@@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
- public override bool AffectsCombo => false;
-
protected override int NumericResultFor(HitResult result) => 20;
protected override double HealthIncreaseFor(HitResult result)
diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
index c2f8fb8678..53db676a54 100644
--- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
@@ -25,8 +25,10 @@ namespace osu.Game.Rulesets.Mania.Judgements
return 200;
case HitResult.Great:
- case HitResult.Perfect:
return 300;
+
+ case HitResult.Perfect:
+ return 320;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index a37aaa8cc4..68dce8b139 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -12,6 +12,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -30,9 +31,11 @@ using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Scoring;
+using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania
{
+ [ExcludeFromDynamicCompile]
public class ManiaRuleset : Ruleset, ILegacyRuleset
{
///
@@ -44,6 +47,8 @@ namespace osu.Game.Rulesets.Mania
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
+ public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.2);
+
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score);
@@ -307,6 +312,21 @@ namespace osu.Game.Rulesets.Mania
{
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v);
}
+
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ {
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ }
+ };
}
public enum PlayfieldType
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
new file mode 100644
index 0000000000..fec1360b26
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
@@ -0,0 +1,33 @@
+{
+ "Mappings": [{
+ "StartTime": 1000.0,
+ "Objects": [{
+ "StartTime": 1000.0,
+ "EndTime": 2750.0,
+ "Column": 1,
+ "NodeSamples": [
+ ["normal-hitnormal"],
+ ["soft-hitnormal"],
+ ["drum-hitnormal"]
+ ],
+ "Samples": ["drum-hitnormal"]
+ }, {
+ "StartTime": 1875.0,
+ "EndTime": 2750.0,
+ "Column": 0,
+ "NodeSamples": [
+ ["soft-hitnormal"],
+ ["drum-hitnormal"]
+ ],
+ "Samples": ["drum-hitnormal"]
+ }]
+ }, {
+ "StartTime": 3750.0,
+ "Objects": [{
+ "StartTime": 3750.0,
+ "EndTime": 3750.0,
+ "Column": 3,
+ "Samples": ["normal-hitnormal"]
+ }]
+ }]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu
new file mode 100644
index 0000000000..fea1de6614
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu
@@ -0,0 +1,16 @@
+osu file format v14
+
+[Difficulty]
+HPDrainRate:5
+CircleSize:5
+OverallDifficulty:5
+ApproachRate:5
+SliderMultiplier:1.4
+SliderTickRate:1
+
+[TimingPoints]
+0,500,4,1,0,100,1,0
+
+[HitObjects]
+88,99,1000,6,0,L|306:259,2,245,0|0|0,1:0|2:0|3:0,0:0:0:0:
+259,118,3750,1,0,1:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json
new file mode 100644
index 0000000000..1aca75a796
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json
@@ -0,0 +1,27 @@
+{
+ "Mappings": [{
+ "StartTime": 500.0,
+ "Objects": [{
+ "StartTime": 500.0,
+ "EndTime": 1500.0,
+ "Column": 0,
+ "NodeSamples": [
+ ["normal-hitnormal"],
+ []
+ ],
+ "Samples": ["normal-hitnormal"]
+ }]
+ }, {
+ "StartTime": 2000.0,
+ "Objects": [{
+ "StartTime": 2000.0,
+ "EndTime": 3000.0,
+ "Column": 2,
+ "NodeSamples": [
+ ["drum-hitnormal"],
+ []
+ ],
+ "Samples": ["drum-hitnormal"]
+ }]
+ }]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
new file mode 100644
index 0000000000..7c75b45e5f
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
@@ -0,0 +1,19 @@
+osu file format v14
+
+[General]
+Mode: 3
+
+[Difficulty]
+HPDrainRate:5
+CircleSize:5
+OverallDifficulty:5
+ApproachRate:5
+SliderMultiplier:1.4
+SliderTickRate:1
+
+[TimingPoints]
+0,500,4,1,0,100,1,0
+
+[HitObjects]
+51,192,500,128,0,1500:1:0:0:0:
+256,192,2000,128,0,3000:3:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json
new file mode 100644
index 0000000000..e3768a90d7
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json
@@ -0,0 +1,21 @@
+{
+ "Mappings": [{
+ "StartTime": 8470.0,
+ "Objects": [{
+ "StartTime": 8470.0,
+ "EndTime": 8470.0,
+ "Column": 0,
+ "Samples": ["normal-hitnormal", "normal-hitclap"]
+ }, {
+ "StartTime": 8626.470587768974,
+ "EndTime": 8626.470587768974,
+ "Column": 1,
+ "Samples": ["normal-hitnormal"]
+ }, {
+ "StartTime": 8782.941175537948,
+ "EndTime": 8782.941175537948,
+ "Column": 2,
+ "Samples": ["normal-hitnormal", "normal-hitclap"]
+ }]
+ }]
+}
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu
new file mode 100644
index 0000000000..08e90ce807
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu
@@ -0,0 +1,15 @@
+osu file format v14
+
+[Difficulty]
+HPDrainRate:6
+CircleSize:4
+OverallDifficulty:8
+ApproachRate:9.5
+SliderMultiplier:2.00000000596047
+SliderTickRate:1
+
+[TimingPoints]
+0,312.941176470588,4,1,0,100,1,0
+
+[HitObjects]
+82,216,8470,6,0,P|52:161|99:113,2,100,8|0|8,1:0|1:0|1:0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
index 0c9bc97ba9..a749f80855 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject)
{
- string imageName = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
+ string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L";
sprite = skin.GetAnimation(imageName, true, true).With(d =>
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
index 1a097405ac..64a7641421 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
@@ -32,28 +32,28 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
- string lightImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightImage, 0)?.Value
+ string lightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightImage)?.Value
?? "mania-stage-light";
- float leftLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth)
+ float leftLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth)
?.Value ?? 1;
- float rightLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth)
+ float rightLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth)
?.Value ?? 1;
bool hasLeftLine = leftLineWidth > 0;
bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
|| isLastColumn;
- float lightPosition = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value
+ float lightPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value
?? 0;
- Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value
+ Color4 lineColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value
?? Color4.White;
- Color4 backgroundColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value
+ Color4 backgroundColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value
?? Color4.Black;
- Color4 lightColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
+ Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
?? Color4.White;
InternalChildren = new Drawable[]
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
index ce0b9fe4b6..bc93bb2615 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
@@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
- string imageName = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionImage)?.Value
+ string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionImage)?.Value
?? "lightingN";
- float explosionScale = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionScale)?.Value
+ float explosionScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionScale)?.Value
?? 1;
// Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
index 40752d3f4b..d055ef3480 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
@@ -14,7 +14,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning
{
- public class LegacyHitTarget : LegacyManiaElement
+ public class LegacyHitTarget : CompositeDrawable
{
private readonly IBindable direction = new Bindable();
@@ -28,13 +28,13 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
- string targetImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HitTargetImage)?.Value
+ string targetImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitTargetImage)?.Value
?? "mania-stage-hint";
- bool showJudgementLine = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ShowJudgementLine)?.Value
+ bool showJudgementLine = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ShowJudgementLine)?.Value
?? true;
- Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.JudgementLineColour)?.Value
+ Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.JudgementLineColour)?.Value
?? Color4.White;
InternalChild = directionContainer = new Container
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
index 7c8d1cd303..44f3e7d7b3 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
@@ -33,10 +33,10 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
- string upImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImage)?.Value
+ string upImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImage)?.Value
?? $"mania-key{FallbackColumnIndex}";
- string downImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImageDown)?.Value
+ string downImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImageDown)?.Value
?? $"mania-key{FallbackColumnIndex}D";
InternalChild = directionContainer = new Container
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs
index 05b731ec5d..3c0c632c14 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
///
/// A which is placed somewhere within a .
///
- public class LegacyManiaColumnElement : LegacyManiaElement
+ public class LegacyManiaColumnElement : CompositeDrawable
{
[Resolved]
protected Column Column { get; private set; }
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
}
}
- protected override IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null)
- => base.GetManiaSkinConfig(skin, lookup, index ?? Column.Index);
+ protected IBindable GetColumnSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup)
+ => skin.GetManiaSkinConfig(lookup, Column.Index);
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
index 85523ae3c0..515c941d65 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
break;
}
- string noteImage = GetManiaSkinConfig(skin, lookup)?.Value
+ string noteImage = GetColumnSkinConfig(skin, lookup)?.Value
?? $"mania-note{FallbackColumnIndex}{suffix}";
return skin.GetTexture(noteImage);
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
index f177284399..7f5de601ca 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
@@ -3,13 +3,14 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning
{
- public class LegacyStageBackground : LegacyManiaElement
+ public class LegacyStageBackground : CompositeDrawable
{
private Drawable leftSprite;
private Drawable rightSprite;
@@ -22,10 +23,10 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
- string leftImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value
+ string leftImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value
?? "mania-stage-left";
- string rightImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightStageImage)?.Value
+ string rightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightStageImage)?.Value
?? "mania-stage-right";
InternalChildren = new[]
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs
index 9719005d54..4609fcc849 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs
@@ -4,13 +4,14 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning
{
- public class LegacyStageForeground : LegacyManiaElement
+ public class LegacyStageForeground : CompositeDrawable
{
private readonly IBindable direction = new Bindable();
@@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
- string bottomImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
+ string bottomImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
?? "mania-stage-bottom";
sprite = skin.GetAnimation(bottomImage, true, true)?.With(d =>
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
index e64178083a..e167135556 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
@@ -3,22 +3,52 @@
using System;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Textures;
-using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
+using System.Collections.Generic;
+using osu.Framework.Audio.Sample;
+using osu.Game.Audio;
+using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Rulesets.Mania.Skinning
{
- public class ManiaLegacySkinTransformer : ISkin
+ public class ManiaLegacySkinTransformer : LegacySkinTransformer
{
- private readonly ISkin source;
private readonly ManiaBeatmap beatmap;
+ ///
+ /// Mapping of to their corresponding
+ /// value.
+ ///
+ private static readonly IReadOnlyDictionary hitresult_mapping
+ = new Dictionary
+ {
+ { HitResult.Perfect, LegacyManiaSkinConfigurationLookups.Hit300g },
+ { HitResult.Great, LegacyManiaSkinConfigurationLookups.Hit300 },
+ { HitResult.Good, LegacyManiaSkinConfigurationLookups.Hit200 },
+ { HitResult.Ok, LegacyManiaSkinConfigurationLookups.Hit100 },
+ { HitResult.Meh, LegacyManiaSkinConfigurationLookups.Hit50 },
+ { HitResult.Miss, LegacyManiaSkinConfigurationLookups.Hit0 }
+ };
+
+ ///
+ /// Mapping of to their corresponding
+ /// default filenames.
+ ///
+ private static readonly IReadOnlyDictionary default_hitresult_skin_filenames
+ = new Dictionary
+ {
+ { HitResult.Perfect, "mania-hit300g" },
+ { HitResult.Great, "mania-hit300" },
+ { HitResult.Good, "mania-hit200" },
+ { HitResult.Ok, "mania-hit100" },
+ { HitResult.Meh, "mania-hit50" },
+ { HitResult.Miss, "mania-hit0" }
+ };
+
private Lazy isLegacySkin;
///
@@ -28,29 +58,28 @@ namespace osu.Game.Rulesets.Mania.Skinning
private Lazy hasKeyTexture;
public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap)
+ : base(source)
{
- this.source = source;
this.beatmap = (ManiaBeatmap)beatmap;
- source.SourceChanged += sourceChanged;
+ Source.SourceChanged += sourceChanged;
sourceChanged();
}
private void sourceChanged()
{
- isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null);
- hasKeyTexture = new Lazy(() => source.GetAnimation(
- GetConfig(
- new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value
+ isLegacySkin = new Lazy(() => Source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null);
+ hasKeyTexture = new Lazy(() => Source.GetAnimation(
+ this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
?? "mania-key1", true, true) != null);
}
- public Drawable GetDrawableComponent(ISkinComponent component)
+ public override Drawable GetDrawableComponent(ISkinComponent component)
{
switch (component)
{
case GameplaySkinComponent resultComponent:
- return getResult(resultComponent);
+ return getResult(resultComponent.Component);
case ManiaSkinComponent maniaComponent:
if (!isLegacySkin.Value || !hasKeyTexture.Value)
@@ -95,42 +124,29 @@ namespace osu.Game.Rulesets.Mania.Skinning
return null;
}
- private Drawable getResult(GameplaySkinComponent resultComponent)
+ private Drawable getResult(HitResult result)
{
- switch (resultComponent.Component)
- {
- case HitResult.Miss:
- return this.GetAnimation("mania-hit0", true, true);
+ string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value
+ ?? default_hitresult_skin_filenames[result];
- case HitResult.Meh:
- return this.GetAnimation("mania-hit50", true, true);
-
- case HitResult.Ok:
- return this.GetAnimation("mania-hit100", true, true);
-
- case HitResult.Good:
- return this.GetAnimation("mania-hit200", true, true);
-
- case HitResult.Great:
- return this.GetAnimation("mania-hit300", true, true);
-
- case HitResult.Perfect:
- return this.GetAnimation("mania-hit300g", true, true);
- }
-
- return null;
+ return this.GetAnimation(filename, true, true);
}
- public Texture GetTexture(string componentName) => source.GetTexture(componentName);
+ public override SampleChannel GetSample(ISampleInfo sampleInfo)
+ {
+ // layered hit sounds never play in mania
+ if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
+ return new SampleChannelVirtual();
- public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
+ return Source.GetSample(sampleInfo);
+ }
- public IBindable GetConfig(TLookup lookup)
+ public override IBindable GetConfig(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
- return source.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
+ return Source.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
- return source.GetConfig(lookup);
+ return Source.GetConfig(lookup);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
similarity index 71%
rename from osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs
rename to osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
index 11fdd663a1..2e17a6bef1 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
@@ -2,15 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
-using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning
{
- ///
- /// A mania legacy skin element.
- ///
- public class LegacyManiaElement : CompositeDrawable
+ public static class ManiaSkinConfigExtensions
{
///
/// Retrieve a per-column-count skin configuration.
@@ -18,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
/// The skin from which configuration is retrieved.
/// The value to retrieve.
/// If not null, denotes the index of the column to which the entry applies.
- protected virtual IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null)
+ public static IBindable GetManiaSkinConfig(this ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null)
=> skin.GetConfig(
new ManiaSkinConfigurationLookup(lookup, index));
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs
new file mode 100644
index 0000000000..7697f46160
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.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.Osu.Tests.Mods
+{
+ public class OsuModTestScene : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
index 69415b70e3..49c1fe8540 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
@@ -9,17 +9,11 @@ using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
- public class TestSceneOsuModDifficultyAdjust : ModTestScene
+ public class TestSceneOsuModDifficultyAdjust : OsuModTestScene
{
- public TestSceneOsuModDifficultyAdjust()
- : base(new OsuRuleset())
- {
- }
-
[Test]
public void TestNoAdjustment() => CreateModTest(new ModTestData
{
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs
index dcf19ad993..335ef31019 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs
@@ -4,17 +4,11 @@
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
- public class TestSceneOsuModDoubleTime : ModTestScene
+ public class TestSceneOsuModDoubleTime : OsuModTestScene
{
- public TestSceneOsuModDoubleTime()
- : base(new OsuRuleset())
- {
- }
-
[TestCase(0.5)]
[TestCase(1.01)]
[TestCase(1.5)]
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
index 8bd3d3c7cc..40f1c4a52f 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
@@ -8,18 +8,12 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
- public class TestSceneOsuModHidden : ModTestScene
+ public class TestSceneOsuModHidden : OsuModTestScene
{
- public TestSceneOsuModHidden()
- : base(new OsuRuleset())
- {
- }
-
[Test]
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
{
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs
index b03a894085..985baa8cf5 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs
@@ -13,8 +13,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModPerfect : ModPerfectTestScene
{
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+
public TestSceneOsuModPerfect()
- : base(new OsuRuleset(), new OsuModPerfect())
+ : base(new OsuModPerfect())
{
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs
new file mode 100644
index 0000000000..10d9d7ffde
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs
@@ -0,0 +1,132 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Framework.Threading;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Osu.Statistics;
+using osu.Game.Scoring;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene
+ {
+ private Box background;
+ private Drawable object1;
+ private Drawable object2;
+ private TestAccuracyHeatmap accuracyHeatmap;
+ private ScheduledDelegate automaticAdditionDelegate;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ automaticAdditionDelegate?.Cancel();
+ automaticAdditionDelegate = null;
+
+ Children = new[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex("#333"),
+ },
+ object1 = new BorderCircle
+ {
+ Position = new Vector2(256, 192),
+ Colour = Color4.Yellow,
+ },
+ object2 = new BorderCircle
+ {
+ Position = new Vector2(100, 300),
+ },
+ accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo })
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(130)
+ }
+ };
+ });
+
+ [Test]
+ public void TestManyHitPointsAutomatic()
+ {
+ AddStep("add scheduled delegate", () =>
+ {
+ automaticAdditionDelegate = Scheduler.AddDelayed(() =>
+ {
+ var randomPos = new Vector2(
+ RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2),
+ RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2));
+
+ // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene).
+ accuracyHeatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500));
+ InputManager.MoveMouseTo(background.ToScreenSpace(randomPos));
+ }, 1, true);
+ });
+
+ AddWaitStep("wait for some hit points", 10);
+ }
+
+ [Test]
+ public void TestManualPlacement()
+ {
+ AddStep("return user input", () => InputManager.UseParentInput = true);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50);
+ return true;
+ }
+
+ private class TestAccuracyHeatmap : AccuracyHeatmap
+ {
+ public TestAccuracyHeatmap(ScoreInfo score)
+ : base(score, new TestBeatmap(new OsuRuleset().RulesetInfo))
+ {
+ }
+
+ public new void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
+ => base.AddPoint(start, end, hitPoint, radius);
+ }
+
+ private class BorderCircle : CircularContainer
+ {
+ public BorderCircle()
+ {
+ Origin = Anchor.Centre;
+ Size = new Vector2(100);
+
+ Masking = true;
+ BorderThickness = 2;
+ BorderColour = Color4.White;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ },
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(4),
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs
index 4aca34bf64..9239034a53 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs
@@ -9,9 +9,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture]
public class TestSceneEditor : EditorTestScene
{
- public TestSceneEditor()
- : base(new OsuRuleset())
- {
- }
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
index b99cd523ff..8cf29ddfbf 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
@@ -4,19 +4,13 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneHitCircleLongCombo : PlayerTestScene
+ public class TestSceneHitCircleLongCombo : TestSceneOsuPlayer
{
- public TestSceneHitCircleLongCombo()
- : base(new OsuRuleset())
- {
- }
-
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
index 5f3596976d..f3221ffe32 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
@@ -19,10 +19,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneMissHitWindowJudgements : ModTestScene
{
- public TestSceneMissHitWindowJudgements()
- : base(new OsuRuleset())
- {
- }
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
[Test]
public void TestMissViaEarlyHit()
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs
index 0a33b09ba8..f5b28b36c0 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs
@@ -9,9 +9,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture]
public class TestSceneOsuPlayer : PlayerTestScene
{
- public TestSceneOsuPlayer()
- : base(new OsuRuleset())
- {
- }
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index d39e24fc1f..b357e20ee8 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -25,13 +25,12 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneSkinFallbacks : PlayerTestScene
+ public class TestSceneSkinFallbacks : TestSceneOsuPlayer
{
private readonly TestSource testUserSkin;
private readonly TestSource testBeatmapSkin;
public TestSceneSkinFallbacks()
- : base(new OsuRuleset())
{
testUserSkin = new TestSource("user");
testBeatmapSkin = new TestSource("beatmap");
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
index f5b20fd1c5..a69646507a 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
@@ -61,7 +61,9 @@ namespace osu.Game.Rulesets.Osu.Tests
private DrawableSlider slider;
[SetUpSteps]
- public override void SetUpSteps() { }
+ public override void SetUpSteps()
+ {
+ }
[TestCase(0)]
[TestCase(1)]
@@ -132,10 +134,9 @@ namespace osu.Game.Rulesets.Osu.Tests
checkPositionChange(16600, sliderRepeat, positionDecreased);
}
- private void retrieveDrawableSlider(int index) => AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () =>
- {
- slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index);
- });
+ private void retrieveDrawableSlider(int index) =>
+ AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () =>
+ slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index));
private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased);
private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index 65bed071cd..8cb7f3f4b6 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests
if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1)
{
// force completion only once to not break human interaction
- Disc.RotationAbsolute = Spinner.SpinsRequired * 360;
+ Disc.CumulativeRotation = Spinner.SpinsRequired * 360;
auto = false;
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index ea006ec607..6b1394d799 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -14,6 +14,12 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Scoring;
using osu.Game.Storyboards;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
@@ -36,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}
private DrawableSpinner drawableSpinner;
+ private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single();
[SetUpSteps]
public override void SetUpSteps()
@@ -50,25 +57,78 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestSpinnerRewindingRotation()
{
addSeekStep(5000);
- AddAssert("is rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100));
+ AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100));
+ AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100));
addSeekStep(0);
- AddAssert("is rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100));
+ AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100));
+ AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100));
}
[Test]
public void TestSpinnerMiddleRewindingRotation()
{
- double estimatedRotation = 0;
+ double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0;
addSeekStep(5000);
- AddStep("retrieve rotation", () => estimatedRotation = drawableSpinner.Disc.RotationAbsolute);
+ AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.Disc.Rotation);
+ AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.Disc.CumulativeRotation);
+ AddStep("retrieve spinner symbol rotation", () => finalSpinnerSymbolRotation = spinnerSymbol.Rotation);
addSeekStep(2500);
+ AddUntilStep("disc rotation rewound",
+ // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
+ () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation / 2, 100));
+ AddUntilStep("symbol rotation rewound",
+ () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, 100));
+
addSeekStep(5000);
- AddAssert("is rotation absolute almost same", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, estimatedRotation, 100));
+ AddAssert("is disc rotation almost same",
+ () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation, 100));
+ AddAssert("is symbol rotation almost same",
+ () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, 100));
+ AddAssert("is disc rotation absolute almost same",
+ () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, finalAbsoluteDiscRotation, 100));
}
+ [Test]
+ public void TestRotationDirection([Values(true, false)] bool clockwise)
+ {
+ if (clockwise)
+ {
+ AddStep("flip replay", () =>
+ {
+ var drawableRuleset = this.ChildrenOfType().Single();
+ var score = drawableRuleset.ReplayScore;
+ var scoreWithFlippedReplay = new Score
+ {
+ ScoreInfo = score.ScoreInfo,
+ Replay = flipReplay(score.Replay)
+ };
+ drawableRuleset.SetReplayScore(scoreWithFlippedReplay);
+ });
+ }
+
+ addSeekStep(5000);
+
+ AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.Disc.Rotation > 0 : drawableSpinner.Disc.Rotation < 0);
+ AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
+ }
+
+ private Replay flipReplay(Replay scoreReplay) => new Replay
+ {
+ Frames = scoreReplay
+ .Frames
+ .Cast()
+ .Select(replayFrame =>
+ {
+ var flippedPosition = new Vector2(OsuPlayfield.BASE_SIZE.X - replayFrame.Position.X, replayFrame.Position.Y);
+ return new OsuReplayFrame(replayFrame.Time, flippedPosition, replayFrame.Actions.ToArray());
+ })
+ .Cast()
+ .ToList()
+ };
+
[Test]
public void TestSpinPerMinuteOnRewind()
{
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs
new file mode 100644
index 0000000000..9b33e746b3
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.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 osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Judgements
+{
+ public class OsuHitCircleJudgementResult : OsuJudgementResult
+ {
+ ///
+ /// The .
+ ///
+ public HitCircle HitCircle => (HitCircle)HitObject;
+
+ ///
+ /// The position of the player's cursor when was hit.
+ ///
+ public Vector2? CursorPositionAtHit;
+
+ public OsuHitCircleJudgementResult(HitObject hitObject, Judgement judgement)
+ : base(hitObject, judgement)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 8a0ef22c4a..2c41e6b0e9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -135,8 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
fp.Alpha = 0;
fp.Scale = new Vector2(1.5f * osuEnd.Scale);
- if (firstTransformStartTime == null)
- firstTransformStartTime = fadeInTime;
+ firstTransformStartTime ??= fadeInTime;
fp.AnimationStartTime = fadeInTime;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index d73ad888f4..854fc4c91c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -7,8 +7,11 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input;
using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
using osuTK;
@@ -32,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
+ private InputManager inputManager;
+
public DrawableHitCircle(HitCircle h)
: base(h)
{
@@ -86,6 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true);
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+ }
+
public override double LifetimeStart
{
get => base.LifetimeStart;
@@ -126,7 +138,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
}
- ApplyResult(r => r.Type = result);
+ ApplyResult(r =>
+ {
+ var circleResult = (OsuHitCircleJudgementResult)r;
+
+ // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
+ if (result != HitResult.Miss)
+ {
+ var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
+ circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
+ }
+
+ circleResult.Type = result;
+ });
}
protected override void UpdateInitialTransforms()
@@ -172,6 +196,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Drawable ProxiedLayer => ApproachCircle;
+ protected override JudgementResult CreateResult(Judgement judgement) => new OsuHitCircleJudgementResult(HitObject, judgement);
+
public class HitReceptor : CompositeDrawable, IKeyBindingHandler
{
// IsHovered is used
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 3c8ab0f5ab..be6766509c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
@@ -137,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindTo(HitObject.PositionBindable);
}
- public float Progress => Math.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1);
+ public float Progress => Math.Clamp(Disc.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
@@ -190,12 +191,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
circle.Rotation = Disc.Rotation;
Ticks.Rotation = Disc.Rotation;
- SpmCounter.SetRotation(Disc.RotationAbsolute);
+ SpmCounter.SetRotation(Disc.CumulativeRotation);
float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
- Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint);
+ float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress;
+ Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
- symbol.RotateTo(Disc.Rotation / 2, 500, Easing.OutQuint);
+ symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
}
protected override void UpdateInitialTransforms()
@@ -205,9 +207,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
circleContainer.ScaleTo(Spinner.Scale * 0.3f);
circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint);
- Disc.RotateTo(-720);
- symbol.RotateTo(-720);
-
mainContainer
.ScaleTo(0)
.ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt - 150, Easing.OutQuint)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs
index 1a5195acf8..ae43006e76 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
};
}
- protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!drawableRepeat.IsHit)
Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
index d4ef039b79..35819cd05e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
@@ -73,6 +73,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
}
+ ///
+ /// The total rotation performed on the spinner disc, disregarding the spin direction.
+ ///
+ ///
+ /// This value is always non-negative and is monotonically increasing with time
+ /// (i.e. will only increase if time is passing forward, but can decrease during rewind).
+ ///
+ ///
+ /// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
+ /// this property will return the value of 720 (as opposed to 0 for ).
+ ///
+ public float CumulativeRotation;
+
///
/// Whether currently in the correct time range to allow spinning.
///
@@ -88,10 +101,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private float lastAngle;
private float currentRotation;
- public float RotationAbsolute;
private int completeTick;
- private bool updateCompleteTick() => completeTick != (completeTick = (int)(RotationAbsolute / 360));
+ private bool updateCompleteTick() => completeTick != (completeTick = (int)(CumulativeRotation / 360));
private bool rotationTransferred;
@@ -149,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
currentRotation += angle;
- RotationAbsolute += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
+ CumulativeRotation += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index c11e20c9e7..1e54b576f1 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
@@ -4,6 +4,7 @@
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
@@ -24,6 +25,13 @@ namespace osu.Game.Rulesets.Osu.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
- public override Judgement CreateJudgement() => new SliderRepeat.SliderRepeatJudgement();
+ public override Judgement CreateJudgement() => new SliderTailJudgement();
+
+ public class SliderTailJudgement : OsuJudgement
+ {
+ protected override int NumericResultFor(HitResult result) => 0;
+
+ public override bool AffectsCombo => false;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 689a7b35ea..eaa5d8937a 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -29,9 +29,15 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using System;
+using System.Linq;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Statistics;
+using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu
{
+ [ExcludeFromDynamicCompile]
public class OsuRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
@@ -186,5 +192,31 @@ namespace osu.Game.Rulesets.Osu
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
+
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ {
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList())
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ },
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ }
+ };
}
}
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index 79a6ea7e92..86ec76e373 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -4,13 +4,27 @@
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuScoreProcessor : ScoreProcessor
{
- protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement);
+ protected override HitEvent CreateHitEvent(JudgementResult result)
+ => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
+
+ protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
+ {
+ switch (hitObject)
+ {
+ case HitCircle _:
+ return new OsuHitCircleJudgementResult(hitObject, judgement);
+
+ default:
+ return new OsuJudgementResult(hitObject, judgement);
+ }
+ }
public override HitWindows CreateHitWindows() => new OsuHitWindows();
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index ba0003b5cd..3e5758ca01 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -2,20 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-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.Osu.Skinning
{
- public class OsuLegacySkinTransformer : ISkin
+ public class OsuLegacySkinTransformer : LegacySkinTransformer
{
- private readonly ISkin source;
-
private Lazy hasHitCircle;
///
@@ -26,19 +21,18 @@ namespace osu.Game.Rulesets.Osu.Skinning
public const float LEGACY_CIRCLE_RADIUS = 64 - 5;
public OsuLegacySkinTransformer(ISkinSource source)
+ : base(source)
{
- this.source = source;
-
- source.SourceChanged += sourceChanged;
+ Source.SourceChanged += sourceChanged;
sourceChanged();
}
private void sourceChanged()
{
- hasHitCircle = new Lazy(() => source.GetTexture("hitcircle") != null);
+ hasHitCircle = new Lazy(() => Source.GetTexture("hitcircle") != null);
}
- public Drawable GetDrawableComponent(ISkinComponent component)
+ public override Drawable GetDrawableComponent(ISkinComponent component)
{
if (!(component is OsuSkinComponent osuComponent))
return null;
@@ -85,13 +79,13 @@ namespace osu.Game.Rulesets.Osu.Skinning
return null;
case OsuSkinComponents.Cursor:
- if (source.GetTexture("cursor") != null)
+ if (Source.GetTexture("cursor") != null)
return new LegacyCursor();
return null;
case OsuSkinComponents.CursorTrail:
- if (source.GetTexture("cursortrail") != null)
+ if (Source.GetTexture("cursortrail") != null)
return new LegacyCursorTrail();
return null;
@@ -102,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
return !hasFont(font)
? null
- : new LegacySpriteText(source, font)
+ : new LegacySpriteText(Source, font)
{
// stable applies a blanket 0.8x scale to hitcircle fonts
Scale = new Vector2(0.8f),
@@ -113,16 +107,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
return null;
}
- public Texture GetTexture(string componentName) => source.GetTexture(componentName);
-
- public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
-
- public IBindable GetConfig(TLookup lookup)
+ public override IBindable GetConfig(TLookup lookup)
{
switch (lookup)
{
case OsuSkinColour colour:
- return source.GetConfig(new SkinCustomColourLookup(colour));
+ return Source.GetConfig(new SkinCustomColourLookup(colour));
case OsuSkinConfiguration osuLookup:
switch (osuLookup)
@@ -136,16 +126,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
case OsuSkinConfiguration.HitCircleOverlayAboveNumber:
// See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D
// HitCircleOverlayAboveNumer (with typo) should still be supported for now.
- return source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
- source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
+ return Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
+ Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
}
break;
}
- return source.GetConfig(lookup);
+ return Source.GetConfig(lookup);
}
- private bool hasFont(string fontName) => source.GetTexture($"{fontName}-0") != null;
+ private bool hasFont(string fontName) => Source.GetTexture($"{fontName}-0") != null;
}
}
diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
new file mode 100644
index 0000000000..20adbc1c02
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
@@ -0,0 +1,297 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Statistics
+{
+ public class AccuracyHeatmap : CompositeDrawable
+ {
+ ///
+ /// Size of the inner circle containing the "hit" points, relative to the size of this .
+ /// All other points outside of the inner circle are "miss" points.
+ ///
+ private const float inner_portion = 0.8f;
+
+ ///
+ /// Number of rows/columns of points.
+ /// ~4px per point @ 128x128 size (the contents of the are always square). 1089 total points.
+ ///
+ private const int points_per_dimension = 33;
+
+ private const float rotation = 45;
+
+ private BufferedContainer bufferedGrid;
+ private GridContainer pointGrid;
+
+ private readonly ScoreInfo score;
+ private readonly IBeatmap playableBeatmap;
+
+ private const float line_thickness = 2;
+
+ ///
+ /// The highest count of any point currently being displayed.
+ ///
+ protected float PeakValue { get; private set; }
+
+ public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap)
+ {
+ this.score = score;
+ this.playableBeatmap = playableBeatmap;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Children = new Drawable[]
+ {
+ new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(inner_portion),
+ Masking = true,
+ BorderThickness = line_thickness,
+ BorderColour = Color4.White,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex("#202624")
+ }
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(1),
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ EdgeSmoothness = new Vector2(1),
+ RelativeSizeAxes = Axes.Y,
+ Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
+ Width = line_thickness / 2,
+ Rotation = -rotation,
+ Alpha = 0.3f,
+ },
+ new Box
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ EdgeSmoothness = new Vector2(1),
+ RelativeSizeAxes = Axes.Y,
+ Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
+ Width = line_thickness / 2, // adjust for edgesmoothness
+ Rotation = rotation
+ },
+ }
+ },
+ },
+ new Box
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Width = 10,
+ EdgeSmoothness = new Vector2(1),
+ Height = line_thickness / 2, // adjust for edgesmoothness
+ },
+ new Box
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ EdgeSmoothness = new Vector2(1),
+ Width = line_thickness / 2, // adjust for edgesmoothness
+ Height = 10,
+ }
+ }
+ },
+ bufferedGrid = new BufferedContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CacheDrawnFrameBuffer = true,
+ BackgroundColour = Color4Extensions.FromHex("#202624").Opacity(0),
+ Child = pointGrid = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ }
+ };
+
+ Vector2 centre = new Vector2(points_per_dimension) / 2;
+ float innerRadius = centre.X * inner_portion;
+
+ Drawable[][] points = new Drawable[points_per_dimension][];
+
+ for (int r = 0; r < points_per_dimension; r++)
+ {
+ points[r] = new Drawable[points_per_dimension];
+
+ for (int c = 0; c < points_per_dimension; c++)
+ {
+ HitPointType pointType = Vector2.Distance(new Vector2(c, r), centre) <= innerRadius
+ ? HitPointType.Hit
+ : HitPointType.Miss;
+
+ var point = new HitPoint(pointType, this)
+ {
+ Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255)
+ };
+
+ points[r][c] = point;
+ }
+ }
+
+ pointGrid.Content = points;
+
+ if (score.HitEvents == null || score.HitEvents.Count == 0)
+ return;
+
+ // Todo: This should probably not be done like this.
+ float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2;
+
+ foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
+ {
+ if (e.LastHitObject == null || e.Position == null)
+ continue;
+
+ AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value, radius);
+ }
+ }
+
+ protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
+ {
+ if (pointGrid.Content.Length == 0)
+ return;
+
+ double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
+ double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point.
+ double finalAngle = angle2 - angle1; // Angle between start, end, and hit points.
+ float normalisedDistance = Vector2.Distance(hitPoint, end) / radius;
+
+ // Consider two objects placed horizontally, with the start on the left and the end on the right.
+ // The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form:
+ // +pi | 0
+ // O --------- O -----> Note: Math.Atan2 has a range (-pi <= theta <= +pi)
+ // -pi | 0
+ // E.g. If the hit point was directly above end, it would have an angle pi/2.
+ //
+ // It also calculated the angle separating hitPoint from the line joining {start, end}, that is anti-clockwise in the form:
+ // 0 | pi
+ // O --------- O ----->
+ // 2pi | pi
+ //
+ // However keep in mind that cos(0)=1 and cos(2pi)=1, whereas we actually want these values to appear on the left, so the x-coordinate needs to be inverted.
+ // Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
+ //
+ // We also need to apply the anti-clockwise rotation.
+ var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
+ var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
+
+ Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
+ float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies.
+ Vector2 localPoint = localCentre + localRadius * rotatedCoordinate;
+
+ // Find the most relevant hit point.
+ int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1);
+ int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1);
+
+ PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment());
+
+ bufferedGrid.ForceRedraw();
+ }
+
+ private class HitPoint : Circle
+ {
+ private readonly HitPointType pointType;
+ private readonly AccuracyHeatmap heatmap;
+
+ public override bool IsPresent => count > 0;
+
+ public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap)
+ {
+ this.pointType = pointType;
+ this.heatmap = heatmap;
+
+ RelativeSizeAxes = Axes.Both;
+ Alpha = 1;
+ }
+
+ private int count;
+
+ ///
+ /// Increment the value of this point by one.
+ ///
+ /// The value after incrementing.
+ public int Increment()
+ {
+ return ++count;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // the point at which alpha is saturated and we begin to adjust colour lightness.
+ const float lighten_cutoff = 0.95f;
+
+ // the amount of lightness to attribute regardless of relative value to peak point.
+ const float non_relative_portion = 0.2f;
+
+ float amount = 0;
+
+ // give some amount of alpha regardless of relative count
+ amount += non_relative_portion * Math.Min(1, count / 10f);
+
+ // add relative portion
+ amount += (1 - non_relative_portion) * (count / heatmap.PeakValue);
+
+ // apply easing
+ amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount));
+
+ Debug.Assert(amount <= 1);
+
+ Alpha = Math.Min(amount / lighten_cutoff, 1);
+ if (pointType == HitPointType.Hit)
+ Colour = ((Color4)Colour).Lighten(Math.Max(0, amount - lighten_cutoff));
+ }
+ }
+
+ private enum HitPointType
+ {
+ Hit,
+ Miss
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs
index 26c90ad295..a83cc16413 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs
@@ -12,8 +12,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public class TestSceneTaikoModPerfect : ModPerfectTestScene
{
+ protected override Ruleset CreatePlayerRuleset() => new TestTaikoRuleset();
+
public TestSceneTaikoModPerfect()
- : base(new TestTaikoRuleset(), new TaikoModPerfect())
+ : base(new TaikoModPerfect())
{
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs
index 089a7ad00b..411fe08bcf 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs
@@ -9,9 +9,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestFixture]
public class TestSceneEditor : EditorTestScene
{
- public TestSceneEditor()
- : base(new TaikoRuleset())
- {
- }
+ protected override Ruleset CreateEditorRuleset() => new TaikoRuleset();
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs
index d541aa8de8..4ba9c447fb 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs
@@ -6,7 +6,6 @@ using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
@@ -14,13 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
/// Taiko has some interesting rules for legacy mappings.
///
[HeadlessTest]
- public class TestSceneSampleOutput : PlayerTestScene
+ public class TestSceneSampleOutput : TestSceneTaikoPlayer
{
- public TestSceneSampleOutput()
- : base(new TaikoRuleset())
- {
- }
-
public override void SetUpSteps()
{
base.SetUpSteps();
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
index 923e28a45e..75049b7467 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
@@ -5,17 +5,11 @@ using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
- public class TestSceneSwellJudgements : PlayerTestScene
+ public class TestSceneSwellJudgements : TestSceneTaikoPlayer
{
- public TestSceneSwellJudgements()
- : base(new TaikoRuleset())
- {
- }
-
[Test]
public void TestZeroTickTimeOffsets()
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs
new file mode 100644
index 0000000000..cd7511241a
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.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.Taiko.Tests
+{
+ public class TestSceneTaikoPlayer : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
index 2ab041e191..aaa634648a 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
@@ -11,13 +11,8 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
- public class TestSceneTaikoSuddenDeath : PlayerTestScene
+ public class TestSceneTaikoSuddenDeath : TestSceneTaikoPlayer
{
- public TestSceneTaikoSuddenDeath()
- : base(new TaikoRuleset())
- {
- }
-
protected override bool AllowFail => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs
index b5471e6976..f515a35c18 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.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 osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -8,7 +9,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Backgrounds;
using osuTK.Graphics;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Effects;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
};
}
- protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs
index 7508c75231..4bbb6be6b1 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
};
}
- protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index 6e9a37eb93..23d675cfb0 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -6,23 +6,20 @@ using System.Collections.Generic;
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.Rulesets.Taiko.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Skinning
{
- public class TaikoLegacySkinTransformer : ISkin
+ public class TaikoLegacySkinTransformer : LegacySkinTransformer
{
- private readonly ISkinSource source;
-
public TaikoLegacySkinTransformer(ISkinSource source)
+ : base(source)
{
- this.source = source;
}
- public Drawable GetDrawableComponent(ISkinComponent component)
+ public override Drawable GetDrawableComponent(ISkinComponent component)
{
if (!(component is TaikoSkinComponent taikoComponent))
return null;
@@ -100,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null;
}
- return source.GetDrawableComponent(component);
+ return Source.GetDrawableComponent(component);
}
private string getHitName(TaikoSkinComponents component)
@@ -120,11 +117,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning
throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type");
}
- public Texture GetTexture(string componentName) => source.GetTexture(componentName);
+ public override SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
- public SampleChannel GetSample(ISampleInfo sampleInfo) => source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
-
- public IBindable GetConfig(TLookup lookup) => source.GetConfig(lookup);
+ public override IBindable GetConfig(TLookup lookup) => Source.GetConfig(lookup);
private class LegacyTaikoSampleInfo : ISampleInfo
{
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 4cdd1fbc24..2011842591 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -21,13 +21,18 @@ using osu.Game.Rulesets.Taiko.Difficulty;
using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Scoring;
using System;
+using System.Linq;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Edit;
+using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Skinning;
+using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko
{
+ [ExcludeFromDynamicCompile]
public class TaikoRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
@@ -155,5 +160,20 @@ namespace osu.Game.Rulesets.Taiko
public int LegacyID => 1;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
+
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ {
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList())
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ }
+ };
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
index 407ab30e12..b937beae3c 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
@@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.UI
lastObjectHit = result.IsHit;
}
- protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
kiaiMode = effectPoint.KiaiMode;
}
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
index 7de1593ab6..caddc8b122 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
+ Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
Masking = true,
BorderColour = Color4.White,
BorderThickness = border_thickness,
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE),
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE),
Masking = true,
BorderColour = Color4.White,
BorderThickness = border_thickness,
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
index cce2be7758..6f25a5f662 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Taiko.UI
textureAnimation.Seek(0);
}
- protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
// assume that if the animation is playing on its own, it's independent from the beat and doesn't need to be touched.
if (textureAnimation.FrameCount == 0 || textureAnimation.IsPlaying)
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index 5eb11a3264..0151678db3 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -5,16 +5,15 @@ using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Platform;
using osu.Game.IPC;
using osu.Framework.Allocation;
+using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Resources;
@@ -22,6 +21,7 @@ using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers.Zip;
+using FileInfo = System.IO.FileInfo;
namespace osu.Game.Tests.Beatmaps.IO
{
@@ -93,6 +93,166 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
+ [Test]
+ public async Task TestImportThenImportWithReZip()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithReZip)))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ var imported = await LoadOszIntoOsu(osu);
+
+ string hashBefore = hashFile(temp);
+
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ // zip files differ because different compression or encoder.
+ Assert.AreNotEqual(hashBefore, hashFile(temp));
+
+ var importedSecondTime = await osu.Dependencies.Get().Import(temp);
+
+ ensureLoaded(osu);
+
+ // but contents doesn't, so existing should still be used.
+ Assert.IsTrue(imported.ID == importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ private string hashFile(string filename)
+ {
+ using (var s = File.OpenRead(filename))
+ return s.ComputeMD5Hash();
+ }
+
+ [Test]
+ public async Task TestImportThenImportWithChangedFile()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithChangedFile)))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ var imported = await LoadOszIntoOsu(osu);
+
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ // arbitrary write to non-hashed file
+ using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText())
+ await sw.WriteLineAsync("text");
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ var importedSecondTime = await osu.Dependencies.Get().Import(temp);
+
+ ensureLoaded(osu);
+
+ // check the newly "imported" beatmap is not the original.
+ Assert.IsTrue(imported.ID != importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public async Task TestImportThenImportWithDifferentFilename()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithDifferentFilename)))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ var imported = await LoadOszIntoOsu(osu);
+
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ // change filename
+ var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First());
+ firstFile.MoveTo(Path.Combine(firstFile.DirectoryName, $"{firstFile.Name}-changed{firstFile.Extension}"));
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ var importedSecondTime = await osu.Dependencies.Get().Import(temp);
+
+ ensureLoaded(osu);
+
+ // check the newly "imported" beatmap is not the original.
+ Assert.IsTrue(imported.ID != importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
[Test]
public async Task TestImportCorruptThenImport()
{
@@ -175,7 +335,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var breakTemp = TestResources.GetTestBeatmapForImport();
MemoryStream brokenOsu = new MemoryStream();
- MemoryStream brokenOsz = new MemoryStream(File.ReadAllBytes(breakTemp));
+ MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(breakTemp));
File.Delete(breakTemp);
@@ -212,37 +372,6 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
- [Test]
- public async Task TestImportThenImportDifferentHash()
- {
- // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportDifferentHash)))
- {
- try
- {
- var osu = loadOsu(host);
- var manager = osu.Dependencies.Get();
-
- var imported = await LoadOszIntoOsu(osu);
-
- imported.Hash += "-changed";
- manager.Update(imported);
-
- var importedSecondTime = await LoadOszIntoOsu(osu);
-
- Assert.IsTrue(imported.ID != importedSecondTime.ID);
- Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID);
-
- // only one beatmap will exist as the online set ID matched, causing purging of the first import.
- checkBeatmapSetCount(osu, 1);
- }
- finally
- {
- host.Exit();
- }
- }
- }
-
[Test]
public async Task TestImportThenDeleteThenImport()
{
@@ -522,7 +651,7 @@ namespace osu.Game.Tests.Beatmaps.IO
using (var resourceForkFile = File.CreateText(resourceForkFilePath))
{
- resourceForkFile.WriteLine("adding content so that it's not empty");
+ await resourceForkFile.WriteLineAsync("adding content so that it's not empty");
}
try
@@ -599,23 +728,17 @@ namespace osu.Game.Tests.Beatmaps.IO
await osu.Dependencies.Get().Import(temp);
BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0];
+
+ var beatmapInfo = setToUpdate.Beatmaps.First(b => b.RulesetID == 0);
Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap;
BeatmapSetFileInfo fileToUpdate = setToUpdate.Files.First(f => beatmapToUpdate.BeatmapInfo.Path.Contains(f.Filename));
- using (var stream = new MemoryStream())
- {
- using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- {
- beatmapToUpdate.HitObjects.Clear();
- beatmapToUpdate.HitObjects.Add(new HitCircle { StartTime = 5000 });
+ string oldMd5Hash = beatmapToUpdate.BeatmapInfo.MD5Hash;
- new LegacyBeatmapEncoder(beatmapToUpdate).Encode(writer);
- }
+ beatmapToUpdate.HitObjects.Clear();
+ beatmapToUpdate.HitObjects.Add(new HitCircle { StartTime = 5000 });
- stream.Seek(0, SeekOrigin.Begin);
-
- manager.UpdateFile(setToUpdate, fileToUpdate, stream);
- }
+ manager.Save(beatmapInfo, beatmapToUpdate);
// Check that the old file reference has been removed
Assert.That(manager.QueryBeatmapSet(s => s.ID == setToUpdate.ID).Files.All(f => f.ID != fileToUpdate.ID));
@@ -624,6 +747,7 @@ namespace osu.Game.Tests.Beatmaps.IO
Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(manager.QueryBeatmap(b => b.ID == beatmapToUpdate.BeatmapInfo.ID)).Beatmap;
Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1));
Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000));
+ Assert.That(updatedBeatmap.BeatmapInfo.MD5Hash, Is.Not.EqualTo(oldMd5Hash));
}
finally
{
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index fbb0416c45..600c820ce1 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -428,22 +428,27 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(5, result.Links.Count);
Link f = result.Links.Find(l => l.Url == "https://osu.ppy.sh/wiki/wiki links");
+ Assert.That(f, Is.Not.Null);
Assert.AreEqual(44, f.Index);
Assert.AreEqual(10, f.Length);
f = result.Links.Find(l => l.Url == "http://www.simple-test.com");
+ Assert.That(f, Is.Not.Null);
Assert.AreEqual(10, f.Index);
Assert.AreEqual(11, f.Length);
f = result.Links.Find(l => l.Url == "http://google.com");
+ Assert.That(f, Is.Not.Null);
Assert.AreEqual(97, f.Index);
Assert.AreEqual(4, f.Length);
f = result.Links.Find(l => l.Url == "https://osu.ppy.sh");
+ Assert.That(f, Is.Not.Null);
Assert.AreEqual(78, f.Index);
Assert.AreEqual(18, f.Length);
f = result.Links.Find(l => l.Url == "\uD83D\uDE12");
+ Assert.That(f, Is.Not.Null);
Assert.AreEqual(101, f.Index);
Assert.AreEqual(3, f.Length);
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs
new file mode 100644
index 0000000000..cd3669f160
--- /dev/null
+++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneGameplayClockContainer : OsuTestScene
+ {
+ [Test]
+ public void TestStartThenElapsedTime()
+ {
+ GameplayClockContainer gcc = null;
+
+ AddStep("create container", () => Add(gcc = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0)));
+ AddStep("start track", () => gcc.Start());
+ AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
index f611f2717e..737946e1e0 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
@@ -1,58 +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 System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Framework.Audio;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
-using osu.Framework.Timing;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.Formats;
-using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
-using osu.Game.Storyboards;
+using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
-using osu.Game.Tests.Visual;
-using osu.Game.Users;
namespace osu.Game.Tests.Gameplay
{
[HeadlessTest]
- public class TestSceneHitObjectSamples : PlayerTestScene
+ public class TestSceneHitObjectSamples : HitObjectSampleTest
{
- private readonly SkinInfo userSkinInfo = new SkinInfo();
-
- private readonly BeatmapInfo beatmapInfo = new BeatmapInfo
- {
- BeatmapSet = new BeatmapSetInfo(),
- Metadata = new BeatmapMetadata
- {
- Author = User.SYSTEM_USER
- }
- };
-
- private readonly TestResourceStore userSkinResourceStore = new TestResourceStore();
- private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore();
-
- protected override bool HasCustomSteps => true;
-
- public TestSceneHitObjectSamples()
- : base(new OsuRuleset())
- {
- }
-
- private SkinSourceDependencyContainer dependencies;
-
- protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
- => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent)));
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+ protected override IResourceStore Resources => TestResources.GetStore();
///
/// Tests that a hitobject which provides no custom sample set retrieves samples from the user skin.
@@ -62,11 +26,11 @@ namespace osu.Game.Tests.Gameplay
{
const string expected_sample = "normal-hitnormal";
- setupSkins(expected_sample, expected_sample);
+ SetupSkins(expected_sample, expected_sample);
- createTestWithBeatmap("hitobject-skin-sample.osu");
+ CreateTestWithBeatmap("hitobject-skin-sample.osu");
- assertUserLookup(expected_sample);
+ AssertUserLookup(expected_sample);
}
///
@@ -77,11 +41,11 @@ namespace osu.Game.Tests.Gameplay
{
const string expected_sample = "normal-hitnormal";
- setupSkins(expected_sample, expected_sample);
+ SetupSkins(expected_sample, expected_sample);
- createTestWithBeatmap("hitobject-beatmap-sample.osu");
+ CreateTestWithBeatmap("hitobject-beatmap-sample.osu");
- assertBeatmapLookup(expected_sample);
+ AssertBeatmapLookup(expected_sample);
}
///
@@ -92,11 +56,11 @@ namespace osu.Game.Tests.Gameplay
{
const string expected_sample = "normal-hitnormal";
- setupSkins(null, expected_sample);
+ SetupSkins(null, expected_sample);
- createTestWithBeatmap("hitobject-beatmap-sample.osu");
+ CreateTestWithBeatmap("hitobject-beatmap-sample.osu");
- assertUserLookup(expected_sample);
+ AssertUserLookup(expected_sample);
}
///
@@ -108,11 +72,11 @@ namespace osu.Game.Tests.Gameplay
[TestCase("normal-hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
{
- setupSkins(expectedSample, expectedSample);
+ SetupSkins(expectedSample, expectedSample);
- createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
+ CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
- assertBeatmapLookup(expectedSample);
+ AssertBeatmapLookup(expectedSample);
}
///
@@ -124,11 +88,11 @@ namespace osu.Game.Tests.Gameplay
[TestCase("normal-hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
{
- setupSkins(string.Empty, expectedSample);
+ SetupSkins(string.Empty, expectedSample);
- createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
+ CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
- assertUserLookup(expectedSample);
+ AssertUserLookup(expectedSample);
}
///
@@ -139,11 +103,11 @@ namespace osu.Game.Tests.Gameplay
{
const string expected_sample = "hit_1.wav";
- setupSkins(expected_sample, expected_sample);
+ SetupSkins(expected_sample, expected_sample);
- createTestWithBeatmap("file-beatmap-sample.osu");
+ CreateTestWithBeatmap("file-beatmap-sample.osu");
- assertBeatmapLookup(expected_sample);
+ AssertBeatmapLookup(expected_sample);
}
///
@@ -154,11 +118,11 @@ namespace osu.Game.Tests.Gameplay
{
const string expected_sample = "normal-hitnormal";
- setupSkins(expected_sample, expected_sample);
+ SetupSkins(expected_sample, expected_sample);
- createTestWithBeatmap("controlpoint-skin-sample.osu");
+ CreateTestWithBeatmap("controlpoint-skin-sample.osu");
- assertUserLookup(expected_sample);
+ AssertUserLookup(expected_sample);
}
///
@@ -169,11 +133,11 @@ namespace osu.Game.Tests.Gameplay
{
const string expected_sample = "normal-hitnormal";
- setupSkins(expected_sample, expected_sample);
+ SetupSkins(expected_sample, expected_sample);
- createTestWithBeatmap("controlpoint-beatmap-sample.osu");
+ CreateTestWithBeatmap("controlpoint-beatmap-sample.osu");
- assertBeatmapLookup(expected_sample);
+ AssertBeatmapLookup(expected_sample);
}
///
@@ -183,11 +147,11 @@ namespace osu.Game.Tests.Gameplay
[TestCase("normal-hitnormal")]
public void TestControlPointCustomSampleFromBeatmap(string sampleName)
{
- setupSkins(sampleName, sampleName);
+ SetupSkins(sampleName, sampleName);
- createTestWithBeatmap("controlpoint-beatmap-custom-sample.osu");
+ CreateTestWithBeatmap("controlpoint-beatmap-custom-sample.osu");
- assertBeatmapLookup(sampleName);
+ AssertBeatmapLookup(sampleName);
}
///
@@ -198,149 +162,70 @@ namespace osu.Game.Tests.Gameplay
{
const string expected_sample = "normal-hitnormal3";
- setupSkins(expected_sample, expected_sample);
+ SetupSkins(expected_sample, expected_sample);
- createTestWithBeatmap("hitobject-beatmap-custom-sample-override.osu");
+ CreateTestWithBeatmap("hitobject-beatmap-custom-sample-override.osu");
- assertBeatmapLookup(expected_sample);
+ AssertBeatmapLookup(expected_sample);
}
- protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap;
-
- protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio);
-
- private IBeatmap currentTestBeatmap;
-
- private void createTestWithBeatmap(string filename)
+ ///
+ /// Tests that when a custom sample bank is used, both the normal and additional sounds will be looked up.
+ ///
+ [Test]
+ public void TestHitObjectCustomSampleBank()
{
- CreateTest(() =>
+ string[] expectedSamples =
{
- AddStep("clear performed lookups", () =>
- {
- userSkinResourceStore.PerformedLookups.Clear();
- beatmapSkinResourceStore.PerformedLookups.Clear();
- });
+ "normal-hitnormal2",
+ "normal-hitwhistle2"
+ };
- AddStep($"load {filename}", () =>
- {
- using (var reader = new LineBufferedReader(TestResources.OpenResource($"SampleLookups/{filename}")))
- currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader);
- });
- });
+ SetupSkins(expectedSamples[0], expectedSamples[1]);
+
+ CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertBeatmapLookup(expectedSamples[0]);
+ AssertUserLookup(expectedSamples[1]);
}
- private void setupSkins(string beatmapFile, string userFile)
+ ///
+ /// Tests that when a custom sample bank is used, but is disabled,
+ /// only the additional sound will be looked up.
+ ///
+ [Test]
+ public void TestHitObjectCustomSampleBankWithoutLayered()
{
- AddStep("setup skins", () =>
- {
- userSkinInfo.Files = new List
- {
- new SkinFileInfo
- {
- Filename = userFile,
- FileInfo = new IO.FileInfo { Hash = userFile }
- }
- };
+ const string expected_sample = "normal-hitwhistle2";
+ const string unwanted_sample = "normal-hitnormal2";
- beatmapInfo.BeatmapSet.Files = new List
- {
- new BeatmapSetFileInfo
- {
- Filename = beatmapFile,
- FileInfo = new IO.FileInfo { Hash = beatmapFile }
- }
- };
+ SetupSkins(expected_sample, unwanted_sample);
+ disableLayeredHitSounds();
- // Need to refresh the cached skin source to refresh the skin resource store.
- dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio));
- });
+ CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertBeatmapLookup(expected_sample);
+ AssertNoLookup(unwanted_sample);
}
- private void assertBeatmapLookup(string name) => AddAssert($"\"{name}\" looked up from beatmap skin",
- () => !userSkinResourceStore.PerformedLookups.Contains(name) && beatmapSkinResourceStore.PerformedLookups.Contains(name));
-
- private void assertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin",
- () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name));
-
- private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer
+ ///
+ /// Tests that when a normal sample bank is used and is disabled,
+ /// the normal sound will be looked up anyway.
+ ///
+ [Test]
+ public void TestHitObjectNormalSampleBankWithoutLayered()
{
- public ISkinSource SkinSource;
+ const string expected_sample = "normal-hitnormal";
- private readonly IReadOnlyDependencyContainer fallback;
+ SetupSkins(expected_sample, expected_sample);
+ disableLayeredHitSounds();
- public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback)
- {
- this.fallback = fallback;
- }
+ CreateTestWithBeatmap("hitobject-beatmap-sample.osu");
- public object Get(Type type)
- {
- if (type == typeof(ISkinSource))
- return SkinSource;
-
- return fallback.Get(type);
- }
-
- public object Get(Type type, CacheInfo info)
- {
- if (type == typeof(ISkinSource))
- return SkinSource;
-
- return fallback.Get(type, info);
- }
-
- public void Inject(T instance) where T : class
- {
- // Never used directly
- }
+ AssertBeatmapLookup(expected_sample);
}
- private class TestResourceStore : IResourceStore
- {
- public readonly List PerformedLookups = new List();
-
- public byte[] Get(string name)
- {
- markLookup(name);
- return Array.Empty();
- }
-
- public Task GetAsync(string name)
- {
- markLookup(name);
- return Task.FromResult(Array.Empty());
- }
-
- public Stream GetStream(string name)
- {
- markLookup(name);
- return new MemoryStream();
- }
-
- private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf(Path.DirectorySeparatorChar) + 1));
-
- public IEnumerable GetAvailableResources() => Enumerable.Empty();
-
- public void Dispose()
- {
- }
- }
-
- private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap
- {
- private readonly BeatmapInfo skinBeatmapInfo;
- private readonly IResourceStore resourceStore;
-
- public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio,
- double length = 60000)
- : base(beatmap, storyboard, referenceClock, audio, length)
- {
- this.skinBeatmapInfo = skinBeatmapInfo;
- this.resourceStore = resourceStore;
- }
-
- protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager);
- }
+ private void disableLayeredHitSounds()
+ => AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[GlobalSkinConfiguration.LayeredHitSounds.ToString()] = "0");
}
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 84506739ab..b30870d057 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@@ -10,7 +11,14 @@ using osu.Framework.Audio.Sample;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Audio;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.Play;
using osu.Game.Skinning;
+using osu.Game.Storyboards;
+using osu.Game.Storyboards.Drawables;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
@@ -43,6 +51,71 @@ namespace osu.Game.Tests.Gameplay
AddAssert("sample is non-null", () => channel != null);
}
+ [Test]
+ public void TestSamplePlaybackAtZero()
+ {
+ GameplayClockContainer gameplayContainer = null;
+ DrawableStoryboardSample sample = null;
+
+ AddStep("create container", () =>
+ {
+ Add(gameplayContainer = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0));
+
+ gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
+ {
+ Clock = gameplayContainer.GameplayClock
+ });
+ });
+
+ AddStep("start time", () => gameplayContainer.Start());
+
+ AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue);
+ }
+
+ [TestCase(typeof(OsuModDoubleTime), 1.5)]
+ [TestCase(typeof(OsuModHalfTime), 0.75)]
+ [TestCase(typeof(ModWindUp), 1.5)]
+ [TestCase(typeof(ModWindDown), 0.75)]
+ [TestCase(typeof(OsuModDoubleTime), 2)]
+ [TestCase(typeof(OsuModHalfTime), 0.5)]
+ [TestCase(typeof(ModWindUp), 2)]
+ [TestCase(typeof(ModWindDown), 0.5)]
+ public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate)
+ {
+ GameplayClockContainer gameplayContainer = null;
+ TestDrawableStoryboardSample sample = null;
+
+ Mod testedMod = Activator.CreateInstance(expectedMod) as Mod;
+
+ switch (testedMod)
+ {
+ case ModRateAdjust m:
+ m.SpeedChange.Value = expectedRate;
+ break;
+
+ case ModTimeRamp m:
+ m.InitialRate.Value = m.FinalRate.Value = expectedRate;
+ break;
+ }
+
+ AddStep("setup storyboard sample", () =>
+ {
+ Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
+ SelectedMods.Value = new[] { testedMod };
+
+ Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0));
+
+ gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
+ {
+ Clock = gameplayContainer.GameplayClock
+ });
+ });
+
+ AddStep("start", () => gameplayContainer.Start());
+
+ AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate);
+ }
+
private class TestSkin : LegacySkin
{
public TestSkin(string resourceName, AudioManager audioManager)
@@ -60,11 +133,11 @@ namespace osu.Game.Tests.Gameplay
this.resourceName = resourceName;
}
- public byte[] Get(string name) => name == resourceName ? TestResources.GetStore().Get("Resources/test-sample.mp3") : null;
+ public byte[] Get(string name) => name == resourceName ? TestResources.GetStore().Get("Resources/Samples/test-sample.mp3") : null;
- public Task GetAsync(string name) => name == resourceName ? TestResources.GetStore().GetAsync("Resources/test-sample.mp3") : null;
+ public Task GetAsync(string name) => name == resourceName ? TestResources.GetStore().GetAsync("Resources/Samples/test-sample.mp3") : null;
- public Stream GetStream(string name) => name == resourceName ? TestResources.GetStore().GetStream("Resources/test-sample.mp3") : null;
+ public Stream GetStream(string name) => name == resourceName ? TestResources.GetStore().GetStream("Resources/Samples/test-sample.mp3") : null;
public IEnumerable GetAvailableResources() => new[] { resourceName };
@@ -72,5 +145,28 @@ namespace osu.Game.Tests.Gameplay
{
}
}
+
+ private class TestCustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
+ {
+ private readonly AudioManager audio;
+
+ public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, AudioManager audio)
+ : base(ruleset, null, audio)
+ {
+ this.audio = audio;
+ }
+
+ protected override ISkin GetSkin() => new TestSkin("test-sample", audio);
+ }
+
+ private class TestDrawableStoryboardSample : DrawableStoryboardSample
+ {
+ public TestDrawableStoryboardSample(StoryboardSampleInfo sampleInfo)
+ : base(sampleInfo)
+ {
+ }
+
+ public new SampleChannel Channel => base.Channel;
+ }
}
}
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index f3d54d876a..199e69a19d 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -19,24 +19,18 @@ namespace osu.Game.Tests.NonVisual
[TestFixture]
public class CustomDataDirectoryTest
{
- [SetUp]
- public void SetUp()
- {
- if (Directory.Exists(customPath))
- Directory.Delete(customPath, true);
- }
-
[Test]
public void TestDefaultDirectory()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestDefaultDirectory)))
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestDefaultDirectory)))
{
try
{
+ string defaultStorageLocation = getDefaultLocationFor(nameof(TestDefaultDirectory));
+
var osu = loadOsu(host);
var storage = osu.Dependencies.Get();
- string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestDefaultDirectory));
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
}
finally
@@ -46,21 +40,14 @@ namespace osu.Game.Tests.NonVisual
}
}
- private string customPath => Path.Combine(RuntimeInfo.StartupDirectory, "custom-path");
-
[Test]
public void TestCustomDirectory()
{
- using (var host = new HeadlessGameHost(nameof(TestCustomDirectory)))
+ string customPath = prepareCustomPath();
+
+ using (var host = new CustomTestHeadlessGameHost(nameof(TestCustomDirectory)))
{
- string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestCustomDirectory));
-
- // need access before the game has constructed its own storage yet.
- Storage storage = new DesktopStorage(defaultStorageLocation, host);
- // manual cleaning so we can prepare a config file.
- storage.DeleteDirectory(string.Empty);
-
- using (var storageConfig = new StorageConfigManager(storage))
+ using (var storageConfig = new StorageConfigManager(host.InitialStorage))
storageConfig.Set(StorageConfig.FullPath, customPath);
try
@@ -68,7 +55,7 @@ namespace osu.Game.Tests.NonVisual
var osu = loadOsu(host);
// switch to DI'd storage
- storage = osu.Dependencies.Get();
+ var storage = osu.Dependencies.Get();
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
}
@@ -82,16 +69,11 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestSubDirectoryLookup()
{
- using (var host = new HeadlessGameHost(nameof(TestSubDirectoryLookup)))
+ string customPath = prepareCustomPath();
+
+ using (var host = new CustomTestHeadlessGameHost(nameof(TestSubDirectoryLookup)))
{
- string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestSubDirectoryLookup));
-
- // need access before the game has constructed its own storage yet.
- Storage storage = new DesktopStorage(defaultStorageLocation, host);
- // manual cleaning so we can prepare a config file.
- storage.DeleteDirectory(string.Empty);
-
- using (var storageConfig = new StorageConfigManager(storage))
+ using (var storageConfig = new StorageConfigManager(host.InitialStorage))
storageConfig.Set(StorageConfig.FullPath, customPath);
try
@@ -99,7 +81,7 @@ namespace osu.Game.Tests.NonVisual
var osu = loadOsu(host);
// switch to DI'd storage
- storage = osu.Dependencies.Get();
+ var storage = osu.Dependencies.Get();
string actualTestFile = Path.Combine(customPath, "rulesets", "test");
@@ -120,13 +102,20 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigration()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigration)))
+ string customPath = prepareCustomPath();
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigration)))
{
try
{
+ string defaultStorageLocation = getDefaultLocationFor(nameof(TestMigration));
+
var osu = loadOsu(host);
var storage = osu.Dependencies.Get();
+ // Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
+ string originalDirectory = storage.GetFullPath(".");
+
// ensure we perform a save
host.Dependencies.Get().Save();
@@ -136,8 +125,6 @@ namespace osu.Game.Tests.NonVisual
// for testing nested files are not ignored (only top level)
host.Storage.GetStorageForDirectory("test-nested").GetStorageForDirectory("cache");
- string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration));
-
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
osu.Migrate(customPath);
@@ -145,25 +132,25 @@ namespace osu.Game.Tests.NonVisual
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
// ensure cache was not moved
- Assert.That(host.Storage.ExistsDirectory("cache"));
+ Assert.That(Directory.Exists(Path.Combine(originalDirectory, "cache")));
// ensure nested cache was moved
- Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
+ Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
foreach (var file in OsuStorage.IGNORE_FILES)
{
- Assert.That(host.Storage.Exists(file), Is.True);
+ Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False);
}
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
{
- Assert.That(host.Storage.ExistsDirectory(dir), Is.True);
+ Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.ExistsDirectory(dir), Is.False);
}
- Assert.That(new StreamReader(host.Storage.GetStream("storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
+ Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
}
finally
{
@@ -175,14 +162,15 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationBetweenTwoTargets()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets)))
+ string customPath = prepareCustomPath();
+ string customPath2 = prepareCustomPath("-2");
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets)))
{
try
{
var osu = loadOsu(host);
- string customPath2 = $"{customPath}-2";
-
const string database_filename = "client.db";
Assert.DoesNotThrow(() => osu.Migrate(customPath));
@@ -204,7 +192,9 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationToSameTargetFails()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails)))
+ string customPath = prepareCustomPath();
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSameTargetFails)))
{
try
{
@@ -223,7 +213,9 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationToNestedTargetFails()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToNestedTargetFails)))
+ string customPath = prepareCustomPath();
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToNestedTargetFails)))
{
try
{
@@ -250,7 +242,9 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationToSeeminglyNestedTarget()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget)))
+ string customPath = prepareCustomPath();
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget)))
{
try
{
@@ -279,6 +273,7 @@ namespace osu.Game.Tests.NonVisual
var osu = new OsuGameBase();
Task.Run(() => host.Run(osu));
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
+
return osu;
}
@@ -291,5 +286,39 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
+
+ private static string getDefaultLocationFor(string testTypeName)
+ {
+ string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName);
+
+ if (Directory.Exists(path))
+ Directory.Delete(path, true);
+
+ return path;
+ }
+
+ private string prepareCustomPath(string suffix = "")
+ {
+ string path = Path.Combine(RuntimeInfo.StartupDirectory, $"custom-path{suffix}");
+
+ if (Directory.Exists(path))
+ Directory.Delete(path, true);
+
+ return path;
+ }
+
+ public class CustomTestHeadlessGameHost : HeadlessGameHost
+ {
+ public Storage InitialStorage { get; }
+
+ public CustomTestHeadlessGameHost(string name)
+ : base(name)
+ {
+ string defaultStorageLocation = getDefaultLocationFor(name);
+
+ InitialStorage = new DesktopStorage(defaultStorageLocation, this);
+ InitialStorage.DeleteDirectory(string.Empty);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Online/TestAPIModSerialization.cs b/osu.Game.Tests/Online/TestAPIModSerialization.cs
index d9318aa822..5948582d77 100644
--- a/osu.Game.Tests/Online/TestAPIModSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModSerialization.cs
@@ -49,9 +49,32 @@ namespace osu.Game.Tests.Online
Assert.That(converted.TestSetting.Value, Is.EqualTo(2));
}
+ [Test]
+ public void TestDeserialiseTimeRampMod()
+ {
+ // Create the mod with values different from default.
+ var apiMod = new APIMod(new TestModTimeRamp
+ {
+ AdjustPitch = { Value = false },
+ InitialRate = { Value = 1.25 },
+ FinalRate = { Value = 0.25 }
+ });
+
+ var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod));
+ var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset());
+
+ Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false));
+ Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25));
+ Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
+ }
+
private class TestRuleset : Ruleset
{
- public override IEnumerable GetModsFor(ModType type) => new[] { new TestMod() };
+ public override IEnumerable GetModsFor(ModType type) => new Mod[]
+ {
+ new TestMod(),
+ new TestModTimeRamp(),
+ };
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException();
@@ -78,5 +101,39 @@ namespace osu.Game.Tests.Online
Precision = 0.01,
};
}
+
+ private class TestModTimeRamp : ModTimeRamp
+ {
+ public override string Name => "Test Mod";
+ public override string Acronym => "TMTR";
+ public override double ScoreMultiplier => 1;
+
+ [SettingSource("Initial rate", "The starting speed of the track")]
+ public override BindableNumber InitialRate { get; } = new BindableDouble
+ {
+ MinValue = 1,
+ MaxValue = 2,
+ Default = 1.5,
+ Value = 1.5,
+ Precision = 0.01,
+ };
+
+ [SettingSource("Final rate", "The speed increase to ramp towards")]
+ public override BindableNumber FinalRate { get; } = new BindableDouble
+ {
+ MinValue = 0,
+ MaxValue = 1,
+ Default = 0.5,
+ Value = 0.5,
+ Precision = 0.01,
+ };
+
+ [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
+ public override BindableBool AdjustPitch { get; } = new BindableBool
+ {
+ Default = true,
+ Value = true
+ };
+ }
}
}
diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-bank.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-bank.osu
new file mode 100644
index 0000000000..c50c921839
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-bank.osu
@@ -0,0 +1,7 @@
+osu file format v14
+
+[TimingPoints]
+0,300,4,0,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,2,0:0:0:0:
diff --git a/osu.Game.Tests/Resources/test-sample.mp3 b/osu.Game.Tests/Resources/Samples/test-sample.mp3
similarity index 100%
rename from osu.Game.Tests/Resources/test-sample.mp3
rename to osu.Game.Tests/Resources/Samples/test-sample.mp3
diff --git a/osu.Game.Tests/Resources/Textures/test-image.png b/osu.Game.Tests/Resources/Textures/test-image.png
new file mode 100644
index 0000000000..5d0092edc8
Binary files /dev/null and b/osu.Game.Tests/Resources/Textures/test-image.png differ
diff --git a/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini b/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini
new file mode 100644
index 0000000000..3c0dae6b13
--- /dev/null
+++ b/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini
@@ -0,0 +1,5 @@
+[General]
+Version: latest
+
+[Colours]
+Combo1: 255,255,255,0
\ No newline at end of file
diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
index 90bf419644..57f0d7e957 100644
--- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
+++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
@@ -183,11 +183,8 @@ namespace osu.Game.Tests.Scores.IO
{
var beatmapManager = osu.Dependencies.Get();
- if (score.Beatmap == null)
- score.Beatmap = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
-
- if (score.Ruleset == null)
- score.Ruleset = new OsuRuleset().RulesetInfo;
+ score.Beatmap ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
+ score.Ruleset ??= new OsuRuleset().RulesetInfo;
var scoreManager = osu.Dependencies.Get();
await scoreManager.Import(score, archive);
diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
index aedf26ee75..c408d2f182 100644
--- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
+++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
@@ -108,5 +108,15 @@ namespace osu.Game.Tests.Skins
using (var stream = new LineBufferedReader(resStream))
Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m));
}
+
+ [Test]
+ public void TestDecodeColourWithZeroAlpha()
+ {
+ var decoder = new LegacySkinDecoder();
+
+ using (var resStream = TestResources.OpenResource("skin-zero-alpha-colour.ini"))
+ using (var stream = new LineBufferedReader(resStream))
+ Assert.That(decoder.Decode(stream).ComboColours[0].A, Is.EqualTo(1.0f));
+ }
}
}
diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
new file mode 100644
index 0000000000..80f1b02794
--- /dev/null
+++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
@@ -0,0 +1,95 @@
+// 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.Allocation;
+using osu.Framework.Audio.Track;
+using osu.Framework.Configuration.Tracking;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Configuration;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osu.Game.Tests.Resources;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Testing
+{
+ ///
+ /// A test scene ensuring the dependencies for the
+ /// provided ruleset below are cached at the base implementation.
+ ///
+ [HeadlessTest]
+ public class TestSceneRulesetDependencies : OsuTestScene
+ {
+ protected override Ruleset CreateRuleset() => new TestRuleset();
+
+ [Test]
+ public void TestRetrieveTexture()
+ {
+ AddAssert("ruleset texture retrieved", () =>
+ Dependencies.Get().Get(@"test-image") != null);
+ }
+
+ [Test]
+ public void TestRetrieveSample()
+ {
+ AddAssert("ruleset sample retrieved", () =>
+ Dependencies.Get().Get(@"test-sample") != null);
+ }
+
+ [Test]
+ public void TestResolveConfigManager()
+ {
+ AddAssert("ruleset config resolved", () =>
+ Dependencies.Get() != null);
+ }
+
+ private class TestRuleset : Ruleset
+ {
+ public override string Description => string.Empty;
+ public override string ShortName => string.Empty;
+
+ public TestRuleset()
+ {
+ // temporary ID to let RulesetConfigCache pass our
+ // config manager to the ruleset dependencies.
+ RulesetInfo.ID = -1;
+ }
+
+ public override IResourceStore CreateResourceStore() => new NamespacedResourceStore(TestResources.GetStore(), @"Resources");
+ public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new TestRulesetConfigManager();
+
+ public override IEnumerable GetModsFor(ModType type) => Array.Empty();
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null;
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => null;
+ }
+
+ private class TestRulesetConfigManager : IRulesetConfigManager
+ {
+ public void Load()
+ {
+ }
+
+ public bool Save() => true;
+
+ public TrackedSettings CreateTrackedSettings() => new TrackedSettings();
+
+ public void LoadInto(TrackedSettings settings)
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index d601f40afe..19294d12fc 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -19,6 +19,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
@@ -27,6 +28,7 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
+using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
@@ -186,9 +188,15 @@ namespace osu.Game.Tests.Visual.Background
public void TestTransition()
{
performFullSetup();
+
FadeAccessibleResults results = null;
- AddStep("Transition to Results", () => player.Push(results =
- new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" } })));
+
+ AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo
+ {
+ User = new User { Username = "osu!" },
+ Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo
+ })));
+
AddUntilStep("Wait for results is current", () => results.IsCurrentScreen());
AddUntilStep("Screen is undimmed, original background retained", () =>
songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && results.IsBlurCorrect());
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
index 20862e9cac..293a6e6869 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
@@ -4,6 +4,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
@@ -13,13 +14,10 @@ namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorChangeStates : EditorTestScene
{
- public TestSceneEditorChangeStates()
- : base(new OsuRuleset())
- {
- }
-
private EditorBeatmap editorBeatmap;
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
public override void SetUpSteps()
{
base.SetUpSteps();
diff --git a/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs b/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs
new file mode 100644
index 0000000000..cbf8515567
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.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 osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ ///
+ /// A with an arbitrary ruleset value to test with.
+ ///
+ public abstract class OsuPlayerTestScene : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
index 512584bd42..79275d70a7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
@@ -10,14 +10,13 @@ using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
- public class TestSceneCompletionCancellation : PlayerTestScene
+ public class TestSceneCompletionCancellation : OsuPlayerTestScene
{
private Track track;
@@ -29,11 +28,6 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool AllowFail => false;
- public TestSceneCompletionCancellation()
- : base(new OsuRuleset())
- {
- }
-
[SetUpSteps]
public override void SetUpSteps()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
index a95e806862..1c55595c97 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
@@ -14,6 +15,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private FailingLayer layer;
+ private readonly Bindable showHealth = new Bindable();
+
[Resolved]
private OsuConfigManager config { get; set; }
@@ -24,8 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Child = layer = new FailingLayer();
layer.BindHealthProcessor(new DrainingHealthProcessor(1));
+ layer.ShowHealth.BindTo(showHealth);
});
+ AddStep("show health", () => showHealth.Value = true);
AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
AddUntilStep("layer is visible", () => layer.IsPresent);
}
@@ -69,5 +74,27 @@ namespace osu.Game.Tests.Visual.Gameplay
AddWaitStep("wait for potential fade", 10);
AddAssert("layer is still visible", () => layer.IsPresent);
}
+
+ [Test]
+ public void TestLayerVisibilityWithDifferentOptions()
+ {
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+
+ AddStep("don't show health", () => showHealth.Value = false);
+ AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
+ AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
+
+ AddStep("don't show health", () => showHealth.Value = false);
+ AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
+ AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
+
+ AddStep("show health", () => showHealth.Value = true);
+ AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
+ AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
+
+ AddStep("show health", () => showHealth.Value = true);
+ AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
+ AddUntilStep("layer fade is visible", () => layer.IsPresent);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
index 310746d179..2a119f5199 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
@@ -10,23 +10,17 @@ using osu.Framework.Utils;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
- public class TestSceneGameplayRewinding : PlayerTestScene
+ public class TestSceneGameplayRewinding : OsuPlayerTestScene
{
[Resolved]
private AudioManager audioManager { get; set; }
- public TestSceneGameplayRewinding()
- : base(new OsuRuleset())
- {
- }
-
private Track track;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 944e6ca6be..1961a224c1 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -10,14 +10,13 @@ using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
- public class TestScenePause : PlayerTestScene
+ public class TestScenePause : OsuPlayerTestScene
{
protected new PausePlayer Player => (PausePlayer)base.Player;
@@ -26,7 +25,6 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Container Content => content;
public TestScenePause()
- : base(new OsuRuleset())
{
base.Content.Add(content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both });
}
@@ -176,9 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestExitFromGameplay()
{
- AddStep("exit", () => Player.Exit());
- confirmPaused();
-
+ // an externally triggered exit should immediately exit, skipping all pause logic.
AddStep("exit", () => Player.Exit());
confirmExited();
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
index a83320048b..e43e5ba3ce 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
@@ -8,12 +8,11 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.Gameplay
{
[HeadlessTest] // we alter unsafe properties on the game host to test inactive window state.
- public class TestScenePauseWhenInactive : PlayerTestScene
+ public class TestScenePauseWhenInactive : OsuPlayerTestScene
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
@@ -27,11 +26,6 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private GameHost host { get; set; }
- public TestScenePauseWhenInactive()
- : base(new OsuRuleset())
- {
- }
-
[Test]
public void TestDoesntPauseDuringIntro()
{
diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
index 2d2f1a1618..f71d13ed35 100644
--- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
+++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
@@ -19,10 +19,10 @@ namespace osu.Game.Tests.Visual.Menus
[Cached]
private OsuLogo logo;
+ protected OsuScreenStack IntroStack;
+
protected IntroTestScene()
{
- OsuScreenStack introStack = null;
-
Children = new Drawable[]
{
new Box
@@ -45,17 +45,17 @@ namespace osu.Game.Tests.Visual.Menus
logo.FinishTransforms();
logo.IsTracking = false;
- introStack?.Expire();
+ IntroStack?.Expire();
- Add(introStack = new OsuScreenStack
+ Add(IntroStack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both,
});
- introStack.Push(CreateScreen());
+ IntroStack.Push(CreateScreen());
});
- AddUntilStep("wait for menu", () => introStack.CurrentScreen is MainMenu);
+ AddUntilStep("wait for menu", () => IntroStack.CurrentScreen is MainMenu);
}
protected abstract IScreen CreateScreen();
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
new file mode 100644
index 0000000000..8f20e38494
--- /dev/null
+++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.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 NUnit.Framework;
+using osu.Framework.Audio.Track;
+using osu.Framework.Screens;
+using osu.Game.Screens.Menu;
+
+namespace osu.Game.Tests.Visual.Menus
+{
+ [TestFixture]
+ public class TestSceneIntroWelcome : IntroTestScene
+ {
+ protected override IScreen CreateScreen() => new IntroWelcome();
+
+ public TestSceneIntroWelcome()
+ {
+ AddUntilStep("wait for load", () => getTrack() != null);
+
+ AddAssert("check if menu music loops", () => getTrack().Looping);
+ }
+
+ private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track;
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs
new file mode 100644
index 0000000000..46bc279d5c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs
@@ -0,0 +1,60 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Game.Beatmaps;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets;
+using osu.Game.Screens.Multi;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public abstract class RoomManagerTestScene : MultiplayerTestScene
+ {
+ [Cached(Type = typeof(IRoomManager))]
+ protected TestRoomManager RoomManager { get; } = new TestRoomManager();
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("clear rooms", () => RoomManager.Rooms.Clear());
+ }
+
+ protected void AddRooms(int count, RulesetInfo ruleset = null)
+ {
+ AddStep("add rooms", () =>
+ {
+ for (int i = 0; i < count; i++)
+ {
+ var room = new Room
+ {
+ RoomID = { Value = i },
+ Name = { Value = $"Room {i}" },
+ Host = { Value = new User { Username = "Host" } },
+ EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }
+ };
+
+ if (ruleset != null)
+ {
+ room.Playlist.Add(new PlaylistItem
+ {
+ Ruleset = { Value = ruleset },
+ Beatmap =
+ {
+ Value = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata()
+ }
+ }
+ });
+ }
+
+ RoomManager.Rooms.Add(room);
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs
new file mode 100644
index 0000000000..67a53307fc
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Bindables;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Screens.Multi;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestRoomManager : IRoomManager
+ {
+ public event Action RoomsUpdated
+ {
+ add { }
+ remove { }
+ }
+
+ public readonly BindableList Rooms = new BindableList();
+
+ public Bindable InitialRoomsReceived { get; } = new Bindable(true);
+
+ IBindableList IRoomManager.Rooms => Rooms;
+
+ public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room);
+
+ public void JoinRoom(Room room, Action onSuccess = null, Action onError = null)
+ {
+ }
+
+ public void PartRoom()
+ {
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
index 5ef4dd6773..55b026eff6 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
@@ -4,12 +4,18 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Platform;
using osu.Framework.Testing;
+using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
+using osu.Game.Overlays;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Multi;
@@ -23,6 +29,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private TestPlaylist playlist;
+ private BeatmapManager manager;
+ private RulesetStore rulesets;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, AudioManager audio)
+ {
+ Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+ Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
+
+ manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait();
+ }
+
[Test]
public void TestNonEditableNonSelectable()
{
@@ -182,6 +200,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
}
+ [Test]
+ public void TestDownloadButtonHiddenInitiallyWhenBeatmapExists()
+ {
+ createPlaylist(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo);
+
+ AddAssert("download button hidden", () => !playlist.ChildrenOfType().Single().IsPresent);
+ }
+
+ [Test]
+ public void TestDownloadButtonVisibleInitiallyWhenBeatmapDoesNotExist()
+ {
+ var byOnlineId = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
+ byOnlineId.BeatmapSet.OnlineBeatmapSetID = 1337; // Some random ID that does not exist locally.
+
+ var byChecksum = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
+ byChecksum.MD5Hash = "1337"; // Some random checksum that does not exist locally.
+
+ createPlaylist(byOnlineId, byChecksum);
+
+ AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent));
+ }
+
private void moveToItem(int index, Vector2? offset = null)
=> AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType>().ElementAt(index), offset));
@@ -235,6 +275,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
}
+ private void createPlaylist(params BeatmapInfo[] beatmaps)
+ {
+ AddStep("create playlist", () =>
+ {
+ Child = playlist = new TestPlaylist(false, false)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500, 300)
+ };
+
+ int index = 0;
+
+ foreach (var b in beatmaps)
+ {
+ playlist.Items.Add(new PlaylistItem
+ {
+ ID = index++,
+ Beatmap = { Value = b },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ RequiredMods =
+ {
+ new OsuModHardRock(),
+ new OsuModDoubleTime(),
+ new OsuModAutoplay()
+ }
+ });
+ }
+ });
+
+ AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
+ }
+
private class TestPlaylist : DrawableRoomPlaylist
{
public new IReadOnlyDictionary> ItemMap => base.ItemMap;
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
index 8b74eb5f27..cdad37a9ad 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public void Setup() => Schedule(() =>
{
- Room.CopyFrom(new Room());
+ Room = new Room();
Child = new RoomInfo
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 77b41c89b0..b1f6ee3e3a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -1,30 +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 System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
-using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
-using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Lounge.Components;
-using osu.Game.Users;
using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneLoungeRoomsContainer : MultiplayerTestScene
+ public class TestSceneLoungeRoomsContainer : RoomManagerTestScene
{
- [Cached(Type = typeof(IRoomManager))]
- private TestRoomManager roomManager = new TestRoomManager();
-
private RoomsContainer container;
[BackgroundDependencyLoader]
@@ -39,34 +31,57 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
}
- public override void SetUpSteps()
- {
- base.SetUpSteps();
-
- AddStep("clear rooms", () => roomManager.Rooms.Clear());
- }
-
[Test]
public void TestBasicListChanges()
{
- addRooms(3);
+ AddRooms(3);
AddAssert("has 3 rooms", () => container.Rooms.Count == 3);
- AddStep("remove first room", () => roomManager.Rooms.Remove(roomManager.Rooms.FirstOrDefault()));
+ AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault()));
AddAssert("has 2 rooms", () => container.Rooms.Count == 2);
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
AddStep("select first room", () => container.Rooms.First().Action?.Invoke());
- AddAssert("first room selected", () => Room == roomManager.Rooms.First());
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
AddStep("join first room", () => container.Rooms.First().Action?.Invoke());
- AddAssert("first room joined", () => roomManager.Rooms.First().Status.Value is JoinedRoomStatus);
+ AddAssert("first room joined", () => RoomManager.Rooms.First().Status.Value is JoinedRoomStatus);
+ }
+
+ [Test]
+ public void TestKeyboardNavigation()
+ {
+ AddRooms(3);
+
+ AddAssert("no selection", () => checkRoomSelected(null));
+
+ press(Key.Down);
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+
+ press(Key.Up);
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+
+ press(Key.Down);
+ press(Key.Down);
+ AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last()));
+
+ press(Key.Enter);
+ AddAssert("last room joined", () => RoomManager.Rooms.Last().Status.Value is JoinedRoomStatus);
+ }
+
+ private void press(Key down)
+ {
+ AddStep($"press {down}", () =>
+ {
+ InputManager.PressKey(down);
+ InputManager.ReleaseKey(down);
+ });
}
[Test]
public void TestStringFiltering()
{
- addRooms(4);
+ AddRooms(4);
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
@@ -82,8 +97,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRulesetFiltering()
{
- addRooms(2, new OsuRuleset().RulesetInfo);
- addRooms(3, new CatchRuleset().RulesetInfo);
+ AddRooms(2, new OsuRuleset().RulesetInfo);
+ AddRooms(3, new CatchRuleset().RulesetInfo);
AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
@@ -96,64 +111,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
- private void addRooms(int count, RulesetInfo ruleset = null)
- {
- AddStep("add rooms", () =>
- {
- for (int i = 0; i < count; i++)
- {
- var room = new Room
- {
- RoomID = { Value = i },
- Name = { Value = $"Room {i}" },
- Host = { Value = new User { Username = "Host" } },
- EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }
- };
-
- if (ruleset != null)
- {
- room.Playlist.Add(new PlaylistItem
- {
- Ruleset = { Value = ruleset },
- Beatmap =
- {
- Value = new BeatmapInfo
- {
- Metadata = new BeatmapMetadata()
- }
- }
- });
- }
-
- roomManager.Rooms.Add(room);
- }
- });
- }
+ private bool checkRoomSelected(Room room) => Room == room;
private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus();
- private class TestRoomManager : IRoomManager
- {
- public event Action RoomsUpdated
- {
- add { }
- remove { }
- }
-
- public readonly BindableList Rooms = new BindableList();
- IBindableList IRoomManager.Rooms => Rooms;
-
- public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room);
-
- public void JoinRoom(Room room, Action onSuccess = null, Action onError = null)
- {
- }
-
- public void PartRoom()
- {
- }
- }
-
private class JoinedRoomStatus : RoomStatus
{
public override string Message => "Joined";
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs
new file mode 100644
index 0000000000..68987127d2
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs
@@ -0,0 +1,58 @@
+// 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.Screens;
+using osu.Framework.Testing;
+using osu.Game.Graphics.Containers;
+using osu.Game.Screens.Multi.Lounge;
+using osu.Game.Screens.Multi.Lounge.Components;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneLoungeSubScreen : RoomManagerTestScene
+ {
+ private LoungeSubScreen loungeScreen;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("push screen", () => LoadScreen(loungeScreen = new LoungeSubScreen
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 0.5f,
+ }));
+
+ AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
+ }
+
+ private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First();
+
+ [Test]
+ public void TestScrollSelectedIntoView()
+ {
+ AddRooms(30);
+
+ AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First()));
+
+ AddStep("select last room", () => roomsContainer.Rooms.Last().Action?.Invoke());
+
+ AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms.First()));
+ AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms.Last()));
+ }
+
+ private bool checkRoomVisible(DrawableRoom room) =>
+ loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad
+ .Contains(room.ScreenSpaceDrawQuad.Centre);
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
index 24d9f5ab12..01cd26fbe5 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public void Setup() => Schedule(() =>
{
- Room.Playlist.Clear();
+ Room = new Room();
Child = new MatchBeatmapDetailArea
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
index 38eb3181bf..e5943105b7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public TestSceneMatchHeader()
{
+ Room = new Room();
Room.Playlist.Add(new PlaylistItem
{
Beatmap =
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
index 7ba1782a28..c24c6c4ba3 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
@@ -6,6 +6,7 @@ using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.API;
+using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Users;
using osuTK;
@@ -18,7 +19,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public TestSceneMatchLeaderboard()
{
- Room.RoomID.Value = 3;
+ Room = new Room { RoomID = { Value = 3 } };
Add(new MatchLeaderboard
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs
deleted file mode 100644
index 72bbc11cd0..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Screens.Multi.Match.Components;
-using osuTK;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public class TestSceneMatchLeaderboardChatDisplay : MultiplayerTestScene
- {
- protected override bool UseOnlineAPI => true;
-
- public TestSceneMatchLeaderboardChatDisplay()
- {
- Room.RoomID.Value = 7;
-
- Add(new Container
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500),
- Child = new LeaderboardChatDisplay
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
index 34c6940552..fdc20dc477 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
@@ -133,6 +133,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
remove { }
}
+ public Bindable InitialRoomsReceived { get; } = new Bindable(true);
+
public IBindableList Rooms { get; } = null;
public void CreateRoom(Room room, Action onSuccess = null, Action onError = null)
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
index 5cff2d7d05..c62479faa0 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
@@ -14,6 +14,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Multi.Components;
@@ -95,7 +96,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public void Setup() => Schedule(() =>
{
- Room.Playlist.Clear();
+ Room = new Room();
});
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
index d678d5a814..2e22317539 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
@@ -5,7 +5,9 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Bindables;
+using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -29,18 +31,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached(typeof(IRoomManager))]
private readonly TestRoomManager roomManager = new TestRoomManager();
- [Resolved]
- private BeatmapManager beatmaps { get; set; }
-
- [Resolved]
- private RulesetStore rulesets { get; set; }
+ private BeatmapManager manager;
+ private RulesetStore rulesets;
private TestMatchSubScreen match;
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, AudioManager audio)
+ {
+ Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+ Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
+
+ manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait();
+ }
+
[SetUp]
public void Setup() => Schedule(() =>
{
- Room.CopyFrom(new Room());
+ Room = new Room();
});
[SetUpSteps]
@@ -50,6 +58,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for load", () => match.IsCurrentScreen());
}
+ [Test]
+ public void TestLoadSimpleMatch()
+ {
+ AddStep("set room properties", () =>
+ {
+ Room.RoomID.Value = 1;
+ Room.Name.Value = "my awesome room";
+ Room.Host.Value = new User { Id = 2, Username = "peppy" };
+ Room.RecentParticipants.Add(Room.Host.Value);
+ Room.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo }
+ });
+ });
+ }
+
[Test]
public void TestPlaylistItemSelectedOnCreate()
{
@@ -75,10 +100,49 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("first playlist item selected", () => match.SelectedItem.Value == Room.Playlist[0]);
}
+ [Test]
+ public void TestBeatmapUpdatedOnReImport()
+ {
+ BeatmapSetInfo importedSet = null;
+
+ AddStep("import altered beatmap", () =>
+ {
+ var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo);
+ beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1;
+
+ importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result;
+ });
+
+ AddStep("load room", () =>
+ {
+ Room.Name.Value = "my awesome room";
+ Room.Host.Value = new User { Id = 2, Username = "peppy" };
+ Room.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = importedSet.Beatmaps[0] },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo }
+ });
+ });
+
+ AddStep("create room", () =>
+ {
+ InputManager.MoveMouseTo(match.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize == 1);
+
+ AddStep("re-import original beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait());
+
+ AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize != 1);
+ }
+
private class TestMatchSubScreen : MatchSubScreen
{
public new Bindable SelectedItem => base.SelectedItem;
+ public new Bindable Beatmap => base.Beatmap;
+
public TestMatchSubScreen(Room room)
: base(room)
{
@@ -93,6 +157,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
remove => throw new NotImplementedException();
}
+ public Bindable InitialRoomsReceived { get; } = new Bindable(true);
+
public IBindableList Rooms { get; } = new BindableList();
public void CreateRoom(Room room, Action onSuccess = null, Action onError = null)
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs
index 7ea3bba23f..2b4cac06bd 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
using osuTK;
@@ -12,10 +13,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
protected override bool UseOnlineAPI => true;
- public TestSceneOverlinedParticipants()
+ [SetUp]
+ public void Setup() => Schedule(() =>
{
- Room.RoomID.Value = 7;
- }
+ Room = new Room { RoomID = { Value = 7 } };
+ });
[Test]
public void TestHorizontalLayout()
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs
index 14b7934dc7..88b2a6a4bc 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public TestSceneOverlinedPlaylist()
{
+ Room = new Room { RoomID = { Value = 7 } };
+
for (int i = 0; i < 10; i++)
{
Room.Playlist.Add(new PlaylistItem
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs
index 9c4c45f94a..f71c5fc5d2 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.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 NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
namespace osu.Game.Tests.Visual.Multiplayer
@@ -10,10 +12,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
protected override bool UseOnlineAPI => true;
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Room = new Room { RoomID = { Value = 7 } };
+ });
+
public TestSceneParticipantsList()
{
- Room.RoomID.Value = 7;
-
Add(new ParticipantsList { RelativeSizeAxes = Axes.Both });
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
new file mode 100644
index 0000000000..9fc7c336cb
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
@@ -0,0 +1,124 @@
+// 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.Threading.Tasks;
+using NUnit.Framework;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Multi.Ranking;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneTimeshiftResultsScreen : ScreenTestScene
+ {
+ private bool roomsReceived;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ roomsReceived = false;
+ bindHandler();
+ });
+
+ [Test]
+ public void TestShowResultsWithScore()
+ {
+ createResults(new TestScoreInfo(new OsuRuleset().RulesetInfo));
+ AddWaitStep("wait for display", 5);
+ }
+
+ [Test]
+ public void TestShowResultsNullScore()
+ {
+ createResults(null);
+ AddWaitStep("wait for display", 5);
+ }
+
+ [Test]
+ public void TestShowResultsNullScoreWithDelay()
+ {
+ AddStep("bind delayed handler", () => bindHandler(3000));
+ createResults(null);
+ AddUntilStep("wait for rooms to be received", () => roomsReceived);
+ AddWaitStep("wait for display", 5);
+ }
+
+ private void createResults(ScoreInfo score)
+ {
+ AddStep("load results", () =>
+ {
+ LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo }
+ }));
+ });
+ }
+
+ private void bindHandler(double delay = 0)
+ {
+ var roomScores = new List();
+
+ for (int i = 0; i < 10; i++)
+ {
+ roomScores.Add(new RoomScore
+ {
+ ID = i,
+ Accuracy = 0.9 - 0.01 * i,
+ EndedAt = DateTimeOffset.Now.Subtract(TimeSpan.FromHours(i)),
+ Passed = true,
+ Rank = ScoreRank.B,
+ MaxCombo = 999,
+ TotalScore = 999999 - i * 1000,
+ User = new User
+ {
+ Id = 2,
+ Username = $"peppy{i}",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ },
+ Statistics =
+ {
+ { HitResult.Miss, 1 },
+ { HitResult.Meh, 50 },
+ { HitResult.Good, 100 },
+ { HitResult.Great, 300 },
+ }
+ });
+ }
+
+ ((DummyAPIAccess)API).HandleRequest = request =>
+ {
+ switch (request)
+ {
+ case GetRoomPlaylistScoresRequest r:
+ if (delay == 0)
+ success();
+ else
+ {
+ Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(delay));
+ Schedule(success);
+ });
+ }
+
+ void success()
+ {
+ r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores });
+ roomsReceived = true;
+ }
+
+ break;
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
index 31afce86ae..c4acf4f7da 100644
--- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
+++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
@@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Rulesets;
+using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osuTK.Graphics;
@@ -100,6 +101,8 @@ namespace osu.Game.Tests.Visual.Navigation
public new BeatmapManager BeatmapManager => base.BeatmapManager;
+ public new ScoreManager ScoreManager => base.ScoreManager;
+
public new SettingsPanel Settings => base.Settings;
public new MusicController MusicController => base.MusicController;
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
new file mode 100644
index 0000000000..b2e18849c9
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
@@ -0,0 +1,155 @@
+// Copyright (c) ppy Pty Ltd