diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml
index 91ca622f55..ff6d869e72 100644
--- a/.github/ISSUE_TEMPLATE/bug-issue.yml
+++ b/.github/ISSUE_TEMPLATE/bug-issue.yml
@@ -58,7 +58,8 @@ body:
The default places to find the logs on desktop platforms are as follows:
- `%AppData%/osu/logs` *on Windows*
- - `~/.local/share/osu/logs` *on Linux & macOS*
+ - `~/.local/share/osu/logs` *on Linux*
+ - `~/Library/Application Support/osu/logs` *on macOS*
If you have selected a custom location for the game files, you can find the `logs` folder there.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 56b3ebe87b..213c5082ab 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ jobs:
run: dotnet tool restore
- name: Restore Packages
- run: dotnet restore
+ run: dotnet restore osu.Desktop.slnf
- name: Restore inspectcode cache
uses: actions/cache@v3
@@ -113,27 +113,36 @@ jobs:
with:
dotnet-version: "6.0.x"
- - name: Setup MSBuild
- uses: microsoft/setup-msbuild@v1
+ - name: Install .NET workloads
+ run: dotnet workload install maui-android
- - name: Build
- run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
+ - name: Compile
+ run: dotnet build -c Debug osu.Android.slnf
build-only-ios:
name: Build only (iOS)
- runs-on: macos-latest
+ # change to macos-latest once GitHub finishes migrating all repositories to macOS 12.
+ runs-on: macos-12
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
+ # see https://github.com/actions/runner-images/issues/6771#issuecomment-1354713617
+ # remove once all workflow VMs use Xcode 14.1
+ - name: Set Xcode Version
+ shell: bash
+ run: |
+ sudo xcode-select -s "/Applications/Xcode_14.1.app"
+ echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.1.app" >> $GITHUB_ENV
+
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x"
- # Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
- # cannot accept .sln(f) files as arguments.
- # Build just the main game for now.
+ - name: Install .NET Workloads
+ run: dotnet workload install maui-ios
+
- name: Build
- run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
+ run: dotnet build -c Debug osu.iOS
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ae2bdd2e82..9f7d88f5c7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,136 +2,87 @@
Thank you for showing interest in the development of osu!. 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)
+1. [Reporting bugs](#reporting-bugs)
+2. [Providing general feedback](#providing-general-feedback)
+3. [Issue or discussion?](#issue-or-discussion)
+4. [Submitting pull requests](#submitting-pull-requests)
+5. [Resources](#resources)
-## I would like to submit an issue!
+## Reporting bugs
-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.
+A **bug** is a situation in which there is something clearly *and objectively* wrong with the game. Examples of applicable bug reports are:
-* **Before submitting an issue, try searching existing issues first.**
+- The game crashes to desktop when I start a beatmap
+- Friends appear twice in the friend listing
+- The game slows down a lot when I play this specific map
+- A piece of text is overlapping another piece of text on the screen
- 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.
+To track bug reports, we primarily use GitHub **issues**. When opening an issue, please keep in mind the following:
-* **When submitting a bug report, please try to include as much detail as possible.**
+- Before opening the issue, please search for any similar existing issues using the text search bar and the issue labels. This includes both open and closed issues (we may have already fixed something, but the fix hasn't yet been released).
+- When opening the issue, please fill out as much of the issue template as you can. In particular, please make sure to include logs and screenshots as much as possible. The instructions on how to find the log files are included in the issue template.
+- We may ask you for follow-up information to reproduce or debug the problem. Please look out for this and provide follow-up info if we request it.
- 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:
+If we cannot reproduce the issue, it is deemed low priority, or it is deemed to be specific to your setup in some way, the issue may be downgraded to a discussion. This will be done by a maintainer for you.
- * the in-game logs, which are located at:
- * `%AppData%/osu/logs` (on Windows),
- * `~/.local/share/osu/logs` (on Linux and macOS),
- * `Android/data/sh.ppy.osulazer/files/logs` (on Android),
- * on iOS they can be obtained by connecting your device to your desktop and [copying the `logs` directory from the app's own document storage using iTunes](https://support.apple.com/en-us/HT201301#copy-to-computer),
- * your system specifications (including the operating system and platform you are playing on),
- * a reproduction scenario (list of steps you have performed leading up to the occurrence of the bug),
- * a video or picture of the bug, if at all possible.
+## Providing general feedback
-* **Provide more information when asked to do so.**
+If you wish to:
- 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 osu! 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!
+- provide *subjective* feedback on the game (about how the UI looks, about how the default skin works, about game mechanics, about how the PP and scoring systems work, etc.),
+- suggest a new feature to be added to the game,
+- report a non-specific problem with the game that you think may be connected to your hardware or operating system specifically,
-* **When submitting a feature proposal, please describe it in the most understandable way you can.**
+then it is generally best to start with a **discussion** first. Discussions are a good avenue to group subjective feedback on a single topic, or gauge interest in a particular feature request.
- 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.
+When opening a discussion, please keep in mind the following:
-* **Refrain from posting "+1" comments.**
+- Use the search function to see if your idea has been proposed before, or if there is already a thread about a particular issue you wish to raise.
+- If proposing a feature, please try to explain the feature in as much detail as possible.
+- If you're reporting a non-specific problem, please provide applicable logs, screenshots, or video that illustrate the issue.
- 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.
+If a discussion gathers enough traction, then it may be converted into an issue. This will be done by a maintainer for you.
-* **Refrain from asking if an issue has been resolved yet.**
+## Issue or discussion?
- 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.
+We realise that the line between an issue and a discussion may be fuzzy, so while we ask you to use your best judgement based on the description above, please don't think about it too hard either. Feedback in a slightly wrong place is better than no feedback at all.
-* **Avoid long discussions about non-development topics.**
+When in doubt, it's probably best to start with a discussion first. We will escalate to issues as needed.
- 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.
+## Submitting pull requests
-## I would like to submit a pull request!
+While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
-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.
+The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
-However, do keep in mind that the core team is committed to bringing osu!(lazer) up to par with osu!(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).
+In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
-Here are some key things to note before jumping in:
+If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library).
-* **Make sure you are comfortable with C\# and your development environment.**
+Aside from the above, below is a brief checklist of things to watch out when you're preparing your code changes:
- 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.
+- Make sure you're comfortable with the principles of object-oriented programming, the syntax of C\# and your development environment.
+- Make sure you are familiar with [git](https://git-scm.com/) and [the pull request workflow](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests).
+- Please do not make code changes via the GitHub web interface.
+- Please add tests for your changes. We expect most new features and bugfixes to have test coverage, unless the effort of adding them is prohibitive. The visual testing methodology we use is described in more detail [here](https://github.com/ppy/osu-framework/wiki/Development-and-Testing).
+- Please run tests and code style analysis (via `InspectCode.{ps1,sh}` scripts in the root of this repository) before opening the PR. This is particularly important if you're a first-time contributor, as CI will not run for your PR until we allow it to do so.
- 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.
+After you're done with your changes and you wish to open the PR, please observe the following recommendations:
-* **Make sure you are familiar with git and the pull request workflow.**
+- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
+- Please avoid pushing untested or incomplete code.
+- Please do not force-push or rebase unless we ask you to.
+- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
- [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.
+We are highly committed to quality when it comes to the osu! 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.
- 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).
+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, discussion, 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.
-* **Double-check designs before starting work on new functionality.**
+## Resources
- 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 osu! 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.
+- [Development roadmap](https://github.com/orgs/ppy/projects/7/views/6): What the core team is currently working on
+- [`ppy/osu-framework` wiki](https://github.com/ppy/osu-framework/wiki): Contains introductory information about osu!framework, the bespoke 2D game framework we use for the game
+- [`ppy/osu` wiki](https://github.com/ppy/osu/wiki): Contains articles about various technical aspects of the game
+- [Public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library): Contains finished and draft designs for osu!
diff --git a/README.md b/README.md
index 75d61dad4d..f3f025fa10 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:**
-| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
+| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
| ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
@@ -101,9 +101,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it
## Contributing
-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.
+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. Please refer to the [contributing guidelines](CONTRIBUTING.md) to understand how to help in the most effective way possible.
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
index 092a013614..d09e7647e0 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
osu.Game.Rulesets.EmptyFreeform
Library
AnyCPU
@@ -12,4 +12,4 @@
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
index a3607343c9..9c8867f4ef 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
osu.Game.Rulesets.Pippidon
Library
AnyCPU
@@ -12,4 +12,4 @@
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
index 2ea52429ab..5bf3884f53 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
osu.Game.Rulesets.EmptyScrolling
Library
AnyCPU
@@ -12,4 +12,4 @@
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
index a3607343c9..9c8867f4ef 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
osu.Game.Rulesets.Pippidon
Library
AnyCPU
@@ -12,4 +12,4 @@
-
\ No newline at end of file
+
diff --git a/appveyor.yml b/appveyor.yml
index 5be73f9875..ed48a997e8 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,6 +1,6 @@
clone_depth: 1
version: '{branch}-{build}'
-image: Visual Studio 2019
+image: Visual Studio 2022
cache:
- '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
@@ -11,6 +11,8 @@ dotnet_csproj:
before_build:
- cmd: dotnet --info # Useful when version mismatch between CI and local
+ - cmd: dotnet workload install maui-android # Change to `dotnet workload restore` once there's no old projects
+ - cmd: dotnet workload install maui-ios # Change to `dotnet workload restore` once there's no old projects
- cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
build:
diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml
index adf98848bc..175c8d0f1b 100644
--- a/appveyor_deploy.yml
+++ b/appveyor_deploy.yml
@@ -1,6 +1,6 @@
clone_depth: 1
version: '{build}'
-image: Visual Studio 2019
+image: Visual Studio 2022
test: off
skip_non_tags: true
configuration: Release
@@ -83,4 +83,4 @@ artifacts:
deploy:
- provider: Environment
- name: nuget
\ No newline at end of file
+ name: nuget
diff --git a/osu.Android.props b/osu.Android.props
index a4900132b1..c6cf7812d1 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -1,61 +1,20 @@
- 8.0
- bin\$(Configuration)
- 4
- 2.0
- false
- false
- Library
- 512
- Off
- True
- Xamarin.Android.Net.AndroidClientHandler
- v10.0
- false
- true
- armeabi-v7a;x86;arm64-v8a
- true
- cjk,mideast,other,rare,west
- SdkOnly
- prompt
-
-
- True
- portable
- False
- DEBUG;TRACE
- false
- true
- false
-
-
- false
- None
- True
- false
- False
+ 21.0
+ android-x86;android-arm;android-arm64
+ apk
+ CJK;Mideast;Rare;West;Other;
+ Xamarin.Android.Net.AndroidMessageHandler
+
+ true
true
-
- osu.licenseheader
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ true
+
diff --git a/osu.Android/Properties/AndroidManifest.xml b/osu.Android/AndroidManifest.xml
similarity index 71%
rename from osu.Android/Properties/AndroidManifest.xml
rename to osu.Android/AndroidManifest.xml
index 165a64a424..bc2f49b1a9 100644
--- a/osu.Android/Properties/AndroidManifest.xml
+++ b/osu.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index be40db7508..ca3d628447 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
@@ -74,11 +75,23 @@ namespace osu.Android
Debug.Assert(Resources?.DisplayMetrics != null);
Point displaySize = new Point();
+#pragma warning disable 618 // GetSize is deprecated
WindowManager.DefaultDisplay.GetSize(displaySize);
+#pragma warning restore 618
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
bool isTablet = smallestWidthDp >= 600f;
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
+
+ // Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
+ // The assembly files are not available as files either after native AOT.
+ // Manually load them so that they can be loaded by RulesetStore.loadFromAppDomain.
+ // REMEMBER to fully uninstall previous version every time when investigating this!
+ // Don't forget osu.Game.Tests.Android too.
+ Assembly.Load("osu.Game.Rulesets.Osu");
+ Assembly.Load("osu.Game.Rulesets.Taiko");
+ Assembly.Load("osu.Game.Rulesets.Catch");
+ Assembly.Load("osu.Game.Rulesets.Mania");
}
protected override void OnNewIntent(Intent intent) => handleIntent(intent);
@@ -127,7 +140,7 @@ namespace osu.Android
cursor.MoveToFirst();
- int filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
+ int filenameColumn = cursor.GetColumnIndex(IOpenableColumns.DisplayName);
string filename = cursor.GetString(filenameColumn);
// SharpCompress requires archive streams to be seekable, which the stream opened by
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 1c6f41a7ec..0227d2aec2 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -5,7 +5,7 @@
using System;
using Android.App;
-using Android.OS;
+using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Input.Handlers;
@@ -14,7 +14,6 @@ using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Updater;
using osu.Game.Utils;
-using Xamarin.Essentials;
namespace osu.Android
{
@@ -48,7 +47,7 @@ namespace osu.Android
// https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
string versionName = string.Empty;
- if (Build.VERSION.SdkInt >= BuildVersionCodes.P)
+ if (OperatingSystem.IsAndroidVersionAtLeast(28))
{
versionName = packageInfo.LongVersionCode.ToString();
// ensure we only read the trailing portion of long (the part we are interested in).
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 004cc8c39c..1507bfaa29 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -1,73 +1,22 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Android
osu.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
- false
-
-
- cjk;mideast;other;rare;west
- d8
- r8
-
-
- None
- cjk;mideast;other;rare;west
- true
+ true
+
+ false
+ 0.0.0
+ 1
+ $(Version)
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
- {58f6c80c-1253-4a0e-a465-b8c85ebeadf3}
- osu.Game.Rulesets.Catch
-
-
- {48f4582b-7687-4621-9cbe-5c24197cb536}
- osu.Game.Rulesets.Mania
-
-
- {c92a607b-1fdd-4954-9f92-03ff547d9080}
- osu.Game.Rulesets.Osu
-
-
- {f167e17a-7de6-4af5-b920-a5112296c695}
- osu.Game.Rulesets.Taiko
-
-
- {2a66dd92-adb1-4994-89e2-c94e04acda0d}
- osu.Game
-
-
-
-
-
-
-
- 5.0.0
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
index f8c3fcd894..bf7c0bfeca 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
index 94fdba4a3e..4ee3219442 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
@@ -1,49 +1,24 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Rulesets.Catch.Tests
osu.Game.Rulesets.Catch.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
- None
- cjk;mideast;other;rare;west
- true
-
-
-
-
-
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
+
+
+ %(RecursiveDir)%(Filename)%(Extension)
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {58f6c80c-1253-4a0e-a465-b8c85ebeadf3}
- osu.Game.Rulesets.Catch
-
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
+
+
-
-
- 5.0.0
-
-
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
index 71d943ece1..1fcb0aa427 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Rulesets.Catch.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
index 16a2b99997..5ace6c07f5 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
index be6044bbd0..acf12bb0ac 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
@@ -1,35 +1,19 @@
-
-
+
- Debug
- iPhoneSimulator
- {4004C7B7-1A62-43F1-9DF2-52450FA67E70}
Exe
+ net6.0-ios
+ 13.4
osu.Game.Rulesets.Catch.Tests
osu.Game.Rulesets.Catch.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}
- osu.Game.Rulesets.Catch
-
+
+
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
index 50b928d509..c48bf7adc9 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
@@ -18,6 +18,36 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+ [Test]
+ public void TestAlwaysHidden()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new CatchModNoScope
+ {
+ HiddenComboCount = { Value = 0 },
+ },
+ Autoplay = true,
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Fruit
+ {
+ X = CatchPlayfield.CENTER_X * 0.5f,
+ StartTime = 1000,
+ },
+ new Fruit
+ {
+ X = CatchPlayfield.CENTER_X * 1.5f,
+ StartTime = 2000,
+ }
+ }
+ }
+ });
+ }
+
[Test]
public void TestVisibleDuringBreak()
{
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
index ac39b91f00..f009c10a9c 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 9f5d007114..7774a7da09 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index 835f7c2d27..ab61b14ac4 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index f37479f84a..42cfde268e 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -51,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
- CatchHitObject lastObject = null;
+ CatchHitObject? lastObject = null;
List objects = new List();
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceAttributes.cs
index ccdfd30200..1335fc2d23 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceAttributes.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceAttributes.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Catch.Difficulty
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index 2a07b8019e..b30b85be2d 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
index c44480776f..3bcfce3a56 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Catch.Objects;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 827c28f7de..cfb3fe40be 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Preprocessing;
diff --git a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
index e64a51f03a..31075db7d1 100644
--- a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
+++ b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
index 166fa44303..5f22ef5c12 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
index 2c545e8f0e..f6dd67889e 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
index 94373147d2..d2d605a6fe 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -20,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
[Resolved]
- private Playfield playfield { get; set; }
+ private Playfield playfield { get; set; } = null!;
public CatchPlacementBlueprint()
: base(new THitObject())
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
index 87c33a9cb8..8220fb88b4 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -31,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
[Resolved]
- private Playfield playfield { get; set; }
+ private Playfield playfield { get; set; } = null!;
protected CatchSelectionBlueprint(THitObject hitObject)
: base(hitObject)
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
index 006ea6e9cf..74d6565600 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
@@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -42,9 +39,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
private readonly List previousVertexStates = new List();
- [Resolved(CanBeNull = true)]
- [CanBeNull]
- private IBeatSnapProvider beatSnapProvider { get; set; }
+ [Resolved]
+ private IBeatSnapProvider? beatSnapProvider { get; set; }
protected EditablePath(Func positionToTime)
{
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
index dc2b038e01..c7805544ea 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs
index ac0c850bca..c1f46539fa 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs
index ba2f5ed0eb..3a7d6d87f2 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Game.Rulesets.Catch.Objects;
using osuTK;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
index 6deb5a174f..a22abcb76d 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
index 3a44f7ac8a..c7a26ca15a 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
@@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
@@ -25,9 +22,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
// To handle when the editor is scrolled while dragging.
private Vector2 dragStartPosition;
- [Resolved(CanBeNull = true)]
- [CanBeNull]
- private IEditorChangeHandler changeHandler { get; set; }
+ [Resolved]
+ private IEditorChangeHandler? changeHandler { get; set; }
public SelectionEditablePath(Func positionToTime)
: base(positionToTime)
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
index a1d5d7ae3e..9d450cd355 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs
index 49570d3735..07d7c72698 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -15,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public partial class VertexPiece : Circle
{
[Resolved]
- private OsuColour osuColour { get; set; }
+ private OsuColour osuColour { get; set; } = null!;
public VertexPiece()
{
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
index af75023e68..72592891fb 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
index 319b1b5e20..2737b283ef 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
index 0e2ee334ff..03ec674abb 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
@@ -24,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private int lastEditablePathId = -1;
- private InputManager inputManager;
+ private InputManager inputManager = null!;
public JuiceStreamPlacementBlueprint()
{
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
index 99ec5e2b0c..49d778ad08 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
@@ -1,11 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
@@ -53,9 +50,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private Vector2 rightMouseDownPosition;
- [Resolved(CanBeNull = true)]
- [CanBeNull]
- private EditorBeatmap editorBeatmap { get; set; }
+ [Resolved]
+ private EditorBeatmap? editorBeatmap { get; set; }
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
: base(hitObject)
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs
index 6570a19a92..c7a41a4e22 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Catch.Edit.Checks;
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
index 9408a9f95c..3979d30616 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -20,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit
protected override SelectionHandler CreateSelectionHandler() => new CatchSelectionHandler();
- public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
+ public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
{
switch (hitObject)
{
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs
index e5cdcf706c..cf6ddc66ed 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs
@@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -39,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit
private readonly List verticalLineVertices = new List();
[Resolved]
- private Playfield playfield { get; set; }
+ private Playfield playfield { get; set; } = null!;
private ScrollingHitObjectContainer hitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
@@ -106,8 +103,7 @@ namespace osu.Game.Rulesets.Catch.Edit
}
}
- [CanBeNull]
- public SnapResult GetSnappedPosition(Vector2 screenSpacePosition)
+ public SnapResult? GetSnappedPosition(Vector2 screenSpacePosition)
{
double time = hitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition);
@@ -121,9 +117,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return new SnapResult(originPosition, StartTime);
}
- return enumerateSnappingCandidates(time)
- .OrderBy(pos => Vector2.DistanceSquared(screenSpacePosition, pos.ScreenSpacePosition))
- .FirstOrDefault();
+ return enumerateSnappingCandidates(time).MinBy(pos => Vector2.DistanceSquared(screenSpacePosition, pos.ScreenSpacePosition));
}
private IEnumerable enumerateSnappingCandidates(double time)
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
index bca89c6024..c9481c2757 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index bbdffbf39c..ea5f54a775 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
@@ -32,9 +29,9 @@ namespace osu.Game.Rulesets.Catch.Edit
{
private const float distance_snap_radius = 50;
- private CatchDistanceSnapGrid distanceSnapGrid;
+ private CatchDistanceSnapGrid distanceSnapGrid = null!;
- private InputManager inputManager;
+ private InputManager inputManager = null!;
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
{
@@ -117,7 +114,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return base.OnPressed(e);
}
- protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) =>
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
{
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
@@ -150,8 +147,7 @@ namespace osu.Game.Rulesets.Catch.Edit
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
- [CanBeNull]
- private PalpableCatchHitObject getLastSnappableHitObject(double time)
+ private PalpableCatchHitObject? getLastSnappableHitObject(double time)
{
var hitObject = EditorBeatmap.HitObjects.OfType().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower));
@@ -168,8 +164,7 @@ namespace osu.Game.Rulesets.Catch.Edit
}
}
- [CanBeNull]
- private PalpableCatchHitObject getDistanceSnapGridSourceHitObject()
+ private PalpableCatchHitObject? getDistanceSnapGridSourceHitObject()
{
switch (BlueprintContainer.CurrentTool)
{
@@ -188,7 +183,8 @@ namespace osu.Game.Rulesets.Catch.Edit
if (EditorBeatmap.PlacementObject.Value is JuiceStream)
{
// Juice stream path is not subject to snapping.
- return null;
+ if (BlueprintContainer.CurrentPlacement.PlacementActive is PlacementBlueprint.PlacementState.Active)
+ return null;
}
double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(inputManager.CurrentState.Mouse.Position);
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs
index 889d3909bd..bd33080109 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Catch.Objects;
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
index d9d7047920..418351e2f3 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -23,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Edit
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
[Resolved]
- private Playfield playfield { get; set; }
+ private Playfield playfield { get; set; } = null!;
public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
index 0a50ad1df4..7ad2106ab9 100644
--- a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
+++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@@ -16,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Edit
{
public readonly BindableDouble TimeRangeMultiplier = new BindableDouble(1);
- public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null)
: base(ruleset, beatmap, mods)
{
}
diff --git a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
index 5c13692b51..f776fe39c1 100644
--- a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
+++ b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
diff --git a/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs
index 85cf89f700..cb66e2952e 100644
--- a/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs
+++ b/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
index 15f6e4a64d..b919102215 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
index 90aa6f41a1..8fd7b93e4c 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Judgements
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
index e5d6429660..ccafe0abc4 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs
index 6cc79f9619..4cec61d016 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using JetBrains.Annotations;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@@ -22,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
///
public bool CatcherHyperDash;
- public CatchJudgementResult([NotNull] HitObject hitObject, [NotNull] Judgement judgement)
+ public CatchJudgementResult(HitObject hitObject, Judgement judgement)
: base(hitObject, judgement)
{
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
index c9052e3c39..d957d4171b 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Judgements
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
index 19b4a39f97..ddeea51ecb 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
@@ -26,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.Mods
var catchPlayfield = (CatchPlayfield)playfield;
bool shouldAlwaysShowCatcher = IsBreakTime.Value;
float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha;
+
+ // AlwaysPresent required for catcher to still act on input when fully hidden.
+ catchPlayfield.CatcherArea.AlwaysPresent = true;
catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index e5541e49c1..b45f95a8e6 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Threading;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index cd2b8348e2..f4bd515995 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using Newtonsoft.Json;
using osu.Framework.Bindables;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs
index 65d91bffe2..bfeb37b1b7 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.cs
index ed8bf17747..d228c629c0 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs
index d296052220..99dcac5268 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
index 436edf6367..0c26c52171 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -19,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[Cached(typeof(IHasCatchObjectState))]
public abstract partial class CaughtObject : SkinnableDrawable, IHasCatchObjectState
{
- public PalpableCatchHitObject HitObject { get; private set; }
+ public PalpableCatchHitObject HitObject { get; private set; } = null!;
public Bindable AccentColour { get; } = new Bindable();
public Bindable HyperDash { get; } = new Bindable();
public Bindable IndexInBeatmap { get; } = new Bindable();
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
index 51addaebd5..26e304cf3f 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default;
@@ -18,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
}
- public DrawableBanana([CanBeNull] Banana h)
+ public DrawableBanana(Banana? h)
: base(h)
{
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
index c5ae1b5526..03adbce885 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
@@ -19,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
}
- public DrawableBananaShower([CanBeNull] BananaShower s)
+ public DrawableBananaShower(BananaShower? s)
: base(s)
{
RelativeSizeAxes = Axes.X;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index c25bc7d076..7f8c17861d 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
XOffsetBindable.UnbindFrom(HitObject.XOffsetBindable);
}
+ [CanBeNull]
public Func CheckPosition;
protected override JudgementResult CreateResult(Judgement judgement) => new CatchJudgementResult(HitObject, judgement);
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
index e8b0c4a9fb..8f32cdcc31 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default;
@@ -18,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
}
- public DrawableDroplet([CanBeNull] CatchHitObject h)
+ public DrawableDroplet(CatchHitObject? h)
: base(h)
{
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
index 4347c77383..52c53523e6 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default;
@@ -18,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
}
- public DrawableFruit([CanBeNull] Fruit h)
+ public DrawableFruit(Fruit? h)
: base(h)
{
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
index 1ad1664122..41ecf59276 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
@@ -19,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
}
- public DrawableJuiceStream([CanBeNull] JuiceStream s)
+ public DrawableJuiceStream(JuiceStream? s)
: base(s)
{
RelativeSizeAxes = Axes.X;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
index 8468cc0a6a..4a9661f108 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -42,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public float DisplayRotation => ScalingContainer.Rotation;
- protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h)
+ protected DrawablePalpableCatchHitObject(CatchHitObject? h)
: base(h)
{
Origin = Anchor.Centre;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
index 8e98efdbda..f820ccdc62 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
@@ -1,10 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using JetBrains.Annotations;
-
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public partial class DrawableTinyDroplet : DrawableDroplet
@@ -16,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
}
- public DrawableTinyDroplet([CanBeNull] TinyDroplet h)
+ public DrawableTinyDroplet(TinyDroplet? h)
: base(h)
{
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
index f30ef0831a..18fc0db6e3 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Bindables;
using osuTK;
using osuTK.Graphics;
diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs
index ecaa4bfaf4..9c1004a04b 100644
--- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
index bdf8b3f28d..4818fe2cad 100644
--- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
diff --git a/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.cs b/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.cs
index e5d013dafc..7ec7050245 100644
--- a/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.cs
+++ b/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Catch.Objects
{
public enum FruitVisualRepresentation
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 015457e84f..96e2d5c4e5 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -142,13 +140,8 @@ namespace osu.Game.Rulesets.Catch.Objects
set
{
path.ControlPoints.Clear();
- path.ExpectedDistance.Value = null;
-
- if (value != null)
- {
- path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
- path.ExpectedDistance.Value = value.ExpectedDistance.Value;
- }
+ path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
+ path.ExpectedDistance.Value = value.ExpectedDistance.Value;
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
index c9bc9ca2ac..197029aeeb 100644
--- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
@@ -34,13 +32,13 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public bool HyperDash => hyperDash.Value;
- private CatchHitObject hyperDashTarget;
+ private CatchHitObject? hyperDashTarget;
///
/// The target fruit if we are to initiate a hyperdash.
///
[JsonIgnore]
- public CatchHitObject HyperDashTarget
+ public CatchHitObject? HyperDashTarget
{
get => hyperDashTarget;
set
diff --git a/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs
index 6bd5f0ac2a..1bf160b5a6 100644
--- a/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
diff --git a/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs
index 6c7f0478a7..26f20b223a 100644
--- a/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs
+++ b/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Runtime.CompilerServices;
// We publish our internal attributes to other sub-projects of the framework.
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index b784fc4c19..b6a42407da 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs
deleted file mode 100644
index 82d10e500d..0000000000
--- a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs
+++ /dev/null
@@ -1,193 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Utils;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Scoring;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Rulesets.Catch.Skinning.Argon
-{
- public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
- {
- protected readonly HitResult Result;
-
- protected SpriteText JudgementText { get; private set; } = null!;
-
- private RingExplosion? ringExplosion;
-
- [Resolved]
- private OsuColour colours { get; set; } = null!;
-
- public ArgonJudgementPiece(HitResult result)
- {
- Result = result;
- Origin = Anchor.Centre;
- Y = 160;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- AutoSizeAxes = Axes.Both;
-
- InternalChildren = new Drawable[]
- {
- JudgementText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Text = Result.GetDescription().ToUpperInvariant(),
- Colour = colours.ForHitResult(Result),
- Blending = BlendingParameters.Additive,
- Spacing = new Vector2(10, 0),
- Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
- },
- };
-
- if (Result.IsHit())
- {
- AddInternal(ringExplosion = new RingExplosion(Result)
- {
- Colour = colours.ForHitResult(Result),
- });
- }
- }
-
- ///
- /// Plays the default animation for this judgement piece.
- ///
- ///
- /// The base implementation only handles fade (for all result types) and misses.
- /// Individual rulesets are recommended to implement their appropriate hit animations.
- ///
- public virtual void PlayAnimation()
- {
- switch (Result)
- {
- default:
- JudgementText
- .ScaleTo(Vector2.One)
- .ScaleTo(new Vector2(1.4f), 1800, Easing.OutQuint);
- break;
-
- case HitResult.Miss:
- this.ScaleTo(1.6f);
- this.ScaleTo(1, 100, Easing.In);
-
- this.MoveTo(Vector2.Zero);
- this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
-
- this.RotateTo(0);
- this.RotateTo(40, 800, Easing.InQuint);
- break;
- }
-
- this.FadeOutFromOne(800);
-
- ringExplosion?.PlayAnimation();
- }
-
- public Drawable? GetAboveHitObjectsProxiedContent() => null;
-
- private partial class RingExplosion : CompositeDrawable
- {
- private readonly float travel = 52;
-
- public RingExplosion(HitResult result)
- {
- const float thickness = 4;
-
- const float small_size = 9;
- const float large_size = 14;
-
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
-
- Blending = BlendingParameters.Additive;
-
- int countSmall = 0;
- int countLarge = 0;
-
- switch (result)
- {
- case HitResult.Meh:
- countSmall = 3;
- travel *= 0.3f;
- break;
-
- case HitResult.Ok:
- case HitResult.Good:
- countSmall = 4;
- travel *= 0.6f;
- break;
-
- case HitResult.Great:
- case HitResult.Perfect:
- countSmall = 4;
- countLarge = 4;
- break;
- }
-
- for (int i = 0; i < countSmall; i++)
- AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
-
- for (int i = 0; i < countLarge; i++)
- AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
- }
-
- public void PlayAnimation()
- {
- foreach (var c in InternalChildren)
- {
- const float start_position_ratio = 0.3f;
-
- float direction = RNG.NextSingle(0, 360);
- float distance = RNG.NextSingle(travel / 2, travel);
-
- c.MoveTo(new Vector2(
- MathF.Cos(direction) * distance * start_position_ratio,
- MathF.Sin(direction) * distance * start_position_ratio
- ));
-
- c.MoveTo(new Vector2(
- MathF.Cos(direction) * distance,
- MathF.Sin(direction) * distance
- ), 600, Easing.OutQuint);
- }
-
- this.FadeOutFromOne(1000, Easing.OutQuint);
- }
-
- public partial class RingPiece : CircularContainer
- {
- public RingPiece(float thickness = 9)
- {
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
-
- Masking = true;
- BorderThickness = thickness;
- BorderColour = Color4.White;
-
- Child = new Box
- {
- AlwaysPresent = true,
- Alpha = 0,
- RelativeSizeAxes = Axes.Both
- };
- }
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
index b36d7f11cb..f6b2c52498 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -32,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
- foreach (var state in Enum.GetValues(typeof(CatcherAnimationState)).Cast())
+ foreach (var state in Enum.GetValues())
{
AddInternal(drawables[state] = getDrawableFor(state).With(d =>
{
diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
index ffdc5299d0..dbbe905879 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
}
- [Resolved(canBeNull: true)]
+ [Resolved]
private Player? player { get; set; }
protected override void LoadComplete()
diff --git a/osu.Game.Rulesets.Catch/UI/CatchRelaxCursorContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchCursorContainer.cs
similarity index 78%
rename from osu.Game.Rulesets.Catch/UI/CatchRelaxCursorContainer.cs
rename to osu.Game.Rulesets.Catch/UI/CatchCursorContainer.cs
index f30b8f0f36..4ae61ef8c7 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchRelaxCursorContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchCursorContainer.cs
@@ -6,9 +6,9 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.UI
{
- public partial class CatchRelaxCursorContainer : GameplayCursorContainer
+ public partial class CatchCursorContainer : GameplayCursorContainer
{
- // Just hide the cursor in relax.
+ // Just hide the cursor.
// The main goal here is to show that we have a cursor so the game never shows the global one.
protected override Drawable CreateCursor() => Empty();
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 184ff38cc6..c33d021876 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -1,16 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -41,9 +37,9 @@ namespace osu.Game.Rulesets.Catch.UI
// only check the X position; handle all vertical space.
base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y));
- internal Catcher Catcher { get; private set; }
+ internal Catcher Catcher { get; private set; } = null!;
- internal CatcherArea CatcherArea { get; private set; }
+ internal CatcherArea CatcherArea { get; private set; } = null!;
private readonly IBeatmapDifficultyInfo difficulty;
@@ -52,13 +48,7 @@ namespace osu.Game.Rulesets.Catch.UI
this.difficulty = difficulty;
}
- protected override GameplayCursorContainer CreateCursor()
- {
- if (Mods != null && Mods.Any(m => m is ModRelax))
- return new CatchRelaxCursorContainer();
-
- return base.CreateCursor();
- }
+ protected override GameplayCursorContainer CreateCursor() => new CatchCursorContainer();
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
index c03179dc50..74cbc665c0 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI;
diff --git a/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs
index 9ea150a2cf..32ede8f205 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Replays;
diff --git a/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs b/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
index d23913136d..10e43cf74a 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
@@ -11,7 +11,6 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
-using osuTK.Input;
namespace osu.Game.Rulesets.Catch.UI
{
@@ -106,41 +105,17 @@ namespace osu.Game.Rulesets.Catch.UI
return false;
}
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- return updateAction(e.Button, getTouchCatchActionFromInput(e.ScreenSpaceMousePosition));
- }
-
protected override bool OnTouchDown(TouchDownEvent e)
{
return updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
}
- protected override bool OnMouseMove(MouseMoveEvent e)
- {
- Show();
-
- TouchCatchAction? action = getTouchCatchActionFromInput(e.ScreenSpaceMousePosition);
-
- // multiple mouse buttons may be pressed and handling the same action.
- foreach (MouseButton button in e.PressedButtons)
- updateAction(button, action);
-
- return false;
- }
-
protected override void OnTouchMove(TouchMoveEvent e)
{
updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
base.OnTouchMove(e);
}
- protected override void OnMouseUp(MouseUpEvent e)
- {
- updateAction(e.Button, null);
- base.OnMouseUp(e);
- }
-
protected override void OnTouchUp(TouchUpEvent e)
{
updateAction(e.Touch.Source, null);
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 086b4ff285..411330f6fc 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
+using System.Diagnostics;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -123,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.UI
private double hyperDashModifier = 1;
private int hyperDashDirection;
private float hyperDashTargetPosition;
- private Bindable hitLighting;
+ private Bindable hitLighting = null!;
private readonly HitExplosionContainer hitExplosionContainer;
@@ -131,7 +129,7 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool caughtBananaPool;
private readonly DrawablePool caughtDropletPool;
- public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty = null)
+ public Catcher(DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo? difficulty = null)
{
this.droppedObjectTarget = droppedObjectTarget;
@@ -231,9 +229,8 @@ namespace osu.Game.Rulesets.Catch.UI
// droplet doesn't affect the catcher state
if (hitObject is TinyDroplet) return;
- if (result.IsHit && hitObject.HyperDash)
+ if (result.IsHit && hitObject.HyperDashTarget is CatchHitObject target)
{
- var target = hitObject.HyperDashTarget;
double timeDifference = target.StartTime - hitObject.StartTime;
double positionDifference = target.EffectiveX - X;
double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
@@ -385,7 +382,7 @@ namespace osu.Game.Rulesets.Catch.UI
private void addLighting(JudgementResult judgementResult, Color4 colour, float x) =>
hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, judgementResult, colour, x));
- private CaughtObject getCaughtObject(PalpableCatchHitObject source)
+ private CaughtObject? getCaughtObject(PalpableCatchHitObject source)
{
switch (source)
{
@@ -406,6 +403,7 @@ namespace osu.Game.Rulesets.Catch.UI
private CaughtObject getDroppedObject(CaughtObject caughtObject)
{
var droppedObject = getCaughtObject(caughtObject.HitObject);
+ Debug.Assert(droppedObject != null);
droppedObject.CopyStateFrom(caughtObject);
droppedObject.Anchor = Anchor.TopLeft;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs
index 82591eb47f..566e9d1911 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Catch.UI
{
public enum CatcherAnimationState
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index d0da05bc44..4f7535d13a 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -35,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly CatcherTrailDisplay catcherTrails;
- private Catcher catcher;
+ private Catcher catcher = null!;
///
/// -1 when only left button is pressed.
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
index f486633e12..762f95828a 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Rulesets.Objects.Pooling;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailAnimation.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailAnimation.cs
index 02bc5be863..0a5281cd10 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailAnimation.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailAnimation.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Catch.UI
{
public enum CatcherTrailAnimation
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
index e982be53d8..e3e01c1b39 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
@@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -41,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly Container hyperDashAfterImages;
[Resolved]
- private ISkinSource skin { get; set; }
+ private ISkinSource skin { get; set; } = null!;
public CatcherTrailDisplay()
{
@@ -130,7 +129,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Dispose(isDisposing);
- if (skin != null)
+ if (skin.IsNotNull())
skin.SourceChanged -= skinSourceChanged;
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailEntry.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailEntry.cs
index 78d6979b78..3a40ab26cc 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailEntry.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailEntry.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics.Performance;
using osuTK;
diff --git a/osu.Game.Rulesets.Catch/UI/Direction.cs b/osu.Game.Rulesets.Catch/UI/Direction.cs
index 15e4aed86b..65f064b7fb 100644
--- a/osu.Game.Rulesets.Catch/UI/Direction.cs
+++ b/osu.Game.Rulesets.Catch/UI/Direction.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Catch.UI
{
public enum Direction
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index 0be271b236..7930a07551 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.UI
protected override bool UserScrollSpeedAdjustment => false;
- public DrawableCatchRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ public DrawableCatchRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null)
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Down;
@@ -54,6 +52,6 @@ namespace osu.Game.Rulesets.Catch.UI
protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
- public override DrawableHitObject CreateDrawableRepresentation(CatchHitObject h) => null;
+ public override DrawableHitObject? CreateDrawableRepresentation(CatchHitObject h) => null;
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
index cb2d8498cc..df1e932ad5 100644
--- a/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.Objects.Drawables;
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs
index e1dd665bf2..1e2d94433c 100644
--- a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Pooling;
diff --git a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs
index 5d027edbaa..cfb6879067 100644
--- a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs
+++ b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osuTK.Graphics;
diff --git a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs
index 78a71f26a2..bcc59a5e4f 100644
--- a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
index e2f95ca177..ecce7c1b3f 100644
--- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
+++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
catch the fruit. to the beat.
@@ -15,4 +15,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
index de7935b2ef..4a1545a423 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
index 9674186039..25335754d2 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
@@ -1,49 +1,24 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {531F1092-DB27-445D-AA33-2A77C7187C99}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Rulesets.Mania.Tests
osu.Game.Rulesets.Mania.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
- None
- cjk;mideast;other;rare;west
- true
-
-
-
-
-
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
+
+
+ %(RecursiveDir)%(Filename)%(Extension)
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {48f4582b-7687-4621-9cbe-5c24197cb536}
- osu.Game.Rulesets.Mania
-
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
+
+
-
-
- 5.0.0
-
-
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
index 2d1015387a..a508198f7f 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Rulesets.Mania.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
index 82d1c8ea24..ff5dde856e 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
index 88ad484bc1..51e07dd6c1 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
@@ -1,35 +1,19 @@
-
-
+
- Debug
- iPhoneSimulator
- {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}
Exe
+ net6.0-ios
+ 13.4
osu.Game.Rulesets.Mania.Tests
osu.Game.Rulesets.Mania.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {48F4582B-7687-4621-9CBE-5C24197CB536}
- osu.Game.Rulesets.Mania
-
+
+
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index 308238d87a..77f93b4ef9 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
@@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(hitObject, beatmap, previousPattern)
{
- if (random == null) throw new ArgumentNullException(nameof(random));
- if (originalBeatmap == null) throw new ArgumentNullException(nameof(originalBeatmap));
+ ArgumentNullException.ThrowIfNull(random);
+ ArgumentNullException.ThrowIfNull(originalBeatmap);
Random = random;
OriginalBeatmap = originalBeatmap;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
index b2e89c3410..931673f337 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
@@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
protected PatternGenerator(HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern)
{
- if (hitObject == null) throw new ArgumentNullException(nameof(hitObject));
- if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
- if (previousPattern == null) throw new ArgumentNullException(nameof(previousPattern));
+ ArgumentNullException.ThrowIfNull(hitObject);
+ ArgumentNullException.ThrowIfNull(beatmap);
+ ArgumentNullException.ThrowIfNull(previousPattern);
HitObject = hitObject;
Beatmap = beatmap;
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 6162184c9a..0cec0ee075 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
- public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new ManiaHealthProcessor(drainStartTime, 0.5);
+ public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new ManiaHealthProcessor(drainStartTime);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
diff --git a/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
index 1a67117c03..4d93826240 100644
--- a/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
+++ b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
@@ -22,8 +22,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
public static void Sort(T[] keys, IComparer comparer)
{
- if (keys == null)
- throw new ArgumentNullException(nameof(keys));
+ ArgumentNullException.ThrowIfNull(keys);
if (keys.Length == 0)
return;
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs
index 5c6682ed73..16f7af0d0a 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs
@@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
public partial class ManiaHealthProcessor : DrainingHealthProcessor
{
///
- public ManiaHealthProcessor(double drainStartTime, double drainLenience = 0)
- : base(drainStartTime, drainLenience)
+ public ManiaHealthProcessor(double drainStartTime)
+ : base(drainStartTime, 1.0)
{
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
index 2dbf475c7e..4ce3c50f7c 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -18,20 +17,18 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
- public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
- protected readonly HitResult Result;
-
- protected SpriteText JudgementText { get; private set; } = null!;
-
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
+ : base(result)
{
- Result = result;
+ AutoSizeAxes = Axes.Both;
+
Origin = Anchor.Centre;
Y = 160;
}
@@ -39,22 +36,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
[BackgroundDependencyLoader]
private void load()
{
- AutoSizeAxes = Axes.Both;
-
- InternalChildren = new Drawable[]
- {
- JudgementText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Text = Result.GetDescription().ToUpperInvariant(),
- Colour = colours.ForHitResult(Result),
- Blending = BlendingParameters.Additive,
- Spacing = new Vector2(10, 0),
- Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
- },
- };
-
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@@ -64,6 +45,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
}
}
+ protected override SpriteText CreateJudgementText() =>
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(10, 0),
+ Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
+ };
+
///
/// Plays the default animation for this judgement piece.
///
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
index eb7f63fbe2..057b7eb0d9 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
@@ -27,6 +27,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
switch (lookup)
{
case GameplaySkinComponentLookup resultComponent:
+ // This should eventually be moved to a skin setting, when supported.
+ if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
+ return Drawable.Empty();
+
return new ArgonJudgementPiece(resultComponent.Component);
case ManiaSkinComponentLookup maniaComponent:
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 6a31fb3fda..6ca830a82f 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -134,6 +134,9 @@ namespace osu.Game.Rulesets.Mania.UI
protected override void Dispose(bool isDisposing)
{
+ // must happen before children are disposed in base call to prevent illegal accesses to the hit explosion pool.
+ NewResult -= OnNewResult;
+
base.Dispose(isDisposing);
if (skin != null)
@@ -206,18 +209,6 @@ namespace osu.Game.Rulesets.Mania.UI
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
}
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- keyBindingContainer?.TriggerPressed(column.Action.Value);
- return base.OnMouseDown(e);
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- keyBindingContainer?.TriggerReleased(column.Action.Value);
- base.OnMouseUp(e);
- }
-
protected override bool OnTouchDown(TouchDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 01e9926ad7..e3ebadc836 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Mania.UI
public ManiaPlayfield(List stageDefinitions)
{
- if (stageDefinitions == null)
- throw new ArgumentNullException(nameof(stageDefinitions));
+ ArgumentNullException.ThrowIfNull(stageDefinitions);
if (stageDefinitions.Count <= 0)
throw new ArgumentException("Can't have zero or fewer stages.");
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index fc38a96a35..c1d3e85bf1 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -156,6 +156,9 @@ namespace osu.Game.Rulesets.Mania.UI
protected override void Dispose(bool isDisposing)
{
+ // must happen before children are disposed in base call to prevent illegal accesses to the judgement pool.
+ NewResult -= OnNewResult;
+
base.Dispose(isDisposing);
if (currentSkin != null)
diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
index 4f6840f9ca..72f172188e 100644
--- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
+++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
smash the keys. to the beat.
@@ -15,4 +15,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
index 3ce17ccc27..45d27dda70 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
index f4b673f10b..e8a46a9828 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
@@ -1,49 +1,27 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {90CAB706-39CB-4B93-9629-3218A6FF8E9B}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Rulesets.Osu.Tests
osu.Game.Rulesets.Osu.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
- None
- cjk;mideast;other;rare;west
- true
-
-
-
-
-
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
+
+
+ %(RecursiveDir)%(Filename)%(Extension)
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {c92a607b-1fdd-4954-9f92-03ff547d9080}
- osu.Game.Rulesets.Osu
-
-
- {2a66dd92-adb1-4994-89e2-c94e04acda0d}
- osu.Game
-
+
+
-
- 5.0.0
-
+
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
index ad23f3ee33..6ef29fa68e 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Rulesets.Osu.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
index a88b74695c..1e33f2ff16 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
index 545abcec6c..7d50deb8ba 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
@@ -1,35 +1,20 @@
-
-
+
- Debug
- iPhoneSimulator
- {6653CA6F-DB06-4604-A3FD-762E25C2AF96}
+ Exe
+ net6.0-ios
+ 13.4
Exe
osu.Game.Rulesets.Osu.Tests
osu.Game.Rulesets.Osu.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {C92A607B-1FDD-4954-9F92-03FF547D9080}
- osu.Game.Rulesets.Osu
-
+
+
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index 32d0cc8939..1e9f931b74 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -160,9 +160,9 @@ namespace osu.Game.Rulesets.Osu.Tests
static bool assertSamples(HitObject hitObject) => hitObject.Samples.All(s => s.Name != HitSampleInfo.HIT_CLAP && s.Name != HitSampleInfo.HIT_WHISTLE);
}
- private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
+ private Drawable testSimpleBig(int repeats = 0) => createSlider(repeats: repeats);
- private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
+ private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(repeats: repeats, stackHeight: 10);
private Drawable testDistanceOverflow(int repeats = 0)
{
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index 3a175888d9..66d99e0bf1 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -133,6 +133,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(e.NewItems != null);
+
// If inserting in the path (not appending),
// update indices of existing connections after insert location
if (e.NewStartingIndex < Pieces.Count)
@@ -164,6 +166,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(e.OldItems != null);
+
foreach (var point in e.OldItems.Cast())
{
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
index 1cb3208c30..74e16f7e0b 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
@@ -82,10 +82,10 @@ namespace osu.Game.Rulesets.Osu.Replays
private class ReplayFrameComparer : IComparer
{
- public int Compare(ReplayFrame f1, ReplayFrame f2)
+ public int Compare(ReplayFrame? f1, ReplayFrame? f2)
{
- if (f1 == null) throw new ArgumentNullException(nameof(f1));
- if (f2 == null) throw new ArgumentNullException(nameof(f2));
+ ArgumentNullException.ThrowIfNull(f1);
+ ArgumentNullException.ThrowIfNull(f2);
return f1.Time.CompareTo(f2.Time);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
index 95c75164aa..fca3e70236 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
@@ -1,35 +1,64 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public partial class ArgonFollowCircle : FollowCircle
{
+ private readonly CircularContainer circleContainer;
+ private readonly Box circleFill;
+
+ private readonly IBindable accentColour = new Bindable();
+
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? parentObject { get; set; }
+
public ArgonFollowCircle()
{
- InternalChild = new CircularContainer
+ InternalChild = circleContainer = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 4,
- BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
Blending = BlendingParameters.Additive,
- Child = new Box
+ Child = circleFill = new Box
{
- Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Alpha = 0.3f,
}
};
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ if (parentObject != null)
+ accentColour.BindTo(parentObject.AccentColour);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ accentColour.BindValueChanged(colour =>
+ {
+ circleContainer.BorderColour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
+ circleFill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
+ }, true);
+ }
+
protected override void OnSliderPress()
{
const float duration = 300f;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
index f5f410210b..6f55d93eff 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -17,42 +16,24 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
- public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
- protected readonly HitResult Result;
-
- protected SpriteText JudgementText { get; private set; } = null!;
-
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
+ : base(result)
{
- Result = result;
+ AutoSizeAxes = Axes.Both;
+
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
- AutoSizeAxes = Axes.Both;
-
- InternalChildren = new Drawable[]
- {
- JudgementText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Text = Result.GetDescription().ToUpperInvariant(),
- Colour = colours.ForHitResult(Result),
- Blending = BlendingParameters.Additive,
- Spacing = new Vector2(5, 0),
- Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
- },
- };
-
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@@ -62,6 +43,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
}
}
+ protected override SpriteText CreateJudgementText() =>
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(5, 0),
+ Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
+ };
+
///
/// Plays the default animation for this judgement piece.
///
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
index 48b43f359d..d6ce793c7e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -21,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
+ private readonly IBindable accentColour = new Bindable();
+
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
@@ -37,7 +41,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
fill = new Box
{
- Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -53,10 +56,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
};
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ if (parentObject != null)
+ accentColour.BindTo(parentObject.AccentColour);
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
+ accentColour.BindValueChanged(colour =>
+ {
+ fill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
+ }, true);
+
if (parentObject != null)
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
index 86194d2c43..f98a47097d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
@@ -19,6 +19,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
switch (lookup)
{
case GameplaySkinComponentLookup resultComponent:
+ // This should eventually be moved to a skin setting, when supported.
+ if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
+ return Drawable.Empty();
+
return new ArgonJudgementPiece(resultComponent.Component);
case OsuSkinComponentLookup osuComponent:
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index 6547b058c2..cadac4d319 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
@@ -29,8 +30,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly bool hasNumber;
- protected Drawable CircleSprite = null!;
- protected Drawable OverlaySprite = null!;
+ protected LegacyKiaiFlashingDrawable CircleSprite = null!;
+ protected LegacyKiaiFlashingDrawable OverlaySprite = null!;
protected Container OverlayLayer { get; private set; } = null!;
@@ -65,7 +66,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
-
InternalChildren = new[]
{
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
@@ -114,7 +114,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
base.LoadComplete();
- accentColour.BindValueChanged(colour => CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
+ accentColour.BindValueChanged(colour =>
+ {
+ Color4 objectColour = colour.NewValue;
+ int add = Math.Max(25, 300 - (int)(objectColour.R * 255) - (int)(objectColour.G * 255) - (int)(objectColour.B * 255));
+
+ var kiaiTintColour = new Color4(
+ (byte)Math.Min((byte)(objectColour.R * 255) + add, 255),
+ (byte)Math.Min((byte)(objectColour.G * 255) + add, 255),
+ (byte)Math.Min((byte)(objectColour.B * 255) + add, 255),
+ 255);
+
+ CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue);
+ OverlaySprite.KiaiGlowColour = CircleSprite.KiaiGlowColour = LegacyColourCompatibility.DisallowZeroAlpha(kiaiTintColour);
+ }, true);
+
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 122330d09b..ed02284a4b 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.UI
HitPolicy = new StartTimeOrderedHitPolicy();
var hitWindows = new OsuHitWindows();
- foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
+ foreach (var result in Enum.GetValues().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
AddRangeInternal(poolDictionary.Values);
diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
index 98f1e69bd1..bf776fe5dd 100644
--- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
+++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
click the circles. to the beat.
@@ -15,4 +15,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
index d9de0fde4e..452b9683ec 100644
--- a/osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
index 4d4dabebe6..a639326ebd 100644
--- a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
@@ -1,49 +1,24 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {3701A0A1-8476-42C6-B5C4-D24129B4A484}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Rulesets.Taiko.Tests
osu.Game.Rulesets.Taiko.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
- None
- cjk;mideast;other;rare;west
- true
-
-
-
-
-
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
+
+
+ %(RecursiveDir)%(Filename)%(Extension)
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {f167e17a-7de6-4af5-b920-a5112296c695}
- osu.Game.Rulesets.Taiko
-
-
- {2a66dd92-adb1-4994-89e2-c94e04acda0d}
- osu.Game
-
+
+
-
-
- 5.0.0
-
-
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs b/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs
index 1ebbd61a94..0e3a953728 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Rulesets.Taiko.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
index 9628475b3e..76cb3c0db0 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj b/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj
index 8ee640cd99..e648a11299 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj
@@ -1,35 +1,19 @@
-
-
+
- Debug
- iPhoneSimulator
- {7E408809-66AC-49D1-AF4D-98834F9B979A}
Exe
+ net6.0-ios
+ 13.4
osu.Game.Rulesets.Taiko.Tests
osu.Game.Rulesets.Taiko.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {F167E17A-7DE6-4AF5-B920-A5112296C695}
- osu.Game.Rulesets.Taiko
-
+
+
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index f7fdd447d6..3b3b3e606c 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
- drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
+ drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
index 6756001089..bbd62ff85b 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -18,20 +17,16 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
- public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
- protected readonly HitResult Result;
-
- protected SpriteText JudgementText { get; private set; } = null!;
-
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
+ : base(result)
{
- Result = result;
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Both;
}
@@ -39,21 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
[BackgroundDependencyLoader]
private void load()
{
- InternalChildren = new Drawable[]
- {
- JudgementText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Text = Result.GetDescription().ToUpperInvariant(),
- Colour = colours.ForHitResult(Result),
- Blending = BlendingParameters.Additive,
- Spacing = new Vector2(10, 0),
- RelativePositionAxes = Axes.Both,
- Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular),
- },
- };
-
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@@ -64,6 +44,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
}
}
+ protected override SpriteText CreateJudgementText() =>
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(10, 0),
+ RelativePositionAxes = Axes.Both,
+ Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular),
+ };
+
///
/// Plays the default animation for this judgement piece.
///
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs
index a5d091a1c8..780018af4e 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs
@@ -19,6 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
switch (component)
{
case GameplaySkinComponentLookup resultComponent:
+ // This should eventually be moved to a skin setting, when supported.
+ if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
+ return Drawable.Empty();
+
return new ArgonJudgementPiece(resultComponent.Component);
case TaikoSkinComponentLookup taikoComponent:
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 32a83d87b8..146daa8c27 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
- public readonly BindableBool LockPlayfieldAspect = new BindableBool(true);
+ public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
- LockPlayfieldAspect = { BindTarget = LockPlayfieldAspect }
+ LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs
index ab8c0a484e..0232c10d65 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs
@@ -107,24 +107,6 @@ namespace osu.Game.Rulesets.Taiko.UI
return false;
}
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- if (!validMouse(e))
- return false;
-
- handleDown(e.Button, e.ScreenSpaceMousePosition);
- return true;
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- if (!validMouse(e))
- return;
-
- handleUp(e.Button);
- base.OnMouseUp(e);
- }
-
protected override bool OnTouchDown(TouchDownEvent e)
{
handleDown(e.Touch.Source, e.ScreenSpaceTouchDownPosition);
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 9493de624a..9f9debe7d7 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Taiko.UI
var hitWindows = new TaikoHitWindows();
- foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => hitWindows.IsHitResultAllowed(r)))
+ foreach (var result in Enum.GetValues().Where(r => hitWindows.IsHitResultAllowed(r)))
{
judgementPools.Add(result, new DrawablePool(15));
explosionPools.Add(result, new HitExplosionPool(result));
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 79c5c36e08..42732d90e4 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
- public readonly IBindable LockPlayfieldAspect = new BindableBool(true);
+ public readonly IBindable LockPlayfieldMaxAspect = new BindableBool(true);
protected override void Update()
{
@@ -21,7 +21,12 @@ namespace osu.Game.Rulesets.Taiko.UI
float height = default_relative_height;
- if (LockPlayfieldAspect.Value)
+ // Players coming from stable expect to be able to change the aspect ratio regardless of the window size.
+ // We originally wanted to limit this more, but there was considerable pushback from the community.
+ //
+ // As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
+ // This is still a bit weird, because readability changes with window size, but it is what it is.
+ if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Height = height;
diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj
index b752c13d18..f0e1cb8e8f 100644
--- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj
+++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
bash the drum. to the beat.
diff --git a/osu.Game.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Tests.Android/AndroidManifest.xml
index 4a63f0c357..f25b2e5328 100644
--- a/osu.Game.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Tests.Android/MainActivity.cs b/osu.Game.Tests.Android/MainActivity.cs
index 6c4f9bac58..bdb947fbb4 100644
--- a/osu.Game.Tests.Android/MainActivity.cs
+++ b/osu.Game.Tests.Android/MainActivity.cs
@@ -3,7 +3,9 @@
#nullable disable
+using System.Reflection;
using Android.App;
+using Android.OS;
using osu.Framework.Android;
namespace osu.Game.Tests.Android
@@ -12,5 +14,16 @@ namespace osu.Game.Tests.Android
public class MainActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
+
+ protected override void OnCreate(Bundle savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+
+ // See the comment in OsuGameActivity
+ Assembly.Load("osu.Game.Rulesets.Osu");
+ Assembly.Load("osu.Game.Rulesets.Taiko");
+ Assembly.Load("osu.Game.Rulesets.Catch");
+ Assembly.Load("osu.Game.Rulesets.Mania");
+ }
}
}
diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
index afafec6b1f..b745d91980 100644
--- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
+++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
@@ -1,88 +1,34 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Tests
osu.Game.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
-
-
-
-
$(NoWarn);CA2007
-
- None
- cjk;mideast;other;rare;west
- true
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
-
-
+
+
%(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {58f6c80c-1253-4a0e-a465-b8c85ebeadf3}
- osu.Game.Rulesets.Catch
-
-
- {48f4582b-7687-4621-9cbe-5c24197cb536}
- osu.Game.Rulesets.Mania
-
-
- {c92a607b-1fdd-4954-9f92-03ff547d9080}
- osu.Game.Rulesets.Osu
-
-
- {f167e17a-7de6-4af5-b920-a5112296c695}
- osu.Game.Rulesets.Taiko
-
-
- {2a66dd92-adb1-4994-89e2-c94e04acda0d}
- osu.Game
-
+
+
+
+
+
-
- 5.0.0
-
-
diff --git a/osu.Game.Tests.iOS/Application.cs b/osu.Game.Tests.iOS/Application.cs
index cf36fea139..4678be4fb8 100644
--- a/osu.Game.Tests.iOS/Application.cs
+++ b/osu.Game.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist
index 31e2b3f257..ac661f6263 100644
--- a/osu.Game.Tests.iOS/Info.plist
+++ b/osu.Game.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
index 05b3cad6da..79771fcd50 100644
--- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
+++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
@@ -1,54 +1,26 @@
-
-
+
- Debug
- iPhoneSimulator
Exe
- {65FF8E19-6934-469B-B690-23C6D6E56A17}
+ net6.0-ios
+ 13.4
osu.Game.Tests
osu.Game.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- $(NoWarn);CA2007
-
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {C92A607B-1FDD-4954-9F92-03FF547D9080}
- osu.Game.Rulesets.Osu
-
-
- {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}
- osu.Game.Rulesets.Catch
-
-
- {48F4582B-7687-4621-9CBE-5C24197CB536}
- osu.Game.Rulesets.Mania
-
-
- {F167E17A-7DE6-4AF5-B920-A5112296C695}
- osu.Game.Rulesets.Taiko
-
+
+
+
+
+
-
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
index cd6e5e7919..93cda34ef7 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
@@ -16,7 +16,9 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
@@ -179,6 +181,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
+ [Test]
+ public void TestSoloScoreData()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ scoreInfo.Mods = new Mod[]
+ {
+ new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
+ };
+
+ var beatmap = new TestBeatmap(ruleset);
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
+ }
+ }
+ };
+
+ var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(decodedAfterEncode.ScoreInfo.Statistics, Is.EqualTo(scoreInfo.Statistics));
+ Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
+ Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
+ });
+ }
+
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index ebfa9bd8b7..3c35dc311f 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -26,6 +26,16 @@ namespace osu.Game.Tests.Chat
MessageFormatter.WebsiteRootUrl = originalWebsiteRootUrl;
}
+ [Test]
+ public void TestUnsupportedProtocolLink()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a gopher://really-old-protocol we don't support." });
+
+ Assert.AreEqual(result.Content, result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("gopher://really-old-protocol", result.Links[0].Url);
+ }
+
[Test]
public void TestBareLink()
{
diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
index cdd192cfe1..3a4c55c65c 100644
--- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs
+++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
@@ -75,6 +75,8 @@ namespace osu.Game.Tests.Chat
return false;
};
});
+
+ AddUntilStep("wait for notifications client", () => channelManager.NotificationsConnected);
}
[Test]
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index 56964aa8b2..446eb72b04 100644
--- a/osu.Game.Tests/Database/BeatmapImporterTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -564,7 +564,7 @@ namespace osu.Game.Tests.Database
var imported = await importer.Import(
progressNotification,
- new ImportTask(zipStream, string.Empty)
+ new[] { new ImportTask(zipStream, string.Empty) }
);
realm.Run(r => r.Refresh());
@@ -1052,7 +1052,7 @@ namespace osu.Game.Tests.Database
{
string? temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
- var importedSet = await importer.Import(new ImportTask(temp), batchImport);
+ var importedSet = await importer.Import(new ImportTask(temp), new ImportParameters { Batch = batchImport });
Assert.NotNull(importedSet);
Debug.Assert(importedSet != null);
diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
index 3c6ec28da2..393217f371 100644
--- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
@@ -73,16 +73,6 @@ namespace osu.Game.Tests.Gameplay
}
[Test]
- [FlakyTest]
- /*
- * Fail rate around 0.15%
- *
- * TearDown : osu.Framework.Testing.Drawables.Steps.AssertButton+TracedException : gameplay clock time = 2500
- * --TearDown
- * at osu.Framework.Threading.ScheduledDelegate.RunTaskInternal()
- * at osu.Framework.Threading.Scheduler.Update()
- * at osu.Framework.Graphics.Drawable.UpdateSubTree()
- */
public void TestSeekPerformsInGameplayTime(
[Values(1.0, 0.5, 2.0)] double clockRate,
[Values(0.0, 200.0, -200.0)] double userOffset,
@@ -92,6 +82,9 @@ namespace osu.Game.Tests.Gameplay
ClockBackedTestWorkingBeatmap working = null;
GameplayClockContainer gameplayClockContainer = null;
+ // ReSharper disable once NotAccessedVariable
+ BindableDouble trackAdjustment = null; // keeping a reference for track adjustment
+
if (setAudioOffsetBeforeConstruction)
AddStep($"preset audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
@@ -103,16 +96,16 @@ namespace osu.Game.Tests.Gameplay
gameplayClockContainer.Reset(startClock: !whileStopped);
});
- AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate)));
+ AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, trackAdjustment = new BindableDouble(clockRate)));
if (!setAudioOffsetBeforeConstruction)
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
- AddStep("gameplay clock time = 2500", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 2500, 10f));
+ AddAssert("gameplay clock time = 2500", () => gameplayClockContainer.CurrentTime, () => Is.EqualTo(2500).Within(10f));
AddStep("seek to 10000", () => gameplayClockContainer.Seek(10000));
- AddStep("gameplay clock time = 10000", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 10000, 10f));
+ AddAssert("gameplay clock time = 10000", () => gameplayClockContainer.CurrentTime, () => Is.EqualTo(10000).Within(10f));
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
index 3ce7aa72d9..90c7688443 100644
--- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements;
@@ -139,6 +140,29 @@ namespace osu.Game.Tests.Gameplay
Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1));
}
+ [Test]
+ public void TestAccuracyModes()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = Enumerable.Range(0, 4).Select(_ => new TestHitObject(HitResult.Great)).ToList()
+ };
+
+ var scoreProcessor = new ScoreProcessor(new OsuRuleset());
+ scoreProcessor.ApplyBeatmap(beatmap);
+
+ Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
+ Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo(0));
+ Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo(1));
+
+ scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
+ scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.Great });
+
+ Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo((double)(100 + 300) / (2 * 300)).Within(Precision.DOUBLE_EPSILON));
+ Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo((double)(100 + 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
+ Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo((double)(100 + 3 * 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
+ }
+
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }
diff --git a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
index b8a3828a64..ca5240a39d 100644
--- a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
+++ b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
@@ -60,6 +60,6 @@ namespace osu.Game.Tests.Mods
/// This local helper is used rather than , because the aforementioned method flattens multi mods.
/// >
private static IEnumerable getMultiMods(Ruleset ruleset)
- => Enum.GetValues(typeof(ModType)).Cast().SelectMany(ruleset.GetModsFor).OfType();
+ => Enum.GetValues().SelectMany(ruleset.GetModsFor).OfType();
}
}
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index e1d8e08c5e..585fd516bd 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -226,12 +226,12 @@ namespace osu.Game.Tests.Online
this.testBeatmapManager = testBeatmapManager;
}
- public override Live ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
+ public override Live ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default)
{
if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
throw new TimeoutException("Timeout waiting for import to be allowed.");
- return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
+ return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, parameters, cancellationToken));
}
}
}
diff --git a/osu.Game.Tests/Resources/Archives/modified-default-20221205.osk b/osu.Game.Tests/Resources/Archives/modified-default-20221205.osk
new file mode 100644
index 0000000000..ae421fc323
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-default-20221205.osk differ
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 9c85f61330..adf28afc8e 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -12,6 +12,7 @@ using System.Threading;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
+using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@@ -93,10 +94,12 @@ namespace osu.Game.Tests.Resources
{
// Create random metadata, then we can check if sorting works based on these
Artist = "Some Artist " + RNG.Next(0, 9),
- Title = $"Some Song (set id {setId}) {Guid.NewGuid()}",
+ Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}",
Author = { Username = "Some Guy " + RNG.Next(0, 9) },
};
+ Logger.Log($"🛠️ Generating beatmap set \"{metadata}\" for test consumption.");
+
var beatmapSet = new BeatmapSetInfo
{
OnlineID = setId,
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index 5c20f46787..0bd40e9962 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -360,7 +360,7 @@ namespace osu.Game.Tests.Skins.IO
private async Task> loadSkinIntoOsu(OsuGameBase osu, ImportTask import, bool batchImport = false)
{
var skinManager = osu.Dependencies.Get();
- return await skinManager.Import(import, batchImport);
+ return await skinManager.Import(import, new ImportParameters { Batch = batchImport });
}
}
}
diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
index ff665499ae..39e8ec5215 100644
--- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
+++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
@@ -42,7 +42,9 @@ namespace osu.Game.Tests.Skins
// Covers longest combo counter
"Archives/modified-default-20221012.osk",
// Covers TextElement and BeatmapInfoDrawable
- "Archives/modified-default-20221102.osk"
+ "Archives/modified-default-20221102.osk",
+ // Covers BPM counter.
+ "Archives/modified-default-20221205.osk"
};
///
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
index ccd2feef9c..f255dd08a8 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
@@ -6,6 +6,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
@@ -21,7 +22,13 @@ namespace osu.Game.Tests.Visual.Editing
public TestSceneEditorSummaryTimeline()
{
- editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
+ var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
+
+ beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = 100 });
+ beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
+ beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 };
+
+ editorBeatmap = new EditorBeatmap(beatmap);
}
protected override void LoadComplete()
diff --git a/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs b/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs
new file mode 100644
index 0000000000..3319788c8a
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.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.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public partial class TestScenePreviewTime : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ [Test]
+ public void TestSceneSetPreviewTimingPoint()
+ {
+ AddStep("seek to 1000", () => EditorClock.Seek(1000));
+ AddAssert("time is 1000", () => EditorClock.CurrentTime == 1000);
+ AddStep("set current time as preview point", () => Editor.SetPreviewPointToCurrentTime());
+ AddAssert("preview time is 1000", () => EditorBeatmap.PreviewTime.Value == 1000);
+ }
+
+ [Test]
+ public void TestScenePreviewTimeline()
+ {
+ AddStep("set preview time to -1", () => EditorBeatmap.PreviewTime.Value = -1);
+ AddAssert("preview time line should not show", () => !Editor.ChildrenOfType().Single().Children.Any());
+ AddStep("set preview time to 1000", () => EditorBeatmap.PreviewTime.Value = 1000);
+ AddAssert("preview time line should show", () => Editor.ChildrenOfType().Single().Children.Single().Alpha == 1);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
index 6bc2922253..a141e4d431 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneZoomableScrollContainer : OsuManualInputManagerTestScene
{
- private ZoomableScrollContainer scrollContainer;
+ private TestZoomableScrollContainer scrollContainer;
private Drawable innerBox;
[SetUpSteps]
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(30)
},
- scrollContainer = new ZoomableScrollContainer(1, 60, 1)
+ scrollContainer = new TestZoomableScrollContainer(1, 60, 1)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -93,6 +93,14 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
}
+ [Test]
+ public void TestWidthUpdatesOnSecondZoomSetup()
+ {
+ AddAssert("Inner container width = 1x", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
+ AddStep("reload zoom", () => scrollContainer.SetupZoom(10, 10, 60));
+ AddAssert("Inner container width = 10x", () => innerBox.DrawWidth == scrollContainer.DrawWidth * 10);
+ }
+
[Test]
public void TestZoom0()
{
@@ -190,5 +198,15 @@ namespace osu.Game.Tests.Visual.Editing
private Quad scrollQuad => scrollContainer.ScreenSpaceDrawQuad;
private Quad boxQuad => innerBox.ScreenSpaceDrawQuad;
+
+ private partial class TestZoomableScrollContainer : ZoomableScrollContainer
+ {
+ public TestZoomableScrollContainer(int minimum, float maximum, float initial)
+ : base(minimum, maximum, initial)
+ {
+ }
+
+ public new void SetupZoom(float initial, float minimum, float maximum) => base.SetupZoom(initial, minimum, maximum);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
index 2fbdfbc198..1a7ea20cc0 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -181,7 +181,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddStep("exit", () => Player.Exit());
- AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
+ AddUntilStep("wait for submission", () => Player.SubmittedScore != null);
+ AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false);
}
[Test]
@@ -209,7 +210,9 @@ namespace osu.Game.Tests.Visual.Gameplay
addFakeHit();
AddStep("exit", () => Player.Exit());
- AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
+
+ AddUntilStep("wait for submission", () => Player.SubmittedScore != null);
+ AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false);
}
[Test]
@@ -257,7 +260,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTestAPI(true);
- createPlayerTest(false, createRuleset: () => new OsuRuleset
+ createPlayerTest(createRuleset: () => new OsuRuleset
{
RulesetInfo =
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
index 8f1eb98c79..ffd034e4d2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
@@ -261,7 +261,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestFinalFramesPurgedBeforeEndingPlay()
{
- AddStep("begin playing", () => spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), new Score()));
+ AddStep("begin playing", () => spectatorClient.BeginPlaying(0, TestGameplayState.Create(new OsuRuleset()), new Score()));
AddStep("send frames and finish play", () =>
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index 1ad1da0994..794860b9ec 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
};
- spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), recordingScore);
+ spectatorClient.BeginPlaying(0, TestGameplayState.Create(new OsuRuleset()), recordingScore);
spectatorClient.OnNewFrames += onNewFrames;
});
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index 0bc42b06dd..aef6f9ade0 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -201,6 +201,23 @@ namespace osu.Game.Tests.Visual.Menus
AddAssert("volume not changed", () => Audio.Volume.Value == 0.5);
}
+ [Test]
+ public void TestRulesetSelectorOverflow()
+ {
+ AddStep("set toolbar width", () =>
+ {
+ toolbar.RelativeSizeAxes = Axes.None;
+ toolbar.Width = 400;
+ });
+ AddStep("move mouse over news toggle button", () =>
+ {
+ var button = toolbar.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(button);
+ });
+ AddAssert("no ruleset toggle buttons hovered", () => !toolbar.ChildrenOfType().Any(button => button.IsHovered));
+ AddUntilStep("toolbar gradient visible", () => toolbar.ChildrenOfType().Single().Children.All(d => d.Alpha > 0));
+ }
+
public partial class TestToolbar : Toolbar
{
public new Bindable OverlayActivationMode => base.OverlayActivationMode as Bindable;
diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
index fa7d2c04f4..649c662e41 100644
--- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
@@ -117,11 +117,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
BeatmapID = 0,
RulesetID = 0,
Mods = user.Mods,
- MaximumScoringValues = new ScoringValues
+ MaximumStatistics = new Dictionary
{
- BaseScore = 10000,
- MaxCombo = 1000,
- CountBasicHitObjects = 1000
+ { HitResult.Perfect, 100 }
}
};
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
index 91e9ce5ea2..ae27db0dd1 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -46,10 +47,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
}
- [Test]
- public void TestNextItemSelectedAfterDeletion()
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestNextItemSelectedAfterDeletion(bool allowSelection)
{
- createPlaylist();
+ createPlaylist(p =>
+ {
+ p.AllowSelection = allowSelection;
+ });
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
@@ -57,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
- AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+ AddAssert("item 0 is " + (allowSelection ? "selected" : "not selected"), () => playlist.SelectedItem.Value == (allowSelection ? playlist.Items[0] : null));
}
[Test]
@@ -117,7 +122,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset);
});
- private void createPlaylist()
+ private void createPlaylist(Action setupPlaylist = null)
{
AddStep("create playlist", () =>
{
@@ -154,6 +159,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
}
+
+ setupPlaylist?.Invoke(playlist);
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index d8fda5b21f..7bde2e747d 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -19,6 +19,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
+using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
@@ -26,6 +27,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Lounge;
+using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@@ -79,7 +81,25 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for return to playlist screen", () => playlistScreen.CurrentSubScreen is PlaylistsRoomSubScreen);
+ AddStep("go back to song select", () =>
+ {
+ InputManager.MoveMouseTo(playlistScreen.ChildrenOfType().Single(b => b.Text == "Edit playlist"));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddUntilStep("wait for song select", () => (playlistScreen.CurrentSubScreen as PlaylistsSongSelect)?.BeatmapSetsLoaded == true);
+
+ AddStep("press home button", () =>
+ {
+ InputManager.MoveMouseTo(Game.Toolbar.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("confirmation dialog shown", () => Game.ChildrenOfType().Single().CurrentDialog is not null);
+
pushEscape();
+ pushEscape();
+
AddAssert("confirmation dialog shown", () => Game.ChildrenOfType().Single().CurrentDialog is not null);
AddStep("confirm exit", () => InputManager.Key(Key.Enter));
@@ -175,11 +195,17 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
- return (player = Game.ScreenStack.CurrentScreen as Player) != null;
+ player = Game.ScreenStack.CurrentScreen as Player;
+ return player?.IsLoaded == true;
});
AddAssert("retry count is 0", () => player.RestartCount == 0);
+ // todo: see https://github.com/ppy/osu/issues/22220
+ // tests are supposed to be immune to this edge case by the logic in TestPlayer,
+ // but we're running a full game instance here, so we have to work around it manually.
+ AddStep("end spectator before retry", () => Game.SpectatorClient.EndPlaying(player.GameplayState));
+
AddStep("attempt to retry", () => player.ChildrenOfType().First().Action());
AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player);
@@ -515,6 +541,28 @@ namespace osu.Game.Tests.Visual.Navigation
AddWaitStep("wait two frames", 2);
}
+ [Test]
+ public void TestFeaturedArtistDisclaimerDialog()
+ {
+ BeatmapListingOverlay getBeatmapListingOverlay() => Game.ChildrenOfType().FirstOrDefault();
+
+ AddStep("Wait for notifications to load", () => Game.SearchBeatmapSet(string.Empty));
+ AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null);
+
+ AddUntilStep("Wait for beatmap overlay to load", () => getBeatmapListingOverlay()?.State.Value == Visibility.Visible);
+ AddAssert("featured artist filter is on", () => getBeatmapListingOverlay().ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+ AddStep("toggle featured artist filter",
+ () => getBeatmapListingOverlay().ChildrenOfType>().First(i => i.Value == SearchGeneral.FeaturedArtists).TriggerClick());
+
+ AddAssert("disclaimer dialog is shown", () => Game.ChildrenOfType().Single().CurrentDialog != null);
+ AddAssert("featured artist filter is still on", () => getBeatmapListingOverlay().ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+
+ AddStep("confirm", () => InputManager.Key(Key.Enter));
+ AddAssert("dialog dismissed", () => Game.ChildrenOfType().Single().CurrentDialog == null);
+
+ AddUntilStep("featured artist filter is off", () => !getBeatmapListingOverlay().ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+ }
+
[Test]
public void TestMainOverlaysClosesNotificationOverlay()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index c64343b47b..5e49cb633e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
@@ -80,6 +80,15 @@ namespace osu.Game.Tests.Visual.Online
AddStep("reset size", () => localConfig.SetValue(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal));
}
+ [Test]
+ public void TestFeaturedArtistFilter()
+ {
+ AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
+ AddAssert("featured artist filter is on", () => overlay.ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+ AddStep("toggle featured artist filter", () => overlay.ChildrenOfType>().First(i => i.Value == SearchGeneral.FeaturedArtists).TriggerClick());
+ AddAssert("featured artist filter is off", () => !overlay.ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+ }
+
[Test]
public void TestHideViaBack()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 3335f69dbb..5d13421195 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -14,6 +14,8 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Resources.Localisation.Web;
@@ -54,6 +56,8 @@ namespace osu.Game.Tests.Visual.Online
{
overlay.ShowBeatmapSet(new APIBeatmapSet
{
+ Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Future genre" },
+ Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "Future language" },
OnlineID = 1235,
Title = @"an awesome beatmap",
Artist = @"naru narusegawa",
@@ -239,6 +243,44 @@ namespace osu.Game.Tests.Visual.Online
AddStep(@"show without reload", overlay.Show);
}
+ [TestCase(BeatmapSetLookupType.BeatmapId)]
+ [TestCase(BeatmapSetLookupType.SetId)]
+ public void TestFetchLookupType(BeatmapSetLookupType lookupType)
+ {
+ string type = string.Empty;
+
+ AddStep("register request handling", () =>
+ {
+ ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case GetBeatmapSetRequest getBeatmapSet:
+ type = getBeatmapSet.Type.ToString();
+ return true;
+ }
+
+ return false;
+ };
+ });
+
+ AddStep(@"fetch", () =>
+ {
+ switch (lookupType)
+ {
+ case BeatmapSetLookupType.BeatmapId:
+ overlay.FetchAndShowBeatmap(55);
+ break;
+
+ case BeatmapSetLookupType.SetId:
+ overlay.FetchAndShowBeatmapSet(55);
+ break;
+ }
+ });
+
+ AddAssert(@"type is correct", () => type == lookupType.ToString());
+ }
+
private APIBeatmapSet createManyDifficultiesBeatmapSet()
{
var set = getBeatmapSet();
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 8cc4eabcd7..a8369dd6d9 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -530,6 +530,52 @@ namespace osu.Game.Tests.Visual.Online
});
}
+ [Test]
+ public void TestTextBoxSavePerChannel()
+ {
+ var testPMChannel = new Channel(testUser);
+
+ AddStep("show overlay", () => chatOverlay.Show());
+ joinTestChannel(0);
+ joinChannel(testPMChannel);
+
+ AddAssert("listing is visible", () => listingIsVisible);
+ AddStep("search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
+ AddAssert("'number 2' saved to selector", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "number 2");
+
+ AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
+ AddAssert("text box cleared on normal channel", () => chatOverlayTextBox.Text == string.Empty);
+ AddAssert("nothing saved on normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
+ AddStep("type '727'", () => chatOverlayTextBox.Text = "727");
+ AddAssert("'727' saved to normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "727");
+
+ AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
+ AddAssert("text box cleared on PM channel", () => chatOverlayTextBox.Text == string.Empty);
+ AddAssert("nothing saved on PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
+ AddStep("type 'hello'", () => chatOverlayTextBox.Text = "hello");
+ AddAssert("'hello' saved to PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "hello");
+
+ AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
+ AddAssert("text box contains '727'", () => chatOverlayTextBox.Text == "727");
+
+ AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
+ AddAssert("text box contains 'hello'", () => chatOverlayTextBox.Text == "hello");
+ AddStep("click close button", () =>
+ {
+ ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType().Single();
+ clickDrawable(closeButton);
+ });
+
+ AddAssert("listing is visible", () => listingIsVisible);
+ AddAssert("text box contains 'channel 2'", () => chatOverlayTextBox.Text == "number 2");
+ AddUntilStep("only channel 2 visible", () =>
+ {
+ IEnumerable listingItems = chatOverlay.ChildrenOfType()
+ .Where(item => item.IsPresent);
+ return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
+ });
+ }
+
private void joinTestChannel(int i)
{
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
index 7981e212d4..d925141510 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
@@ -77,14 +77,14 @@ namespace osu.Game.Tests.Visual.Online
{
var comments = this.ChildrenOfType();
var ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
- return ourComment != null && ourComment.ChildrenOfType().Any(x => x.Text == "Delete");
+ return ourComment != null && ourComment.ChildrenOfType().Any(x => x.Text == "delete");
});
AddAssert("Second doesn't", () =>
{
var comments = this.ChildrenOfType();
var ourComment = comments.Single(x => x.Comment.Id == 2);
- return ourComment.ChildrenOfType().All(x => x.Text != "Delete");
+ return ourComment.ChildrenOfType().All(x => x.Text != "delete");
});
}
@@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("It has delete button", () =>
{
- var btn = ourComment.ChildrenOfType().Single(x => x.Text == "Delete");
+ var btn = ourComment.ChildrenOfType().Single(x => x.Text == "delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
@@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("It has delete button", () =>
{
- var btn = ourComment.ChildrenOfType().Single(x => x.Text == "Delete");
+ var btn = ourComment.ChildrenOfType().Single(x => x.Text == "delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
@@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Online
if (request is not CommentDeleteRequest req)
return false;
- req.TriggerFailure(new Exception());
+ req.TriggerFailure(new InvalidOperationException());
delete = true;
return false;
};
@@ -245,7 +245,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("Click the button", () =>
{
- var btn = targetComment.ChildrenOfType().Single(x => x.Text == "Report");
+ var btn = targetComment.ChildrenOfType().Single(x => x.Text == "report");
InputManager.MoveMouseTo(btn);
InputManager.Click(MouseButton.Left);
});
diff --git a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs
index d884c0cf14..fdc567d4ad 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -11,7 +9,9 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
+using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections;
+using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.Online
{
@@ -39,8 +39,8 @@ namespace osu.Game.Tests.Visual.Online
Child = section = new HistoricalSection(),
});
- AddStep("Show peppy", () => section.User.Value = new APIUser { Id = 2 });
- AddStep("Show WubWoofWolf", () => section.User.Value = new APIUser { Id = 39828 });
+ AddStep("Show peppy", () => section.User.Value = new UserProfileData(new APIUser { Id = 2 }, new OsuRuleset().RulesetInfo));
+ AddStep("Show WubWoofWolf", () => section.User.Value = new UserProfileData(new APIUser { Id = 39828 }, new OsuRuleset().RulesetInfo));
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
index e753632474..25a56196eb 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
@@ -1,18 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Overlays.Profile.Sections.Kudosu;
using System.Collections.Generic;
using System;
using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Online
{
@@ -20,6 +18,9 @@ namespace osu.Game.Tests.Visual.Online
{
private readonly Box background;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
+
public TestSceneKudosuHistory()
{
FillFlowContainer content;
@@ -44,9 +45,9 @@ namespace osu.Game.Tests.Visual.Online
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load()
{
- background.Colour = colours.GreySeaFoam;
+ background.Colour = colourProvider.Background4;
}
private readonly IEnumerable items = new[]
diff --git a/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs
index 15e411b9d8..106433d7ce 100644
--- a/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs
+++ b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Overlays.Profile.Sections.Historical;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -14,6 +12,8 @@ using System.Linq;
using osu.Framework.Testing;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.Profile;
+using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.Online
{
@@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red);
- private readonly Bindable user = new Bindable();
+ private readonly Bindable user = new Bindable();
private readonly PlayHistorySubsection section;
public TestScenePlayHistorySubsection()
@@ -45,49 +45,49 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestNullValues()
{
- AddStep("Load user", () => user.Value = user_with_null_values);
+ AddStep("Load user", () => user.Value = new UserProfileData(user_with_null_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is hidden", () => section.Alpha == 0);
}
[Test]
public void TestEmptyValues()
{
- AddStep("Load user", () => user.Value = user_with_empty_values);
+ AddStep("Load user", () => user.Value = new UserProfileData(user_with_empty_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is hidden", () => section.Alpha == 0);
}
[Test]
public void TestOneValue()
{
- AddStep("Load user", () => user.Value = user_with_one_value);
+ AddStep("Load user", () => user.Value = new UserProfileData(user_with_one_value, new OsuRuleset().RulesetInfo));
AddAssert("Section is hidden", () => section.Alpha == 0);
}
[Test]
public void TestTwoValues()
{
- AddStep("Load user", () => user.Value = user_with_two_values);
+ AddStep("Load user", () => user.Value = new UserProfileData(user_with_two_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
}
[Test]
public void TestConstantValues()
{
- AddStep("Load user", () => user.Value = user_with_constant_values);
+ AddStep("Load user", () => user.Value = new UserProfileData(user_with_constant_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
}
[Test]
public void TestConstantZeroValues()
{
- AddStep("Load user", () => user.Value = user_with_zero_values);
+ AddStep("Load user", () => user.Value = new UserProfileData(user_with_zero_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
}
[Test]
public void TestFilledValues()
{
- AddStep("Load user", () => user.Value = user_with_filled_values);
+ AddStep("Load user", () => user.Value = new UserProfileData(user_with_filled_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
AddAssert("Array length is the same", () => user_with_filled_values.MonthlyPlayCounts.Length == getChartValuesLength());
}
@@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestMissingValues()
{
- AddStep("Load user", () => user.Value = user_with_missing_values);
+ AddStep("Load user", () => user.Value = new UserProfileData(user_with_missing_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
AddAssert("Array length is 7", () => getChartValuesLength() == 7);
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs
index e81b7a2ac8..bb2ef1c1b0 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Rulesets.Catch;
@@ -13,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Game.Overlays;
using osu.Framework.Allocation;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.Profile;
namespace osu.Game.Tests.Visual.Online
{
@@ -23,25 +22,24 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneProfileRulesetSelector()
{
- ProfileRulesetSelector selector;
- var user = new Bindable();
+ var user = new Bindable();
- Child = selector = new ProfileRulesetSelector
+ Child = new ProfileRulesetSelector
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
User = { BindTarget = user }
};
+ AddStep("User on osu ruleset", () => user.Value = new UserProfileData(new APIUser { Id = 0, PlayMode = "osu" }, new OsuRuleset().RulesetInfo));
+ AddStep("User on taiko ruleset", () => user.Value = new UserProfileData(new APIUser { Id = 1, PlayMode = "osu" }, new TaikoRuleset().RulesetInfo));
+ AddStep("User on catch ruleset", () => user.Value = new UserProfileData(new APIUser { Id = 2, PlayMode = "osu" }, new CatchRuleset().RulesetInfo));
+ AddStep("User on mania ruleset", () => user.Value = new UserProfileData(new APIUser { Id = 3, PlayMode = "osu" }, new ManiaRuleset().RulesetInfo));
- AddStep("set osu! as default", () => selector.SetDefaultRuleset(new OsuRuleset().RulesetInfo));
- AddStep("set taiko as default", () => selector.SetDefaultRuleset(new TaikoRuleset().RulesetInfo));
- AddStep("set catch as default", () => selector.SetDefaultRuleset(new CatchRuleset().RulesetInfo));
- AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().RulesetInfo));
+ AddStep("User with osu as default", () => user.Value = new UserProfileData(new APIUser { Id = 0, PlayMode = "osu" }, new OsuRuleset().RulesetInfo));
+ AddStep("User with taiko as default", () => user.Value = new UserProfileData(new APIUser { Id = 1, PlayMode = "taiko" }, new OsuRuleset().RulesetInfo));
+ AddStep("User with catch as default", () => user.Value = new UserProfileData(new APIUser { Id = 2, PlayMode = "fruits" }, new OsuRuleset().RulesetInfo));
+ AddStep("User with mania as default", () => user.Value = new UserProfileData(new APIUser { Id = 3, PlayMode = "mania" }, new OsuRuleset().RulesetInfo));
- AddStep("User with osu as default", () => user.Value = new APIUser { Id = 0, PlayMode = "osu" });
- AddStep("User with taiko as default", () => user.Value = new APIUser { Id = 1, PlayMode = "taiko" });
- AddStep("User with catch as default", () => user.Value = new APIUser { Id = 2, PlayMode = "fruits" });
- AddStep("User with mania as default", () => user.Value = new APIUser { Id = 3, PlayMode = "mania" });
AddStep("null user", () => user.Value = null);
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
new file mode 100644
index 0000000000..e62e53bd02
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
@@ -0,0 +1,302 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Models;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Solo;
+using osu.Game.Online.Spectator;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [HeadlessTest]
+ public partial class TestSceneSoloStatisticsWatcher : OsuTestScene
+ {
+ protected override bool UseOnlineAPI => false;
+
+ private SoloStatisticsWatcher watcher = null!;
+
+ [Resolved]
+ private SpectatorClient spectatorClient { get; set; } = null!;
+
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
+ private Action? handleGetUsersRequest;
+ private Action? handleGetUserRequest;
+
+ private IDisposable? subscription;
+
+ private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("clear server-side stats", () => serverSideStatistics.Clear());
+ AddStep("set up request handling", () =>
+ {
+ handleGetUserRequest = null;
+ handleGetUsersRequest = null;
+
+ dummyAPI.HandleRequest = request =>
+ {
+ switch (request)
+ {
+ case GetUsersRequest getUsersRequest:
+ if (handleGetUsersRequest != null)
+ {
+ handleGetUsersRequest?.Invoke(getUsersRequest);
+ }
+ else
+ {
+ int userId = getUsersRequest.UserIds.Single();
+ var response = new GetUsersResponse
+ {
+ Users = new List
+ {
+ new APIUser
+ {
+ Id = userId,
+ RulesetsStatistics = new Dictionary
+ {
+ ["osu"] = tryGetStatistics(userId, "osu"),
+ ["taiko"] = tryGetStatistics(userId, "taiko"),
+ ["fruits"] = tryGetStatistics(userId, "fruits"),
+ ["mania"] = tryGetStatistics(userId, "mania"),
+ }
+ }
+ }
+ };
+ getUsersRequest.TriggerSuccess(response);
+ }
+
+ return true;
+
+ case GetUserRequest getUserRequest:
+ if (handleGetUserRequest != null)
+ {
+ handleGetUserRequest.Invoke(getUserRequest);
+ }
+ else
+ {
+ int userId = int.Parse(getUserRequest.Lookup);
+ string rulesetName = getUserRequest.Ruleset.ShortName;
+ var response = new APIUser
+ {
+ Id = userId,
+ Statistics = tryGetStatistics(userId, rulesetName)
+ };
+ getUserRequest.TriggerSuccess(response);
+ }
+
+ return true;
+
+ default:
+ return false;
+ }
+ };
+ });
+
+ AddStep("create watcher", () =>
+ {
+ Child = watcher = new SoloStatisticsWatcher();
+ });
+ }
+
+ private UserStatistics tryGetStatistics(int userId, string rulesetName)
+ => serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics();
+
+ [Test]
+ public void TestStatisticsUpdateFiredAfterRegistrationAddedAndScoreProcessed()
+ {
+ int userId = getUserId();
+ long scoreId = getScoreId();
+ setUpUser(userId);
+
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddUntilStep("update received", () => update != null);
+ AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
+ AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(5_000_000));
+ }
+
+ [Test]
+ public void TestStatisticsUpdateFiredAfterScoreProcessedAndRegistrationAdded()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ // note ordering - in this test processing completes *before* the registration is added.
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddUntilStep("update received", () => update != null);
+ AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
+ AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(5_000_000));
+ }
+
+ [Test]
+ public void TestStatisticsUpdateNotFiredIfUserLoggedOut()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("log out user", () => dummyAPI.Logout());
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddWaitStep("wait a bit", 5);
+ AddAssert("update not received", () => update == null);
+
+ AddStep("log in user", () => dummyAPI.Login("user", "password"));
+ }
+
+ [Test]
+ public void TestStatisticsUpdateNotFiredIfAnotherUserLoggedIn()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("change user", () => dummyAPI.LocalUser.Value = new APIUser { Id = getUserId() });
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddWaitStep("wait a bit", 5);
+ AddAssert("update not received", () => update == null);
+ }
+
+ [Test]
+ public void TestStatisticsUpdateNotFiredIfScoreIdDoesNotMatch()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("signal another score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, getScoreId()));
+ AddWaitStep("wait a bit", 5);
+ AddAssert("update not received", () => update == null);
+ }
+
+ // the behaviour exercised in this test may not be final, it is mostly assumed for simplicity.
+ // in the long run we may want each score's update to be entirely isolated from others, rather than have prior unobserved updates merge into the latest.
+ [Test]
+ public void TestIgnoredScoreUpdateIsMergedIntoNextOne()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long firstScoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, firstScoreId));
+
+ long secondScoreId = getScoreId();
+
+ feignScoreProcessing(userId, ruleset, 6_000_000);
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
+ AddUntilStep("update received", () => update != null);
+ AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
+ AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000));
+ }
+
+ [Test]
+ public void TestStatisticsUpdateNotFiredAfterSubscriptionDisposal()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+ AddStep("unsubscribe", () => subscription!.Dispose());
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddWaitStep("wait a bit", 5);
+ AddAssert("update not received", () => update == null);
+ }
+
+ private int nextUserId = 2000;
+ private long nextScoreId = 50000;
+
+ private int getUserId() => ++nextUserId;
+ private long getScoreId() => ++nextScoreId;
+
+ private void setUpUser(int userId)
+ {
+ AddStep("fetch initial stats", () =>
+ {
+ serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 };
+ serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 };
+ serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 };
+ serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 };
+
+ dummyAPI.LocalUser.Value = new APIUser { Id = userId };
+ });
+ }
+
+ private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action onUpdateReady) =>
+ AddStep("register for updates", () => subscription = watcher.RegisterForStatisticsUpdateAfter(
+ new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
+ {
+ Ruleset = rulesetInfo,
+ OnlineID = scoreId
+ },
+ onUpdateReady));
+
+ private void feignScoreProcessing(int userId, RulesetInfo rulesetInfo, long newTotalScore)
+ => AddStep("feign score processing", () => serverSideStatistics[(userId, rulesetInfo.ShortName)] = new UserStatistics { TotalScore = newTotalScore });
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
index ebd5e12acb..d7f79d3e30 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
@@ -50,6 +50,8 @@ namespace osu.Game.Tests.Visual.Online
private ChannelManager channelManager;
private TestStandAloneChatDisplay chatDisplay;
+ private TestStandAloneChatDisplay chatWithTextBox;
+ private TestStandAloneChatDisplay chatWithTextBox2;
private int messageIdSequence;
private Channel testChannel;
@@ -78,7 +80,7 @@ namespace osu.Game.Tests.Visual.Online
private void reinitialiseDrawableDisplay()
{
- Children = new[]
+ Children = new Drawable[]
{
chatDisplay = new TestStandAloneChatDisplay
{
@@ -88,13 +90,28 @@ namespace osu.Game.Tests.Visual.Online
Size = new Vector2(400, 80),
Channel = { Value = testChannel },
},
- new TestStandAloneChatDisplay(true)
+ new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
Margin = new MarginPadding(20),
- Size = new Vector2(400, 150),
- Channel = { Value = testChannel },
+ Children = new[]
+ {
+ chatWithTextBox = new TestStandAloneChatDisplay(true)
+ {
+ Margin = new MarginPadding(20),
+ Size = new Vector2(400, 150),
+ Channel = { Value = testChannel },
+ },
+ chatWithTextBox2 = new TestStandAloneChatDisplay(true)
+ {
+ Margin = new MarginPadding(20),
+ Size = new Vector2(400, 150),
+ Channel = { Value = testChannel },
+ },
+ }
}
};
}
@@ -351,6 +368,13 @@ namespace osu.Game.Tests.Visual.Online
checkScrolledToBottom();
}
+ [Test]
+ public void TestTextBoxSync()
+ {
+ AddStep("type 'hello' to text box 1", () => chatWithTextBox.ChildrenOfType().Single().Text = "hello");
+ AddAssert("text box 2 contains 'hello'", () => chatWithTextBox2.ChildrenOfType().Single().Text == "hello");
+ }
+
private void fillChat(int count = 10)
{
AddStep("fill chat", () =>
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
index d93bf059dd..454242270d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs
index 75743d788a..513c473830 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using NUnit.Framework;
@@ -11,6 +9,7 @@ using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
+using osu.Game.Rulesets.Osu;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
@@ -20,7 +19,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
- private ProfileHeader header;
+ private ProfileHeader header = null!;
[SetUpSteps]
public void SetUpSteps()
@@ -31,36 +30,37 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestBasic()
{
- AddStep("Show example user", () => header.User.Value = TestSceneUserProfileOverlay.TEST_USER);
+ AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo));
}
[Test]
public void TestOnlineState()
{
- AddStep("Show online user", () => header.User.Value = new APIUser
+ AddStep("Show online user", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 1001,
Username = "IAmOnline",
LastVisit = DateTimeOffset.Now,
IsOnline = true,
- });
+ }, new OsuRuleset().RulesetInfo));
- AddStep("Show offline user", () => header.User.Value = new APIUser
+ AddStep("Show offline user", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 1002,
Username = "IAmOffline",
LastVisit = DateTimeOffset.Now.AddDays(-10),
IsOnline = false,
- });
+ }, new OsuRuleset().RulesetInfo));
}
[Test]
public void TestRankedState()
{
- AddStep("Show ranked user", () => header.User.Value = new APIUser
+ AddStep("Show ranked user", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 2001,
Username = "RankedUser",
+ Groups = new[] { new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" } },
Statistics = new UserStatistics
{
IsRanked = true,
@@ -72,9 +72,9 @@ namespace osu.Game.Tests.Visual.Online
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
},
}
- });
+ }, new OsuRuleset().RulesetInfo));
- AddStep("Show unranked user", () => header.User.Value = new APIUser
+ AddStep("Show unranked user", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 2002,
Username = "UnrankedUser",
@@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Online
Data = Enumerable.Range(2345, 85).ToArray()
},
}
- });
+ }, new OsuRuleset().RulesetInfo));
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index 02d01b4a46..f7e88e4437 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -1,14 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
-using osu.Game.Overlays.Profile;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
@@ -16,9 +17,66 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public partial class TestSceneUserProfileOverlay : OsuTestScene
{
- protected override bool UseOnlineAPI => true;
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
- private readonly TestUserProfileOverlay profile;
+ private UserProfileOverlay profile = null!;
+
+ [SetUpSteps]
+ public void SetUp()
+ {
+ AddStep("create profile overlay", () => Child = profile = new UserProfileOverlay());
+ }
+
+ [Test]
+ public void TestBlank()
+ {
+ AddStep("show overlay", () => profile.Show());
+ }
+
+ [Test]
+ public void TestActualUser()
+ {
+ AddStep("set up request handling", () =>
+ {
+ dummyAPI.HandleRequest = req =>
+ {
+ if (req is GetUserRequest getUserRequest)
+ {
+ getUserRequest.TriggerSuccess(TEST_USER);
+ return true;
+ }
+
+ return false;
+ };
+ });
+ AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
+ AddToggleStep("toggle visibility", visible => profile.State.Value = visible ? Visibility.Visible : Visibility.Hidden);
+ AddStep("log out", () => dummyAPI.Logout());
+ AddStep("log back in", () => dummyAPI.Login("username", "password"));
+ }
+
+ [Test]
+ public void TestLoading()
+ {
+ GetUserRequest pendingRequest = null!;
+
+ AddStep("set up request handling", () =>
+ {
+ dummyAPI.HandleRequest = req =>
+ {
+ if (req is GetUserRequest getUserRequest)
+ {
+ pendingRequest = getUserRequest;
+ return true;
+ }
+
+ return false;
+ };
+ });
+ AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
+ AddWaitStep("wait some", 3);
+ AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
+ }
public static readonly APIUser TEST_USER = new APIUser
{
@@ -28,7 +86,21 @@ namespace osu.Game.Tests.Visual.Online
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
JoinDate = DateTimeOffset.Now.AddDays(-1),
LastVisit = DateTimeOffset.Now,
- ProfileOrder = new[] { "me" },
+ Groups = new[]
+ {
+ new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
+ new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
+ },
+ ProfileOrder = new[]
+ {
+ @"me",
+ @"recent_activity",
+ @"beatmaps",
+ @"historical",
+ @"kudosu",
+ @"top_ranks",
+ @"medals"
+ },
Statistics = new UserStatistics
{
IsRanked = true,
@@ -65,61 +137,12 @@ namespace osu.Game.Tests.Visual.Online
Title = "osu!volunteer",
Colour = "ff0000",
Achievements = Array.Empty(),
+ PlayMode = "osu",
+ Kudosu = new APIUser.KudosuCount
+ {
+ Available = 10,
+ Total = 50
+ }
};
-
- public TestSceneUserProfileOverlay()
- {
- Add(profile = new TestUserProfileOverlay());
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- AddStep("Show offline dummy", () => profile.ShowUser(TEST_USER));
-
- AddStep("Show null dummy", () => profile.ShowUser(new APIUser
- {
- Username = @"Null",
- Id = 1,
- }));
-
- AddStep("Show ppy", () => profile.ShowUser(new APIUser
- {
- Username = @"peppy",
- Id = 2,
- IsSupporter = true,
- CountryCode = CountryCode.AU,
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg"
- }));
-
- AddStep("Show flyte", () => profile.ShowUser(new APIUser
- {
- Username = @"flyte",
- Id = 3103765,
- CountryCode = CountryCode.JP,
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
- }));
-
- AddStep("Show bancho", () => profile.ShowUser(new APIUser
- {
- Username = @"BanchoBot",
- Id = 3,
- IsBot = true,
- CountryCode = CountryCode.SH,
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg"
- }));
-
- AddStep("Show ppy from username", () => profile.ShowUser(new APIUser { Username = @"peppy" }));
- AddStep("Show flyte from username", () => profile.ShowUser(new APIUser { Username = @"flyte" }));
-
- AddStep("Hide", profile.Hide);
- AddStep("Show without reload", profile.Show);
- }
-
- private partial class TestUserProfileOverlay : UserProfileOverlay
- {
- public new ProfileHeader Header => base.Header;
- }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs
index fcefb31716..921738d331 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
@@ -14,7 +12,7 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
{
- private PreviousUsernames container;
+ private PreviousUsernames container = null!;
[SetUp]
public void SetUp() => Schedule(() =>
@@ -50,7 +48,7 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("Is hidden", () => container.Alpha == 0);
}
- private static readonly APIUser[] users =
+ private static readonly APIUser?[] users =
{
new APIUser { Id = 1, PreviousUsernames = new[] { "username1" } },
new APIUser { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } },
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs
index f8432118d4..9d0ea80533 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
index 6f0ef10911..5249e8694d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs b/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs
index ef3a677efc..a3825f4694 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -12,7 +10,9 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
+using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections;
+using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.Online
{
@@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Online
}
});
- AddStep("Show cookiezi", () => ranks.User.Value = new APIUser { Id = 124493 });
+ AddStep("Show cookiezi", () => ranks.User.Value = new UserProfileData(new APIUser { Id = 124493 }, new OsuRuleset().RulesetInfo));
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
index b486f800c6..b353123649 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
@@ -16,13 +16,17 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers.Markdown;
+using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki.Markdown;
+using osu.Game.Users.Drawables;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
- public partial class TestSceneWikiMarkdownContainer : OsuTestScene
+ public partial class TestSceneWikiMarkdownContainer : OsuManualInputManagerTestScene
{
+ private OverlayScrollContainer scrollContainer;
private TestMarkdownContainer markdownContainer;
[Cached]
@@ -38,15 +42,25 @@ namespace osu.Game.Tests.Visual.Online
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
- new BasicScrollContainer
+ scrollContainer = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
- Child = markdownContainer = new TestMarkdownContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- }
+ }
+ };
+
+ scrollContainer.Child = new DependencyProvidingContainer
+ {
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(OverlayScrollContainer), scrollContainer)
+ },
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = markdownContainer = new TestMarkdownContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
}
};
});
@@ -197,6 +211,63 @@ Line after image";
markdownContainer.CurrentPath = @"https://dev.ppy.sh";
markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::";
});
+ AddAssert("Two flags visible", () => markdownContainer.ChildrenOfType().Count(), () => Is.EqualTo(2));
+ }
+
+ [Test]
+ public void TestHeadingWithIdAttribute()
+ {
+ AddStep("Add heading with ID", () =>
+ {
+ markdownContainer.Text = "# This is a heading with an ID {#this-is-the-id}";
+ });
+ AddAssert("ID not visible", () => markdownContainer.ChildrenOfType().All(spriteText => spriteText.Text != "{#this-is-the-id}"));
+ }
+
+ [Test]
+ public void TestFootnotes()
+ {
+ AddStep("set content", () => markdownContainer.Text = @"This text has a footnote[^test].
+
+Here's some more text[^test2] with another footnote!
+
+# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam efficitur laoreet posuere. Ut accumsan tortor in ipsum tincidunt ultrices. Suspendisse a malesuada tellus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce a sagittis nibh. In et velit sit amet mauris aliquet consectetur quis vehicula lorem. Etiam sit amet tellus ac velit ornare maximus. Donec quis metus eget libero ullamcorper imperdiet id vitae arcu. Vivamus iaculis rhoncus purus malesuada mollis. Vestibulum dictum at nisi sed tincidunt. Suspendisse finibus, ipsum ut dapibus commodo, leo eros porttitor sapien, non scelerisque nisi ligula sed ex. Pellentesque magna orci, hendrerit eu iaculis sit amet, ullamcorper in urna. Vivamus dictum mauris orci, nec facilisis dolor fringilla eu. Sed at porttitor nisi, at venenatis urna. Ut at orci vitae libero semper ullamcorper eu ut risus. Mauris hendrerit varius enim, ut varius nisi feugiat mattis.
+
+## In at eros urna. Sed ipsum lorem, tempor sit amet purus in, vehicula pellentesque leo. Fusce volutpat pellentesque velit sit amet porttitor. Nulla eget erat ex. Praesent eu lacinia est, quis vehicula lacus. Donec consequat ultrices neque, at finibus quam efficitur vel. Vestibulum molestie nisl sit amet metus semper, at vestibulum massa rhoncus. Quisque imperdiet suscipit augue, et dignissim odio eleifend ut.
+
+Aliquam sed vestibulum mauris, ut lobortis elit. Sed quis lacinia erat. Nam ultricies, risus non pellentesque sollicitudin, mauris dolor tincidunt neque, ac porta ipsum dui quis libero. Integer eget velit neque. Vestibulum venenatis mauris vitae rutrum vestibulum. Maecenas suscipit eu purus eu tempus. Nam dui nisl, bibendum condimentum mollis et, gravida vel dui. Sed et eros rutrum, facilisis sapien eu, mattis ligula. Fusce finibus pulvinar dolor quis consequat.
+
+Donec ipsum felis, feugiat vel fermentum at, commodo eu sapien. Suspendisse nec enim vitae felis laoreet laoreet. Phasellus purus quam, fermentum a pharetra vel, tempor et urna. Integer vitae quam diam. Aliquam tincidunt tortor a iaculis convallis. Suspendisse potenti. Cras quis risus quam. Nullam tincidunt in lorem posuere sagittis.
+
+Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed urna tristique, laoreet pharetra nulla. Vivamus maximus turpis purus, eu viverra dolor sodales porttitor. Praesent bibendum sapien purus, sed ultricies dolor iaculis sed. Fusce congue hendrerit malesuada. Nulla nulla est, auctor ac fringilla sed, ornare a lorem. Donec quis velit imperdiet, imperdiet sem non, pellentesque sapien. Maecenas in orci id ipsum placerat facilisis non sed nisi. Duis dictum lorem sodales odio dictum eleifend. Vestibulum bibendum euismod quam, eget pharetra orci facilisis sed. Vivamus at diam non ipsum consequat tristique. Pellentesque gravida dignissim pellentesque. Donec ullamcorper lacinia orci, id consequat purus faucibus quis. Phasellus metus nunc, iaculis a interdum vel, congue sed erat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam eros libero, hendrerit luctus nulla vitae, luctus maximus nunc.
+
+[^test]: This is a **footnote**.
+[^test2]: This is another footnote [with a link](https://google.com/)!");
+ AddStep("shrink scroll height", () => scrollContainer.Height = 0.5f);
+
+ AddStep("press second footnote link", () =>
+ {
+ InputManager.MoveMouseTo(markdownContainer.ChildrenOfType().ElementAt(1));
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("second footnote scrolled into view", () =>
+ {
+ var footnote = markdownContainer.ChildrenOfType().ElementAt(1);
+ return scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.TopLeft)
+ && scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.BottomRight);
+ });
+
+ AddStep("press first footnote backlink", () =>
+ {
+ InputManager.MoveMouseTo(markdownContainer.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("first footnote link scrolled into view", () =>
+ {
+ var footnote = markdownContainer.ChildrenOfType().First();
+ return scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.TopLeft)
+ && scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.BottomRight);
+ });
}
private partial class TestMarkdownContainer : WikiMarkdownContainer
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
index 620fd710e3..b0e4303ca4 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
@@ -4,8 +4,11 @@
#nullable disable
using System;
+using System.Linq;
using System.Net;
using NUnit.Framework;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -29,6 +32,15 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Show main page", () => wiki.Show());
}
+ [Test]
+ public void TestCancellationDoesntShowError()
+ {
+ AddStep("Show main page", () => wiki.Show());
+ AddStep("Show another page", () => wiki.ShowPage("Article_styling_criteria/Formatting"));
+
+ AddUntilStep("Current path is not error", () => wiki.CurrentPath != "error");
+ }
+
[Test]
public void TestArticlePage()
{
@@ -56,7 +68,9 @@ namespace osu.Game.Tests.Visual.Online
public void TestErrorPage()
{
setUpWikiResponse(responseArticlePage);
- AddStep("Show Error Page", () => wiki.ShowPage("Error"));
+ AddStep("Show nonexistent page", () => wiki.ShowPage("This_page_will_error_out"));
+ AddUntilStep("Wait for error page", () => wiki.CurrentPath == "error");
+ AddUntilStep("Error message correct", () => wiki.ChildrenOfType().Any(text => text.Text == "\"This_page_will_error_out\"."));
}
private void setUpWikiResponse(APIWikiPage r, string redirectionPath = null)
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs
new file mode 100644
index 0000000000..355a572f95
--- /dev/null
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs
@@ -0,0 +1,117 @@
+// 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.Solo;
+using osu.Game.Scoring;
+using osu.Game.Screens.Ranking.Statistics.User;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Ranking
+{
+ public partial class TestSceneOverallRanking : OsuTestScene
+ {
+ private OverallRanking overallRanking = null!;
+
+ [Test]
+ public void TestUpdatePending()
+ {
+ createDisplay();
+ }
+
+ [Test]
+ public void TestAllIncreased()
+ {
+ createDisplay();
+ displayUpdate(
+ new UserStatistics
+ {
+ GlobalRank = 12_345,
+ Accuracy = 98.99,
+ MaxCombo = 2_322,
+ RankedScore = 23_123_543_456,
+ TotalScore = 123_123_543_456,
+ PP = 5_072
+ },
+ new UserStatistics
+ {
+ GlobalRank = 1_234,
+ Accuracy = 99.07,
+ MaxCombo = 2_352,
+ RankedScore = 23_124_231_435,
+ TotalScore = 123_124_231_435,
+ PP = 5_434
+ });
+ }
+
+ [Test]
+ public void TestAllDecreased()
+ {
+ createDisplay();
+ displayUpdate(
+ new UserStatistics
+ {
+ GlobalRank = 1_234,
+ Accuracy = 99.07,
+ MaxCombo = 2_352,
+ RankedScore = 23_124_231_435,
+ TotalScore = 123_124_231_435,
+ PP = 5_434
+ },
+ new UserStatistics
+ {
+ GlobalRank = 12_345,
+ Accuracy = 98.99,
+ MaxCombo = 2_322,
+ RankedScore = 23_123_543_456,
+ TotalScore = 123_123_543_456,
+ PP = 5_072
+ });
+ }
+
+ [Test]
+ public void TestNoChanges()
+ {
+ var statistics = new UserStatistics
+ {
+ GlobalRank = 12_345,
+ Accuracy = 98.99,
+ MaxCombo = 2_322,
+ RankedScore = 23_123_543_456,
+ TotalScore = 123_123_543_456,
+ PP = 5_072
+ };
+
+ createDisplay();
+ displayUpdate(statistics, statistics);
+ }
+
+ [Test]
+ public void TestNotRanked()
+ {
+ var statistics = new UserStatistics
+ {
+ GlobalRank = null,
+ Accuracy = 98.99,
+ MaxCombo = 2_322,
+ RankedScore = 23_123_543_456,
+ TotalScore = 123_123_543_456,
+ PP = null
+ };
+
+ createDisplay();
+ displayUpdate(statistics, statistics);
+ }
+
+ private void createDisplay() => AddStep("create display", () => Child = overallRanking = new OverallRanking
+ {
+ Width = 400,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ });
+
+ private void displayUpdate(UserStatistics before, UserStatistics after) =>
+ AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new SoloStatisticsUpdate(new ScoreInfo(), before, after));
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 2d1c5ef120..ea9508ecff 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -16,7 +16,9 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;
@@ -72,7 +74,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray();
return visibleBeatmapPanels.Length == 1
- && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1;
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 0) == 1;
});
AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria
@@ -86,8 +88,8 @@ namespace osu.Game.Tests.Visual.SongSelect
var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray();
return visibleBeatmapPanels.Length == 2
- && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1
- && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 1) == 1;
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 0) == 1
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 1) == 1;
});
AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria
@@ -101,8 +103,8 @@ namespace osu.Game.Tests.Visual.SongSelect
var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray();
return visibleBeatmapPanels.Length == 2
- && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1
- && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 2) == 1;
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item!).BeatmapInfo.Ruleset.OnlineID == 0) == 1
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item!).BeatmapInfo.Ruleset.OnlineID == 2) == 1;
});
}
@@ -926,10 +928,7 @@ namespace osu.Game.Tests.Visual.SongSelect
// 10 sets that go osu! -> taiko -> catch -> osu! -> ...
for (int i = 0; i < 10; i++)
- {
- var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
- sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
- }
+ sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { getRuleset(i) }));
// Sort mode is important to keep the ruleset order
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
@@ -937,13 +936,29 @@ namespace osu.Game.Tests.Visual.SongSelect
for (int i = 1; i < 10; i++)
{
- var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
+ var rulesetInfo = getRuleset(i % 3);
+
AddStep($"Set ruleset to {rulesetInfo.ShortName}", () =>
{
carousel.Filter(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }, false);
});
waitForSelection(i + 1, 1);
}
+
+ static RulesetInfo getRuleset(int index)
+ {
+ switch (index % 3)
+ {
+ default:
+ return new OsuRuleset().RulesetInfo;
+
+ case 1:
+ return new TaikoRuleset().RulesetInfo;
+
+ case 2:
+ return new CatchRuleset().RulesetInfo;
+ }
+ }
}
[Test]
@@ -953,10 +968,7 @@ namespace osu.Game.Tests.Visual.SongSelect
// 10 sets that go taiko, osu!, osu!, osu!, taiko, osu!, osu!, osu!, ...
for (int i = 0; i < 10; i++)
- {
- var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 4 == 0 ? 1 : 0);
- sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
- }
+ sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { getRuleset(i) }));
// Sort mode is important to keep the ruleset order
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
@@ -974,6 +986,18 @@ namespace osu.Game.Tests.Visual.SongSelect
carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false);
});
}
+
+ static RulesetInfo getRuleset(int index)
+ {
+ switch (index % 4)
+ {
+ case 0:
+ return new TaikoRuleset().RulesetInfo;
+
+ default:
+ return new OsuRuleset().RulesetInfo;
+ }
+ }
}
private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, int? count = null,
@@ -1069,7 +1093,7 @@ namespace osu.Game.Tests.Visual.SongSelect
return Precision.AlmostEquals(
carousel.ScreenSpaceDrawQuad.Centre,
carousel.Items
- .First(i => i.Item.State.Value == CarouselItemState.Selected)
+ .First(i => i.Item?.State.Value == CarouselItemState.Selected)
.ScreenSpaceDrawQuad.Centre, 100);
});
}
@@ -1103,7 +1127,7 @@ namespace osu.Game.Tests.Visual.SongSelect
if (currentlySelected == null)
return true;
- return currentlySelected.Item.Visible;
+ return currentlySelected.Item!.Visible;
}
private void checkInvisibleDifficultiesUnselectable()
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 01c5ad8358..cfee02dfb8 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType()
- .First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
+ .First(b => !((CarouselBeatmap)b.Item!).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
InputManager.Click(MouseButton.Left);
@@ -235,7 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType()
- .First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
+ .First(b => !((CarouselBeatmap)b.Item!).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
InputManager.PressButton(MouseButton.Left);
@@ -614,7 +614,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("selected only shows expected ruleset (plus converts)", () =>
{
- var selectedPanel = songSelect!.Carousel.ChildrenOfType().First(s => s.Item.State.Value == CarouselItemState.Selected);
+ var selectedPanel = songSelect!.Carousel.ChildrenOfType().First(s => s.Item!.State.Value == CarouselItemState.Selected);
// special case for converts checked here.
return selectedPanel.ChildrenOfType().All(i =>
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
index e47b7e25a8..11d55bc0bd 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
@@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.SongSelect
if (testRequest.Progress >= 0.5f)
{
- testRequest.TriggerFailure(new Exception());
+ testRequest.TriggerFailure(new InvalidOperationException());
return true;
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonsInput.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonsInput.cs
new file mode 100644
index 0000000000..985f613b63
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonsInput.cs
@@ -0,0 +1,129 @@
+// 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.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Overlays.Settings;
+using NUnit.Framework;
+using osuTK;
+using osu.Game.Overlays;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Graphics.UserInterface;
+using osu.Framework.Allocation;
+using osu.Game.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public partial class TestSceneButtonsInput : OsuManualInputManagerTestScene
+ {
+ private const int width = 500;
+
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
+ private readonly SettingsButton settingsButton;
+ private readonly OsuClickableContainer clickableContainer;
+ private readonly RoundedButton roundedButton;
+ private readonly ShearedButton shearedButton;
+
+ public TestSceneButtonsInput()
+ {
+ Add(new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ Width = 500,
+ Spacing = new Vector2(0, 5),
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ clickableContainer = new OsuClickableContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 40,
+ Enabled = { Value = true },
+ Masking = true,
+ CornerRadius = 20,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Red
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "Rounded clickable container"
+ }
+ }
+ },
+ settingsButton = new SettingsButton
+ {
+ Enabled = { Value = true },
+ Text = "Settings button"
+ },
+ roundedButton = new RoundedButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Enabled = { Value = true },
+ Text = "Rounded button"
+ },
+ shearedButton = new ShearedButton(width)
+ {
+ Text = "Sheared button",
+ LighterColour = Colour4.FromHex("#FFFFFF"),
+ DarkerColour = Colour4.FromHex("#FFCC22"),
+ TextColour = Colour4.Black,
+ Height = 40,
+ Enabled = { Value = true },
+ Padding = new MarginPadding(0)
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestSettingsButtonInput()
+ {
+ AddStep("Move cursor to button", () => InputManager.MoveMouseTo(settingsButton));
+ AddAssert("Button is hovered", () => settingsButton.IsHovered);
+ AddStep("Move cursor to padded area", () => InputManager.MoveMouseTo(settingsButton.ScreenSpaceDrawQuad.TopLeft + new Vector2(SettingsPanel.CONTENT_MARGINS / 2f, 10)));
+ AddAssert("Cursor within a button", () => settingsButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
+ AddAssert("Button is not hovered", () => !settingsButton.IsHovered);
+ }
+
+ [Test]
+ public void TestRoundedButtonInput()
+ {
+ AddStep("Move cursor to button", () => InputManager.MoveMouseTo(roundedButton));
+ AddAssert("Button is hovered", () => roundedButton.IsHovered);
+ AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(roundedButton.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
+ AddAssert("Cursor within a button", () => roundedButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
+ AddAssert("Button is not hovered", () => !roundedButton.IsHovered);
+ }
+
+ [Test]
+ public void TestShearedButtonInput()
+ {
+ AddStep("Move cursor to button", () => InputManager.MoveMouseTo(shearedButton));
+ AddAssert("Button is hovered", () => shearedButton.IsHovered);
+ AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(shearedButton.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
+ AddAssert("Cursor within a button", () => shearedButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
+ AddAssert("Button is not hovered", () => !shearedButton.IsHovered);
+ }
+
+ [Test]
+ public void TestRoundedClickableContainerInput()
+ {
+ AddStep("Move cursor to button", () => InputManager.MoveMouseTo(clickableContainer));
+ AddAssert("Button is hovered", () => clickableContainer.IsHovered);
+ AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(clickableContainer.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
+ AddAssert("Cursor within a button", () => clickableContainer.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
+ AddAssert("Button is not hovered", () => !clickableContainer.IsHovered);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
index 99e1702870..e7840d4a2a 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
@@ -1,13 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Localisation;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Comments;
using osuTK;
@@ -20,8 +23,8 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
- private TestCommentEditor commentEditor;
- private TestCancellableCommentEditor cancellableCommentEditor;
+ private TestCommentEditor commentEditor = null!;
+ private TestCancellableCommentEditor cancellableCommentEditor = null!;
[SetUp]
public void SetUp() => Schedule(() =>
@@ -45,15 +48,16 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("click on text box", () =>
{
- InputManager.MoveMouseTo(commentEditor);
+ InputManager.MoveMouseTo(commentEditor.ChildrenOfType().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("enter text", () => commentEditor.Current.Value = "text");
AddStep("press Enter", () => InputManager.Key(Key.Enter));
+ AddUntilStep("button is loading", () => commentEditor.IsSpinnerShown);
AddAssert("text committed", () => commentEditor.CommittedText == "text");
- AddAssert("button is loading", () => commentEditor.IsLoading);
+ AddUntilStep("button is not loading", () => !commentEditor.IsSpinnerShown);
}
[Test]
@@ -61,14 +65,14 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("click on text box", () =>
{
- InputManager.MoveMouseTo(commentEditor);
+ InputManager.MoveMouseTo(commentEditor.ChildrenOfType().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("press Enter", () => InputManager.Key(Key.Enter));
- AddAssert("no text committed", () => commentEditor.CommittedText == null);
- AddAssert("button is not loading", () => !commentEditor.IsLoading);
+ AddAssert("button is not loading", () => !commentEditor.IsSpinnerShown);
+ AddAssert("no text committed", () => commentEditor.CommittedText.Length == 0);
}
[Test]
@@ -76,7 +80,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("click on text box", () =>
{
- InputManager.MoveMouseTo(commentEditor);
+ InputManager.MoveMouseTo(commentEditor.ChildrenOfType().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("enter text", () => commentEditor.Current.Value = "some other text");
@@ -87,8 +91,9 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left);
});
+ AddUntilStep("button is loading", () => commentEditor.IsSpinnerShown);
AddAssert("text committed", () => commentEditor.CommittedText == "some other text");
- AddAssert("button is loading", () => commentEditor.IsLoading);
+ AddUntilStep("button is not loading", () => !commentEditor.IsSpinnerShown);
}
[Test]
@@ -96,7 +101,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("click cancel button", () =>
{
- InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer);
+ InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[1]);
InputManager.Click(MouseButton.Left);
});
@@ -108,28 +113,27 @@ namespace osu.Game.Tests.Visual.UserInterface
public new Bindable Current => base.Current;
public new FillFlowContainer ButtonsContainer => base.ButtonsContainer;
- public string CommittedText { get; private set; }
+ public string CommittedText { get; private set; } = string.Empty;
- public TestCommentEditor()
- {
- OnCommit = onCommit;
- }
+ public bool IsSpinnerShown => this.ChildrenOfType().Single().IsPresent;
- private void onCommit(string value)
+ protected override void OnCommit(string value)
{
+ ShowLoadingSpinner = true;
CommittedText = value;
- Scheduler.AddDelayed(() => IsLoading = false, 1000);
+ Scheduler.AddDelayed(() => ShowLoadingSpinner = false, 1000);
}
- protected override string FooterText => @"Footer text. And it is pretty long. Cool.";
- protected override string CommitButtonText => @"Commit";
- protected override string TextBoxPlaceholder => @"This text box is empty";
+ protected override LocalisableString FooterText => @"Footer text. And it is pretty long. Cool.";
+ protected override LocalisableString CommitButtonText => @"Commit";
+ protected override LocalisableString TextBoxPlaceholder => @"This text box is empty";
}
private partial class TestCancellableCommentEditor : CancellableCommentEditor
{
public new FillFlowContainer ButtonsContainer => base.ButtonsContainer;
- protected override string FooterText => @"Wow, another one. Sicc";
+
+ protected override LocalisableString FooterText => @"Wow, another one. Sicc";
public bool Cancelled { get; private set; }
@@ -138,8 +142,12 @@ namespace osu.Game.Tests.Visual.UserInterface
OnCancel = () => Cancelled = true;
}
- protected override string CommitButtonText => @"Save";
- protected override string TextBoxPlaceholder => @"Multiline textboxes soon";
+ protected override void OnCommit(string text)
+ {
+ }
+
+ protected override LocalisableString CommitButtonText => @"Save";
+ protected override LocalisableString TextBoxPlaceholder => @"Multiline textboxes soon";
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs
deleted file mode 100644
index 41e5d47093..0000000000
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using NUnit.Framework;
-using osu.Framework.Graphics;
-using osu.Game.Graphics.UserInterface;
-using osuTK;
-
-namespace osu.Game.Tests.Visual.UserInterface
-{
- public partial class TestSceneOsuButton : OsuTestScene
- {
- [Test]
- public void TestToggleEnabled()
- {
- OsuButton button = null;
-
- AddStep("add button", () => Child = button = new OsuButton
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(200),
- Text = "Button"
- });
-
- AddToggleStep("toggle enabled", toggle =>
- {
- for (int i = 0; i < 6; i++)
- button.Action = toggle ? () => { } : null;
- });
- }
-
- [Test]
- public void TestInitiallyDisabled()
- {
- AddStep("add button", () => Child = new OsuButton
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(200),
- Text = "Button"
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs
new file mode 100644
index 0000000000..d9c2774611
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs
@@ -0,0 +1,106 @@
+// 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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Overlays.Volume;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+using Box = osu.Framework.Graphics.Shapes.Box;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public partial class TestSceneOverlayContainer : OsuManualInputManagerTestScene
+ {
+ [SetUp]
+ public void SetUp() => Schedule(() => Child = new TestOverlay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.5f)
+ });
+
+ [Test]
+ public void TestScrollBlocked()
+ {
+ OsuScrollContainer scroll = null!;
+
+ AddStep("add scroll container", () =>
+ {
+ Add(scroll = new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = DrawHeight * 10,
+ Colour = ColourInfo.GradientVertical(Colour4.Black, Colour4.White),
+ }
+ });
+ });
+
+ AddStep("perform scroll", () =>
+ {
+ InputManager.MoveMouseTo(Content);
+ InputManager.ScrollVerticalBy(-10);
+ });
+
+ AddAssert("scroll didn't receive input", () => scroll.Current == 0);
+ }
+
+ [Test]
+ public void TestAltScrollNotBlocked()
+ {
+ bool scrollReceived = false;
+
+ AddStep("add volume control receptor", () => Add(new VolumeControlReceptor
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue,
+ ScrollActionRequested = (_, _, _) => scrollReceived = true,
+ }));
+
+ AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
+ AddStep("perform scroll", () =>
+ {
+ InputManager.MoveMouseTo(Content);
+ InputManager.ScrollVerticalBy(10);
+ });
+
+ AddAssert("receptor received scroll input", () => scrollReceived);
+ AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
+ }
+
+ private partial class TestOverlay : OsuFocusedOverlayContainer
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ State.Value = Visibility.Visible;
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "Overlay content",
+ Colour = Color4.Black,
+ },
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
index 8f10065d17..e90041774e 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
@@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.UserInterface
protected override OverlayTitle CreateTitle() => new TestTitle();
- protected override Drawable CreateTitleContent() => new OverlayRulesetSelector();
+ protected override Drawable CreateTabControlContent() => new OverlayRulesetSelector();
public TestStringTabControlHeader()
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
index b4b45da133..c51095f360 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Game.Overlays.Profile.Sections;
using osu.Framework.Testing;
@@ -18,7 +16,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
- private ProfileSubsectionHeader header;
+ private ProfileSubsectionHeader header = null!;
[Test]
public void TestHiddenCounter()
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSegmentedGraph.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSegmentedGraph.cs
new file mode 100644
index 0000000000..80941569af
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSegmentedGraph.cs
@@ -0,0 +1,154 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public partial class TestSceneSegmentedGraph : OsuTestScene
+ {
+ private readonly SegmentedGraph graph;
+
+ public TestSceneSegmentedGraph()
+ {
+ Children = new Drawable[]
+ {
+ graph = new SegmentedGraph(6)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(1, 0.5f),
+ },
+ };
+
+ graph.TierColours = new[]
+ {
+ Colour4.Red,
+ Colour4.OrangeRed,
+ Colour4.Orange,
+ Colour4.Yellow,
+ Colour4.YellowGreen,
+ Colour4.Green
+ };
+
+ AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).ToArray());
+ AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).ToArray());
+ AddStep("values from 1-500", () => graph.Values = Enumerable.Range(1, 500).ToArray());
+ AddStep("sin() function of size 100", () => sinFunction());
+ AddStep("sin() function of size 500", () => sinFunction(500));
+ AddStep("bumps of size 100", () => bumps());
+ AddStep("bumps of size 500", () => bumps(500));
+ AddStep("100 random values", () => randomValues());
+ AddStep("500 random values", () => randomValues(500));
+ AddStep("beatmap density with granularity of 200", () => beatmapDensity());
+ AddStep("beatmap density with granularity of 300", () => beatmapDensity(300));
+ AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().ToArray());
+ AddStep("change colour", () =>
+ {
+ graph.TierColours = new[]
+ {
+ Colour4.White,
+ Colour4.LightBlue,
+ Colour4.Aqua,
+ Colour4.Blue
+ };
+ });
+ AddStep("reset colour", () =>
+ {
+ graph.TierColours = new[]
+ {
+ Colour4.Red,
+ Colour4.OrangeRed,
+ Colour4.Orange,
+ Colour4.Yellow,
+ Colour4.YellowGreen,
+ Colour4.Green
+ };
+ });
+ }
+
+ private void sinFunction(int size = 100)
+ {
+ const int max_value = 255;
+ graph.Values = new int[size];
+
+ float step = 2 * MathF.PI / size;
+ float x = 0;
+
+ for (int i = 0; i < size; i++)
+ {
+ graph.Values[i] = (int)(max_value * MathF.Sin(x));
+ x += step;
+ }
+ }
+
+ private void bumps(int size = 100)
+ {
+ const int max_value = 255;
+ graph.Values = new int[size];
+
+ float step = 2 * MathF.PI / size;
+ float x = 0;
+
+ for (int i = 0; i < size; i++)
+ {
+ graph.Values[i] = (int)(max_value * Math.Abs(MathF.Sin(x)));
+ x += step;
+ }
+ }
+
+ private void randomValues(int size = 100)
+ {
+ Random rng = new Random();
+
+ graph.Values = new int[size];
+
+ for (int i = 0; i < size; i++)
+ {
+ graph.Values[i] = rng.Next(255);
+ }
+ }
+
+ private void beatmapDensity(int granularity = 200)
+ {
+ var ruleset = new OsuRuleset();
+ var beatmap = CreateBeatmap(ruleset.RulesetInfo);
+ IEnumerable objects = beatmap.HitObjects;
+
+ // Taken from SongProgressGraph
+ graph.Values = new int[granularity];
+
+ if (!objects.Any())
+ return;
+
+ double firstHit = objects.First().StartTime;
+ double lastHit = objects.Max(o => o.GetEndTime());
+
+ if (lastHit == 0)
+ lastHit = objects.Last().StartTime;
+
+ double interval = (lastHit - firstHit + 1) / granularity;
+
+ foreach (var h in objects)
+ {
+ double endTime = h.GetEndTime();
+
+ Debug.Assert(endTime >= h.StartTime);
+
+ int startRange = (int)((h.StartTime - firstHit) / interval);
+ int endRange = (int)((endTime - firstHit) / interval);
+ for (int i = startRange; i <= endRange; i++)
+ graph.Values[i]++;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
index 71b98ed9af..f96d2feba8 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
@@ -55,6 +55,16 @@ namespace osu.Game.Tests.Visual.UserInterface
};
});
+ [Test]
+ public void TestDisplay()
+ {
+ AddRepeatStep("toggle expanded state", () =>
+ {
+ InputManager.MoveMouseTo(group.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ }, 5);
+ }
+
[Test]
public void TestClickExpandButtonMultipleTimes()
{
diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index 52769321a9..1157b50377 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Tournament.Components
public TournamentBeatmapPanel(TournamentBeatmap beatmap, string mod = null)
{
- if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
+ ArgumentNullException.ThrowIfNull(beatmap);
Beatmap = beatmap;
this.mod = mod;
diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs
index f940571ffe..7babb3ea5a 100644
--- a/osu.Game.Tournament/IPC/FileBasedIPC.cs
+++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs
@@ -127,7 +127,7 @@ namespace osu.Game.Tournament.IPC
using (var stream = IPCStorage.GetStream(file_ipc_state_filename))
using (var sr = new StreamReader(stream))
{
- State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine().AsNonNull());
+ State.Value = Enum.Parse(sr.ReadLine().AsNonNull());
}
}
catch (Exception)
@@ -245,8 +245,10 @@ namespace osu.Game.Tournament.IPC
{
string stableInstallPath;
+#pragma warning disable CA1416
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
+#pragma warning restore CA1416
if (ipcFileExistsInDirectory(stableInstallPath))
return stableInstallPath;
diff --git a/osu.Game.Tournament/SaveChangesOverlay.cs b/osu.Game.Tournament/SaveChangesOverlay.cs
index a81f11cbe1..1bc576bc63 100644
--- a/osu.Game.Tournament/SaveChangesOverlay.cs
+++ b/osu.Game.Tournament/SaveChangesOverlay.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Tournament
private async Task checkForChanges()
{
- string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder());
+ string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder()).ConfigureAwait(false);
// If a save hasn't been triggered by the user yet, populate the initial value
lastSerialisedLadder ??= serialisedLadder;
diff --git a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs
index c230607343..74afb42c1a 100644
--- a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
continue;
// ReSharper disable once PossibleNullReferenceException
- string[] split = line.Split(':');
+ string[] split = line.Split(':', StringSplitOptions.TrimEntries);
if (split.Length < 2)
{
@@ -55,9 +55,9 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
teams.Add(new TournamentTeam
{
- FullName = { Value = split[1].Trim(), },
- Acronym = { Value = split.Length >= 3 ? split[2].Trim() : null, },
- FlagName = { Value = split[0].Trim() }
+ FullName = { Value = split[1], },
+ Acronym = { Value = split.Length >= 3 ? split[2] : null, },
+ FlagName = { Value = split[0] }
});
}
}
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index 988f0a02f0..c9d897ca11 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -44,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Editors
{
var countries = new List();
- foreach (var country in Enum.GetValues(typeof(CountryCode)).Cast().Skip(1))
+ foreach (var country in Enum.GetValues().Skip(1))
{
countries.Add(new TournamentTeam
{
@@ -54,8 +53,6 @@ namespace osu.Game.Tournament.Screens.Editors
});
}
- Debug.Assert(countries != null);
-
foreach (var c in countries)
Storage.Add(c);
}
diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
index 8f0d1de0cb..0fb6c1367b 100644
--- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -102,10 +103,14 @@ namespace osu.Game.Tournament.Screens.Editors
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(args.NewItems != null);
+
args.NewItems.Cast().ForEach(i => flow.Add(CreateDrawable(i)));
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(args.OldItems != null);
+
args.OldItems.Cast().ForEach(i => flow.RemoveAll(d => d.Model == i, true));
break;
}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
index 603a7830c7..5aea551c00 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -20,8 +21,6 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
{
public partial class LadderEditorSettings : PlayerSettingsGroup
{
- private const int padding = 10;
-
private SettingsDropdown roundDropdown;
private PlayerCheckbox losersCheckbox;
private DateTextBox dateTimeBox;
@@ -103,10 +102,14 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(args.NewItems != null);
+
args.NewItems.Cast().ForEach(add);
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(args.OldItems != null);
+
args.OldItems.Cast().ForEach(i => Control.RemoveDropdownItem(i));
break;
}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
index c90cdb7775..f7a42e4f50 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -25,10 +26,14 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(args.NewItems != null);
+
args.NewItems.Cast().ForEach(add);
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(args.OldItems != null);
+
args.OldItems.Cast().ForEach(i => Control.RemoveDropdownItem(i));
break;
}
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
index 595f08ed36..176c06c0e5 100644
--- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
+++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Caching;
@@ -81,11 +82,15 @@ namespace osu.Game.Tournament.Screens.Ladder
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(args.NewItems != null);
+
foreach (var p in args.NewItems.Cast())
addMatch(p);
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(args.OldItems != null);
+
foreach (var p in args.OldItems.Cast())
{
foreach (var d in MatchesContainer.Where(d => d.Match == p))
@@ -153,7 +158,7 @@ namespace osu.Game.Tournament.Screens.Ladder
foreach (var round in LadderInfo.Rounds)
{
- var topMatch = MatchesContainer.Where(p => !p.Match.Losers.Value && p.Match.Round.Value == round).OrderBy(p => p.Y).FirstOrDefault();
+ var topMatch = MatchesContainer.Where(p => !p.Match.Losers.Value && p.Match.Round.Value == round).MinBy(p => p.Y);
if (topMatch == null) continue;
@@ -167,7 +172,7 @@ namespace osu.Game.Tournament.Screens.Ladder
foreach (var round in LadderInfo.Rounds)
{
- var topMatch = MatchesContainer.Where(p => p.Match.Losers.Value && p.Match.Round.Value == round).OrderBy(p => p.Y).FirstOrDefault();
+ var topMatch = MatchesContainer.Where(p => p.Match.Losers.Value && p.Match.Round.Value == round).MinBy(p => p.Y);
if (topMatch == null) continue;
diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj
index b049542bb0..ab67e490cd 100644
--- a/osu.Game.Tournament/osu.Game.Tournament.csproj
+++ b/osu.Game.Tournament/osu.Game.Tournament.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
tools for tournaments.
@@ -11,4 +11,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs
index 19c78f34b2..0191f96825 100644
--- a/osu.Game/Audio/SampleInfo.cs
+++ b/osu.Game/Audio/SampleInfo.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Audio
public bool Equals(SampleInfo? other)
=> other != null && sampleNames.SequenceEqual(other.sampleNames);
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
=> obj is SampleInfo other && Equals(other);
}
}
diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index bcb1d7f961..4752a88199 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps
public override async Task?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
{
- var imported = await Import(notification, importTask);
+ var imported = await Import(notification, new[] { importTask }).ConfigureAwait(true);
if (!imported.Any())
return null;
@@ -203,10 +203,10 @@ namespace osu.Game.Beatmaps
}
}
- protected override void PostImport(BeatmapSetInfo model, Realm realm, bool batchImport)
+ protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
{
- base.PostImport(model, realm, batchImport);
- ProcessBeatmap?.Invoke((model, batchImport));
+ base.PostImport(model, realm, parameters);
+ ProcessBeatmap?.Invoke((model, parameters.Batch));
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 965cc43815..eafd1e96e8 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -44,6 +44,16 @@ namespace osu.Game.Beatmaps
public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
+ public override bool PauseImports
+ {
+ get => base.PauseImports;
+ set
+ {
+ base.PauseImports = value;
+ beatmapImporter.PauseImports = value;
+ }
+ }
+
public BeatmapManager(Storage storage, RealmAccess realm, IAPIProvider? api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null,
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
: base(storage, realm)
@@ -456,15 +466,16 @@ namespace osu.Game.Beatmaps
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
- public Task Import(params ImportTask[] tasks) => beatmapImporter.Import(tasks);
+ public Task Import(ImportTask[] tasks, ImportParameters parameters = default) => beatmapImporter.Import(tasks, parameters);
- public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) => beatmapImporter.Import(notification, tasks);
+ public Task>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) =>
+ beatmapImporter.Import(notification, tasks, parameters);
- public Task?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) =>
- beatmapImporter.Import(task, batchImport, cancellationToken);
+ public Task?> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
+ beatmapImporter.Import(task, parameters, cancellationToken);
public Live? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) =>
- beatmapImporter.ImportModel(item, archive, false, cancellationToken);
+ beatmapImporter.ImportModel(item, archive, default, cancellationToken);
public IEnumerable HandledExtensions => beatmapImporter.HandledExtensions;
diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineNomination.cs b/osu.Game/Beatmaps/BeatmapSetOnlineNomination.cs
new file mode 100644
index 0000000000..f6de414628
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapSetOnlineNomination.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Beatmaps
+{
+ public struct BeatmapSetOnlineNomination
+ {
+ [JsonProperty(@"beatmapset_id")]
+ public int BeatmapsetId { get; set; }
+
+ [JsonProperty(@"reset")]
+ public bool Reset { get; set; }
+
+ [JsonProperty(@"rulesets")]
+ public string[]? Rulesets { get; set; }
+
+ [JsonProperty(@"user_id")]
+ public int UserId { get; set; }
+ }
+}
diff --git a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs
index 2fd1d06b7b..71d40b1a48 100644
--- a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs
+++ b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs
@@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps
{
try
{
- await cacheDownloadRequest.PerformAsync();
+ await cacheDownloadRequest.PerformAsync().ConfigureAwait(false);
}
catch
{
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 0a09e6e7e6..f46e4af332 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time;
- public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
+ public int CompareTo(ControlPoint? other) => Time.CompareTo(other?.Time);
public virtual Color4 GetRepresentingColour(OsuColour colours) => colours.Yellow;
@@ -32,7 +32,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public ControlPoint DeepClone()
{
- var copy = (ControlPoint)Activator.CreateInstance(GetType());
+ var copy = (ControlPoint)Activator.CreateInstance(GetType())!;
copy.CopyFrom(this);
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs
index db479f0e5b..1f34f3777d 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Beatmaps.ControlPoints
Time = time;
}
- public int CompareTo(ControlPointGroup other) => Time.CompareTo(other.Time);
+ public int CompareTo(ControlPointGroup? other) => Time.CompareTo(other?.Time);
public void Add(ControlPoint point)
{
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index 422e306450..29b7191ecf 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -70,14 +70,14 @@ namespace osu.Game.Beatmaps.ControlPoints
///
[JsonIgnore]
public double BPMMaximum =>
- 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
+ 60000 / (TimingPoints.MinBy(c => c.BeatLength) ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Finds the minimum BPM represented by any timing control point.
///
[JsonIgnore]
public double BPMMinimum =>
- 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
+ 60000 / (TimingPoints.MaxBy(c => c.BeatLength) ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Remove all s and return to a pristine state.
@@ -211,8 +211,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public static T BinarySearch(IReadOnlyList list, double time)
where T : class, IControlPoint
{
- if (list == null)
- throw new ArgumentNullException(nameof(list));
+ ArgumentNullException.ThrowIfNull(list);
if (list.Count == 0)
return null;
@@ -300,7 +299,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public ControlPointInfo DeepClone()
{
- var controlPointInfo = (ControlPointInfo)Activator.CreateInstance(GetType());
+ var controlPointInfo = (ControlPointInfo)Activator.CreateInstance(GetType())!;
foreach (var point in AllControlPoints)
controlPointInfo.Add(point.Time, point.DeepClone());
diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs
index 7a23b32c84..ec00756fd9 100644
--- a/osu.Game/Beatmaps/DifficultyRecommender.cs
+++ b/osu.Game/Beatmaps/DifficultyRecommender.cs
@@ -51,11 +51,11 @@ namespace osu.Game.Beatmaps
if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation))
continue;
- BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r)).OrderBy(b =>
+ BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b =>
{
double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
- }).FirstOrDefault();
+ });
if (beatmapInfo != null)
return beatmapInfo;
@@ -90,7 +90,7 @@ namespace osu.Game.Beatmaps
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
- .Where(r => !r.Equals(ruleset.Value.ShortName))
+ .Where(r => !r.Equals(ruleset.Value.ShortName, StringComparison.Ordinal))
.Prepend(ruleset.Value.ShortName);
}
}
diff --git a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
index d31a7ae2fe..767504fcb1 100644
--- a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
+++ b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
@@ -15,8 +15,7 @@ namespace osu.Game.Beatmaps.Drawables
public BeatmapBackgroundSprite(IWorkingBeatmap working)
{
- if (working == null)
- throw new ArgumentNullException(nameof(working));
+ ArgumentNullException.ThrowIfNull(working);
this.working = working;
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
index 00f9a6b3d5..94b2956b4e 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
@@ -5,16 +5,19 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
+using osu.Game.Localisation;
namespace osu.Game.Beatmaps.Drawables.Cards
{
- public abstract partial class BeatmapCard : OsuClickableContainer
+ public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu
{
public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10;
@@ -96,5 +99,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
throw new ArgumentOutOfRangeException(nameof(size), size, @"Unsupported card size");
}
}
+
+ public MenuItem[] ContextMenuItems => new MenuItem[]
+ {
+ new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, Action),
+ };
}
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
index d4cbe6ddd0..7deb5f768c 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
@@ -138,9 +138,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards
// This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
this.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
- background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
- dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
- borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ if (Expanded.Value)
+ {
+ background.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ dropdownContent.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ borderContainer.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ }
+ else
+ {
+ background.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
+ dropdownContent.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
+ borderContainer.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
+ }
content.TweenEdgeEffectTo(new EdgeEffectParameters
{
diff --git a/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs
index e4ffc1d553..fc7c14e734 100644
--- a/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs
+++ b/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs
@@ -18,8 +18,7 @@ namespace osu.Game.Beatmaps.Drawables
public OnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover)
{
- if (set == null)
- throw new ArgumentNullException(nameof(set));
+ ArgumentNullException.ThrowIfNull(set);
this.set = set;
this.type = type;
diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs
index ca1bcc97fd..4f0f11d053 100644
--- a/osu.Game/Beatmaps/Formats/Decoder.cs
+++ b/osu.Game/Beatmaps/Formats/Decoder.cs
@@ -57,8 +57,7 @@ namespace osu.Game.Beatmaps.Formats
public static Decoder GetDecoder(LineBufferedReader stream)
where T : new()
{
- if (stream == null)
- throw new ArgumentNullException(nameof(stream));
+ ArgumentNullException.ThrowIfNull(stream);
if (!decoders.TryGetValue(typeof(T), out var typedDecoders))
throw new IOException(@"Unknown decoder type");
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 5f0a2a0824..9c710b690e 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -160,7 +160,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"SampleSet":
- defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value);
+ defaultSampleBank = Enum.Parse(pair.Value);
break;
case @"SampleVolume":
@@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"Countdown":
- beatmap.BeatmapInfo.Countdown = (CountdownType)Enum.Parse(typeof(CountdownType), pair.Value);
+ beatmap.BeatmapInfo.Countdown = Enum.Parse(pair.Value);
break;
case @"CountdownOffset":
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 03c63ff4f2..7e058d755e 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps.Formats
{
var comboColour = colours[i];
- writer.Write(FormattableString.Invariant($"Combo{i}: "));
+ writer.Write(FormattableString.Invariant($"Combo{1 + i}: "));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},"));
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index a4e15f790a..704756e3dd 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -132,13 +132,7 @@ namespace osu.Game.Beatmaps.Formats
protected KeyValuePair SplitKeyVal(string line, char separator = ':', bool shouldTrim = true)
{
- string[] split = line.Split(separator, 2);
-
- if (shouldTrim)
- {
- for (int i = 0; i < split.Length; i++)
- split[i] = split[i].Trim();
- }
+ string[] split = line.Split(separator, 2, shouldTrim ? StringSplitOptions.TrimEntries : StringSplitOptions.None);
return new KeyValuePair
(
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index 2b4f377ab6..44dbb3cc9f 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
@@ -301,11 +301,11 @@ namespace osu.Game.Beatmaps.Formats
}
}
- private string parseLayer(string value) => Enum.Parse(typeof(LegacyStoryLayer), value).ToString();
+ private string parseLayer(string value) => Enum.Parse(value).ToString();
private Anchor parseOrigin(string value)
{
- var origin = (LegacyOrigins)Enum.Parse(typeof(LegacyOrigins), value);
+ var origin = Enum.Parse(value);
switch (origin)
{
@@ -343,8 +343,8 @@ namespace osu.Game.Beatmaps.Formats
private AnimationLoopType parseAnimationLoopType(string value)
{
- var parsed = (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), value);
- return Enum.IsDefined(typeof(AnimationLoopType), parsed) ? parsed : AnimationLoopType.LoopForever;
+ var parsed = Enum.Parse(value);
+ return Enum.IsDefined(parsed) ? parsed : AnimationLoopType.LoopForever;
}
private void handleVariables(string line)
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 393c4ba892..ab790617bb 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
// TODO: remove once the fallback lookup is not required (and access via `working.BeatmapInfo.Metadata` directly).
public BeatmapMetadata Metadata => BeatmapInfo.Metadata;
- public Waveform Waveform => waveform.Value;
-
public Storyboard Storyboard => storyboard.Value;
public Texture Background => GetBackground(); // Texture uses ref counting, so we want to return a new instance every usage.
@@ -48,10 +46,11 @@ namespace osu.Game.Beatmaps
private readonly object beatmapFetchLock = new object();
- private readonly Lazy waveform;
private readonly Lazy storyboard;
private readonly Lazy skin;
+
private Track track; // track is not Lazy as we allow transferring and loading multiple times.
+ private Waveform waveform; // waveform is also not Lazy as the track may change.
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
{
@@ -60,7 +59,6 @@ namespace osu.Game.Beatmaps
BeatmapInfo = beatmapInfo;
BeatmapSetInfo = beatmapInfo.BeatmapSet ?? new BeatmapSetInfo();
- waveform = new Lazy(GetWaveform);
storyboard = new Lazy(GetStoryboard);
skin = new Lazy(GetSkin);
}
@@ -108,7 +106,16 @@ namespace osu.Game.Beatmaps
public virtual bool TrackLoaded => track != null;
- public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
+ public Track LoadTrack()
+ {
+ track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
+
+ // the track may have changed, recycle the current waveform.
+ waveform?.Dispose();
+ waveform = null;
+
+ return track;
+ }
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
{
@@ -171,6 +178,12 @@ namespace osu.Game.Beatmaps
#endregion
+ #region Waveform
+
+ public Waveform Waveform => waveform ??= GetWaveform();
+
+ #endregion
+
#region Beatmap
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index adb5f8c433..e6f96330e7 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -141,6 +141,9 @@ namespace osu.Game.Beatmaps
try
{
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
+
+ // TODO: check validity of file
+
var stream = GetStream(fileStorePath);
if (stream == null)
diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs
index 12a30a0c84..276563e163 100644
--- a/osu.Game/Configuration/SessionStatics.cs
+++ b/osu.Game/Configuration/SessionStatics.cs
@@ -19,6 +19,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LoginOverlayDisplayed, false);
SetDefault(Static.MutedAudioNotificationShownOnce, false);
SetDefault(Static.LowBatteryNotificationShownOnce, false);
+ SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.SeasonalBackgrounds, null);
}
@@ -42,6 +43,7 @@ namespace osu.Game.Configuration
LoginOverlayDisplayed,
MutedAudioNotificationShownOnce,
LowBatteryNotificationShownOnce,
+ FeaturedArtistDisclaimerShownOnce,
///
/// Info about seasonal backgrounds available fetched from API - see .
@@ -53,6 +55,6 @@ namespace osu.Game.Configuration
/// The last playback time in milliseconds of a hover sample (from ).
/// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like .
///
- LastHoverSoundPlaybackTime
+ LastHoverSoundPlaybackTime,
}
}
diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 043bba3134..1e425c88a6 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -91,15 +91,15 @@ namespace osu.Game.Configuration
OrderPosition = orderPosition;
}
- public int CompareTo(SettingSourceAttribute other)
+ public int CompareTo(SettingSourceAttribute? other)
{
- if (OrderPosition == other.OrderPosition)
+ if (OrderPosition == other?.OrderPosition)
return 0;
// unordered items come last (are greater than any ordered items).
if (OrderPosition == null)
return 1;
- if (other.OrderPosition == null)
+ if (other?.OrderPosition == null)
return -1;
// ordered items are sorted by the order value.
@@ -113,7 +113,7 @@ namespace osu.Game.Configuration
{
foreach (var (attr, property) in obj.GetOrderedSettingsSourceProperties())
{
- object value = property.GetValue(obj);
+ object value = property.GetValue(obj)!;
if (attr.SettingControlType != null)
{
@@ -121,7 +121,7 @@ namespace osu.Game.Configuration
if (controlType.EnumerateBaseTypes().All(t => !t.IsGenericType || t.GetGenericTypeDefinition() != typeof(SettingsItem<>)))
throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} had an unsupported custom control type ({controlType.ReadableName()})");
- var control = (Drawable)Activator.CreateInstance(controlType);
+ var control = (Drawable)Activator.CreateInstance(controlType)!;
controlType.GetProperty(nameof(SettingsItem
/// The model to be imported.
/// An optional archive to use for model population.
- /// If true, imports will be skipped before they begin, given an existing model matches on hash and filenames. Should generally only be used for large batch imports, as it may defy user expectations when updating an existing model.
+ /// Parameters to further configure the import process.
/// An optional cancellation token.
- public virtual Live? ImportModel(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm =>
+ public virtual Live? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm =>
{
- cancellationToken.ThrowIfCancellationRequested();
+ pauseIfNecessary(cancellationToken);
TModel? existing;
- if (batchImport && archive != null)
+ if (parameters.Batch && archive != null)
{
// this is a fast bail condition to improve large import performance.
item.Hash = computeHashFast(archive);
@@ -303,7 +311,7 @@ namespace osu.Game.Database
foreach (var filenames in getShortenedFilenames(archive))
{
using (Stream s = archive.GetStream(filenames.original))
- files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false), filenames.shortened));
+ files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false, parameters.PreferHardLinks), filenames.shortened));
}
}
@@ -358,7 +366,7 @@ namespace osu.Game.Database
// import to store
realm.Add(item);
- PostImport(item, realm, batchImport);
+ PostImport(item, realm, parameters);
transaction.Commit();
}
@@ -493,8 +501,8 @@ namespace osu.Game.Database
///
/// The model prepared for import.
/// The current realm context.
- /// Whether the import was part of a batch.
- protected virtual void PostImport(TModel model, Realm realm, bool batchImport)
+ /// Parameters to further configure the import process.
+ protected virtual void PostImport(TModel model, Realm realm, ImportParameters parameters)
{
}
@@ -551,6 +559,23 @@ namespace osu.Game.Database
/// Whether to perform deletion.
protected virtual bool ShouldDeleteArchive(string path) => false;
+ private void pauseIfNecessary(CancellationToken cancellationToken)
+ {
+ if (!PauseImports)
+ return;
+
+ Logger.Log($@"{GetType().Name} is being paused.");
+
+ while (PauseImports)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ Thread.Sleep(500);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+ Logger.Log($@"{GetType().Name} is being resumed.");
+ }
+
private IEnumerable getIDs(IEnumerable files)
{
foreach (var f in files.OrderBy(f => f.Filename))
diff --git a/osu.Game/Database/RealmFileStore.cs b/osu.Game/Database/RealmFileStore.cs
index 036b15ea17..f75d3be725 100644
--- a/osu.Game/Database/RealmFileStore.cs
+++ b/osu.Game/Database/RealmFileStore.cs
@@ -10,6 +10,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Extensions;
+using osu.Game.IO;
using osu.Game.Models;
using Realms;
@@ -41,7 +42,8 @@ namespace osu.Game.Database
/// The file data stream.
/// The realm instance to add to. Should already be in a transaction.
/// Whether the should immediately be added to the underlying realm. If false is provided here, the instance must be manually added.
- public RealmFile Add(Stream data, Realm realm, bool addToRealm = true)
+ /// Whether this import should use hard links rather than file copy operations if available.
+ public RealmFile Add(Stream data, Realm realm, bool addToRealm = true, bool preferHardLinks = false)
{
string hash = data.ComputeSHA2Hash();
@@ -50,7 +52,7 @@ namespace osu.Game.Database
var file = existing ?? new RealmFile { Hash = hash };
if (!checkFileExistsAndMatchesHash(file))
- copyToStore(file, data);
+ copyToStore(file, data, preferHardLinks);
if (addToRealm && !file.IsManaged)
realm.Add(file);
@@ -58,8 +60,15 @@ namespace osu.Game.Database
return file;
}
- private void copyToStore(RealmFile file, Stream data)
+ private void copyToStore(RealmFile file, Stream data, bool preferHardLinks)
{
+ if (data is FileStream fs && preferHardLinks)
+ {
+ // attempt to do a fast hard link rather than copy.
+ if (HardLinkHelper.TryCreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name))
+ return;
+ }
+
data.Seek(0, SeekOrigin.Begin);
using (var output = Storage.CreateFileSafely(file.GetStoragePath()))
diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs
index 35f2d61437..65b9e46764 100644
--- a/osu.Game/Extensions/DrawableExtensions.cs
+++ b/osu.Game/Extensions/DrawableExtensions.cs
@@ -66,10 +66,10 @@ namespace osu.Game.Extensions
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
- if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
+ if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
continue;
- skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue);
+ skinnable.CopyAdjustedSetting(((IBindable)property.GetValue(component)!), settingValue);
}
}
diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs
index b4a0c02e35..43abb59042 100644
--- a/osu.Game/Extensions/TaskExtensions.cs
+++ b/osu.Game/Extensions/TaskExtensions.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Extensions
if (cancellationToken.IsCancellationRequested)
{
- tcs.SetCanceled();
+ tcs.SetCanceled(cancellationToken);
}
else
{
diff --git a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs
index 735b8b4e7d..984d60d35e 100644
--- a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs
+++ b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs
@@ -36,8 +36,7 @@ namespace osu.Game.Graphics.Containers
/// The easing type of the initial transform.
public void StartTracking(OsuLogo logo, double duration = 0, Easing easing = Easing.None)
{
- if (logo == null)
- throw new ArgumentNullException(nameof(logo));
+ ArgumentNullException.ThrowIfNull(logo);
if (logo.IsTracking && Logo == null)
throw new InvalidOperationException($"Cannot track an instance of {typeof(OsuLogo)} to multiple {typeof(LogoTrackingContainer)}s");
diff --git a/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs b/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs
new file mode 100644
index 0000000000..caed4b26b9
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Markdig;
+using Markdig.Extensions.GenericAttributes;
+using Markdig.Renderers;
+using Markdig.Syntax;
+
+namespace osu.Game.Graphics.Containers.Markdown.Extensions
+{
+ ///
+ /// A variant of
+ /// which only handles generic attributes in the current markdown and ignores inline generic attributes.
+ ///
+ ///
+ /// For rationale, see implementation of .
+ ///
+ public class BlockAttributeExtension : IMarkdownExtension
+ {
+ private readonly GenericAttributesExtension genericAttributesExtension = new GenericAttributesExtension();
+
+ public void Setup(MarkdownPipelineBuilder pipeline)
+ {
+ genericAttributesExtension.Setup(pipeline);
+
+ // GenericAttributesExtension registers a GenericAttributesParser in pipeline.InlineParsers.
+ // this conflicts with the CustomContainerExtension, leading to some custom containers (e.g. flags) not displaying.
+ // as a workaround, remove the inline parser here before it can do damage.
+ pipeline.InlineParsers.RemoveAll(parser => parser is GenericAttributesParser);
+ }
+
+ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) => genericAttributesExtension.Setup(pipeline, renderer);
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Extensions/OsuMarkdownExtensions.cs b/osu.Game/Graphics/Containers/Markdown/Extensions/OsuMarkdownExtensions.cs
new file mode 100644
index 0000000000..10542abe71
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Extensions/OsuMarkdownExtensions.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Markdig;
+
+namespace osu.Game.Graphics.Containers.Markdown.Extensions
+{
+ public static class OsuMarkdownExtensions
+ {
+ ///
+ /// Uses the block attributes extension.
+ ///
+ /// The pipeline.
+ /// The modified pipeline.
+ public static MarkdownPipelineBuilder UseBlockAttributes(this MarkdownPipelineBuilder pipeline)
+ {
+ pipeline.Extensions.AddIfNotAlready();
+ return pipeline;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnote.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnote.cs
new file mode 100644
index 0000000000..e92d866eed
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnote.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Markdig.Extensions.Footnotes;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Containers.Markdown.Footnotes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+
+namespace osu.Game.Graphics.Containers.Markdown.Footnotes
+{
+ public partial class OsuMarkdownFootnote : MarkdownFootnote
+ {
+ public OsuMarkdownFootnote(Footnote footnote)
+ : base(footnote)
+ {
+ }
+
+ public override SpriteText CreateOrderMarker(int order) => CreateSpriteText().With(marker =>
+ {
+ marker.Text = LocalisableString.Format("{0}.", order);
+ });
+
+ public override MarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(textFlow =>
+ {
+ textFlow.Margin = new MarginPadding { Left = 30 };
+ });
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteBacklink.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteBacklink.cs
new file mode 100644
index 0000000000..22c02ea720
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteBacklink.cs
@@ -0,0 +1,62 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using Markdig.Extensions.Footnotes;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Graphics.Containers.Markdown.Footnotes
+{
+ public partial class OsuMarkdownFootnoteBacklink : OsuHoverContainer
+ {
+ private readonly FootnoteLink backlink;
+
+ private SpriteIcon spriteIcon = null!;
+
+ [Resolved]
+ private IMarkdownTextComponent parentTextComponent { get; set; } = null!;
+
+ protected override IEnumerable EffectTargets => spriteIcon.Yield();
+
+ public OsuMarkdownFootnoteBacklink(FootnoteLink backlink)
+ {
+ this.backlink = backlink;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider colourProvider, OsuMarkdownContainer markdownContainer, OverlayScrollContainer? scrollContainer)
+ {
+ float fontSize = parentTextComponent.CreateSpriteText().Font.Size;
+ Size = new Vector2(fontSize);
+
+ IdleColour = colourProvider.Light2;
+ HoverColour = colourProvider.Light1;
+
+ Add(spriteIcon = new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding { Left = 5 },
+ Size = new Vector2(fontSize / 2),
+ Icon = FontAwesome.Solid.ArrowUp,
+ });
+
+ if (scrollContainer != null)
+ {
+ Action = () =>
+ {
+ var footnoteLink = markdownContainer.ChildrenOfType().Single(footnoteLink => footnoteLink.FootnoteLink.Index == backlink.Index);
+ scrollContainer.ScrollIntoView(footnoteLink);
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteLink.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteLink.cs
new file mode 100644
index 0000000000..c9bd408e9e
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteLink.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using Markdig.Extensions.Footnotes;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Framework.Testing;
+using osu.Game.Overlays;
+
+namespace osu.Game.Graphics.Containers.Markdown.Footnotes
+{
+ public partial class OsuMarkdownFootnoteLink : OsuHoverContainer, IHasCustomTooltip
+ {
+ public readonly FootnoteLink FootnoteLink;
+
+ private SpriteText spriteText = null!;
+
+ [Resolved]
+ private IMarkdownTextComponent parentTextComponent { get; set; } = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ [Resolved]
+ private OsuMarkdownContainer markdownContainer { get; set; } = null!;
+
+ protected override IEnumerable EffectTargets => spriteText.Yield();
+
+ public OsuMarkdownFootnoteLink(FootnoteLink footnoteLink)
+ {
+ FootnoteLink = footnoteLink;
+
+ AutoSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuMarkdownContainer markdownContainer, OverlayScrollContainer? scrollContainer)
+ {
+ IdleColour = colourProvider.Light2;
+ HoverColour = colourProvider.Light1;
+
+ spriteText = parentTextComponent.CreateSpriteText();
+
+ Add(spriteText.With(t =>
+ {
+ float baseSize = t.Font.Size;
+ t.Font = t.Font.With(size: baseSize * 0.58f);
+ t.Margin = new MarginPadding { Bottom = 0.33f * baseSize };
+ t.Text = LocalisableString.Format("[{0}]", FootnoteLink.Index);
+ }));
+
+ if (scrollContainer != null)
+ {
+ Action = () =>
+ {
+ var footnote = markdownContainer.ChildrenOfType().Single(footnote => footnote.Footnote.Label == FootnoteLink.Footnote.Label);
+ scrollContainer.ScrollIntoView(footnote);
+ };
+ }
+ }
+
+ public object TooltipContent
+ {
+ get
+ {
+ var span = FootnoteLink.Footnote.LastChild.Span;
+ return markdownContainer.Text.Substring(span.Start, span.Length);
+ }
+ }
+
+ public ITooltip GetCustomTooltip() => new OsuMarkdownFootnoteTooltip(colourProvider);
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs
new file mode 100644
index 0000000000..af64913212
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs
@@ -0,0 +1,76 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Markdig.Extensions.Footnotes;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Graphics.Containers.Markdown.Footnotes
+{
+ public partial class OsuMarkdownFootnoteTooltip : CompositeDrawable, ITooltip
+ {
+ private readonly FootnoteMarkdownContainer markdownContainer;
+
+ [Cached]
+ private OverlayColourProvider colourProvider;
+
+ public OsuMarkdownFootnoteTooltip(OverlayColourProvider colourProvider)
+ {
+ this.colourProvider = colourProvider;
+
+ Masking = true;
+ Width = 200;
+ AutoSizeAxes = Axes.Y;
+ CornerRadius = 4;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background6
+ },
+ markdownContainer = new FootnoteMarkdownContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ DocumentMargin = new MarginPadding(),
+ DocumentPadding = new MarginPadding { Horizontal = 10, Vertical = 5 }
+ }
+ };
+ }
+
+ public void Move(Vector2 pos) => Position = pos;
+
+ public void SetContent(object content) => markdownContainer.SetContent((string)content);
+
+ private partial class FootnoteMarkdownContainer : OsuMarkdownContainer
+ {
+ private string? lastFootnote;
+
+ public void SetContent(string footnote)
+ {
+ if (footnote == lastFootnote)
+ return;
+
+ lastFootnote = Text = footnote;
+ }
+
+ public override MarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer();
+ }
+
+ private partial class FootnoteMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
+ {
+ protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink)
+ {
+ // we don't want footnote backlinks to show up in tooltips.
+ }
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs
index e884b5db69..5b1780a068 100644
--- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs
@@ -4,41 +4,25 @@
#nullable disable
using Markdig;
-using Markdig.Extensions.AutoLinks;
-using Markdig.Extensions.CustomContainers;
-using Markdig.Extensions.EmphasisExtras;
using Markdig.Extensions.Footnotes;
using Markdig.Extensions.Tables;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Containers.Markdown.Footnotes;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Graphics.Sprites;
+using osuTK;
namespace osu.Game.Graphics.Containers.Markdown
{
+ [Cached]
public partial class OsuMarkdownContainer : MarkdownContainer
{
- ///
- /// Allows this markdown container to parse and link footnotes.
- ///
- ///
- protected virtual bool Footnotes => false;
-
- ///
- /// Allows this markdown container to make URL text clickable.
- ///
- ///
- protected virtual bool Autolinks => false;
-
- ///
- /// Allows this markdown container to parse custom containers (used for flags and infoboxes).
- ///
- ///
- protected virtual bool CustomContainers => false;
-
public OsuMarkdownContainer()
{
LineSpacing = 21;
@@ -99,25 +83,17 @@ namespace osu.Game.Graphics.Containers.Markdown
return new OsuMarkdownUnorderedListItem(level);
}
- // reference: https://github.com/ppy/osu-web/blob/05488a96b25b5a09f2d97c54c06dd2bae59d1dc8/app/Libraries/Markdown/OsuMarkdown.php#L301
- protected override MarkdownPipeline CreateBuilder()
- {
- var pipeline = new MarkdownPipelineBuilder()
- .UseAutoIdentifiers()
- .UsePipeTables()
- .UseEmphasisExtras(EmphasisExtraOptions.Strikethrough)
- .UseYamlFrontMatter();
+ protected override MarkdownFootnoteGroup CreateFootnoteGroup(FootnoteGroup footnoteGroup) => base.CreateFootnoteGroup(footnoteGroup).With(g => g.Spacing = new Vector2(5));
- if (Footnotes)
- pipeline = pipeline.UseFootnotes();
+ protected override MarkdownFootnote CreateFootnote(Footnote footnote) => new OsuMarkdownFootnote(footnote);
- if (Autolinks)
- pipeline = pipeline.UseAutoLinks();
+ protected sealed override MarkdownPipeline CreateBuilder()
+ => Options.BuildPipeline();
- if (CustomContainers)
- pipeline.UseCustomContainers();
-
- return pipeline.Build();
- }
+ ///
+ /// Creates a instance which is used to determine
+ /// which CommonMark/Markdig extensions should be enabled for this .
+ ///
+ protected virtual OsuMarkdownContainerOptions Options => new OsuMarkdownContainerOptions();
}
}
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainerOptions.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainerOptions.cs
new file mode 100644
index 0000000000..1648ffbf90
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainerOptions.cs
@@ -0,0 +1,71 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Markdig;
+using Markdig.Extensions.AutoLinks;
+using Markdig.Extensions.CustomContainers;
+using Markdig.Extensions.EmphasisExtras;
+using Markdig.Extensions.Footnotes;
+using osu.Game.Graphics.Containers.Markdown.Extensions;
+
+namespace osu.Game.Graphics.Containers.Markdown
+{
+ ///
+ /// Groups options of customising the set of available extensions to instances.
+ ///
+ public class OsuMarkdownContainerOptions
+ {
+ ///
+ /// Allows the to parse and link footnotes.
+ ///
+ ///
+ public bool Footnotes { get; init; }
+
+ ///
+ /// Allows the container to make URL text clickable.
+ ///
+ ///
+ public bool Autolinks { get; init; }
+
+ ///
+ /// Allows the to parse custom containers (used for flags and infoboxes).
+ ///
+ ///
+ public bool CustomContainers { get; init; }
+
+ ///
+ /// Allows the to parse custom attributes in block elements (used e.g. for custom anchor names in the wiki).
+ ///
+ ///
+ public bool BlockAttributes { get; init; }
+
+ ///
+ /// Returns a prepared according to the options specified by the current instance.
+ ///
+ ///
+ /// Compare: https://github.com/ppy/osu-web/blob/05488a96b25b5a09f2d97c54c06dd2bae59d1dc8/app/Libraries/Markdown/OsuMarkdown.php#L301
+ ///
+ public MarkdownPipeline BuildPipeline()
+ {
+ var pipeline = new MarkdownPipelineBuilder()
+ .UseAutoIdentifiers()
+ .UsePipeTables()
+ .UseEmphasisExtras(EmphasisExtraOptions.Strikethrough)
+ .UseYamlFrontMatter();
+
+ if (Footnotes)
+ pipeline = pipeline.UseFootnotes();
+
+ if (Autolinks)
+ pipeline = pipeline.UseAutoLinks();
+
+ if (CustomContainers)
+ pipeline = pipeline.UseCustomContainers();
+
+ if (BlockAttributes)
+ pipeline = pipeline.UseBlockAttributes();
+
+ return pipeline.Build();
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs
index 7de63fe09c..5f5b9acf56 100644
--- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs
@@ -6,6 +6,7 @@
using System;
using System.Linq;
using Markdig.Extensions.CustomContainers;
+using Markdig.Extensions.Footnotes;
using Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -13,6 +14,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Overlays;
using osu.Game.Users;
using osu.Game.Users.Drawables;
@@ -36,6 +38,10 @@ namespace osu.Game.Graphics.Containers.Markdown
Text = codeInline.Content
});
+ protected override void AddFootnoteLink(FootnoteLink footnoteLink) => AddDrawable(new OsuMarkdownFootnoteLink(footnoteLink));
+
+ protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink) => AddDrawable(new OsuMarkdownFootnoteBacklink(footnoteBacklink));
+
protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
=> CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic));
diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
index 4729ddf1a8..ce50dbdc39 100644
--- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
@@ -1,14 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
+using osuTK;
namespace osu.Game.Graphics.Containers
{
@@ -18,6 +18,12 @@ namespace osu.Game.Graphics.Containers
private readonly Container content = new Container { RelativeSizeAxes = Axes.Both };
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
+ // base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
+ base.ReceivePositionalInputAt(screenSpacePos)
+ // Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
+ && Content.ReceivePositionalInputAt(screenSpacePos);
+
protected override Container Content => content;
protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } };
@@ -38,11 +44,11 @@ namespace osu.Game.Graphics.Containers
content.AutoSizeAxes = AutoSizeAxes;
}
- InternalChildren = new Drawable[]
- {
- content,
- CreateHoverSounds(sampleSet)
- };
+ AddInternal(content);
+ Add(CreateHoverSounds(sampleSet));
}
+
+ protected override void ClearInternal(bool disposeChildren = true) =>
+ throw new InvalidOperationException($"Clearing {nameof(InternalChildren)} will cause critical failure. Use {nameof(Clear)} instead.");
}
}
diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
index 740c170f8f..07b5b53e0e 100644
--- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
@@ -25,8 +25,6 @@ namespace osu.Game.Graphics.Containers
protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
- protected override bool BlockScrollInput => false;
-
protected override bool BlockNonPositionalInput => true;
///
@@ -90,6 +88,15 @@ namespace osu.Game.Graphics.Containers
base.OnMouseUp(e);
}
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ // allow for controlling volume when alt is held.
+ // mostly for compatibility with osu-stable.
+ if (e.AltPressed) return false;
+
+ return true;
+ }
+
public virtual bool OnPressed(KeyBindingPressEvent e)
{
if (e.Repeat)
diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
index b63e73e679..8cf47006ab 100644
--- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
@@ -234,7 +234,7 @@ namespace osu.Game.Graphics.Cursor
SampleChannel channel = tapSample.GetChannel();
// Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
- channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
+ channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * OsuGameBase.SFX_STEREO_STRENGTH;
channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
channel.Volume.Value = baseFrequency;
diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs
index d9bb2b610a..c62f53f1d4 100644
--- a/osu.Game/Graphics/DateTooltip.cs
+++ b/osu.Game/Graphics/DateTooltip.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osuTK;
@@ -69,8 +70,8 @@ namespace osu.Game.Graphics
{
DateTimeOffset localDate = date.ToLocalTime();
- dateText.Text = $"{localDate:d MMMM yyyy} ";
- timeText.Text = $"{localDate:HH:mm:ss \"UTC\"z}";
+ dateText.Text = LocalisableString.Interpolate($"{localDate:d MMMM yyyy} ");
+ timeText.Text = LocalisableString.Interpolate($"{localDate:HH:mm:ss \"UTC\"z}");
}
public void Move(Vector2 pos) => Position = pos;
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index 91161d5c71..c5659aaf57 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -22,38 +22,8 @@ namespace osu.Game.Graphics
public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255);
///
- /// Retrieves the colour for a .
+ /// Retrieves the colour for a given point in the star range.
///
- ///
- /// Sourced from the @diff-{rating} variables in https://github.com/ppy/osu-web/blob/71fbab8936d79a7929d13854f5e854b4f383b236/resources/assets/less/variables.less.
- ///
- public Color4 ForDifficultyRating(DifficultyRating difficulty, bool useLighterColour = false)
- {
- switch (difficulty)
- {
- case DifficultyRating.Easy:
- return Color4Extensions.FromHex("4ebfff");
-
- case DifficultyRating.Normal:
- return Color4Extensions.FromHex("66ff91");
-
- case DifficultyRating.Hard:
- return Color4Extensions.FromHex("f7e85d");
-
- case DifficultyRating.Insane:
- return Color4Extensions.FromHex("ff7e68");
-
- case DifficultyRating.Expert:
- return Color4Extensions.FromHex("fe3c71");
-
- case DifficultyRating.ExpertPlus:
- return Color4Extensions.FromHex("6662dd");
-
- default:
- throw new ArgumentOutOfRangeException(nameof(difficulty));
- }
- }
-
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
{
(0.1f, Color4Extensions.FromHex("aaaaaa")),
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index 29e9b0276c..d799e82bc9 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -4,6 +4,7 @@
#nullable disable
using System;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
@@ -117,11 +118,11 @@ namespace osu.Game.Graphics
host.GetClipboard()?.SetImage(image);
- string filename = getFilename();
+ (string filename, var stream) = getWritableStream();
if (filename == null) return;
- using (var stream = storage.CreateFileSafely(filename))
+ using (stream)
{
switch (screenshotFormat.Value)
{
@@ -142,7 +143,7 @@ namespace osu.Game.Graphics
notificationOverlay.Post(new SimpleNotification
{
- Text = $"{filename} saved!",
+ Text = $"Screenshot {filename} saved!",
Activated = () =>
{
storage.PresentFileExternally(filename);
@@ -152,23 +153,28 @@ namespace osu.Game.Graphics
}
});
- private string getFilename()
+ private static readonly object filename_reservation_lock = new object();
+
+ private (string filename, Stream stream) getWritableStream()
{
- var dt = DateTime.Now;
- string fileExt = screenshotFormat.ToString().ToLowerInvariant();
-
- string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}";
- if (!storage.Exists(withoutIndex))
- return withoutIndex;
-
- for (ulong i = 1; i < ulong.MaxValue; i++)
+ lock (filename_reservation_lock)
{
- string indexedName = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}-{i}.{fileExt}";
- if (!storage.Exists(indexedName))
- return indexedName;
- }
+ var dt = DateTime.Now;
+ string fileExt = screenshotFormat.ToString().ToLowerInvariant();
- return null;
+ string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}";
+ if (!storage.Exists(withoutIndex))
+ return (withoutIndex, storage.GetStream(withoutIndex, FileAccess.Write, FileMode.Create));
+
+ for (ulong i = 1; i < ulong.MaxValue; i++)
+ {
+ string indexedName = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}-{i}.{fileExt}";
+ if (!storage.Exists(indexedName))
+ return (indexedName, storage.GetStream(indexedName, FileAccess.Write, FileMode.Create));
+ }
+
+ return (null, null);
+ }
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
index 67b63e120b..fc0770d896 100644
--- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
+++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
@@ -52,8 +52,8 @@ namespace osu.Game.Graphics.UserInterface
public readonly SpriteIcon Chevron;
- //don't allow clicking between transitions and don't make the chevron clickable
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && Text.ReceivePositionalInputAt(screenSpacePos);
+ //don't allow clicking between transitions
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && base.ReceivePositionalInputAt(screenSpacePos);
public override bool HandleNonPositionalInput => State == Visibility.Visible;
public override bool HandlePositionalInput => State == Visibility.Visible;
@@ -95,7 +95,7 @@ namespace osu.Game.Graphics.UserInterface
{
Text.Font = Text.Font.With(size: 18);
Text.Margin = new MarginPadding { Vertical = 8 };
- Padding = new MarginPadding { Right = padding + ChevronSize };
+ Margin = new MarginPadding { Right = padding + ChevronSize };
Add(Chevron = new SpriteIcon
{
Anchor = Anchor.CentreRight,
diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
index efbbaaca85..4eccb37613 100644
--- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
+++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
if (Link != null)
{
- items.Add(new OsuMenuItem("Open", MenuItemType.Standard, () => host.OpenUrlExternally(Link)));
+ items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => host.OpenUrlExternally(Link)));
items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, copyUrl));
}
diff --git a/osu.Game/Graphics/UserInterface/HistoryTextBox.cs b/osu.Game/Graphics/UserInterface/HistoryTextBox.cs
index d74a4f2cdb..b6dc1fcc9b 100644
--- a/osu.Game/Graphics/UserInterface/HistoryTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/HistoryTextBox.cs
@@ -45,6 +45,9 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnKeyDown(KeyDownEvent e)
{
+ if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
+ return false;
+
switch (e.Key)
{
case Key.Up:
diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs
index 4f56872f42..7921dcf593 100644
--- a/osu.Game/Graphics/UserInterface/Nub.cs
+++ b/osu.Game/Graphics/UserInterface/Nub.cs
@@ -114,8 +114,7 @@ namespace osu.Game.Graphics.UserInterface
get => current;
set
{
- if (value == null)
- throw new ArgumentNullException(nameof(value));
+ ArgumentNullException.ThrowIfNull(value);
current.UnbindBindings();
current.BindTo(value);
diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index fa61b06cff..805dfcaa95 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -13,6 +11,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
+using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
@@ -20,16 +19,12 @@ namespace osu.Game.Graphics.UserInterface
///
/// A button with added default sound effects.
///
- public partial class OsuButton : Button
+ public abstract partial class OsuButton : Button
{
public LocalisableString Text
{
- get => SpriteText?.Text ?? default;
- set
- {
- if (SpriteText != null)
- SpriteText.Text = value;
- }
+ get => SpriteText.Text;
+ set => SpriteText.Text = value;
}
private Color4? backgroundColour;
@@ -66,13 +61,19 @@ namespace osu.Game.Graphics.UserInterface
protected override Container Content { get; }
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
+ // base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
+ base.ReceivePositionalInputAt(screenSpacePos)
+ // Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
+ && Content.ReceivePositionalInputAt(screenSpacePos);
+
protected Box Hover;
protected Box Background;
protected SpriteText SpriteText;
private readonly Box flashLayer;
- public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
+ protected OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
{
Height = 40;
@@ -115,7 +116,7 @@ namespace osu.Game.Graphics.UserInterface
});
if (hoverSounds.HasValue)
- AddInternal(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
+ Add(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
index 9ef58f4c49..dc089e3410 100644
--- a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Graphics.UserInterface
{
public OsuEnumDropdown()
{
- Items = (T[])Enum.GetValues(typeof(T));
+ Items = Enum.GetValues();
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
index 1114c29afd..a65204e6b3 100644
--- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
@@ -277,7 +277,7 @@ namespace osu.Game.Graphics.UserInterface
{
var samples = sampleMap[feedbackSampleType];
- if (samples == null || samples.Length == 0)
+ if (samples.Length == 0)
return null;
return samples[RNG.Next(0, samples.Length)]?.GetChannel();
diff --git a/osu.Game/Graphics/UserInterface/SegmentedGraph.cs b/osu.Game/Graphics/UserInterface/SegmentedGraph.cs
new file mode 100644
index 0000000000..8ccba710b8
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/SegmentedGraph.cs
@@ -0,0 +1,337 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Textures;
+using osuTK;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ public partial class SegmentedGraph : Drawable
+ where T : struct, IComparable, IConvertible, IEquatable
+ {
+ private bool graphNeedsUpdate;
+
+ private T[]? values;
+ private int[] tiers = Array.Empty();
+ private readonly SegmentManager segments;
+
+ private int tierCount;
+
+ public SegmentedGraph(int tierCount = 1)
+ {
+ this.tierCount = tierCount;
+ tierColours = new[]
+ {
+ new Colour4(0, 0, 0, 0)
+ };
+ segments = new SegmentManager(tierCount);
+ }
+
+ public T[] Values
+ {
+ get => values ?? Array.Empty();
+ set
+ {
+ if (value == values) return;
+
+ values = value;
+ graphNeedsUpdate = true;
+ }
+ }
+
+ private Colour4[] tierColours;
+
+ public Colour4[] TierColours
+ {
+ get => tierColours;
+ set
+ {
+ if (value.Length == 0 || value == tierColours)
+ return;
+
+ tierCount = value.Length;
+ tierColours = value;
+
+ graphNeedsUpdate = true;
+ }
+ }
+
+ private Texture texture = null!;
+ private IShader shader = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(IRenderer renderer, ShaderManager shaders)
+ {
+ texture = renderer.WhitePixel;
+ shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (graphNeedsUpdate)
+ {
+ recalculateTiers(values);
+ recalculateSegments();
+ Invalidate(Invalidation.DrawNode);
+ graphNeedsUpdate = false;
+ }
+ }
+
+ private void recalculateTiers(T[]? arr)
+ {
+ if (arr == null || arr.Length == 0)
+ {
+ tiers = Array.Empty();
+ return;
+ }
+
+ float[] floatValues = arr.Select(v => Convert.ToSingle(v)).ToArray();
+
+ // Shift values to eliminate negative ones
+ float min = floatValues.Min();
+
+ if (min < 0)
+ {
+ for (int i = 0; i < floatValues.Length; i++)
+ floatValues[i] += Math.Abs(min);
+ }
+
+ // Normalize values
+ float max = floatValues.Max();
+
+ for (int i = 0; i < floatValues.Length; i++)
+ floatValues[i] /= max;
+
+ // Deduce tiers from values
+ tiers = floatValues.Select(v => (int)Math.Floor(v * tierCount)).ToArray();
+ }
+
+ private void recalculateSegments()
+ {
+ segments.Clear();
+
+ if (tiers.Length == 0)
+ {
+ segments.Add(0, 0, 1);
+ return;
+ }
+
+ for (int i = 0; i < tiers.Length; i++)
+ {
+ for (int tier = 0; tier < tierCount; tier++)
+ {
+ if (tier < 0)
+ continue;
+
+ // One tier covers itself and all tiers above it.
+ // By layering multiple transparent boxes, higher tiers will be brighter.
+ // If using opaque colors, higher tiers will be on front, covering lower tiers.
+ if (tiers[i] >= tier)
+ {
+ if (!segments.IsTierStarted(tier))
+ segments.StartSegment(tier, i * 1f / tiers.Length);
+ }
+ else
+ {
+ if (segments.IsTierStarted(tier))
+ segments.EndSegment(tier, i * 1f / tiers.Length);
+ }
+ }
+ }
+
+ segments.EndAllPendingSegments();
+ segments.Sort();
+ }
+
+ private Colour4 getTierColour(int tier) => tier >= 0 ? tierColours[tier] : new Colour4(0, 0, 0, 0);
+
+ protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this);
+
+ protected struct SegmentInfo
+ {
+ ///
+ /// The tier this segment is at.
+ ///
+ public int Tier;
+
+ ///
+ /// The progress at which this segment starts.
+ ///
+ ///
+ /// The value is a normalized float (from 0 to 1).
+ ///
+ public float Start;
+
+ ///
+ /// The progress at which this segment ends.
+ ///
+ ///
+ /// The value is a normalized float (from 0 to 1).
+ ///
+ public float End;
+
+ ///
+ /// The length of this segment.
+ ///
+ ///
+ /// The value is a normalized float (from 0 to 1).
+ ///
+ public float Length => End - Start;
+
+ public override string ToString()
+ {
+ return $"({Tier}, {Start * 100}%, {End * 100}%)";
+ }
+ }
+
+ private class SegmentedGraphDrawNode : DrawNode
+ {
+ public new SegmentedGraph Source => (SegmentedGraph)base.Source;
+
+ private Texture texture = null!;
+ private IShader shader = null!;
+ private readonly List segments = new List();
+ private Vector2 drawSize;
+
+ public SegmentedGraphDrawNode(SegmentedGraph source)
+ : base(source)
+ {
+ }
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ texture = Source.texture;
+ shader = Source.shader;
+ drawSize = Source.DrawSize;
+ segments.Clear();
+ segments.AddRange(Source.segments.Where(s => s.Length * drawSize.X > 1));
+ }
+
+ public override void Draw(IRenderer renderer)
+ {
+ base.Draw(renderer);
+
+ shader.Bind();
+
+ foreach (SegmentInfo segment in segments)
+ {
+ Vector2 topLeft = new Vector2(segment.Start * drawSize.X, 0);
+ Vector2 topRight = new Vector2(segment.End * drawSize.X, 0);
+ Vector2 bottomLeft = new Vector2(segment.Start * drawSize.X, drawSize.Y);
+ Vector2 bottomRight = new Vector2(segment.End * drawSize.X, drawSize.Y);
+
+ renderer.DrawQuad(
+ texture,
+ new Quad(
+ Vector2Extensions.Transform(topLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
+ Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)),
+ Source.getTierColour(segment.Tier));
+ }
+
+ shader.Unbind();
+ }
+ }
+
+ protected class SegmentManager : IEnumerable
+ {
+ private readonly List segments = new List();
+
+ private readonly SegmentInfo?[] pendingSegments;
+
+ public SegmentManager(int tierCount)
+ {
+ pendingSegments = new SegmentInfo?[tierCount];
+ }
+
+ public void StartSegment(int tier, float start)
+ {
+ if (pendingSegments[tier] != null)
+ throw new InvalidOperationException($"Another {nameof(SegmentInfo)} of tier {tier.ToString()} has already been started.");
+
+ pendingSegments[tier] = new SegmentInfo
+ {
+ Tier = tier,
+ Start = Math.Clamp(start, 0, 1)
+ };
+ }
+
+ public void EndSegment(int tier, float end)
+ {
+ SegmentInfo? pendingSegment = pendingSegments[tier];
+ if (pendingSegment == null)
+ throw new InvalidOperationException($"Cannot end {nameof(SegmentInfo)} of tier {tier.ToString()} that has not been started.");
+
+ SegmentInfo segment = pendingSegment.Value;
+ segment.End = Math.Clamp(end, 0, 1);
+ segments.Add(segment);
+ pendingSegments[tier] = null;
+ }
+
+ public void EndAllPendingSegments()
+ {
+ foreach (SegmentInfo? pendingSegment in pendingSegments)
+ {
+ if (pendingSegment == null)
+ continue;
+
+ SegmentInfo finalizedSegment = pendingSegment.Value;
+ finalizedSegment.End = 1;
+ segments.Add(finalizedSegment);
+ }
+ }
+
+ public void Sort() =>
+ segments.Sort((a, b) =>
+ a.Tier != b.Tier
+ ? a.Tier.CompareTo(b.Tier)
+ : a.Start.CompareTo(b.Start));
+
+ public void Add(SegmentInfo segment) => segments.Add(segment);
+
+ public void Clear()
+ {
+ segments.Clear();
+
+ for (int i = 0; i < pendingSegments.Length; i++)
+ pendingSegments[i] = null;
+ }
+
+ public int Count => segments.Count;
+
+ public void Add(int tier, float start, float end)
+ {
+ SegmentInfo segment = new SegmentInfo
+ {
+ Tier = tier,
+ Start = Math.Clamp(start, 0, 1),
+ End = Math.Clamp(end, 0, 1)
+ };
+
+ if (segment.Start > segment.End)
+ throw new InvalidOperationException("Segment start cannot be after segment end.");
+
+ Add(segment);
+ }
+
+ public bool IsTierStarted(int tier) => tier >= 0 && pendingSegments[tier].HasValue;
+
+ public IEnumerator GetEnumerator() => segments.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+ }
+}
diff --git a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
index 8e6c3e5f3d..d47f936eb3 100644
--- a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
+++ b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
@@ -23,8 +23,7 @@ namespace osu.Game.IO.FileAbstraction
public void CloseStream(Stream stream)
{
- if (stream == null)
- throw new ArgumentNullException(nameof(stream));
+ ArgumentNullException.ThrowIfNull(stream);
stream.Close();
}
diff --git a/osu.Game/IO/HardLinkHelper.cs b/osu.Game/IO/HardLinkHelper.cs
new file mode 100644
index 0000000000..619bfdad6e
--- /dev/null
+++ b/osu.Game/IO/HardLinkHelper.cs
@@ -0,0 +1,190 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using Microsoft.Win32.SafeHandles;
+using osu.Framework;
+
+namespace osu.Game.IO
+{
+ internal static class HardLinkHelper
+ {
+ public static bool CheckAvailability(string testDestinationPath, string testSourcePath)
+ {
+ // For simplicity, only support desktop operating systems for now.
+ if (!RuntimeInfo.IsDesktop)
+ return false;
+
+ const string test_filename = "_hard_link_test";
+
+ testDestinationPath = Path.Combine(testDestinationPath, test_filename);
+ testSourcePath = Path.Combine(testSourcePath, test_filename);
+
+ cleanupFiles();
+
+ try
+ {
+ File.WriteAllText(testSourcePath, string.Empty);
+
+ // Test availability by creating an arbitrary hard link between the source and destination paths.
+ return TryCreateHardLink(testDestinationPath, testSourcePath);
+ }
+ catch
+ {
+ return false;
+ }
+ finally
+ {
+ cleanupFiles();
+ }
+
+ void cleanupFiles()
+ {
+ try
+ {
+ File.Delete(testDestinationPath);
+ File.Delete(testSourcePath);
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ ///
+ /// Attempts to create a hard link from to ,
+ /// using platform-specific native methods.
+ ///
+ ///
+ /// Hard links are only available on desktop platforms.
+ ///
+ /// Whether the hard link was successfully created.
+ public static bool TryCreateHardLink(string destinationPath, string sourcePath)
+ {
+ switch (RuntimeInfo.OS)
+ {
+ case RuntimeInfo.Platform.Windows:
+ return CreateHardLink(destinationPath, sourcePath, IntPtr.Zero);
+
+ case RuntimeInfo.Platform.Linux:
+ case RuntimeInfo.Platform.macOS:
+ return link(sourcePath, destinationPath) == 0;
+
+ default:
+ return false;
+ }
+ }
+
+ // For future use (to detect if a file is a hard link with other references existing on disk).
+ public static int GetFileLinkCount(string filePath)
+ {
+ int result = 0;
+
+ switch (RuntimeInfo.OS)
+ {
+ case RuntimeInfo.Platform.Windows:
+ SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
+
+ ByHandleFileInformation fileInfo;
+
+ if (GetFileInformationByHandle(handle, out fileInfo))
+ result = (int)fileInfo.NumberOfLinks;
+ CloseHandle(handle);
+ break;
+
+ case RuntimeInfo.Platform.Linux:
+ case RuntimeInfo.Platform.macOS:
+ if (stat(filePath, out var statbuf) == 0)
+ result = (int)statbuf.st_nlink;
+
+ break;
+ }
+
+ return result;
+ }
+
+ #region Windows native methods
+
+ [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern SafeFileHandle CreateFile(
+ string lpFileName,
+ [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
+ [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
+ IntPtr lpSecurityAttributes,
+ [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
+ [MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
+ IntPtr hTemplateFile);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool GetFileInformationByHandle(SafeFileHandle handle, out ByHandleFileInformation lpFileInformation);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool CloseHandle(SafeHandle hObject);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct ByHandleFileInformation
+ {
+ public readonly uint FileAttributes;
+ public readonly FILETIME CreationTime;
+ public readonly FILETIME LastAccessTime;
+ public readonly FILETIME LastWriteTime;
+ public readonly uint VolumeSerialNumber;
+ public readonly uint FileSizeHigh;
+ public readonly uint FileSizeLow;
+ public readonly uint NumberOfLinks;
+ public readonly uint FileIndexHigh;
+ public readonly uint FileIndexLow;
+ }
+
+ #endregion
+
+ #region Linux native methods
+
+#pragma warning disable IDE1006 // Naming rule violation
+
+ [DllImport("libc", SetLastError = true)]
+ public static extern int link(string oldpath, string newpath);
+
+ [DllImport("libc", SetLastError = true)]
+ private static extern int stat(string pathname, out struct_stat statbuf);
+
+ // ReSharper disable once InconsistentNaming
+ // Struct layout is likely non-portable across unices. Tread with caution.
+ [StructLayout(LayoutKind.Sequential)]
+ private struct struct_stat
+ {
+ public readonly long st_dev;
+ public readonly long st_ino;
+ public readonly long st_nlink;
+ public readonly int st_mode;
+ public readonly int st_uid;
+ public readonly int st_gid;
+ public readonly long st_rdev;
+ public readonly long st_size;
+ public readonly long st_blksize;
+ public readonly long st_blocks;
+ public readonly timespec st_atim;
+ public readonly timespec st_mtim;
+ public readonly timespec st_ctim;
+ }
+
+ // ReSharper disable once InconsistentNaming
+ [StructLayout(LayoutKind.Sequential)]
+ private struct timespec
+ {
+ public readonly long tv_sec;
+ public readonly long tv_nsec;
+ }
+
+#pragma warning restore IDE1006
+
+ #endregion
+ }
+}
diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
index 176e4e240d..de25d3e30e 100644
--- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
+++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
@@ -63,7 +63,7 @@ namespace osu.Game.IO.Serialization.Converters
throw new JsonException("Expected $type token.");
string typeName = lookupTable[(int)tok["$type"]];
- var instance = (T)Activator.CreateInstance(Type.GetType(typeName).AsNonNull());
+ var instance = (T)Activator.CreateInstance(Type.GetType(typeName).AsNonNull())!;
serializer.Populate(itemReader, instance);
list.Add(instance);
diff --git a/osu.Game/Localisation/BeatmapOverlayStrings.cs b/osu.Game/Localisation/BeatmapOverlayStrings.cs
new file mode 100644
index 0000000000..fc818f7596
--- /dev/null
+++ b/osu.Game/Localisation/BeatmapOverlayStrings.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class BeatmapOverlayStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOverlayStrings";
+
+ ///
+ /// "User content disclaimer"
+ ///
+ public static LocalisableString UserContentDisclaimerHeader => new TranslatableString(getKey(@"user_content_disclaimer"), @"User content disclaimer");
+
+ ///
+ /// "By turning off the "Featured Artist" filter, all user-uploaded content will be displayed.
+ ///
+ /// This includes content that may not be correctly licensed for osu! usage. Browse at your own risk."
+ ///
+ public static LocalisableString UserContentDisclaimerDescription => new TranslatableString(getKey(@"by_turning_off_the_featured"), @"By turning off the ""Featured Artist"" filter, all user-uploaded content will be displayed.
+
+This includes content that may not be correctly licensed for osu! usage. Browse at your own risk.");
+
+ ///
+ /// "I understand"
+ ///
+ public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Localisation/ContextMenuStrings.cs b/osu.Game/Localisation/ContextMenuStrings.cs
new file mode 100644
index 0000000000..8bc213016b
--- /dev/null
+++ b/osu.Game/Localisation/ContextMenuStrings.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class ContextMenuStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.ContextMenu";
+
+ ///
+ /// "View profile"
+ ///
+ public static LocalisableString ViewProfile => new TranslatableString(getKey(@"view_profile"), @"View profile");
+
+ ///
+ /// "View beatmap"
+ ///
+ public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
index deac7d8628..f0620245c3 100644
--- a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
+++ b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
@@ -15,10 +15,10 @@ namespace osu.Game.Localisation
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import");
///
- /// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation."
+ /// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way."
///
public static LocalisableString Description => new TranslatableString(getKey(@"description"),
- @"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation.");
+ @"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way.");
///
/// "previous osu! install"
diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs
index 3278b20983..a525af508b 100644
--- a/osu.Game/Localisation/GeneralSettingsStrings.cs
+++ b/osu.Game/Localisation/GeneralSettingsStrings.cs
@@ -64,6 +64,16 @@ namespace osu.Game.Localisation
///
public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard");
+ ///
+ /// "Learn more about lazer"
+ ///
+ public static LocalisableString LearnMoreAboutLazer => new TranslatableString(getKey(@"learn_more_about_lazer"), @"Learn more about lazer");
+
+ ///
+ /// "Check out the feature comparison and FAQ"
+ ///
+ public static LocalisableString LearnMoreAboutLazerTooltip => new TranslatableString(getKey(@"check_out_the_feature_comparison"), @"Check out the feature comparison and FAQ");
+
///
/// "You are running the latest release ({0})"
///
diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs
index 8aa0adf7a0..469f565f1e 100644
--- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs
+++ b/osu.Game/Localisation/MaintenanceSettingsStrings.cs
@@ -54,11 +54,6 @@ namespace osu.Game.Localisation
///
public static LocalisableString RestartAndReOpenRequiredForCompletion => new TranslatableString(getKey(@"restart_and_re_open_required_for_completion"), @"To complete this operation, osu! will close. Please open it again to use the new data location.");
- ///
- /// "Import beatmaps from stable"
- ///
- public static LocalisableString ImportBeatmapsFromStable => new TranslatableString(getKey(@"import_beatmaps_from_stable"), @"Import beatmaps from stable");
-
///
/// "Delete ALL beatmaps"
///
@@ -69,31 +64,16 @@ namespace osu.Game.Localisation
///
public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos");
- ///
- /// "Import scores from stable"
- ///
- public static LocalisableString ImportScoresFromStable => new TranslatableString(getKey(@"import_scores_from_stable"), @"Import scores from stable");
-
///
/// "Delete ALL scores"
///
public static LocalisableString DeleteAllScores => new TranslatableString(getKey(@"delete_all_scores"), @"Delete ALL scores");
- ///
- /// "Import skins from stable"
- ///
- public static LocalisableString ImportSkinsFromStable => new TranslatableString(getKey(@"import_skins_from_stable"), @"Import skins from stable");
-
///
/// "Delete ALL skins"
///
public static LocalisableString DeleteAllSkins => new TranslatableString(getKey(@"delete_all_skins"), @"Delete ALL skins");
- ///
- /// "Import collections from stable"
- ///
- public static LocalisableString ImportCollectionsFromStable => new TranslatableString(getKey(@"import_collections_from_stable"), @"Import collections from stable");
-
///
/// "Delete ALL collections"
///
diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs
index 382e0d81f4..6a9793b20c 100644
--- a/osu.Game/Localisation/NotificationsStrings.cs
+++ b/osu.Game/Localisation/NotificationsStrings.cs
@@ -15,10 +15,52 @@ namespace osu.Game.Localisation
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"notifications");
///
- /// "waiting for 'ya"
+ /// "waiting for 'ya"
///
public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"waiting for 'ya");
- private static string getKey(string key) => $"{prefix}:{key}";
+ ///
+ /// "Running Tasks"
+ ///
+ public static LocalisableString RunningTasks => new TranslatableString(getKey(@"running_tasks"), @"Running Tasks");
+
+ ///
+ /// "Clear All"
+ ///
+ public static LocalisableString ClearAll => new TranslatableString(getKey(@"clear_all"), @"Clear All");
+
+ ///
+ /// "Cancel All"
+ ///
+ public static LocalisableString CancelAll => new TranslatableString(getKey(@"cancel_all"), @"Cancel All");
+
+ ///
+ /// "Your battery level is low! Charge your device to prevent interruptions during gameplay."
+ ///
+ public static LocalisableString BatteryLow => new TranslatableString(getKey(@"battery_low"), @"Your battery level is low! Charge your device to prevent interruptions during gameplay.");
+
+ ///
+ /// "Your game volume is too low to hear anything! Click here to restore it."
+ ///
+ public static LocalisableString GameVolumeTooLow => new TranslatableString(getKey(@"game_volume_too_low"), @"Your game volume is too low to hear anything! Click here to restore it.");
+
+ ///
+ /// "The current ruleset doesn't have an autoplay mod available!"
+ ///
+ public static LocalisableString NoAutoplayMod => new TranslatableString(getKey(@"no_autoplay_mod"), @"The current ruleset doesn't have an autoplay mod available!");
+
+ ///
+ /// "osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting."
+ ///
+ public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"),
+ @"osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting.");
+
+ ///
+ /// "The score overlay is currently disabled. You can toggle this by pressing {0}."
+ ///
+ public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"),
+ @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0);
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
}
}
diff --git a/osu.Game/Models/RealmUser.cs b/osu.Game/Models/RealmUser.cs
index e20ffc0808..6997f04f44 100644
--- a/osu.Game/Models/RealmUser.cs
+++ b/osu.Game/Models/RealmUser.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Models
public bool IsBot => false;
- public bool Equals(RealmUser other)
+ public bool Equals(RealmUser? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index f2b9b6e968..94bb77d6ec 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -259,7 +259,11 @@ namespace osu.Game.Online.API
var friendsReq = new GetFriendsRequest();
friendsReq.Failure += _ => state.Value = APIState.Failing;
- friendsReq.Success += res => friends.AddRange(res);
+ friendsReq.Success += res =>
+ {
+ friends.Clear();
+ friends.AddRange(res);
+ };
if (!handleRequest(friendsReq))
{
@@ -325,12 +329,35 @@ namespace osu.Game.Online.API
{
try
{
- return JObject.Parse(req.GetResponseString().AsNonNull()).SelectToken("form_error", true).AsNonNull().ToObject();
+ return JObject.Parse(req.GetResponseString().AsNonNull()).SelectToken(@"form_error", true).AsNonNull().ToObject();
}
catch
{
- // if we couldn't deserialize the error message let's throw the original exception outwards.
- e.Rethrow();
+ try
+ {
+ // attempt to parse a non-form error message
+ var response = JObject.Parse(req.GetResponseString().AsNonNull());
+
+ string redirect = (string)response.SelectToken(@"url", true);
+ string message = (string)response.SelectToken(@"error", false);
+
+ if (!string.IsNullOrEmpty(redirect))
+ {
+ return new RegistrationRequest.RegistrationRequestErrors
+ {
+ Redirect = redirect,
+ Message = message,
+ };
+ }
+
+ // if we couldn't deserialize the error message let's throw the original exception outwards.
+ e.Rethrow();
+ }
+ catch
+ {
+ // if we couldn't deserialize the error message let's throw the original exception outwards.
+ e.Rethrow();
+ }
}
}
diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs
index 176f10975d..45128375ab 100644
--- a/osu.Game/Online/API/APIMod.cs
+++ b/osu.Game/Online/API/APIMod.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Online.API
foreach (var (_, property) in mod.GetSettingsSourceProperties())
{
- var bindable = (IBindable)property.GetValue(mod);
+ var bindable = (IBindable)property.GetValue(mod)!;
if (!bindable.IsDefault)
Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue());
@@ -60,16 +60,16 @@ namespace osu.Game.Online.API
{
foreach (var (_, property) in resultMod.GetSettingsSourceProperties())
{
- if (!Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
+ if (!Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
continue;
try
{
- resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue);
+ resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod)!, settingValue);
}
catch (Exception ex)
{
- Logger.Log($"Failed to copy mod setting value '{settingValue ?? "null"}' to \"{property.Name}\": {ex.Message}");
+ Logger.Log($"Failed to copy mod setting value '{settingValue}' to \"{property.Name}\": {ex.Message}");
}
}
}
@@ -79,7 +79,7 @@ namespace osu.Game.Online.API
public bool ShouldSerializeSettings() => Settings.Count > 0;
- public bool Equals(APIMod other)
+ public bool Equals(APIMod? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
diff --git a/osu.Game/Online/API/RegistrationRequest.cs b/osu.Game/Online/API/RegistrationRequest.cs
index 6dc867481a..78633f70b7 100644
--- a/osu.Game/Online/API/RegistrationRequest.cs
+++ b/osu.Game/Online/API/RegistrationRequest.cs
@@ -1,17 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
using Newtonsoft.Json;
namespace osu.Game.Online.API
{
public class RegistrationRequest : OsuWebRequest
{
- internal string Username;
- internal string Email;
- internal string Password;
+ internal string Username = string.Empty;
+ internal string Email = string.Empty;
+ internal string Password = string.Empty;
protected override void PrePerform()
{
@@ -24,18 +23,28 @@ namespace osu.Game.Online.API
public class RegistrationRequestErrors
{
- public UserErrors User;
+ ///
+ /// An optional error message.
+ ///
+ public string? Message;
+
+ ///
+ /// An optional URL which the user should be directed towards to complete registration.
+ ///
+ public string? Redirect;
+
+ public UserErrors? User;
public class UserErrors
{
[JsonProperty("username")]
- public string[] Username;
+ public string[] Username = Array.Empty();
[JsonProperty("user_email")]
- public string[] Email;
+ public string[] Email = Array.Empty();
[JsonProperty("password")]
- public string[] Password;
+ public string[] Password = Array.Empty();
}
}
}
diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs
index 7d1d26b75d..f2a2daccb5 100644
--- a/osu.Game/Online/API/Requests/GetScoresRequest.cs
+++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
@@ -11,10 +9,11 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using System.Text;
using System.Collections.Generic;
+using System.Linq;
namespace osu.Game.Online.API.Requests
{
- public class GetScoresRequest : APIRequest
+ public class GetScoresRequest : APIRequest, IEquatable
{
public const int MAX_SCORES_PER_REQUEST = 50;
@@ -23,7 +22,7 @@ namespace osu.Game.Online.API.Requests
private readonly IRulesetInfo ruleset;
private readonly IEnumerable mods;
- public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null)
+ public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable? mods = null)
{
if (beatmapInfo.OnlineID <= 0)
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(IBeatmapInfo.OnlineID)}.");
@@ -51,5 +50,16 @@ namespace osu.Game.Online.API.Requests
return query.ToString();
}
+
+ public bool Equals(GetScoresRequest? other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ return beatmapInfo.Equals(other.beatmapInfo)
+ && scope == other.scope
+ && ruleset.Equals(other.ruleset)
+ && mods.SequenceEqual(other.mods);
+ }
}
}
diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs
index bbaf241384..b57bb215aa 100644
--- a/osu.Game/Online/API/Requests/GetUsersRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests
{
public class GetUsersRequest : APIRequest
{
- private readonly int[] userIds;
+ public readonly int[] UserIds;
private const int max_ids_per_request = 50;
@@ -18,9 +18,9 @@ namespace osu.Game.Online.API.Requests
if (userIds.Length > max_ids_per_request)
throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {max_ids_per_request} IDs at once");
- this.userIds = userIds;
+ UserIds = userIds;
}
- protected override string Target => "users/?ids[]=" + string.Join("&ids[]=", userIds);
+ protected override string Target => "users/?ids[]=" + string.Join("&ids[]=", UserIds);
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
index 8a77801c3a..27ad2a746c 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
@@ -143,7 +143,7 @@ namespace osu.Game.Online.API.Requests.Responses
public bool Equals(IRulesetInfo? other) => other is APIRuleset r && this.MatchesOnlineID(r);
- public int CompareTo(IRulesetInfo other)
+ public int CompareTo(IRulesetInfo? other)
{
if (!(other is APIRuleset ruleset))
throw new ArgumentException($@"Object is not of type {nameof(APIRuleset)}.", nameof(other));
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
index 717a1de6b5..aeae3edde2 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
@@ -111,6 +111,12 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"language")]
public BeatmapSetOnlineLanguage Language { get; set; }
+ [JsonProperty(@"current_nominations")]
+ public BeatmapSetOnlineNomination[]? CurrentNominations { get; set; }
+
+ [JsonProperty(@"related_users")]
+ public APIUser[]? RelatedUsers { get; set; }
+
public string Source { get; set; } = string.Empty;
[JsonProperty(@"tags")]
diff --git a/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
index 2def18926f..c6a8a85407 100644
--- a/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string type
{
- set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.ToPascalCase());
+ set => Type = Enum.Parse(value.ToPascalCase());
}
public RecentActivityType Type;
@@ -29,7 +29,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string scoreRank
{
- set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value);
+ set => ScoreRank = Enum.Parse(value);
}
public ScoreRank ScoreRank;
diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs
index 2b6193f661..0e62b03f1a 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUser.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs
@@ -185,7 +185,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"playstyle")]
private string[] playStyle
{
- set => PlayStyles = value?.Select(str => Enum.Parse(typeof(APIPlayStyle), str, true)).Cast().ToArray();
+ set => PlayStyles = value?.Select(str => Enum.Parse(str, true)).ToArray();
}
public APIPlayStyle[] PlayStyles;
@@ -255,6 +255,9 @@ namespace osu.Game.Online.API.Requests.Responses
[CanBeNull]
public Dictionary RulesetsStatistics { get; set; }
+ [JsonProperty("groups")]
+ public APIUserGroup[] Groups;
+
public override string ToString() => Username;
///
diff --git a/osu.Game/Online/API/Requests/Responses/APIUserGroup.cs b/osu.Game/Online/API/Requests/Responses/APIUserGroup.cs
new file mode 100644
index 0000000000..89631d3d7a
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/APIUserGroup.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ public class APIUserGroup
+ {
+ [JsonProperty(@"colour")]
+ public string? Colour { get; set; }
+
+ [JsonProperty(@"has_listing")]
+ public bool HasListings { get; set; }
+
+ [JsonProperty(@"has_playmodes")]
+ public bool HasPlaymodes { get; set; }
+
+ [JsonProperty(@"id")]
+ public int Id { get; set; }
+
+ [JsonProperty(@"identifier")]
+ public string Identifier { get; set; } = null!;
+
+ [JsonProperty(@"is_probationary")]
+ public bool IsProbationary { get; set; }
+
+ [JsonProperty(@"name")]
+ public string Name { get; set; } = null!;
+
+ [JsonProperty(@"short_name")]
+ public string ShortName { get; set; } = null!;
+
+ [JsonProperty(@"playmodes")]
+ public string[]? Playmodes { get; set; }
+ }
+}
diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs
index 24b384b1d4..761e8aba8d 100644
--- a/osu.Game/Online/Chat/Channel.cs
+++ b/osu.Game/Online/Chat/Channel.cs
@@ -98,6 +98,11 @@ namespace osu.Game.Online.Chat
///
public Bindable HighlightedMessage = new Bindable();
+ ///
+ /// The current text box message while in this .
+ ///
+ public Bindable TextBoxMessage = new Bindable(string.Empty);
+
[JsonConstructor]
public Channel()
{
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index a4661dcbd7..7ab678775f 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -64,6 +64,11 @@ namespace osu.Game.Online.Chat
///
public IBindableList AvailableChannels => availableChannels;
+ ///
+ /// Whether the client responsible for channel notifications is connected.
+ ///
+ public bool NotificationsConnected => connector.IsConnected.Value;
+
private readonly IAPIProvider api;
private readonly NotificationsClientConnector connector;
@@ -71,7 +76,6 @@ namespace osu.Game.Online.Chat
private UserLookupCache users { get; set; }
private readonly IBindable apiState = new Bindable();
- private bool channelsInitialised;
private ScheduledDelegate scheduledAck;
private long? lastSilenceMessageId;
@@ -95,15 +99,7 @@ namespace osu.Game.Online.Chat
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
- connector.PresenceReceived += () => Schedule(() =>
- {
- if (!channelsInitialised)
- {
- channelsInitialised = true;
- // we want this to run after the first presence so we can see if the user is in any channels already.
- initializeChannels();
- }
- });
+ connector.PresenceReceived += () => Schedule(initializeChannels);
connector.Start();
@@ -118,8 +114,7 @@ namespace osu.Game.Online.Chat
///
public void OpenChannel(string name)
{
- if (name == null)
- throw new ArgumentNullException(nameof(name));
+ ArgumentNullException.ThrowIfNull(name);
CurrentChannel.Value = AvailableChannels.FirstOrDefault(c => c.Name == name) ?? throw new ChannelNotFoundException(name);
}
@@ -130,8 +125,7 @@ namespace osu.Game.Online.Chat
/// The user the private channel is opened with.
public void OpenPrivateChannel(APIUser user)
{
- if (user == null)
- throw new ArgumentNullException(nameof(user));
+ ArgumentNullException.ThrowIfNull(user);
if (user.Id == api.LocalUser.Value.Id)
return;
@@ -337,6 +331,11 @@ namespace osu.Game.Online.Chat
private void initializeChannels()
{
+ // This request is self-retrying until it succeeds.
+ // To avoid requests piling up when not logged in (ie. API is unavailable) exit early.
+ if (!api.IsLoggedIn)
+ return;
+
var req = new ListChannelsRequest();
bool joinDefaults = JoinedChannels.Count == 0;
@@ -352,10 +351,11 @@ namespace osu.Game.Online.Chat
joinChannel(ch);
}
};
+
req.Failure += error =>
{
Logger.Error(error, "Fetching channel list failed");
- initializeChannels();
+ Scheduler.AddDelayed(initializeChannels, 60000);
};
api.Queue(req);
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 7a040d9446..523185a7cb 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Online.Chat
{
int index = m.Index - captureOffset;
- string? displayText = string.Format(display,
+ string displayText = string.Format(display,
m.Groups[0],
m.Groups["text"].Value,
m.Groups["url"].Value).Trim();
@@ -109,7 +109,7 @@ namespace osu.Game.Online.Chat
foreach (Match m in regex.Matches(result.Text, startIndex))
{
int index = m.Index;
- string? linkText = m.Groups["link"].Value;
+ string linkText = m.Groups["link"].Value;
int indexLength = linkText.Length;
var details = GetLinkDetails(linkText);
@@ -125,7 +125,7 @@ namespace osu.Game.Online.Chat
public static LinkDetails GetLinkDetails(string url)
{
- string[]? args = url.Split('/', StringSplitOptions.RemoveEmptyEntries);
+ string[] args = url.Split('/', StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
switch (args[0])
@@ -341,6 +341,8 @@ namespace osu.Game.Online.Chat
OpenWiki,
Custom,
OpenChangelog,
+ FilterBeatmapSetGenre,
+ FilterBeatmapSetLanguage,
}
public class Link : IComparable
@@ -362,6 +364,6 @@ namespace osu.Game.Online.Chat
public bool Overlaps(Link otherLink) => Index < otherLink.Index + otherLink.Length && otherLink.Index < Index + Length;
- public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
+ public int CompareTo(Link? otherLink) => Index > otherLink?.Index ? 1 : -1;
}
}
diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs
index 4872d93467..9b2ad666b2 100644
--- a/osu.Game/Online/Chat/MessageNotifier.cs
+++ b/osu.Game/Online/Chat/MessageNotifier.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
@@ -61,12 +62,16 @@ namespace osu.Game.Online.Chat
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(e.NewItems != null);
+
foreach (var channel in e.NewItems.Cast())
channel.NewMessagesArrived += checkNewMessages;
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(e.OldItems != null);
+
foreach (var channel in e.OldItems.Cast())
channel.NewMessagesArrived -= checkNewMessages;
diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
index 7fd6f99102..0a5434822b 100644
--- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs
+++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
@@ -111,8 +111,13 @@ namespace osu.Game.Online.Chat
{
drawableChannel?.Expire();
+ if (e.OldValue != null)
+ TextBox?.Current.UnbindFrom(e.OldValue.TextBoxMessage);
+
if (e.NewValue == null) return;
+ TextBox?.Current.BindTo(e.NewValue.TextBoxMessage);
+
drawableChannel = CreateDrawableChannel(e.NewValue);
drawableChannel.CreateChatLineAction = CreateMessage;
drawableChannel.Padding = new MarginPadding { Bottom = postingTextBox ? text_box_height : 0 };
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index ca6d2932f7..8fd79bd703 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Net;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
@@ -59,17 +60,21 @@ namespace osu.Game.Online
var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options =>
{
- // Use HttpClient.DefaultProxy once on net6 everywhere.
- // The credential setter can also be removed at this point.
- options.Proxy = WebRequest.DefaultWebProxy;
- if (options.Proxy != null)
- options.Proxy.Credentials = CredentialCache.DefaultCredentials;
+ // Configuring proxies is not supported on iOS, see https://github.com/xamarin/xamarin-macios/issues/14632.
+ if (RuntimeInfo.OS != RuntimeInfo.Platform.iOS)
+ {
+ // Use HttpClient.DefaultProxy once on net6 everywhere.
+ // The credential setter can also be removed at this point.
+ options.Proxy = WebRequest.DefaultWebProxy;
+ if (options.Proxy != null)
+ options.Proxy.Credentials = CredentialCache.DefaultCredentials;
+ }
options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
options.Headers.Add("OsuVersionHash", versionHash);
});
- if (RuntimeInfo.SupportsJIT && preferMessagePack)
+ if (RuntimeFeature.IsDynamicCodeCompiled && preferMessagePack)
{
builder.AddMessagePackProtocol(options =>
{
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
index 170f266307..0b2e401f57 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
@@ -136,9 +136,8 @@ namespace osu.Game.Online.Leaderboards
{
if (displayedScore != null)
{
- timestampLabel.Text = prefer24HourTime.Value
- ? $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy HH:mm}"
- : $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy h:mm tt}";
+ timestampLabel.Text = LocalisableString.Format("Played on {0}",
+ displayedScore.Date.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt"));
}
}
diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
index ba7ccb24f7..57311419f7 100644
--- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs
+++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
@@ -68,7 +68,7 @@ namespace osu.Game.Online.Metadata
while (true)
{
Logger.Log($"Requesting catch-up from {lastQueueId.Value}");
- var catchUpChanges = await GetChangesSince(lastQueueId.Value);
+ var catchUpChanges = await GetChangesSince(lastQueueId.Value).ConfigureAwait(true);
lastQueueId.Value = catchUpChanges.LastProcessedQueueID;
@@ -78,7 +78,7 @@ namespace osu.Game.Online.Metadata
break;
}
- await ProcessChanges(catchUpChanges.BeatmapSetIDs);
+ await ProcessChanges(catchUpChanges.BeatmapSetIDs).ConfigureAwait(true);
}
}
catch (Exception e)
@@ -101,7 +101,7 @@ namespace osu.Game.Online.Metadata
if (!catchingUp)
lastQueueId.Value = updates.LastProcessedQueueID;
- await ProcessChanges(updates.BeatmapSetIDs);
+ await ProcessChanges(updates.BeatmapSetIDs).ConfigureAwait(false);
}
public override Task GetChangesSince(int queueId)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index bd87f2d43e..2be7327234 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -822,7 +822,7 @@ namespace osu.Game.Online.Multiplayer
{
if (cancellationToken.IsCancellationRequested)
{
- tcs.SetCanceled();
+ tcs.SetCanceled(cancellationToken);
return;
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
index 681a839b89..d70a2797c4 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
@@ -54,10 +54,10 @@ namespace osu.Game.Online.Multiplayer
return UserID == other.UserID;
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != GetType()) return false;
+ if (obj?.GetType() != GetType()) return false;
return Equals((MultiplayerRoomUser)obj);
}
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 386a3d5262..8ff0ce4065 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -80,7 +80,7 @@ namespace osu.Game.Online.Multiplayer
try
{
- return await connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);
+ return await connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty).ConfigureAwait(false);
}
catch (HubException exception)
{
@@ -88,8 +88,8 @@ namespace osu.Game.Online.Multiplayer
{
Debug.Assert(connector != null);
- await connector.Reconnect();
- return await JoinRoom(roomId, password);
+ await connector.Reconnect().ConfigureAwait(false);
+ return await JoinRoom(roomId, password).ConfigureAwait(false);
}
throw;
diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs
index d2c2e6673c..34ce186cb8 100644
--- a/osu.Game/Online/Notifications/NotificationsClientConnector.cs
+++ b/osu.Game/Online/Notifications/NotificationsClientConnector.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Online.Notifications
protected sealed override async Task BuildConnectionAsync(CancellationToken cancellationToken)
{
- var client = await BuildNotificationClientAsync(cancellationToken);
+ var client = await BuildNotificationClientAsync(cancellationToken).ConfigureAwait(false);
client.ChannelJoined = c => ChannelJoined?.Invoke(c);
client.ChannelParted = c => ChannelParted?.Invoke(c);
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
index d8d78297e3..73e5dcec6f 100644
--- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
+using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
@@ -36,11 +37,11 @@ namespace osu.Game.Online.Notifications.WebSocket
public override async Task ConnectAsync(CancellationToken cancellationToken)
{
await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false);
- await sendMessage(new StartChatRequest(), CancellationToken.None);
+ await sendMessage(new StartChatRequest(), CancellationToken.None).ConfigureAwait(false);
runReadLoop(cancellationToken);
- await base.ConnectAsync(cancellationToken);
+ await base.ConnectAsync(cancellationToken).ConfigureAwait(false);
}
private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () =>
@@ -52,7 +53,7 @@ namespace osu.Game.Online.Notifications.WebSocket
{
try
{
- WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken);
+ WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
switch (result.MessageType)
{
@@ -72,7 +73,7 @@ namespace osu.Game.Online.Notifications.WebSocket
break;
}
- await onMessageReceivedAsync(message);
+ await onMessageReceivedAsync(message).ConfigureAwait(false);
}
break;
@@ -81,12 +82,12 @@ namespace osu.Game.Online.Notifications.WebSocket
throw new NotImplementedException("Binary message type not supported.");
case WebSocketMessageType.Close:
- throw new Exception("Connection closed by remote host.");
+ throw new WebException("Connection closed by remote host.");
}
}
catch (Exception ex)
{
- await InvokeClosed(ex);
+ await InvokeClosed(ex).ConfigureAwait(false);
return;
}
}
@@ -109,7 +110,7 @@ namespace osu.Game.Online.Notifications.WebSocket
if (socket.State != WebSocketState.Open)
return;
- await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken);
+ await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
private async Task onMessageReceivedAsync(SocketMessage message)
@@ -141,7 +142,7 @@ namespace osu.Game.Online.Notifications.WebSocket
Debug.Assert(messageData != null);
foreach (var msg in messageData.Messages)
- HandleChannelJoined(await getChannel(msg.ChannelId));
+ HandleChannelJoined(await getChannel(msg.ChannelId).ConfigureAwait(false));
HandleMessages(messageData.Messages);
break;
@@ -150,7 +151,7 @@ namespace osu.Game.Online.Notifications.WebSocket
private async Task getChannel(long channelId)
{
- if (channelsMap.TryGetValue(channelId, out Channel channel))
+ if (channelsMap.TryGetValue(channelId, out Channel? channel))
return channel;
var tsc = new TaskCompletionSource();
@@ -166,13 +167,13 @@ namespace osu.Game.Online.Notifications.WebSocket
API.Queue(req);
- return await tsc.Task;
+ return await tsc.Task.ConfigureAwait(false);
}
public override async ValueTask DisposeAsync()
{
- await base.DisposeAsync();
- await closeAsync();
+ await base.DisposeAsync().ConfigureAwait(false);
+ await closeAsync().ConfigureAwait(false);
socket.Dispose();
}
}
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
index 21335a3b59..f50369a06c 100644
--- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Online.Notifications.WebSocket
req.Failure += ex => tcs.SetException(ex);
api.Queue(req);
- string endpoint = await tcs.Task;
+ string endpoint = await tcs.Task.ConfigureAwait(false);
ClientWebSocket socket = new ClientWebSocket();
socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}");
diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs
index be76644745..e33924047d 100644
--- a/osu.Game/Online/PersistentEndpointClientConnector.cs
+++ b/osu.Game/Online/PersistentEndpointClientConnector.cs
@@ -65,11 +65,11 @@ namespace osu.Game.Online
{
case APIState.Failing:
case APIState.Offline:
- await disconnect(true);
+ await disconnect(true).ConfigureAwait(true);
break;
case APIState.Online:
- await connect();
+ await connect().ConfigureAwait(true);
break;
}
}
@@ -147,10 +147,10 @@ namespace osu.Game.Online
{
bool hasBeenCancelled = cancellationToken.IsCancellationRequested;
- await disconnect(true);
+ await disconnect(true).ConfigureAwait(false);
if (ex != null)
- await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
+ await handleErrorAndDelay(ex, CancellationToken.None).ConfigureAwait(false);
else
Logger.Log($"{ClientName} disconnected", LoggingTarget.Network);
diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs
index 316452280d..003ec50afd 100644
--- a/osu.Game/Online/ProductionEndpointConfiguration.cs
+++ b/osu.Game/Online/ProductionEndpointConfiguration.cs
@@ -9,7 +9,8 @@ namespace osu.Game.Online
{
public ProductionEndpointConfiguration()
{
- WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh";
+ WebsiteRootUrl = @"https://osu.ppy.sh";
+ APIEndpointUrl = @"https://lazer.ppy.sh";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
APIClientID = "5";
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
diff --git a/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs b/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
index a999cb47f8..86708bee82 100644
--- a/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
+++ b/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
@@ -33,7 +33,8 @@ namespace osu.Game.Online
object? instance = Activator.CreateInstance(resolvedType);
- jsonSerializer.Populate(obj["$value"]!.CreateReader(), instance);
+ if (instance != null)
+ jsonSerializer.Populate(obj["$value"]!.CreateReader(), instance);
return instance;
}
diff --git a/osu.Game/Online/Solo/SoloStatisticsUpdate.cs b/osu.Game/Online/Solo/SoloStatisticsUpdate.cs
new file mode 100644
index 0000000000..cb9dac97c7
--- /dev/null
+++ b/osu.Game/Online/Solo/SoloStatisticsUpdate.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Online.Solo
+{
+ ///
+ /// Contains data about the change in a user's profile statistics after completing a score.
+ ///
+ public class SoloStatisticsUpdate
+ {
+ ///
+ /// The score set by the user that triggered the update.
+ ///
+ public ScoreInfo Score { get; }
+
+ ///
+ /// The user's profile statistics prior to the score being set.
+ ///
+ public UserStatistics Before { get; }
+
+ ///
+ /// The user's profile statistics after the score was set.
+ ///
+ public UserStatistics After { get; }
+
+ ///
+ /// Creates a new .
+ ///
+ /// The score set by the user that triggered the update.
+ /// The user's profile statistics prior to the score being set.
+ /// The user's profile statistics after the score was set.
+ public SoloStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after)
+ {
+ Score = score;
+ Before = before;
+ After = after;
+ }
+ }
+}
diff --git a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs
new file mode 100644
index 0000000000..46449fea73
--- /dev/null
+++ b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs
@@ -0,0 +1,162 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Game.Extensions;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Spectator;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Online.Solo
+{
+ ///
+ /// A persistent component that binds to the spectator server and API in order to deliver updates about the logged in user's gameplay statistics.
+ ///
+ public partial class SoloStatisticsWatcher : Component
+ {
+ [Resolved]
+ private SpectatorClient spectatorClient { get; set; } = null!;
+
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
+
+ private readonly Dictionary callbacks = new Dictionary();
+ private long? lastProcessedScoreId;
+
+ private Dictionary? latestStatistics;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true);
+ spectatorClient.OnUserScoreProcessed += userScoreProcessed;
+ }
+
+ ///
+ /// Registers for a user statistics update after the given has been processed server-side.
+ ///
+ /// The score to listen for the statistics update for.
+ /// The callback to be invoked once the statistics update has been prepared.
+ /// An representing the subscription. Disposing it is equivalent to unsubscribing from future notifications.
+ public IDisposable RegisterForStatisticsUpdateAfter(ScoreInfo score, Action onUpdateReady)
+ {
+ Schedule(() =>
+ {
+ if (!api.IsLoggedIn)
+ return;
+
+ if (!score.Ruleset.IsLegacyRuleset() || score.OnlineID <= 0)
+ return;
+
+ var callback = new StatisticsUpdateCallback(score, onUpdateReady);
+
+ if (lastProcessedScoreId == score.OnlineID)
+ {
+ requestStatisticsUpdate(api.LocalUser.Value.Id, callback);
+ return;
+ }
+
+ callbacks.Add(score.OnlineID, callback);
+ });
+
+ return new InvokeOnDisposal(() => Schedule(() => callbacks.Remove(score.OnlineID)));
+ }
+
+ private void onUserChanged(APIUser? localUser) => Schedule(() =>
+ {
+ callbacks.Clear();
+ lastProcessedScoreId = null;
+ latestStatistics = null;
+
+ if (localUser == null || localUser.OnlineID <= 1)
+ return;
+
+ var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
+ userRequest.Success += initialiseUserStatistics;
+ api.Queue(userRequest);
+ });
+
+ private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() =>
+ {
+ var user = response.Users.SingleOrDefault();
+
+ // possible if the user is restricted or similar.
+ if (user == null)
+ return;
+
+ latestStatistics = new Dictionary();
+
+ if (user.RulesetsStatistics != null)
+ {
+ foreach (var rulesetStats in user.RulesetsStatistics)
+ latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
+ }
+ });
+
+ private void userScoreProcessed(int userId, long scoreId)
+ {
+ if (userId != api.LocalUser.Value?.OnlineID)
+ return;
+
+ lastProcessedScoreId = scoreId;
+
+ if (!callbacks.TryGetValue(scoreId, out var callback))
+ return;
+
+ requestStatisticsUpdate(userId, callback);
+ callbacks.Remove(scoreId);
+ }
+
+ private void requestStatisticsUpdate(int userId, StatisticsUpdateCallback callback)
+ {
+ var request = new GetUserRequest(userId, callback.Score.Ruleset);
+ request.Success += user => Schedule(() => dispatchStatisticsUpdate(callback, user.Statistics));
+ api.Queue(request);
+ }
+
+ private void dispatchStatisticsUpdate(StatisticsUpdateCallback callback, UserStatistics updatedStatistics)
+ {
+ string rulesetName = callback.Score.Ruleset.ShortName;
+
+ if (latestStatistics == null)
+ return;
+
+ latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
+ latestRulesetStatistics ??= new UserStatistics();
+
+ var update = new SoloStatisticsUpdate(callback.Score, latestRulesetStatistics, updatedStatistics);
+ callback.OnUpdateReady.Invoke(update);
+
+ latestStatistics[rulesetName] = updatedStatistics;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ if (spectatorClient.IsNotNull())
+ spectatorClient.OnUserScoreProcessed -= userScoreProcessed;
+
+ base.Dispose(isDisposing);
+ }
+
+ private class StatisticsUpdateCallback
+ {
+ public ScoreInfo Score { get; }
+ public Action OnUpdateReady { get; }
+
+ public StatisticsUpdateCallback(ScoreInfo score, Action onUpdateReady)
+ {
+ Score = score;
+ OnUpdateReady = onUpdateReady;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs
index ccba280001..605ebc4ef0 100644
--- a/osu.Game/Online/Spectator/ISpectatorClient.cs
+++ b/osu.Game/Online/Spectator/ISpectatorClient.cs
@@ -32,5 +32,12 @@ namespace osu.Game.Online.Spectator
/// The user.
/// The frame data.
Task UserSentFrames(int userId, FrameDataBundle data);
+
+ ///
+ /// Signals that a user's submitted score was fully processed.
+ ///
+ /// The ID of the user who achieved the score.
+ /// The ID of the score.
+ Task UserScoreProcessed(int userId, long scoreId);
}
}
diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs
index 25785f60a4..fa9d04792a 100644
--- a/osu.Game/Online/Spectator/ISpectatorServer.cs
+++ b/osu.Game/Online/Spectator/ISpectatorServer.cs
@@ -15,8 +15,9 @@ namespace osu.Game.Online.Spectator
///
/// Signal the start of a new play session.
///
+ /// The score submission token.
/// The state of gameplay.
- Task BeginPlaySession(SpectatorState state);
+ Task BeginPlaySession(long? scoreToken, SpectatorState state);
///
/// Send a bundle of frame data for the current play session.
diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
index d69bd81b57..3118e05053 100644
--- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
+++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
@@ -41,13 +41,14 @@ namespace osu.Game.Online.Spectator
connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
+ connection.On(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed);
};
IsConnected.BindTo(connector.IsConnected);
}
}
- protected override async Task BeginPlayingInternal(SpectatorState state)
+ protected override async Task BeginPlayingInternal(long? scoreToken, SpectatorState state)
{
if (!IsConnected.Value)
return;
@@ -56,7 +57,7 @@ namespace osu.Game.Online.Spectator
try
{
- await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
+ await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), scoreToken, state).ConfigureAwait(false);
}
catch (Exception exception)
{
@@ -64,8 +65,8 @@ namespace osu.Game.Online.Spectator
{
Debug.Assert(connector != null);
- await connector.Reconnect();
- await BeginPlayingInternal(state);
+ await connector.Reconnect().ConfigureAwait(false);
+ await BeginPlayingInternal(scoreToken, state).ConfigureAwait(false);
}
// Exceptions can occur if, for instance, the locally played beatmap doesn't have a server-side counterpart.
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index b0ee0bc37b..55ec75f4ce 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Online.Spectator
///
/// Whether the local user is playing.
///
- protected bool IsPlaying { get; private set; }
+ protected internal bool IsPlaying { get; private set; }
///
/// Called whenever new frames arrive from the server.
@@ -64,6 +64,11 @@ namespace osu.Game.Online.Spectator
///
public virtual event Action? OnUserFinishedPlaying;
+ ///
+ /// Called whenever a user-submitted score has been fully processed.
+ ///
+ public virtual event Action? OnUserScoreProcessed;
+
///
/// A dictionary containing all users currently being watched, with the number of watching components for each user.
///
@@ -76,6 +81,7 @@ namespace osu.Game.Online.Spectator
private IBeatmap? currentBeatmap;
private Score? currentScore;
+ private long? currentScoreToken;
private readonly Queue pendingFrameBundles = new Queue();
@@ -108,7 +114,7 @@ namespace osu.Game.Online.Spectator
// re-send state in case it wasn't received
if (IsPlaying)
// TODO: this is likely sent out of order after a reconnect scenario. needs further consideration.
- BeginPlayingInternal(currentState);
+ BeginPlayingInternal(currentScoreToken, currentState);
}
else
{
@@ -159,7 +165,14 @@ namespace osu.Game.Online.Spectator
return Task.CompletedTask;
}
- public void BeginPlaying(GameplayState state, Score score)
+ Task ISpectatorClient.UserScoreProcessed(int userId, long scoreId)
+ {
+ Schedule(() => OnUserScoreProcessed?.Invoke(userId, scoreId));
+
+ return Task.CompletedTask;
+ }
+
+ public void BeginPlaying(long? scoreToken, GameplayState state, Score score)
{
// This schedule is only here to match the one below in `EndPlaying`.
Schedule(() =>
@@ -174,12 +187,13 @@ namespace osu.Game.Online.Spectator
currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentState.State = SpectatedUserState.Playing;
- currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues;
+ currentState.MaximumStatistics = state.ScoreProcessor.MaximumStatistics;
currentBeatmap = state.Beatmap;
currentScore = score;
+ currentScoreToken = scoreToken;
- BeginPlayingInternal(currentState);
+ BeginPlayingInternal(currentScoreToken, currentState);
});
}
@@ -264,7 +278,7 @@ namespace osu.Game.Online.Spectator
});
}
- protected abstract Task BeginPlayingInternal(SpectatorState state);
+ protected abstract Task BeginPlayingInternal(long? scoreToken, SpectatorState state);
protected abstract Task SendFramesInternal(FrameDataBundle bundle);
diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
index c97871c3aa..1c505ea107 100644
--- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
+++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
@@ -152,12 +152,12 @@ namespace osu.Game.Online.Spectator
scoreInfo.MaxCombo = frame.Header.MaxCombo;
scoreInfo.Statistics = frame.Header.Statistics;
+ scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics;
Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo;
- scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _);
- TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues);
+ TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
}
protected override void Dispose(bool isDisposing)
@@ -184,7 +184,7 @@ namespace osu.Game.Online.Spectator
Header = header;
}
- public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
+ public int CompareTo(TimedFrame? other) => Time.CompareTo(other?.Time);
}
}
}
diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs
index 766b274e63..91df05bf96 100644
--- a/osu.Game/Online/Spectator/SpectatorState.cs
+++ b/osu.Game/Online/Spectator/SpectatorState.cs
@@ -9,7 +9,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MessagePack;
using osu.Game.Online.API;
-using osu.Game.Scoring;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Online.Spectator
{
@@ -31,7 +31,7 @@ namespace osu.Game.Online.Spectator
public SpectatedUserState State { get; set; }
[Key(4)]
- public ScoringValues MaximumScoringValues { get; set; }
+ public Dictionary MaximumStatistics { get; set; } = new Dictionary();
public bool Equals(SpectatorState other)
{
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 5565fa7ef3..4113c9be8f 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -16,6 +16,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
+using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
@@ -45,6 +46,7 @@ using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
+using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Music;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Toolbar;
@@ -305,6 +307,13 @@ namespace osu.Game
// Transfer any runtime changes back to configuration file.
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
+ LocalUserPlaying.BindValueChanged(p =>
+ {
+ BeatmapManager.PauseImports = p.NewValue;
+ SkinManager.PauseImports = p.NewValue;
+ ScoreManager.PauseImports = p.NewValue;
+ }, true);
+
IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true);
Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade);
@@ -332,7 +341,7 @@ namespace osu.Game
/// The link to load.
public void HandleLink(LinkDetails link) => Schedule(() =>
{
- string argString = link.Argument.ToString();
+ string argString = link.Argument.ToString() ?? string.Empty;
switch (link.Action)
{
@@ -352,7 +361,18 @@ namespace osu.Game
break;
case LinkAction.SearchBeatmapSet:
- SearchBeatmapSet(argString);
+ if (link.Argument is RomanisableString romanisable)
+ SearchBeatmapSet(romanisable.GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript));
+ else
+ SearchBeatmapSet(argString);
+ break;
+
+ case LinkAction.FilterBeatmapSetGenre:
+ FilterBeatmapSetGenre((SearchGenre)link.Argument);
+ break;
+
+ case LinkAction.FilterBeatmapSetLanguage:
+ FilterBeatmapSetLanguage((SearchLanguage)link.Argument);
break;
case LinkAction.OpenEditorTimestamp:
@@ -406,6 +426,16 @@ namespace osu.Game
if (url.StartsWith('/'))
url = $"{API.APIEndpointUrl}{url}";
+ if (!url.CheckIsValidUrl())
+ {
+ Notifications.Post(new SimpleErrorNotification
+ {
+ Text = $"The URL {url} has an unsupported or dangerous protocol and will not be opened.",
+ });
+
+ return;
+ }
+
externalLinkOpener.OpenUrlExternally(url, bypassExternalUrlWarning);
});
@@ -449,6 +479,10 @@ namespace osu.Game
/// The query to search for.
public void SearchBeatmapSet(string query) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithSearch(query));
+ public void FilterBeatmapSetGenre(SearchGenre genre) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithGenreFilter(genre));
+
+ public void FilterBeatmapSetLanguage(SearchLanguage language) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithLanguageFilter(language));
+
///
/// Show a wiki's page as an overlay
///
@@ -616,14 +650,14 @@ namespace osu.Game
}, validScreens: validScreens);
}
- public override Task Import(params ImportTask[] imports)
+ public override Task Import(ImportTask[] imports, ImportParameters parameters = default)
{
// encapsulate task as we don't want to begin the import process until in a ready state.
// ReSharper disable once AsyncVoidLambda
// TODO: This is bad because `new Task` doesn't have a Func override.
// Only used for android imports and a bit of a mess. Probably needs rethinking overall.
- var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false));
+ var importTask = new Task(async () => await base.Import(imports, parameters).ConfigureAwait(false));
waitForReady(() => this, _ => importTask.Start());
@@ -712,7 +746,7 @@ namespace osu.Game
{
base.LoadComplete();
- var languages = Enum.GetValues(typeof(Language)).OfType();
+ var languages = Enum.GetValues();
var mappings = languages.Select(language =>
{
@@ -899,9 +933,9 @@ namespace osu.Game
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
- loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true);
+ loadComponentSingleFile(channelManager = new ChannelManager(API), Add, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
- loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
+ loadComponentSingleFile(new MessageNotifier(), Add, true);
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
@@ -1029,7 +1063,7 @@ namespace osu.Game
Logger.NewEntry += entry =>
{
- if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database) return;
+ if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return;
const int short_term_display_limit = 3;
@@ -1043,7 +1077,7 @@ namespace osu.Game
}
else if (recentLogCount == short_term_display_limit)
{
- string logFile = $@"{entry.Target.ToString().ToLowerInvariant()}.log";
+ string logFile = $@"{entry.Target.Value.ToString().ToLowerInvariant()}.log";
Schedule(() => Notifications.Post(new SimpleNotification
{
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 1d5f5a75e5..b27be37591 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -46,6 +46,7 @@ using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Solo;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
@@ -83,6 +84,8 @@ namespace osu.Game
public const int SAMPLE_CONCURRENCY = 6;
+ public const double SFX_STEREO_STRENGTH = 0.75;
+
///
/// Length of debounce (in milliseconds) for commonly occuring sample playbacks that could stack.
///
@@ -186,11 +189,12 @@ namespace osu.Game
private RulesetConfigCache rulesetConfigCache;
- private SpectatorClient spectatorClient;
+ protected SpectatorClient SpectatorClient { get; private set; }
protected MultiplayerClient MultiplayerClient { get; private set; }
private MetadataClient metadataClient;
+ private SoloStatisticsWatcher soloStatisticsWatcher;
private RealmAccess realm;
@@ -292,27 +296,28 @@ namespace osu.Game
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
// Add after all the above cache operations as it depends on them.
- AddInternal(difficultyCache);
+ base.Content.Add(difficultyCache);
// TODO: OsuGame or OsuGameBase?
dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
- dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
+ dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
+ dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher());
- AddInternal(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
+ base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
BeatmapManager.ProcessBeatmap = args => beatmapUpdater.Process(args.beatmapSet, !args.isBatch);
dependencies.Cache(userCache = new UserLookupCache());
- AddInternal(userCache);
+ base.Content.Add(userCache);
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
- AddInternal(beatmapCache);
+ base.Content.Add(beatmapCache);
var scorePerformanceManager = new ScorePerformanceCache();
dependencies.Cache(scorePerformanceManager);
- AddInternal(scorePerformanceManager);
+ base.Content.Add(scorePerformanceManager);
dependencies.CacheAs(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
@@ -339,13 +344,24 @@ namespace osu.Game
// add api components to hierarchy.
if (API is APIAccess apiAccess)
- AddInternal(apiAccess);
+ base.Content.Add(apiAccess);
- AddInternal(spectatorClient);
- AddInternal(MultiplayerClient);
- AddInternal(metadataClient);
+ base.Content.Add(SpectatorClient);
+ base.Content.Add(MultiplayerClient);
+ base.Content.Add(metadataClient);
+ base.Content.Add(soloStatisticsWatcher);
- AddInternal(rulesetConfigCache);
+ base.Content.Add(rulesetConfigCache);
+
+ PreviewTrackManager previewTrackManager;
+ dependencies.Cache(previewTrackManager = new PreviewTrackManager(BeatmapManager.BeatmapTrackStore));
+ base.Content.Add(previewTrackManager);
+
+ base.Content.Add(MusicController = new MusicController());
+ dependencies.CacheAs(MusicController);
+
+ MusicController.TrackChanged += onTrackChanged;
+ base.Content.Add(beatmapClock);
GlobalActionContainer globalBindings;
@@ -372,16 +388,6 @@ namespace osu.Game
dependencies.Cache(globalBindings);
- PreviewTrackManager previewTrackManager;
- dependencies.Cache(previewTrackManager = new PreviewTrackManager(BeatmapManager.BeatmapTrackStore));
- Add(previewTrackManager);
-
- AddInternal(MusicController = new MusicController());
- dependencies.CacheAs(MusicController);
-
- MusicController.TrackChanged += onTrackChanged;
- AddInternal(beatmapClock);
-
Ruleset.BindValueChanged(onRulesetChanged);
Beatmap.BindValueChanged(onBeatmapChanged);
}
@@ -601,7 +607,7 @@ namespace osu.Game
try
{
- foreach (ModType type in Enum.GetValues(typeof(ModType)))
+ foreach (ModType type in Enum.GetValues())
{
dict[type] = instance.GetModsFor(type)
// Rulesets should never return null mods, but let's be defensive just in case.
diff --git a/osu.Game/OsuGameBase_Importing.cs b/osu.Game/OsuGameBase_Importing.cs
index e34e48f21d..cf65460bab 100644
--- a/osu.Game/OsuGameBase_Importing.cs
+++ b/osu.Game/OsuGameBase_Importing.cs
@@ -44,13 +44,13 @@ namespace osu.Game
}
}
- public virtual async Task Import(params ImportTask[] tasks)
+ public virtual async Task Import(ImportTask[] tasks, ImportParameters parameters = default)
{
var tasksPerExtension = tasks.GroupBy(t => Path.GetExtension(t.Path).ToLowerInvariant());
await Task.WhenAll(tasksPerExtension.Select(taskGroup =>
{
var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key));
- return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask;
+ return importer?.Import(taskGroup.ToArray(), parameters) ?? Task.CompletedTask;
})).ConfigureAwait(false);
}
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index ea1ee2c9a9..2e20f83e9e 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -47,6 +47,9 @@ namespace osu.Game.Overlays.AccountCreation
[Resolved]
private GameHost host { get; set; }
+ [Resolved]
+ private OsuGame game { get; set; }
+
[BackgroundDependencyLoader]
private void load()
{
@@ -194,9 +197,20 @@ namespace osu.Game.Overlays.AccountCreation
{
if (errors != null)
{
- usernameDescription.AddErrors(errors.User.Username);
- emailAddressDescription.AddErrors(errors.User.Email);
- passwordDescription.AddErrors(errors.User.Password);
+ if (errors.User != null)
+ {
+ usernameDescription.AddErrors(errors.User.Username);
+ emailAddressDescription.AddErrors(errors.User.Email);
+ passwordDescription.AddErrors(errors.User.Password);
+ }
+
+ if (!string.IsNullOrEmpty(errors.Redirect))
+ {
+ if (!string.IsNullOrEmpty(errors.Message))
+ passwordDescription.AddErrors(new[] { errors.Message });
+
+ game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}");
+ }
}
else
{
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index c5c252fb5d..37a29b1c50 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -145,6 +145,12 @@ namespace osu.Game.Overlays.BeatmapListing
public void Search(string query)
=> Schedule(() => searchControl.Query.Value = query);
+ public void FilterGenre(SearchGenre genre)
+ => Schedule(() => searchControl.Genre.Value = genre);
+
+ public void FilterLanguage(SearchLanguage language)
+ => Schedule(() => searchControl.Language.Value = language);
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
index f28ec9c295..23de1cf76d 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
@@ -146,6 +146,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
});
+ generalFilter.Current.Add(SearchGeneral.FeaturedArtists);
categoryFilter.Current.Value = SearchCategory.Leaderboard;
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs
index 10ec66e396..a4a914db55 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs
@@ -3,10 +3,18 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Configuration;
using osu.Game.Graphics;
+using osu.Game.Localisation;
+using osu.Game.Overlays.Dialog;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
+using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Overlays.BeatmapListing
{
@@ -32,6 +40,8 @@ namespace osu.Game.Overlays.BeatmapListing
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
{
+ private Bindable disclaimerShown;
+
public FeaturedArtistsTabItem()
: base(SearchGeneral.FeaturedArtists)
{
@@ -40,7 +50,60 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved]
private OsuColour colours { get; set; }
+ [Resolved]
+ private SessionStatics sessionStatics { get; set; }
+
+ [Resolved(canBeNull: true)]
+ private IDialogOverlay dialogOverlay { get; set; }
+
protected override Color4 GetStateColour() => colours.Orange1;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ disclaimerShown = sessionStatics.GetBindable(Static.FeaturedArtistDisclaimerShownOnce);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ if (!disclaimerShown.Value && dialogOverlay != null)
+ {
+ dialogOverlay.Push(new FeaturedArtistConfirmDialog(() =>
+ {
+ disclaimerShown.Value = true;
+ base.OnClick(e);
+ }));
+
+ return true;
+ }
+
+ return base.OnClick(e);
+ }
+ }
+ }
+
+ internal partial class FeaturedArtistConfirmDialog : PopupDialog
+ {
+ public FeaturedArtistConfirmDialog(Action confirm)
+ {
+ HeaderText = BeatmapOverlayStrings.UserContentDisclaimerHeader;
+ BodyText = BeatmapOverlayStrings.UserContentDisclaimerDescription;
+
+ Icon = FontAwesome.Solid.ExclamationTriangle;
+
+ Buttons = new PopupDialogButton[]
+ {
+ new PopupDialogDangerousButton
+ {
+ Text = BeatmapOverlayStrings.UserContentConfirmButtonText,
+ Action = confirm
+ },
+ new PopupDialogCancelButton
+ {
+ Text = CommonStrings.ButtonsCancel,
+ },
+ };
}
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
index 79a794a9ad..abd2643a41 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
@@ -5,12 +5,14 @@
using System;
using System.Collections.Generic;
+using System.Collections.Specialized;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osuTK;
@@ -18,6 +20,7 @@ using osuTK;
namespace osu.Game.Overlays.BeatmapListing
{
public partial class BeatmapSearchMultipleSelectionFilterRow : BeatmapSearchFilterRow>
+ where T : Enum
{
public new readonly BindableList Current = new BindableList();
@@ -31,7 +34,7 @@ namespace osu.Game.Overlays.BeatmapListing
[BackgroundDependencyLoader]
private void load()
{
- Current.BindTo(filter.Current);
+ filter.Current.BindTo(Current);
}
protected sealed override Drawable CreateFilter() => filter = CreateMultipleSelectionFilter();
@@ -64,6 +67,14 @@ namespace osu.Game.Overlays.BeatmapListing
foreach (var item in Children)
item.Active.BindValueChanged(active => toggleItem(item.Value, active.NewValue));
+
+ Current.BindCollectionChanged(currentChanged, true);
+ }
+
+ private void currentChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ foreach (var c in Children)
+ c.Active.Value = Current.Contains(c.Value);
}
///
@@ -79,7 +90,10 @@ namespace osu.Game.Overlays.BeatmapListing
private void toggleItem(T value, bool active)
{
if (active)
- Current.Add(value);
+ {
+ if (!Current.Contains(value))
+ Current.Add(value);
+ }
else
Current.Remove(value);
}
@@ -87,9 +101,30 @@ namespace osu.Game.Overlays.BeatmapListing
protected partial class MultipleSelectionFilterTabItem : FilterTabItem
{
+ private readonly Box selectedUnderline;
+
+ protected override bool HighlightOnHoverWhenActive => true;
+
public MultipleSelectionFilterTabItem(T value)
: base(value)
{
+ // This doesn't match any actual design, but should make it easier for the user to understand
+ // that filters are applied until we settle on a final design.
+ AddInternal(selectedUnderline = new Box
+ {
+ Depth = float.MaxValue,
+ RelativeSizeAxes = Axes.X,
+ Height = 1.5f,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.CentreLeft,
+ });
+ }
+
+ protected override void UpdateState()
+ {
+ base.UpdateState();
+ selectedUnderline.FadeTo(Active.Value ? 1 : 0, 200, Easing.OutQuint);
+ selectedUnderline.FadeColour(IsHovered ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
}
protected override bool OnClick(ClickEvent e)
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs
index fa37810f37..96626d0ac6 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs
@@ -6,6 +6,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
+using osu.Game.Extensions;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
@@ -28,7 +29,13 @@ namespace osu.Game.Overlays.BeatmapListing
AddTabItem(new RulesetFilterTabItemAny());
foreach (var r in rulesets.AvailableRulesets)
+ {
+ // Don't display non-legacy rulesets
+ if (!r.IsLegacyRuleset())
+ continue;
+
AddItem(r);
+ }
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
index 7b95ae8ea8..c33d5056fa 100644
--- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
+++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Overlays.BeatmapListing
public partial class FilterTabItem : TabItem
{
[Resolved]
- private OverlayColourProvider colourProvider { get; set; }
+ protected OverlayColourProvider ColourProvider { get; private set; }
private OsuSpriteText text;
@@ -52,38 +52,42 @@ namespace osu.Game.Overlays.BeatmapListing
{
base.LoadComplete();
- updateState();
+ UpdateState();
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
- updateState();
+ UpdateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
- updateState();
+ UpdateState();
}
- protected override void OnActivated() => updateState();
+ protected override void OnActivated() => UpdateState();
- protected override void OnDeactivated() => updateState();
+ protected override void OnDeactivated() => UpdateState();
///
/// Returns the label text to be used for the supplied .
///
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
- private void updateState()
+ protected virtual bool HighlightOnHoverWhenActive => false;
+
+ protected virtual void UpdateState()
{
- text.FadeColour(IsHovered ? colourProvider.Light1 : GetStateColour(), 200, Easing.OutQuint);
- text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
+ bool highlightHover = IsHovered && (!Active.Value || HighlightOnHoverWhenActive);
+
+ text.FadeColour(highlightHover ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
+ text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular);
}
- protected virtual Color4 GetStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
+ protected virtual Color4 GetStateColour() => Active.Value ? ColourProvider.Content1 : ColourProvider.Light2;
}
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index d6d4f1a67b..73961487ed 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -110,6 +110,18 @@ namespace osu.Game.Overlays
ScrollFlow.ScrollToStart();
}
+ public void ShowWithGenreFilter(SearchGenre genre)
+ {
+ ShowWithSearch(string.Empty);
+ filterControl.FilterGenre(genre);
+ }
+
+ public void ShowWithLanguageFilter(SearchLanguage language)
+ {
+ ShowWithSearch(string.Empty);
+ filterControl.FilterLanguage(language);
+ }
+
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs
index fa9c9b5018..858742648c 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs
@@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects;
+using osu.Framework.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
@@ -16,7 +17,7 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
- public partial class BeatmapSetHeader : OverlayHeader
+ public partial class BeatmapSetHeader : TabControlOverlayHeader
{
public readonly Bindable BeatmapSet = new Bindable();
@@ -46,7 +47,7 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet = { BindTarget = BeatmapSet }
};
- protected override Drawable CreateTitleContent() => RulesetSelector = new BeatmapRulesetSelector
+ protected override Drawable CreateTabControlContent() => RulesetSelector = new BeatmapRulesetSelector
{
Current = ruleset
};
@@ -62,4 +63,10 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
}
+
+ public enum BeatmapSetTabs
+ {
+ [LocalisableDescription(typeof(LayoutStrings), nameof(LayoutStrings.HeaderBeatmapsetsShow))]
+ Info,
+ }
}
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
index 0318dad0e3..26e6b1f158 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
@@ -3,26 +3,29 @@
#nullable disable
+using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
-using osu.Game.Graphics.Cursor;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
@@ -41,12 +44,10 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly UpdateableOnlineBeatmapSetCover cover;
private readonly Box coverGradient;
- private readonly OsuSpriteText title, artist;
+ private readonly LinkFlowContainer title, artist;
private readonly AuthorInfo author;
- private readonly ExplicitContentBeatmapBadge explicitContent;
- private readonly SpotlightBeatmapBadge spotlight;
- private readonly FeaturedArtistBeatmapBadge featuredArtist;
+ private ExternalLinkButton externalLink;
private readonly FillFlowContainer downloadButtonsContainer;
private readonly BeatmapAvailability beatmapAvailability;
@@ -65,8 +66,6 @@ namespace osu.Game.Overlays.BeatmapSet
public BeatmapSetHeaderContent()
{
- ExternalLinkButton externalLink;
-
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new Container
@@ -91,118 +90,74 @@ namespace osu.Game.Overlays.BeatmapSet
},
},
},
- new OsuContextMenuContainer
+ new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Child = new Container
+ Padding = new MarginPadding
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding
+ Vertical = BeatmapSetOverlay.Y_PADDING,
+ Left = BeatmapSetOverlay.X_PADDING,
+ Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
+ },
+ Children = new Drawable[]
+ {
+ fadeContent = new FillFlowContainer
{
- Vertical = BeatmapSetOverlay.Y_PADDING,
- Left = BeatmapSetOverlay.X_PADDING,
- Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
- },
- Children = new Drawable[]
- {
- fadeContent = new FillFlowContainer
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ new Container
{
- new Container
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = Picker = new BeatmapPicker(),
+ },
+ title = new MetadataFlowContainer(s =>
+ {
+ s.Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true);
+ })
+ {
+ Margin = new MarginPadding { Top = 15 },
+ },
+ artist = new MetadataFlowContainer(s =>
+ {
+ s.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true);
+ })
+ {
+ Margin = new MarginPadding { Bottom = 20 },
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = author = new AuthorInfo(),
+ },
+ beatmapAvailability = new BeatmapAvailability(),
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = buttons_height,
+ Margin = new MarginPadding { Top = 10 },
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Child = Picker = new BeatmapPicker(),
- },
- new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Top = 15 },
- Children = new Drawable[]
+ favouriteButton = new FavouriteButton
{
- title = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true)
- },
- externalLink = new ExternalLinkButton
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font
- },
- explicitContent = new ExplicitContentBeatmapBadge
- {
- Alpha = 0f,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Left = 10, Bottom = 4 },
- },
- spotlight = new SpotlightBeatmapBadge
- {
- Alpha = 0f,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Left = 10, Bottom = 4 },
- }
- }
- },
- new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Bottom = 20 },
- Children = new Drawable[]
- {
- artist = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true),
- },
- featuredArtist = new FeaturedArtistBeatmapBadge
- {
- Alpha = 0f,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Left = 10 }
- }
- }
- },
- new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Child = author = new AuthorInfo(),
- },
- beatmapAvailability = new BeatmapAvailability(),
- new Container
- {
- RelativeSizeAxes = Axes.X,
- Height = buttons_height,
- Margin = new MarginPadding { Top = 10 },
- Children = new Drawable[]
- {
- favouriteButton = new FavouriteButton
- {
- BeatmapSet = { BindTarget = BeatmapSet }
- },
- downloadButtonsContainer = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
- Spacing = new Vector2(buttons_spacing),
- },
+ BeatmapSet = { BindTarget = BeatmapSet }
},
- },
+ downloadButtonsContainer = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
+ Spacing = new Vector2(buttons_spacing),
+ },
+ }
},
},
- }
- },
+ },
+ }
},
loading = new LoadingSpinner
{
@@ -237,12 +192,17 @@ namespace osu.Game.Overlays.BeatmapSet
Picker.Beatmap.ValueChanged += b =>
{
Details.BeatmapInfo = b.NewValue;
- externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineID}";
+ updateExternalLink();
onlineStatusPill.Status = b.NewValue?.Status ?? BeatmapOnlineStatus.None;
};
}
+ private void updateExternalLink()
+ {
+ if (externalLink != null) externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{Picker.Beatmap.Value?.Ruleset.ShortName}/{Picker.Beatmap.Value?.OnlineID}";
+ }
+
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
@@ -275,12 +235,38 @@ namespace osu.Game.Overlays.BeatmapSet
loading.Hide();
- title.Text = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title);
- artist.Text = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist);
+ var titleText = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title);
+ var artistText = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist);
- explicitContent.Alpha = setInfo.NewValue.HasExplicitContent ? 1 : 0;
- spotlight.Alpha = setInfo.NewValue.FeaturedInSpotlight ? 1 : 0;
- featuredArtist.Alpha = setInfo.NewValue.TrackId != null ? 1 : 0;
+ title.Clear();
+ artist.Clear();
+
+ title.AddLink(titleText, LinkAction.SearchBeatmapSet, titleText);
+
+ title.AddArbitraryDrawable(Empty().With(d => d.Width = 5));
+ title.AddArbitraryDrawable(externalLink = new ExternalLinkButton());
+
+ if (setInfo.NewValue.HasExplicitContent)
+ {
+ title.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
+ title.AddArbitraryDrawable(new ExplicitContentBeatmapBadge());
+ }
+
+ if (setInfo.NewValue.FeaturedInSpotlight)
+ {
+ title.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
+ title.AddArbitraryDrawable(new SpotlightBeatmapBadge());
+ }
+
+ artist.AddLink(artistText, LinkAction.SearchBeatmapSet, artistText);
+
+ if (setInfo.NewValue.TrackId != null)
+ {
+ artist.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
+ artist.AddArbitraryDrawable(new FeaturedArtistBeatmapBadge());
+ }
+
+ updateExternalLink();
onlineStatusPill.FadeIn(500, Easing.OutQuint);
@@ -327,5 +313,32 @@ namespace osu.Game.Overlays.BeatmapSet
break;
}
}
+
+ public partial class MetadataFlowContainer : LinkFlowContainer
+ {
+ public MetadataFlowContainer(Action defaultCreationParameters = null)
+ : base(defaultCreationParameters)
+ {
+ TextAnchor = Anchor.CentreLeft;
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ }
+
+ protected override DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new MetadataLinkCompiler(textPart);
+
+ public partial class MetadataLinkCompiler : DrawableLinkCompiler
+ {
+ public MetadataLinkCompiler(ITextPart part)
+ : base(part)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ IdleColour = Color4.White;
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs
index 514a4ea8cd..58739eb471 100644
--- a/osu.Game/Overlays/BeatmapSet/Info.cs
+++ b/osu.Game/Overlays/BeatmapSet/Info.cs
@@ -3,14 +3,17 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.BeatmapListing;
namespace osu.Game.Overlays.BeatmapSet
{
@@ -34,7 +37,10 @@ namespace osu.Game.Overlays.BeatmapSet
public Info()
{
- MetadataSection source, tags, genre, language;
+ MetadataSectionNominators nominators;
+ MetadataSection source, tags;
+ MetadataSectionGenre genre;
+ MetadataSectionLanguage language;
OsuSpriteText notRankedPlaceholder;
RelativeSizeAxes = Axes.X;
@@ -59,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapSet
Child = new Container
{
RelativeSizeAxes = Axes.Both,
- Child = new MetadataSection(MetadataType.Description),
+ Child = new MetadataSectionDescription(),
},
},
new Container
@@ -76,12 +82,13 @@ namespace osu.Game.Overlays.BeatmapSet
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
- Children = new[]
+ Children = new Drawable[]
{
- source = new MetadataSection(MetadataType.Source),
- genre = new MetadataSection(MetadataType.Genre) { Width = 0.5f },
- language = new MetadataSection(MetadataType.Language) { Width = 0.5f },
- tags = new MetadataSection(MetadataType.Tags),
+ nominators = new MetadataSectionNominators(),
+ source = new MetadataSectionSource(),
+ genre = new MetadataSectionGenre { Width = 0.5f },
+ language = new MetadataSectionLanguage { Width = 0.5f },
+ tags = new MetadataSectionTags(),
},
},
},
@@ -118,10 +125,11 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet.ValueChanged += b =>
{
- source.Text = b.NewValue?.Source ?? string.Empty;
- tags.Text = b.NewValue?.Tags ?? string.Empty;
- genre.Text = b.NewValue?.Genre.Name ?? string.Empty;
- language.Text = b.NewValue?.Language.Name ?? string.Empty;
+ nominators.Metadata = (b.NewValue?.CurrentNominations ?? Array.Empty(), b.NewValue?.RelatedUsers ?? Array.Empty());
+ source.Metadata = b.NewValue?.Source ?? string.Empty;
+ tags.Metadata = b.NewValue?.Tags ?? string.Empty;
+ genre.Metadata = b.NewValue?.Genre ?? new BeatmapSetOnlineGenre { Id = (int)SearchGenre.Unspecified };
+ language.Metadata = b.NewValue?.Language ?? new BeatmapSetOnlineLanguage { Id = (int)SearchLanguage.Unspecified };
bool setHasLeaderboard = b.NewValue?.Status > 0;
successRate.Alpha = setHasLeaderboard ? 1 : 0;
notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1;
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSection.cs b/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
index 6390c52ff3..d32d8e83fb 100644
--- a/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
@@ -11,26 +9,45 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Online.Chat;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
- public partial class MetadataSection : Container
+ public abstract partial class MetadataSection : MetadataSection
+ {
+ public override string Metadata
+ {
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ this.FadeOut(TRANSITION_DURATION);
+ return;
+ }
+
+ base.Metadata = value;
+ }
+ }
+
+ protected MetadataSection(MetadataType type, Action? searchAction = null)
+ : base(type, searchAction)
+ {
+ }
+ }
+
+ public abstract partial class MetadataSection : Container
{
private readonly FillFlowContainer textContainer;
- private readonly MetadataType type;
- private TextFlowContainer textFlow;
+ private TextFlowContainer? textFlow;
- private readonly Action searchAction;
+ protected readonly Action? SearchAction;
- private const float transition_duration = 250;
+ protected const float TRANSITION_DURATION = 250;
- public MetadataSection(MetadataType type, Action searchAction = null)
+ protected MetadataSection(MetadataType type, Action? searchAction = null)
{
- this.type = type;
- this.searchAction = searchAction;
+ SearchAction = searchAction;
Alpha = 0;
@@ -53,7 +70,7 @@ namespace osu.Game.Overlays.BeatmapSet
AutoSizeAxes = Axes.Y,
Child = new OsuSpriteText
{
- Text = this.type.GetLocalisableDescription(),
+ Text = type.GetLocalisableDescription(),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
},
},
@@ -61,23 +78,23 @@ namespace osu.Game.Overlays.BeatmapSet
};
}
- public string Text
+ public virtual T Metadata
{
set
{
- if (string.IsNullOrEmpty(value))
+ if (value == null)
{
- this.FadeOut(transition_duration);
+ this.FadeOut(TRANSITION_DURATION);
return;
}
- this.FadeIn(transition_duration);
+ this.FadeIn(TRANSITION_DURATION);
- setTextAsync(value);
+ setTextFlowAsync(value);
}
}
- private void setTextAsync(string text)
+ private void setTextFlowAsync(T metadata)
{
LoadComponentAsync(new LinkFlowContainer(s => s.Font = s.Font.With(size: 14))
{
@@ -88,44 +105,15 @@ namespace osu.Game.Overlays.BeatmapSet
{
textFlow?.Expire();
- switch (type)
- {
- case MetadataType.Tags:
- string[] tags = text.Split(" ");
-
- for (int i = 0; i <= tags.Length - 1; i++)
- {
- string tag = tags[i];
-
- if (searchAction != null)
- loaded.AddLink(tag, () => searchAction(tag));
- else
- loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
-
- if (i != tags.Length - 1)
- loaded.AddText(" ");
- }
-
- break;
-
- case MetadataType.Source:
- if (searchAction != null)
- loaded.AddLink(text, () => searchAction(text));
- else
- loaded.AddLink(text, LinkAction.SearchBeatmapSet, text);
-
- break;
-
- default:
- loaded.AddText(text);
- break;
- }
+ AddMetadata(metadata, loaded);
textContainer.Add(textFlow = loaded);
// fade in if we haven't yet.
- textContainer.FadeIn(transition_duration);
+ textContainer.FadeIn(TRANSITION_DURATION);
});
}
+
+ protected abstract void AddMetadata(T metadata, LinkFlowContainer loaded);
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionDescription.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionDescription.cs
new file mode 100644
index 0000000000..e6837951c9
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionDescription.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionDescription : MetadataSection
+ {
+ public MetadataSectionDescription(Action? searchAction = null)
+ : base(MetadataType.Description, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
+ {
+ loaded.AddText(metadata);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionGenre.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionGenre.cs
new file mode 100644
index 0000000000..d41115f2b8
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionGenre.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Extensions;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays.BeatmapListing;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionGenre : MetadataSection
+ {
+ public MetadataSectionGenre(Action? searchAction = null)
+ : base(MetadataType.Genre, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(BeatmapSetOnlineGenre metadata, LinkFlowContainer loaded)
+ {
+ var genre = (SearchGenre)metadata.Id;
+
+ if (Enum.IsDefined(genre))
+ loaded.AddLink(genre.GetLocalisableDescription(), LinkAction.FilterBeatmapSetGenre, genre);
+ else
+ loaded.AddText(metadata.Name);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionLanguage.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionLanguage.cs
new file mode 100644
index 0000000000..e831b1eaca
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionLanguage.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Extensions;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays.BeatmapListing;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionLanguage : MetadataSection
+ {
+ public MetadataSectionLanguage(Action? searchAction = null)
+ : base(MetadataType.Language, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(BeatmapSetOnlineLanguage metadata, LinkFlowContainer loaded)
+ {
+ var language = (SearchLanguage)metadata.Id;
+
+ if (Enum.IsDefined(language))
+ loaded.AddLink(language.GetLocalisableDescription(), LinkAction.FilterBeatmapSetLanguage, language);
+ else
+ loaded.AddText(metadata.Name);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionNominators.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionNominators.cs
new file mode 100644
index 0000000000..76dbda3d5e
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionNominators.cs
@@ -0,0 +1,63 @@
+// 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 osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Resources.Localisation.Web;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionNominators : MetadataSection<(BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers)>
+ {
+ public override (BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers) Metadata
+ {
+ set
+ {
+ if (value.CurrentNominations.Length == 0)
+ {
+ this.FadeOut(TRANSITION_DURATION);
+ return;
+ }
+
+ base.Metadata = value;
+ }
+ }
+
+ public MetadataSectionNominators(Action<(BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers)>? searchAction = null)
+ : base(MetadataType.Nominators, searchAction)
+ {
+ }
+
+ protected override void AddMetadata((BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers) metadata, LinkFlowContainer loaded)
+ {
+ int[] nominatorIds = metadata.CurrentNominations.Select(n => n.UserId).ToArray();
+
+ int nominatorsFound = 0;
+
+ foreach (int nominatorId in nominatorIds)
+ {
+ foreach (var user in metadata.RelatedUsers)
+ {
+ if (nominatorId != user.OnlineID) continue;
+
+ nominatorsFound++;
+
+ loaded.AddUserLink(new APIUser
+ {
+ Username = user.Username,
+ Id = nominatorId,
+ });
+
+ if (nominatorsFound < nominatorIds.Length)
+ loaded.AddText(CommonStrings.ArrayAndWordsConnector);
+
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionSource.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionSource.cs
new file mode 100644
index 0000000000..544dc0dfe4
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionSource.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionSource : MetadataSection
+ {
+ public MetadataSectionSource(Action? searchAction = null)
+ : base(MetadataType.Source, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
+ {
+ if (SearchAction != null)
+ loaded.AddLink(metadata, () => SearchAction(metadata));
+ else
+ loaded.AddLink(metadata, LinkAction.SearchBeatmapSet, metadata);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs
new file mode 100644
index 0000000000..fc16ba19d8
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.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.Game.Graphics.Containers;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionTags : MetadataSection
+ {
+ public MetadataSectionTags(Action? searchAction = null)
+ : base(MetadataType.Tags, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
+ {
+ string[] tags = metadata.Split(" ");
+
+ for (int i = 0; i <= tags.Length - 1; i++)
+ {
+ string tag = tags[i];
+
+ if (SearchAction != null)
+ loaded.AddLink(tag, () => SearchAction(tag));
+ else
+ loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
+
+ if (i != tags.Length - 1)
+ loaded.AddText(" ");
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataType.cs b/osu.Game/Overlays/BeatmapSet/MetadataType.cs
index 924e020641..dc96ce99e9 100644
--- a/osu.Game/Overlays/BeatmapSet/MetadataType.cs
+++ b/osu.Game/Overlays/BeatmapSet/MetadataType.cs
@@ -23,6 +23,9 @@ namespace osu.Game.Overlays.BeatmapSet
Genre,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoLanguage))]
- Language
+ Language,
+
+ [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoNominators))]
+ Nominators,
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 006eec2838..425f40258e 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -95,8 +95,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersScore, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)),
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, minSize: 60, maxSize: 70)),
new TableColumn("", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 25)), // flag
- new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersPlayer, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 125)),
- new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120))
+ new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersPlayer, Anchor.CentreLeft, new Dimension(minSize: 125)),
+ new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, Anchor.CentreLeft, new Dimension(minSize: 70, maxSize: 120))
};
// All statistics across all scores, unordered.
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
var displayName = ruleset.GetDisplayNameForHitResult(result);
- columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
+ columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60)));
statisticResultTypes.Add((result, displayName));
}
diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs
index fd831ad4ae..237ce22767 100644
--- a/osu.Game/Overlays/BeatmapSetOverlay.cs
+++ b/osu.Game/Overlays/BeatmapSetOverlay.cs
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet;
@@ -30,6 +31,13 @@ namespace osu.Game.Overlays
private readonly Bindable beatmapSet = new Bindable();
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ private IBindable apiUser;
+
+ private (BeatmapSetLookupType type, int id)? lastLookup;
+
///
/// Isolates the beatmap set overlay from the game-wide selected mods bindable
/// to avoid affecting the beatmap details section (i.e. ).
@@ -72,6 +80,17 @@ namespace osu.Game.Overlays
};
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ apiUser = api.LocalUser.GetBoundCopy();
+ apiUser.BindValueChanged(_ => Schedule(() =>
+ {
+ if (api.IsLoggedIn)
+ performFetch();
+ }));
+ }
+
protected override BeatmapSetHeader CreateHeader() => new BeatmapSetHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
@@ -84,27 +103,20 @@ namespace osu.Game.Overlays
public void FetchAndShowBeatmap(int beatmapId)
{
+ lastLookup = (BeatmapSetLookupType.BeatmapId, beatmapId);
beatmapSet.Value = null;
- var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
- req.Success += res =>
- {
- beatmapSet.Value = res;
- Header.HeaderContent.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineID == beatmapId);
- };
- API.Queue(req);
-
+ performFetch();
Show();
}
public void FetchAndShowBeatmapSet(int beatmapSetId)
{
+ lastLookup = (BeatmapSetLookupType.SetId, beatmapSetId);
+
beatmapSet.Value = null;
- var req = new GetBeatmapSetRequest(beatmapSetId);
- req.Success += res => beatmapSet.Value = res;
- API.Queue(req);
-
+ performFetch();
Show();
}
@@ -118,6 +130,24 @@ namespace osu.Game.Overlays
Show();
}
+ private void performFetch()
+ {
+ if (!api.IsLoggedIn)
+ return;
+
+ if (lastLookup == null)
+ return;
+
+ var req = new GetBeatmapSetRequest(lastLookup.Value.id, lastLookup.Value.type);
+ req.Success += res =>
+ {
+ beatmapSet.Value = res;
+ if (lastLookup.Value.type == BeatmapSetLookupType.BeatmapId)
+ Header.HeaderContent.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineID == lastLookup.Value.id);
+ };
+ API.Queue(req);
+ }
+
private partial class CommentsSection : BeatmapSetLayoutSection
{
public readonly Bindable BeatmapSet = new Bindable();
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index fd7a3f8791..96d5203d14 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -68,11 +68,15 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
+ Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 20 },
- Children = new Drawable[]
+ Child = new FillFlowContainer
{
- new OsuHoverContainer
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Child = new OsuHoverContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs
index d30fd97652..d7c9ff67fe 100644
--- a/osu.Game/Overlays/Changelog/ChangelogListing.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = 20 },
- Text = build.CreatedAt.Date.ToString("dd MMMM yyyy"),
+ Text = build.CreatedAt.Date.ToLocalisableString("dd MMMM yyyy"),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 24),
});
diff --git a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs
index ddee6ff8bb..13a19de22a 100644
--- a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs
@@ -4,10 +4,10 @@
#nullable disable
using System;
-using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -104,27 +104,29 @@ namespace osu.Game.Overlays.Changelog
{
var fill = base.CreateHeader();
- foreach (var existing in fill.Children.OfType())
+ var nestedFill = (FillFlowContainer)fill.Child;
+
+ var buildDisplay = (OsuHoverContainer)nestedFill.Child;
+
+ buildDisplay.Scale = new Vector2(1.25f);
+ buildDisplay.Action = null;
+
+ fill.Add(date = new OsuSpriteText
{
- existing.Scale = new Vector2(1.25f);
- existing.Action = null;
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = Build.CreatedAt.Date.ToLocalisableString("dd MMMM yyyy"),
+ Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14),
+ Margin = new MarginPadding { Top = 5 },
+ Scale = new Vector2(1.25f),
+ });
- existing.Add(date = new OsuSpriteText
- {
- Text = Build.CreatedAt.Date.ToString("dd MMMM yyyy"),
- Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14),
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.TopCentre,
- Margin = new MarginPadding { Top = 5 },
- });
- }
-
- fill.Insert(-1, new NavigationIconButton(Build.Versions?.Previous)
+ nestedFill.Insert(-1, new NavigationIconButton(Build.Versions?.Previous)
{
Icon = FontAwesome.Solid.ChevronLeft,
SelectBuild = b => SelectBuild(b)
});
- fill.Insert(1, new NavigationIconButton(Build.Versions?.Next)
+ nestedFill.Insert(1, new NavigationIconButton(Build.Versions?.Next)
{
Icon = FontAwesome.Solid.ChevronRight,
SelectBuild = b => SelectBuild(b)
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index 90863a90a2..671d649dcf 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Overlays
/// are specified, the header will instantly display them.
public void ShowBuild([NotNull] APIChangelogBuild build)
{
- if (build == null) throw new ArgumentNullException(nameof(build));
+ ArgumentNullException.ThrowIfNull(build);
Current.Value = build;
Show();
@@ -78,8 +78,8 @@ namespace osu.Game.Overlays
public void ShowBuild([NotNull] string updateStream, [NotNull] string version)
{
- if (updateStream == null) throw new ArgumentNullException(nameof(updateStream));
- if (version == null) throw new ArgumentNullException(nameof(version));
+ ArgumentNullException.ThrowIfNull(updateStream);
+ ArgumentNullException.ThrowIfNull(version);
performAfterFetch(() =>
{
diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs
index 2b8718939e..70c3bf181c 100644
--- a/osu.Game/Overlays/Chat/ChatLine.cs
+++ b/osu.Game/Overlays/Chat/ChatLine.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -175,9 +176,7 @@ namespace osu.Game.Overlays.Chat
private void updateTimestamp()
{
- drawableTimestamp.Text = prefer24HourTime.Value
- ? $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"
- : $@"{message.Timestamp.LocalDateTime:hh:mm:ss tt}";
+ drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt");
}
}
}
diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs
index bcf5c1a409..682c96a695 100644
--- a/osu.Game/Overlays/Chat/ChatTextBar.cs
+++ b/osu.Game/Overlays/Chat/ChatTextBar.cs
@@ -128,9 +128,8 @@ namespace osu.Game.Overlays.Chat
chattingTextContainer.FadeTo(showSearch ? 0 : 1);
searchIconContainer.FadeTo(showSearch ? 1 : 0);
- // Clear search terms if any exist when switching back to chat mode
- if (!showSearch)
- OnSearchTermsChanged?.Invoke(string.Empty);
+ if (showSearch)
+ OnSearchTermsChanged?.Invoke(chatTextBox.Current.Value);
}, true);
currentChannel.BindValueChanged(change =>
@@ -151,6 +150,12 @@ namespace osu.Game.Overlays.Chat
chattingText.Text = ChatStrings.TalkingIn(newChannel.Name);
break;
}
+
+ if (change.OldValue != null)
+ chatTextBox.Current.UnbindFrom(change.OldValue.TextBoxMessage);
+
+ if (newChannel != null)
+ chatTextBox.Current.BindTo(newChannel.TextBoxMessage);
}, true);
}
diff --git a/osu.Game/Overlays/Chat/ChatTextBox.cs b/osu.Game/Overlays/Chat/ChatTextBox.cs
index 780c85a9c1..7cd005698e 100644
--- a/osu.Game/Overlays/Chat/ChatTextBox.cs
+++ b/osu.Game/Overlays/Chat/ChatTextBox.cs
@@ -24,7 +24,6 @@ namespace osu.Game.Overlays.Chat
bool showSearch = change.NewValue;
PlaceholderText = showSearch ? HomeStrings.SearchPlaceholder : ChatStrings.InputPlaceholder;
- Text = string.Empty;
}, true);
}
diff --git a/osu.Game/Overlays/Chat/DrawableUsername.cs b/osu.Game/Overlays/Chat/DrawableUsername.cs
index 7026d519a5..8cd16047f3 100644
--- a/osu.Game/Overlays/Chat/DrawableUsername.cs
+++ b/osu.Game/Overlays/Chat/DrawableUsername.cs
@@ -17,9 +17,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
+using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@@ -30,7 +32,7 @@ namespace osu.Game.Overlays.Chat
public Color4 AccentColour { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
- Child.ReceivePositionalInputAt(screenSpacePos);
+ colouredDrawable.ReceivePositionalInputAt(screenSpacePos);
public float FontSize
{
@@ -87,13 +89,13 @@ namespace osu.Game.Overlays.Chat
{
AccentColour = default_colours[user.Id % default_colours.Length];
- Child = colouredDrawable = drawableText;
+ Add(colouredDrawable = drawableText);
}
else
{
AccentColour = Color4Extensions.FromHex(user.Colour);
- Child = new Container
+ Add(new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@@ -127,7 +129,7 @@ namespace osu.Game.Overlays.Chat
}
}
}
- };
+ });
}
}
@@ -148,11 +150,11 @@ namespace osu.Game.Overlays.Chat
List