55 Commits

Author SHA1 Message Date
65f275106e update license information in various places (and readme) 2025-08-31 22:47:36 +03:00
6e374762fd synchronize with github 2025-08-31 21:26:21 +03:00
80646a166c add settings toggle to not delete imported archives 2025-08-31 19:39:13 +03:00
8f0510d903 made beatmap anonymization code a bit less awful 2025-08-31 15:06:24 +03:00
02e7000ee4 update editor beatmap anonymization code to update not just first diff 2025-08-30 22:56:01 +03:00
Dean Herbert
c26e669fc5 Merge pull request #34837 from smoogipoo/more-testable-footers
Add footer to `ScreenTestScene`
2025-08-30 21:52:30 +09:00
Dean Herbert
e621eed0ba Merge pull request #34841 from peppy/interpolate-more
Adjust interpolation workaround to catch-up slightly smoother
2025-08-29 22:04:20 +09:00
Dean Herbert
41b8033ebd Adjust interpolation workaround to catch-up slightly smoother 2025-08-29 21:24:23 +09:00
Dean Herbert
1d9de88aaa Merge pull request #34836 from bdach/online-lookups-on-reenter
Fix song select not performing online lookup on re-enter
2025-08-29 21:21:34 +09:00
Dean Herbert
3ca5e20e70 Merge branch 'master' into online-lookups-on-reenter 2025-08-29 19:03:41 +09:00
Dean Herbert
404044e8d7 Merge pull request #34822 from bdach/carousel-multi-grouping-clean 2025-08-29 19:02:58 +09:00
Dan Balasescu
d5575b4037 Merge pull request #34830 from minetoblend/fix/hitobject-lifetime-delay
Fix hitobject drawables becoming visible 1 frame too late
2025-08-29 18:38:24 +09:00
Dan Balasescu
04ba5aa575 Move footer to ScreenTestScene 2025-08-29 17:49:48 +09:00
Bartłomiej Dach
51ed19cb99 Merge branch 'master' into carousel-multi-grouping-clean 2025-08-29 10:41:46 +02:00
Dean Herbert
0a408a3ac4 Fix tournament test failure due to control change 2025-08-29 17:12:24 +09:00
Dean Herbert
22ba956f25 Merge pull request #34833 from peppy/fix-mod-cosmetics
Fix some mods showing tooltips when settings are default
2025-08-29 17:09:01 +09:00
Bartłomiej Dach
526ee32268 Apply suggested rename 2025-08-29 09:58:56 +02:00
Bartłomiej Dach
df6d6edaca Fix song select not performing online lookup on re-enter
Closes https://github.com/ppy/osu/issues/34825.

Root cause is

	24ec43b3b6/osu.Game/Screens/SelectV2/SongSelect.cs (L345-L356)

not specifying `(..., true)`, therefore the fetch doesn't happen on
enter if song select doesn't change the global beatmap as a side effect
of the enter, which is the case on re-entering.
2025-08-29 09:31:20 +02:00
Bartłomiej Dach
24ec43b3b6 Merge pull request #34834 from peppy/tournament-warmup-better
Use switches for warmup/chat toggles in tournament interface
2025-08-29 09:09:20 +02:00
Dean Herbert
9e77a5b050 Fix obviously incorrect conditional
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-08-29 16:01:49 +09:00
Dean Herbert
12832e9fef Use switches for warmup/chat toggles in tournament interface
As proposed in https://github.com/ppy/osu/discussions/32515.
2025-08-29 14:35:42 +09:00
Dean Herbert
e83f3d5e77 Fix some mods showing tooltips when settings are default 2025-08-29 14:08:18 +09:00
Dean Herbert
f2f5cf19a2 Return early to avoid creating mod description strings unnecessarily 2025-08-29 14:08:06 +09:00
marvin
bb9f9e4d35 Fix operations in PooledDrawableWithLifetimeContainer.CheckChildrenLife being in wrong order
Previously CompositeDrawable.CheckChildrenLife() would be run before lifetimeManager.Update() which lead to the new drawables being inserted into the container but not being made alive immediately, leading to the drawable not becoming visibile until the next update loop.
2025-08-28 23:36:29 +02:00
Bartłomiej Dach
6e1316241a Merge pull request #34774 from genskyff/feat/long-note-ratio
Add long note percentage filter for mania mode
2025-08-28 14:32:52 +02:00
Bartłomiej Dach
3eaa5314ac Merge pull request #34808 from peppy/fix-difficulty-churn
Fix beatmap carousel triggering full filters more often than it needs to
2025-08-28 14:26:25 +02:00
Bartłomiej Dach
6ba72fa481 Adjust tests to new beatmap set model usage in carousel 2025-08-28 13:09:11 +02:00
Bartłomiej Dach
8dd131f17e Support beatmap sets being split apart by the active group mode in beatmap carousel 2025-08-28 13:09:08 +02:00
Dean Herbert
7e109add96 Ensure filtering also runs after local gameplay LastPlayed changes 2025-08-28 19:10:20 +09:00
Dean Herbert
f953d58922 Merge pull request #34740 from bdach/maximised-player-audio
Always use audio from maximised player if there is one in multiplayer spectator
2025-08-28 18:27:56 +09:00
Bartłomiej Dach
47164c61b4 Add failing test coverage of splitting beatmap sets apart 2025-08-28 11:00:23 +02:00
Bartłomiej Dach
311c75aa53 Adjust test after allowing grouping modes to split beatmap sets apart 2025-08-28 11:00:23 +02:00
Dean Herbert
0b40f1d0db Merge pull request #34818 from smoogipoo/completion-handler-override
Preserve pre-post notification completion target
2025-08-28 14:10:59 +09:00
Dan Balasescu
e831d1b6fa Preserve pre-post notification completion target 2025-08-28 13:07:05 +09:00
Dean Herbert
ed15e1fb88 Merge branch 'master' into fix-difficulty-churn 2025-08-28 02:38:10 +09:00
Dean Herbert
33b99a51b1 Merge pull request #34812 from bdach/failed-at-judgement
Fix `HealthProcessor` potentially incorrectly reverting failed state
2025-08-28 02:37:44 +09:00
Dean Herbert
8a6c857719 Fix hidden beatmap state not being reflected immediately 2025-08-28 02:33:18 +09:00
Dean Herbert
5abd93eda7 Merge pull request #34809 from peppy/fix-star-rating-weird-mod-double-handling
Fix beatmap panels locally handling mod and ruleset changes unnecessarily
2025-08-28 01:27:31 +09:00
Bartłomiej Dach
4030383276 Allow grouping modes that apply max aggregate to split beatmap sets apart 2025-08-27 14:45:20 +02:00
Dean Herbert
f9c1b24df4 Apply in more places 2025-08-27 19:59:44 +09:00
Bartłomiej Dach
197c318180 Fix HealthProcessor potentially incorrectly reverting failed state
This stems from me looking into `TestSceneFailAnimation` failures
(https://github.com/ppy/osu/runs/48663953318). As it turns out, I should
not have been mad by CI, and rather should have been mad at myself for
failing to read.

`FailedAtJudgement` in fact does not mean "this judgement, and only this
judgement, triggered failure". If any further judgements occur
post-fail, they will also have `FailedAtJudgement` set to true. It is
essentially a *dump* of the state of `HealthProcessor.Failed` prior to
applying the judgement.

	ec21685c25/osu.Game/Rulesets/Scoring/HealthProcessor.cs (L49-L57)

Because of this, reverting several judgements which occur post-fail
could lead to failed state reverting earlier than intended, and thus
potentially trigger a second fail, thus tripping the `Player` assertion.
2025-08-27 12:27:11 +02:00
Dean Herbert
fda40d7fd5 Fix beatmap panels locally handling mod changes unnecessarily
The `BeatmapDifficultyCache` handles mod changes, so handling locally is
unnecessary. By handling locally, it creates a visual issue when
adjusting mods often. Test using Ctrl +/- at song select and observing
that without this change, the star rating will flicker back to the
default due to the local re-fetch.
2025-08-27 18:31:50 +09:00
Dean Herbert
be6fb9aa77 Fix beatmap carousel re-filtering when it doesn't need to
Local rules ensure we only handle callbacks when we need to.
2025-08-27 18:21:19 +09:00
Dean Herbert
0e57ee9ba6 Avoid triggering changes when add operations are empty
Only seems to happen in tests. I think.
2025-08-27 18:13:13 +09:00
Dean Herbert
043235fed2 Add test coverage ensuring filtering does not occur on unnecessary updates 2025-08-27 18:13:12 +09:00
Dean Herbert
ec21685c25 Merge pull request #34803 from peppy/update-framework-please-no-clock-breakage
Update framework
2025-08-27 13:23:09 +09:00
Dean Herbert
244bad07c7 Update framework 2025-08-26 21:43:09 +09:00
Binwalker
149f18c3f5 test(ManiaFilterCriteriaTest): simplify test case 2025-08-26 21:36:20 +09:00
Binwalker
6a82b7331f refactor(ManiaFilterCriteria): exclude converted beatmaps from long note filter 2025-08-26 21:36:20 +09:00
Binwalker
65253708d8 test(ManiaFilterCriteriaTest): fix some test case for ln filter 2025-08-26 21:36:20 +09:00
Binwalker
556c2469bf fix(ManiaFilterCriteria): converted beatmaps are not included 2025-08-26 21:36:20 +09:00
Binwalker
f7b0e114a9 test(ManiaFilterCriteriaTest): add some testcase 2025-08-26 21:36:20 +09:00
Binwalker
68677200f3 feat(ManiaFilterCriteria): add long note ratio filter for mania 2025-08-26 21:36:19 +09:00
Bartłomiej Dach
083365f332 Always use audio from maximised player if there is one in multiplayer spectator 2025-08-19 12:03:35 +02:00
Bartłomiej Dach
62b4999184 Add failing test case 2025-08-19 12:03:12 +02:00
51 changed files with 1020 additions and 437 deletions

View File

@@ -49,7 +49,7 @@
<PackageProjectUrl>https://github.com/ppy/osu</PackageProjectUrl> <PackageProjectUrl>https://github.com/ppy/osu</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl> <RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes> <PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company> <Company>ppy Pty Ltd, jvnkosu! team</Company>
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright> <Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags> <PackageTags>osu game</PackageTags>
</PropertyGroup> </PropertyGroup>

149
README.md
View File

@@ -1,147 +1,40 @@
<p align="center"> # jvnkosu! client
<img width="500" alt="osu! logo" src="assets/lazer.png">
</p>
# osu! A free-to-win rhythm game based on osu!(lazer). Click is just a *rhythm* away!
[![Build status](https://github.com/ppy/osu/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/ppy/osu/actions/workflows/ci.yml) ## Disclaimer
[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest)
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/osu-web/localized.svg)](https://crowdin.com/project/osu-web)
A free-to-win rhythm game. Rhythm is just a *click* away! *osu!* is a registered trademark of ppy Pty Ltd.
jvnkosu! is not affiliated with, or endorsed by ppy Pty Ltd., but makes use of its open-source components and resources.
This is the future and final iteration of the [osu!](https://osu.ppy.sh) game client which marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge. ## License
Client source code is licensed under the MIT license, see the [LICENCE](LICENCE) file in repository root for more info.
## Status Game assets are included as a NuGet package and licensed under the CC BY-NC 4.0, which prohibits commercial use. See [ppy/osu-resources](https://github.com/ppy/osu-resources) for more info.
This project is under constant development, but we do our best to keep things in a stable state. Players are encouraged to install from a release alongside their stable *osu!* client. This project will continue to evolve until we eventually reach the point where most users prefer it over the previous "osu!stable" release. Registered trademarks "osu!" and "ppy" are property of ppy Pty Ltd., and protected by trademark law.
A few resources are available as starting points to getting involved and understanding the project: ## Compiling from source
Building jvnkosu! from source is pretty much possible (and welcome here).
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer). First, you must have a desktop platform with [.NET Core SDK 8](https://dotnet.microsoft.com/download) installed. Windows, Linux, macOS should work well. You can check if you have the SDK installed by running `dotnet --version` in your command prompt/terminal.
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
- Track our current efforts [towards improving the game](https://github.com/orgs/ppy/projects/7/views/6).
## Running osu! Then, download the source code. You may download it as an archive and unzip it, but using [Git](https://git-scm.com/) instead is recommended:
```
If you are just looking to give the game a whirl, you can grab the latest release for your platform: git clone https://gitea.jvnko.boats/jvnkosu/client
### Latest release:
| [Windows 10+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 12+ ([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) |
|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | ------------- | ------------- |
You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download).
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation.
## Developing a custom ruleset
osu! is designed to allow user-created gameplay variations, called "rulesets". Building one of these allows a developer to harness the power of the osu! beatmap library, game engine, and general UX for a new style of gameplay. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
## Developing osu!
### Prerequisites
Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) plugin installed.
### Downloading the source code
Clone the repository:
```shell
git clone https://github.com/ppy/osu
cd osu
``` ```
To update the source code to the latest commit, run the following command inside the `osu` directory:
```shell To **run** the project, switch to project's directory and run the following:
git pull
``` ```
### Building
#### From an IDE
You should load the solution via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will reduce dependencies and hide platforms that you don't care about. Valid `.slnf` files are:
- `osu.Desktop.slnf` (most common)
- `osu.Android.slnf`
- `osu.iOS.slnf`
Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `osu! (Tests)` project/configuration. More information on this is provided [below](#contributing).
To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install Android/iOS tooling required to complete the build.
#### From CLI
You can also build and run *osu!* from the command-line with a single command:
```shell
dotnet run --project osu.Desktop dotnet run --project osu.Desktop
``` ```
When running locally to do any kind of performance testing, make sure to add `-c Release` to the build command, as the overhead of running with the default `Debug` configuration can be large (especially when testing with local framework modifications as below). To **compile**:
```
If the build fails, try to restore NuGet packages with `dotnet restore`. dotnet build osu.Desktop
### Testing with resource/framework modifications
Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be quickly achieved using included commands:
Windows:
```ps
UseLocalFramework.ps1
UseLocalResources.ps1
``` ```
macOS / Linux: To reduce performance overhead in custom builds, it's recommended to build with the `-c Release` flag, that will use the release profile and remove possibly unneeded debugging code.
```ps ### See the [original readme](README.original.md) for more info.
UseLocalFramework.sh
UseLocalResources.sh
```
Note that these commands assume you have the relevant project(s) checked out in adjacent directories:
```
|- osu // this repository
|- osu-framework
|- osu-resources
```
### Code analysis
Before committing your code, please run a code formatter. This can be achieved by running `dotnet format` in the command line, or using the `Format code` command in your IDE.
We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`. Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## 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. 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).
We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence
*osu!*'s code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
Please note that this *does not cover* the usage of the "osu!" or "ppy" branding in any software, resources, advertising or promotion, as this is protected by trademark law.
Please also note that game resources are covered by a separate licence. Please see the [ppy/osu-resources](https://github.com/ppy/osu-resources) repository for clarifications.

147
README.original.md Normal file
View File

@@ -0,0 +1,147 @@
<p align="center">
<img width="500" alt="osu! logo" src="assets/lazer.png">
</p>
# osu!
[![Build status](https://github.com/ppy/osu/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/ppy/osu/actions/workflows/ci.yml)
[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest)
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/osu-web/localized.svg)](https://crowdin.com/project/osu-web)
A free-to-win rhythm game. Rhythm is just a *click* away!
This is the future and final iteration of the [osu!](https://osu.ppy.sh) game client which marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
## Status
This project is under constant development, but we do our best to keep things in a stable state. Players are encouraged to install from a release alongside their stable *osu!* client. This project will continue to evolve until we eventually reach the point where most users prefer it over the previous "osu!stable" release.
A few resources are available as starting points to getting involved and understanding the project:
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
- Track our current efforts [towards improving the game](https://github.com/orgs/ppy/projects/7/views/6).
## Running osu!
If you are just looking to give the game a whirl, you can grab the latest release for your platform:
### Latest release:
| [Windows 10+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 12+ ([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) |
|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | ------------- | ------------- |
You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download).
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation.
## Developing a custom ruleset
osu! is designed to allow user-created gameplay variations, called "rulesets". Building one of these allows a developer to harness the power of the osu! beatmap library, game engine, and general UX for a new style of gameplay. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
## Developing osu!
### Prerequisites
Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) plugin installed.
### Downloading the source code
Clone the repository:
```shell
git clone https://github.com/ppy/osu
cd osu
```
To update the source code to the latest commit, run the following command inside the `osu` directory:
```shell
git pull
```
### Building
#### From an IDE
You should load the solution via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will reduce dependencies and hide platforms that you don't care about. Valid `.slnf` files are:
- `osu.Desktop.slnf` (most common)
- `osu.Android.slnf`
- `osu.iOS.slnf`
Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `osu! (Tests)` project/configuration. More information on this is provided [below](#contributing).
To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install Android/iOS tooling required to complete the build.
#### From CLI
You can also build and run *osu!* from the command-line with a single command:
```shell
dotnet run --project osu.Desktop
```
When running locally to do any kind of performance testing, make sure to add `-c Release` to the build command, as the overhead of running with the default `Debug` configuration can be large (especially when testing with local framework modifications as below).
If the build fails, try to restore NuGet packages with `dotnet restore`.
### Testing with resource/framework modifications
Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be quickly achieved using included commands:
Windows:
```ps
UseLocalFramework.ps1
UseLocalResources.ps1
```
macOS / Linux:
```ps
UseLocalFramework.sh
UseLocalResources.sh
```
Note that these commands assume you have the relevant project(s) checked out in adjacent directories:
```
|- osu // this repository
|- osu-framework
|- osu-resources
```
### Code analysis
Before committing your code, please run a code formatter. This can be achieved by running `dotnet format` in the command line, or using the `Format code` command in your IDE.
We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`. Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## 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. 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).
We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence
*osu!*'s code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
Please note that this *does not cover* the usage of the "osu!" or "ppy" branding in any software, resources, advertising or promotion, as this is protected by trademark law.
Please also note that game resources are covered by a separate licence. Please see the [ppy/osu-resources](https://github.com/ppy/osu-resources) repository for clarifications.

View File

@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.808.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2025.829.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@@ -3,11 +3,11 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description> <Description>A free-to-win rhythm game based on osu!(lazer). Click is just a *rhythm* away!</Description>
<AssemblyName>osu!</AssemblyName> <AssemblyName>osu!</AssemblyName>
<AssemblyTitle>osu!(lazer)</AssemblyTitle> <AssemblyTitle>jvnkosu!</AssemblyTitle>
<Title>osu!</Title> <Title>jvnkosu!</Title>
<Product>osu!(lazer)</Product> <Product>jvnkosu!</Product>
<ApplicationIcon>lazer.ico</ApplicationIcon> <ApplicationIcon>lazer.ico</ApplicationIcon>
<Version>0.0.0</Version> <Version>0.0.0</Version>
<FileVersion>0.0.0</FileVersion> <FileVersion>0.0.0</FileVersion>

View File

@@ -3,16 +3,19 @@
<metadata> <metadata>
<id>osulazer</id> <id>osulazer</id>
<version>0.0.0</version> <version>0.0.0</version>
<title>osu!</title> <title>jvnkosu!</title>
<authors>ppy Pty Ltd</authors> <authors>ppy Pty Ltd., jvnkosu! team</authors>
<owners>Dean Herbert</owners> <owners>Dean Herbert</owners>
<projectUrl>https://osu.ppy.sh/</projectUrl> <projectUrl>https://osu.jvnko.boats/</projectUrl>
<iconUrl>https://github.com/ppy/osu/blob/master/assets/lazer-nuget.png?raw=true</iconUrl> <iconUrl>https://github.com/ppy/osu/blob/master/assets/lazer-nuget.png?raw=true</iconUrl>
<icon>icon.png</icon> <icon>icon.png</icon>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description> <description>A free-to-win rhythm game based on osu!(lazer). Click is just a *rhythm* away!</description>
<releaseNotes>testing</releaseNotes> <releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright> <copyright>
Copyright (c) 2025 ppy Pty Ltd
Copyright (c) 2025 jvnkosu! team
</copyright>
<language>en-AU</language> <language>en-AU</language>
</metadata> </metadata>
<files> <files>

View File

@@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Mania.Tests
} }
[TestCase] [TestCase]
public void TestFilterIntersection() public void TestKeysFilterIntersection()
{ {
var criteria = new ManiaFilterCriteria(); var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4"); criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
@@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Tests
} }
[TestCase] [TestCase]
public void TestInvalidFilters() public void TestInvalidKeysFilters()
{ {
var criteria = new ManiaFilterCriteria(); var criteria = new ManiaFilterCriteria();
@@ -183,5 +183,132 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text")); Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6")); Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
} }
[TestCase]
public void TestLnsEqual()
{
var criteria = new ManiaFilterCriteria();
var filterCriteria = new FilterCriteria
{
Ruleset = new ManiaRuleset().RulesetInfo
};
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 0,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100");
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 100
};
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1");
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1");
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 1000,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
}
[TestCase]
public void TestLnsGreaterOrEqual()
{
var criteria = new ManiaFilterCriteria();
var filterCriteria = new FilterCriteria
{
Ruleset = new ManiaRuleset().RulesetInfo
};
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 0,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100");
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 100
};
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1");
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1");
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 1000,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
}
[TestCase]
public void TestLnsNotManiaRuleset()
{
var criteria = new ManiaFilterCriteria();
var filterCriteria = new FilterCriteria
{
Ruleset = new ManiaRuleset().RulesetInfo
};
criteria.TryParseCustomKeywordCriteria("lns", Operator.LessOrEqual, "100");
BeatmapInfo beatmapInfo = new BeatmapInfo
{
TotalObjectCount = 100,
EndTimeObjectCount = 50
};
Assert.False(criteria.Matches(beatmapInfo, filterCriteria));
}
[TestCase]
public void TestInvalidLnsFilters()
{
var criteria = new ManiaFilterCriteria();
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text"));
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text"));
}
} }
} }

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@@ -19,12 +20,16 @@ namespace osu.Game.Rulesets.Mania
public class ManiaFilterCriteria : IRulesetFilterCriteria public class ManiaFilterCriteria : IRulesetFilterCriteria
{ {
private readonly HashSet<int> includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet(); private readonly HashSet<int> includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet();
private FilterCriteria.OptionalRange<float> longNotePercentage;
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
{ {
int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods);
return includedKeyCounts.Contains(keyCount); bool keyCountMatch = includedKeyCounts.Contains(keyCount);
bool longNotePercentageMatch = !longNotePercentage.HasFilter || (!isConvertedBeatmap(beatmapInfo) && longNotePercentage.IsInRange(calculateLongNotePercentage(beatmapInfo)));
return keyCountMatch && longNotePercentageMatch;
} }
public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues) public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues)
@@ -84,6 +89,10 @@ namespace osu.Game.Rulesets.Mania
return false; return false;
} }
} }
case "ln":
case "lns":
return FilterQueryParser.TryUpdateCriteriaRange(ref longNotePercentage, op, strValues);
} }
return false; return false;
@@ -103,5 +112,18 @@ namespace osu.Game.Rulesets.Mania
return false; return false;
} }
private static bool isConvertedBeatmap(BeatmapInfo beatmapInfo)
{
return !beatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
}
private static float calculateLongNotePercentage(BeatmapInfo beatmapInfo)
{
int holdNotes = beatmapInfo.EndTimeObjectCount;
int totalNotes = Math.Max(1, beatmapInfo.TotalObjectCount);
return holdNotes / (float)totalNotes * 100;
}
} }
} }

View File

@@ -28,6 +28,7 @@ using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@@ -302,9 +303,69 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 10); AddWaitStep("wait a bit", 10);
} }
[Test]
[Explicit("Test relies on timing of arriving frames to exercise assertions which doesn't work headless.")]
public void TestMaximisedUserIsAudioSource()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();
// With no frames, the synchronisation state will be TooFarAhead.
// In this state, all players should be muted.
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, true);
// Send frames for both players.
sendFrames(PLAYER_1_ID, 20);
sendFrames(PLAYER_2_ID, 40);
waitUntilRunning(PLAYER_1_ID);
AddStep("maximise player 1", () =>
{
InputManager.MoveMouseTo(getInstance(PLAYER_1_ID));
InputManager.Click(MouseButton.Left);
});
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
waitUntilPaused(PLAYER_1_ID);
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
AddStep("minimise player 1", () =>
{
InputManager.MoveMouseTo(getInstance(PLAYER_1_ID));
InputManager.Click(MouseButton.Left);
});
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, false);
AddStep("maximise player 2", () =>
{
InputManager.MoveMouseTo(getInstance(PLAYER_2_ID));
InputManager.Click(MouseButton.Left);
});
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, false);
waitUntilPaused(PLAYER_2_ID);
sendFrames(PLAYER_1_ID, 60);
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, false);
AddStep("minimise player 2", () =>
{
InputManager.MoveMouseTo(getInstance(PLAYER_2_ID));
InputManager.Click(MouseButton.Left);
});
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
}
[Test] [Test]
[FlakyTest] [FlakyTest]
public void TestMostInSyncUserIsAudioSource() public void TestMostInSyncUserIsAudioSourceIfNoneMaximised()
{ {
start(new[] { PLAYER_1_ID, PLAYER_2_ID }); start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen(); loadSpectateScreen();

View File

@@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("beatmap in song select", () => AddUntilStep("beatmap in song select", () =>
{ {
var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen; var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetInfo bsi && bsi.MatchesOnlineID(getImport())); return songSelect.ChildrenOfType<BeatmapCarousel>().Single().GetCarouselItems()!.Any(i => i.Model is GroupedBeatmapSet gbs && gbs.BeatmapSet.MatchesOnlineID(getImport()));
}); });
} }

View File

@@ -133,6 +133,8 @@ namespace osu.Game.Tests.Visual.Settings
{ {
public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>(); public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();
public Bindable<Vector2> AreaSize { get; } = new Bindable<Vector2>(); public Bindable<Vector2> AreaSize { get; } = new Bindable<Vector2>();
public Bindable<Vector2> OutputAreaOffset { get; } = new Bindable<Vector2>();
public Bindable<Vector2> OutputAreaSize { get; } = new Bindable<Vector2>();
public Bindable<float> Rotation { get; } = new Bindable<float>(); public Bindable<float> Rotation { get; } = new Bindable<float>();

View File

@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
]; ];
var results = await runGrouping(GroupMode.None, beatmapSets); var results = await runGrouping(GroupMode.None, beatmapSets);
Assert.That(results.Select(r => r.Model).OfType<BeatmapSetInfo>(), Is.EquivalentTo(beatmapSets)); Assert.That(results.Select(r => r.Model).OfType<GroupedBeatmapSet>().Select(groupedSet => groupedSet.BeatmapSet), Is.EquivalentTo(beatmapSets));
Assert.That(results.Select(r => r.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(allBeatmaps)); Assert.That(results.Select(r => r.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(allBeatmaps));
assertTotal(results, beatmapSets.Count + allBeatmaps.Length); assertTotal(results, beatmapSets.Count + allBeatmaps.Length);
} }
@@ -74,11 +74,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyBeatmap('_'), beatmapSets, out var underscoreBeatmap); addBeatmapSet(applyBeatmap('_'), beatmapSets, out var underscoreBeatmap);
var results = await runGrouping(mode, beatmapSets); var results = await runGrouping(mode, beatmapSets);
assertGroup(results, 0, "0-9", new[] { fiveBeatmap, fourBeatmap }, ref total); assertGroup(results, 0, "0-9", fiveBeatmap.Beatmaps.Concat(fourBeatmap.Beatmaps), ref total);
assertGroup(results, 1, "A", new[] { aBeatmap }, ref total); assertGroup(results, 1, "A", aBeatmap.Beatmaps, ref total);
assertGroup(results, 2, "F", new[] { fBeatmap }, ref total); assertGroup(results, 2, "F", fBeatmap.Beatmaps, ref total);
assertGroup(results, 3, "Z", new[] { zBeatmap }, ref total); assertGroup(results, 3, "Z", zBeatmap.Beatmaps, ref total);
assertGroup(results, 4, "Other", new[] { dashBeatmap, underscoreBeatmap }, ref total); assertGroup(results, 4, "Other", dashBeatmap.Beatmaps.Concat(underscoreBeatmap.Beatmaps), ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -115,12 +115,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddMonths(-2).AddDays(-3), beatmapSets, out var twoMonthsAgoBeatmap); addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddMonths(-2).AddDays(-3), beatmapSets, out var twoMonthsAgoBeatmap);
var results = await runGrouping(GroupMode.DateAdded, beatmapSets); var results = await runGrouping(GroupMode.DateAdded, beatmapSets);
assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total); assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total);
assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total); assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total);
assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total); assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total);
assertGroup(results, 3, "Last month", new[] { lastMonthBeatmap }, ref total); assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total);
assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total); assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total);
assertGroup(results, 5, "2 months ago", new[] { twoMonthsAgoBeatmap }, ref total); assertGroup(results, 5, "2 months ago", twoMonthsAgoBeatmap.Beatmaps, ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -139,13 +139,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyLastPlayed(null), beatmapSets, out var neverBeatmap); addBeatmapSet(applyLastPlayed(null), beatmapSets, out var neverBeatmap);
var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); var results = await runGrouping(GroupMode.LastPlayed, beatmapSets);
assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total); assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total);
assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total); assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total);
assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total); assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total);
assertGroup(results, 3, "Last month", new[] { lastMonthBeatmap }, ref total); assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total);
assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total); assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total);
assertGroup(results, 5, "2 months ago", new[] { twoMonthsBeatmap }, ref total); assertGroup(results, 5, "2 months ago", twoMonthsBeatmap.Beatmaps, ref total);
assertGroup(results, 6, "Never", new[] { neverBeatmap }, ref total); assertGroup(results, 6, "Never", neverBeatmap.Beatmaps, ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -162,7 +162,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); var results = await runGrouping(GroupMode.LastPlayed, beatmapSets);
int total = 0; int total = 0;
assertGroup(results, 0, "Today", new[] { set }, ref total); assertGroup(results, 0, "Today", [set.Beatmaps[2]], ref total);
assertGroup(results, 1, "Never", [set.Beatmaps[0], set.Beatmaps[1]], ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -176,8 +177,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); var results = await runGrouping(GroupMode.LastPlayed, beatmapSets);
int total = 0; int total = 0;
assertGroup(results, 0, "Over 5 months ago", new[] { overFiveMonthsBeatmap }, ref total); assertGroup(results, 0, "Over 5 months ago", overFiveMonthsBeatmap.Beatmaps, ref total);
assertGroup(results, 1, "Never", new[] { neverBeatmap }, ref total); assertGroup(results, 1, "Never", neverBeatmap.Beatmaps, ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -207,14 +208,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(s => s.Status = BeatmapOnlineStatus.LocallyModified, beatmapSets, out var localBeatmap); addBeatmapSet(s => s.Status = BeatmapOnlineStatus.LocallyModified, beatmapSets, out var localBeatmap);
var results = await runGrouping(GroupMode.RankedStatus, beatmapSets); var results = await runGrouping(GroupMode.RankedStatus, beatmapSets);
assertGroup(results, 0, "Ranked", new[] { rankedBeatmap, approvedBeatmap }, ref total); assertGroup(results, 0, "Ranked", rankedBeatmap.Beatmaps.Concat(approvedBeatmap.Beatmaps), ref total);
assertGroup(results, 1, "Qualified", new[] { qualifiedBeatmap }, ref total); assertGroup(results, 1, "Qualified", qualifiedBeatmap.Beatmaps, ref total);
assertGroup(results, 2, "WIP", new[] { wipBeatmap }, ref total); assertGroup(results, 2, "WIP", wipBeatmap.Beatmaps, ref total);
assertGroup(results, 3, "Pending", new[] { pendingBeatmap }, ref total); assertGroup(results, 3, "Pending", pendingBeatmap.Beatmaps, ref total);
assertGroup(results, 4, "Graveyard", new[] { graveyardBeatmap }, ref total); assertGroup(results, 4, "Graveyard", graveyardBeatmap.Beatmaps, ref total);
assertGroup(results, 5, "Local", new[] { localBeatmap }, ref total); assertGroup(results, 5, "Local", localBeatmap.Beatmaps, ref total);
assertGroup(results, 6, "Unknown", new[] { noneBeatmap }, ref total); assertGroup(results, 6, "Unknown", noneBeatmap.Beatmaps, ref total);
assertGroup(results, 7, "Loved", new[] { lovedBeatmap }, ref total); assertGroup(results, 7, "Loved", lovedBeatmap.Beatmaps, ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -240,12 +241,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyBPM(330), beatmapSets, out var beatmap330); addBeatmapSet(applyBPM(330), beatmapSets, out var beatmap330);
var results = await runGrouping(GroupMode.BPM, beatmapSets); var results = await runGrouping(GroupMode.BPM, beatmapSets);
assertGroup(results, 0, "Under 60 BPM", new[] { beatmap30 }, ref total); assertGroup(results, 0, "Under 60 BPM", beatmap30.Beatmaps, ref total);
assertGroup(results, 1, "60 - 70 BPM", new[] { beatmap59, beatmap60 }, ref total); assertGroup(results, 1, "60 - 70 BPM", (beatmap59.Beatmaps.Concat(beatmap60.Beatmaps)), ref total);
assertGroup(results, 2, "90 - 100 BPM", new[] { beatmap90, beatmap95 }, ref total); assertGroup(results, 2, "90 - 100 BPM", (beatmap90.Beatmaps.Concat(beatmap95.Beatmaps)), ref total);
assertGroup(results, 3, "270 - 280 BPM", new[] { beatmap269, beatmap270 }, ref total); assertGroup(results, 3, "270 - 280 BPM", (beatmap269.Beatmaps.Concat(beatmap270.Beatmaps)), ref total);
assertGroup(results, 4, "290 - 300 BPM", new[] { beatmap299 }, ref total); assertGroup(results, 4, "290 - 300 BPM", beatmap299.Beatmaps, ref total);
assertGroup(results, 5, "Over 300 BPM", new[] { beatmap300, beatmap330 }, ref total); assertGroup(results, 5, "Over 300 BPM", (beatmap300.Beatmaps.Concat(beatmap330.Beatmaps)), ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -272,10 +273,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyStars(7), beatmapSets, out var beatmap7); addBeatmapSet(applyStars(7), beatmapSets, out var beatmap7);
var results = await runGrouping(GroupMode.Difficulty, beatmapSets); var results = await runGrouping(GroupMode.Difficulty, beatmapSets);
assertGroup(results, 0, "Below 1 Star", new[] { beatmapBelow1 }, ref total); assertGroup(results, 0, "Below 1 Star", beatmapBelow1.Beatmaps, ref total);
assertGroup(results, 1, "1 Star", new[] { beatmapAbove1, beatmapAlmost2 }, ref total); assertGroup(results, 1, "1 Star", (beatmapAbove1.Beatmaps.Concat(beatmapAlmost2.Beatmaps)), ref total);
assertGroup(results, 2, "2 Stars", new[] { beatmap2, beatmapAbove2 }, ref total); assertGroup(results, 2, "2 Stars", (beatmap2.Beatmaps.Concat(beatmapAbove2.Beatmaps)), ref total);
assertGroup(results, 3, "7 Stars", new[] { beatmap7 }, ref total); assertGroup(results, 3, "7 Stars", beatmap7.Beatmaps, ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -304,11 +305,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyLength(630_000), beatmapSets, out var beatmap10Min30Sec); addBeatmapSet(applyLength(630_000), beatmapSets, out var beatmap10Min30Sec);
var results = await runGrouping(GroupMode.Length, beatmapSets); var results = await runGrouping(GroupMode.Length, beatmapSets);
assertGroup(results, 0, "1 minute or less", new[] { beatmap30Sec, beatmap1Min }, ref total); assertGroup(results, 0, "1 minute or less", (beatmap30Sec.Beatmaps.Concat(beatmap1Min.Beatmaps)), ref total);
assertGroup(results, 1, "2 minutes or less", new[] { beatmap1Min30Sec, beatmap2Min }, ref total); assertGroup(results, 1, "2 minutes or less", (beatmap1Min30Sec.Beatmaps.Concat(beatmap2Min.Beatmaps)), ref total);
assertGroup(results, 2, "5 minutes or less", new[] { beatmap5Min }, ref total); assertGroup(results, 2, "5 minutes or less", beatmap5Min.Beatmaps, ref total);
assertGroup(results, 3, "10 minutes or less", new[] { beatmap6Min, beatmap10Min }, ref total); assertGroup(results, 3, "10 minutes or less", (beatmap6Min.Beatmaps.Concat(beatmap10Min.Beatmaps)), ref total);
assertGroup(results, 4, "Over 10 minutes", new[] { beatmap10Min30Sec }, ref total); assertGroup(results, 4, "Over 10 minutes", beatmap10Min30Sec.Beatmaps, ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -334,10 +335,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(s => s.DateRanked = null, beatmapSets, out var beatmapUnranked); addBeatmapSet(s => s.DateRanked = null, beatmapSets, out var beatmapUnranked);
var results = await runGrouping(GroupMode.DateRanked, beatmapSets); var results = await runGrouping(GroupMode.DateRanked, beatmapSets);
assertGroup(results, 0, "2025", new[] { beatmap2025 }, ref total); assertGroup(results, 0, "2025", beatmap2025.Beatmaps, ref total);
assertGroup(results, 1, "2010", new[] { beatmap2010 }, ref total); assertGroup(results, 1, "2010", beatmap2010.Beatmaps, ref total);
assertGroup(results, 2, "2007", new[] { beatmapOct2007, beatmapDec2007 }, ref total); assertGroup(results, 2, "2007", (beatmapOct2007.Beatmaps.Concat(beatmapDec2007.Beatmaps)), ref total);
assertGroup(results, 3, "Unranked", new[] { beatmapUnranked }, ref total); assertGroup(results, 3, "Unranked", beatmapUnranked.Beatmaps, ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -357,9 +358,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = string.Empty, beatmapSets, out var beatmapUnsourced); addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = string.Empty, beatmapSets, out var beatmapUnsourced);
var results = await runGrouping(GroupMode.Source, beatmapSets); var results = await runGrouping(GroupMode.Source, beatmapSets);
assertGroup(results, 0, "Cool Game", new[] { beatmapCoolGame, beatmapCoolGameB }, ref total); assertGroup(results, 0, "Cool Game", (beatmapCoolGame.Beatmaps.Concat(beatmapCoolGameB.Beatmaps)), ref total);
assertGroup(results, 1, "Nice Movie", new[] { beatmapNiceMovie }, ref total); assertGroup(results, 1, "Nice Movie", beatmapNiceMovie.Beatmaps, ref total);
assertGroup(results, 2, "Unsourced", new[] { beatmapUnsourced }, ref total); assertGroup(results, 2, "Unsourced", beatmapUnsourced.Beatmaps, ref total);
assertTotal(results, total); assertTotal(results, total);
} }
@@ -375,7 +376,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None);
} }
private static void assertGroup(List<CarouselItem> items, int index, string expectedTitle, IEnumerable<BeatmapSetInfo> expectedBeatmapSets, ref int totalItems) private static void assertGroup(List<CarouselItem> items, int index, string expectedTitle, IEnumerable<BeatmapInfo> expectedBeatmaps, ref int totalItems)
{ {
var groupItem = items.Where(i => i.Model is GroupDefinition).ElementAtOrDefault(index); var groupItem = items.Where(i => i.Model is GroupDefinition).ElementAtOrDefault(index);
@@ -390,7 +391,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
var groupModel = (GroupDefinition)groupItem.Model; var groupModel = (GroupDefinition)groupItem.Model;
Assert.That(groupModel.Title, Is.EqualTo(expectedTitle)); Assert.That(groupModel.Title, Is.EqualTo(expectedTitle));
Assert.That(itemsInGroup.Select(i => i.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(expectedBeatmapSets.SelectMany(bs => bs.Beatmaps))); Assert.That(itemsInGroup.Select(i => i.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(expectedBeatmaps));
totalItems += itemsInGroup.Count() + 1; totalItems += itemsInGroup.Count() + 1;
} }

View File

@@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
// Using groupingFilter.SetItems.Count alone doesn't work. // Using groupingFilter.SetItems.Count alone doesn't work.
// When sorting by difficulty, there can be more than one set panel for the same set displayed. // When sorting by difficulty, there can be more than one set panel for the same set displayed.
return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetInfo)); return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is GroupedBeatmapSet));
}, () => Is.EqualTo(expected)); }, () => Is.EqualTo(expected));
} }
@@ -440,7 +440,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo; public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo;
public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet; public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet;
public new BeatmapSetInfo? ExpandedBeatmapSet => base.ExpandedBeatmapSet; public new GroupedBeatmapSet? ExpandedBeatmapSet => base.ExpandedBeatmapSet;
public new GroupDefinition? ExpandedGroup => base.ExpandedGroup; public new GroupDefinition? ExpandedGroup => base.ExpandedGroup;
public TestBeatmapCarousel() public TestBeatmapCarousel()

View File

@@ -22,8 +22,6 @@ using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2; using osu.Game.Screens.SelectV2;
@@ -43,9 +41,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
protected Screens.SelectV2.SongSelect SongSelect { get; private set; } = null!; protected Screens.SelectV2.SongSelect SongSelect { get; private set; } = null!;
protected BeatmapCarousel Carousel => SongSelect.ChildrenOfType<BeatmapCarousel>().Single(); protected BeatmapCarousel Carousel => SongSelect.ChildrenOfType<BeatmapCarousel>().Single();
[Cached]
protected readonly ScreenFooter Footer;
[Cached] [Cached]
private readonly OsuLogo logo; private readonly OsuLogo logo;
@@ -72,10 +67,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{ {
State = { Value = Visibility.Visible }, State = { Value = Visibility.Visible },
}, },
Footer = new ScreenFooter
{
BackButtonPressed = () => Stack.CurrentScreen.Exit(),
},
logo = new OsuLogo logo = new OsuLogo
{ {
Alpha = 0f, Alpha = 0f,
@@ -111,14 +102,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
Add(beatmapStore); Add(beatmapStore);
} }
protected override void LoadComplete()
{
base.LoadComplete();
Stack.ScreenPushed += updateFooter;
Stack.ScreenExited += updateFooter;
}
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps(); base.SetUpSteps();
@@ -207,38 +190,5 @@ namespace osu.Game.Tests.Visual.SongSelectV2
} }
protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen()); protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen());
private void updateFooter(IScreen? _, IScreen? newScreen)
{
if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter)
{
Footer.Show();
if (osuScreen.IsLoaded)
updateFooterButtons();
else
{
// ensure the current buttons are immediately disabled on screen change (so they can't be pressed).
Footer.SetButtons(Array.Empty<ScreenFooterButton>());
osuScreen.OnLoadComplete += _ => updateFooterButtons();
}
void updateFooterButtons()
{
var buttons = osuScreen.CreateFooterButtons();
osuScreen.LoadComponentsAgainstScreenDependencies(buttons);
Footer.SetButtons(buttons);
Footer.Show();
}
}
else
{
Footer.Hide();
Footer.SetButtons(Array.Empty<ScreenFooterButton>());
}
}
} }
} }

View File

@@ -396,7 +396,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3);
SelectNextPanel(); SelectNextPanel();
AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); AddAssert("keyboard selected is first set",
() => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet,
() => Is.EqualTo(BeatmapSets.First()));
} }
[Test] [Test]
@@ -413,7 +415,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3);
SelectPrevPanel(); SelectPrevPanel();
AddAssert("keyboard selected is last set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last())); AddAssert("keyboard selected is last set",
() => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet,
() => Is.EqualTo(BeatmapSets.Last()));
} }
[Test] [Test]
@@ -428,7 +432,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
ApplyToFilterAndWaitForFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); ApplyToFilterAndWaitForFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title);
SelectPrevPanel(); SelectPrevPanel();
AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); AddAssert("keyboard selected is first set",
() => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet,
() => Is.EqualTo(BeatmapSets.First()));
} }
[Test] [Test]
@@ -444,7 +450,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
// Single result is automatically selected for us, so we iterate once backwards to the set header. // Single result is automatically selected for us, so we iterate once backwards to the set header.
SelectPrevPanel(); SelectPrevPanel();
AddAssert("keyboard selected is second set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last())); AddAssert("keyboard selected is second set",
() => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet,
() => Is.EqualTo(BeatmapSets.Last()));
} }
} }
} }

View File

@@ -0,0 +1,120 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselSetsSplitApart : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortAndGroupBy(SortMode.Title, GroupMode.Length);
}
[Test]
public void TestSetTraversal()
{
AddBeatmaps(3, splitApart: true);
AddBeatmaps(3, splitApart: false);
WaitForDrawablePanels();
SelectNextSet();
WaitForSetSelection(set: 0, diff: 0);
SelectNextSet();
WaitForSetSelection(set: 1, diff: 0);
SelectPrevSet();
WaitForSetSelection(set: 0, diff: 0);
SelectPrevSet();
WaitForSetSelection(set: 5, diff: 0);
SelectPrevSet();
SelectPrevSet();
SelectPrevSet();
WaitForSetSelection(set: 2, diff: 4);
AddAssert("only two beatmap panels visible", () => GetVisiblePanels<PanelBeatmap>().Count(), () => Is.EqualTo(2));
}
[Test]
public void TestBeatmapTraversal()
{
AddBeatmaps(3, splitApart: true);
AddBeatmaps(3, splitApart: false);
WaitForDrawablePanels();
SelectNextSet();
WaitForSetSelection(set: 0, diff: 0);
SelectNextPanel();
WaitForSetSelection(set: 0, diff: 1);
SelectNextPanel(); // header of set 1 in group 0
Select();
WaitForSetSelection(set: 1, diff: 0);
SelectPrevPanel(); // header of set 1 in group 0
SelectPrevPanel(); // header of set 0 in group 0
Select();
WaitForSetSelection(set: 0, diff: 0);
SelectPrevPanel(); // header of set 0 in group 0
SelectPrevPanel(); // header of group 0
SelectPrevPanel(); // header of group 2
Select();
SelectNextPanel(); // header of set 0 in group 2
Select();
WaitForSetSelection(set: 0, diff: 4);
}
[Test]
public void TestRandomStaysInGroup()
{
AddBeatmaps(2, splitApart: false);
AddBeatmaps(1, splitApart: true);
WaitForDrawablePanels();
SelectPrevSet();
SelectPrevSet();
WaitForSetSelection(set: 1);
WaitForExpandedGroup(2);
AddStep("select next random", () => Carousel.NextRandom());
WaitForExpandedGroup(2);
AddStep("select next random", () => Carousel.NextRandom());
WaitForExpandedGroup(2);
}
protected void AddBeatmaps(int count, bool splitApart) => AddStep($"add {count} beatmaps ({(splitApart ? "" : "not ")}split apart)", () =>
{
var beatmapSets = new List<BeatmapSetInfo>();
for (int i = 0; i < count; i++)
{
var beatmapSet = CreateTestBeatmapSetInfo(6, false);
for (int j = 0; j < beatmapSet.Beatmaps.Count; j++)
{
beatmapSet.Beatmaps[j].Length = splitApart ? 30_000 * (j + 1) : 180_000;
}
beatmapSets.Add(beatmapSet);
}
BeatmapSets.AddRange(beatmapSets);
});
}
}

View File

@@ -22,12 +22,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{ {
private BeatmapSetInfo baseTestBeatmap = null!; private BeatmapSetInfo baseTestBeatmap = null!;
private const int initial_filter_count = 3;
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
RemoveAllBeatmaps(); RemoveAllBeatmaps();
CreateCarousel(); CreateCarousel();
WaitForFiltering();
AddBeatmaps(1, 3); AddBeatmaps(1, 3);
WaitForFiltering();
AddStep("generate and add test beatmap", () => AddStep("generate and add test beatmap", () =>
{ {
baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3); baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3);
@@ -42,8 +46,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
b.Metadata = metadata; b.Metadata = metadata;
BeatmapSets.Add(baseTestBeatmap); BeatmapSets.Add(baseTestBeatmap);
}); });
WaitForFiltering(); WaitForFiltering();
AddAssert("filter count correct", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count));
} }
[Test] [Test]
@@ -81,12 +86,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddUntilStep("is scrolled to end", () => Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single().IsScrolledToEnd()); AddUntilStep("is scrolled to end", () => Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single().IsScrolledToEnd());
updateBeatmap(b => b.Metadata = new BeatmapMetadata updateBeatmap(b =>
{ {
Artist = "updated test", // hash will be updated when important metadata changes, such as title, difficulty, author etc.
Title = $"beatmap {RNG.Next().ToString()}" b.Hash = "new hash";
b.Metadata = new BeatmapMetadata
{
Artist = "updated test",
Title = $"beatmap {RNG.Next().ToString()}"
};
}); });
assertDidFilter();
WaitForFiltering(); WaitForFiltering();
AddAssert("scroll is still at end", () => Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single().IsScrolledToEnd()); AddAssert("scroll is still at end", () => Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single().IsScrolledToEnd());
@@ -113,8 +124,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddStep("find panel", () => panel = Carousel.ChildrenOfType<PanelBeatmapSet>().Single(p => p.ChildrenOfType<OsuSpriteText>().Any(t => t.Text.ToString() == "beatmap"))); AddStep("find panel", () => panel = Carousel.ChildrenOfType<PanelBeatmapSet>().Single(p => p.ChildrenOfType<OsuSpriteText>().Any(t => t.Text.ToString() == "beatmap")));
updateBeatmap(b => b.Metadata = metadata); updateBeatmap(b =>
{
b.Metadata = metadata;
// hash will be updated when important metadata changes, such as title, difficulty, author etc.
b.Hash = "new hash";
});
assertDidFilter();
WaitForFiltering(); WaitForFiltering();
AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables)); AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables));
@@ -123,7 +140,41 @@ namespace osu.Game.Tests.Visual.SongSelectV2
} }
[Test] [Test]
public void TestSelectionHeld() public void TestOnlineStatusUpdated()
{
List<Panel> originalDrawables = new List<Panel>();
AddStep("store drawable references", () =>
{
originalDrawables.Clear();
originalDrawables.AddRange(Carousel.ChildrenOfType<Panel>().ToList());
});
updateBeatmap(b => b.Status = BeatmapOnlineStatus.Graveyard);
assertDidFilter();
WaitForFiltering();
AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables));
}
[Test]
public void TestNoUpdateTriggeredOnUserTagsChange()
{
var metadata = new BeatmapMetadata
{
Artist = "updated test",
Title = "new beatmap title",
UserTags = { "hi" }
};
updateBeatmap(b => b.Metadata = metadata);
assertDidNotFilter();
}
[TestCase(false)]
[TestCase(true)]
public void TestSelectionHeld(bool hashChanged)
{ {
SelectNextSet(); SelectNextSet();
@@ -131,7 +182,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
updateBeatmap(); updateBeatmap(b =>
{
if (hashChanged)
b.Hash = "new hash";
});
if (hashChanged)
assertDidFilter();
else
assertDidNotFilter();
WaitForFiltering(); WaitForFiltering();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
@@ -148,6 +209,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
updateBeatmap(b => b.DifficultyName = "new name"); updateBeatmap(b => b.DifficultyName = "new name");
assertDidFilter();
WaitForFiltering(); WaitForFiltering();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
@@ -164,6 +226,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
updateBeatmap(b => b.OnlineID = b.OnlineID + 1); updateBeatmap(b => b.OnlineID = b.OnlineID + 1);
assertDidFilter();
WaitForFiltering(); WaitForFiltering();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
@@ -339,6 +402,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder)); AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder));
} }
private void assertDidFilter() => AddAssert("did filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count + 1));
private void assertDidNotFilter() => AddAssert("did not filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count));
private void updateBeatmap(Action<BeatmapInfo>? updateBeatmap = null, Action<BeatmapSetInfo>? updateSet = null) private void updateBeatmap(Action<BeatmapInfo>? updateBeatmap = null, Action<BeatmapSetInfo>? updateSet = null)
{ {
AddStep("update beatmap with different reference", () => AddStep("update beatmap with different reference", () =>

View File

@@ -75,21 +75,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{ {
new PanelBeatmapSet new PanelBeatmapSet
{ {
Item = new CarouselItem(beatmapSet) Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet))
}, },
new PanelBeatmapSet new PanelBeatmapSet
{ {
Item = new CarouselItem(beatmapSet), Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)),
KeyboardSelected = { Value = true } KeyboardSelected = { Value = true }
}, },
new PanelBeatmapSet new PanelBeatmapSet
{ {
Item = new CarouselItem(beatmapSet), Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)),
Expanded = { Value = true } Expanded = { Value = true }
}, },
new PanelBeatmapSet new PanelBeatmapSet
{ {
Item = new CarouselItem(beatmapSet), Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)),
KeyboardSelected = { Value = true }, KeyboardSelected = { Value = true },
Expanded = { Value = true } Expanded = { Value = true }
}, },

View File

@@ -23,7 +23,9 @@ using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.SelectV2; using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osuTK.Input; using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
using FooterButtonMods = osu.Game.Screens.SelectV2.FooterButtonMods; using FooterButtonMods = osu.Game.Screens.SelectV2.FooterButtonMods;
using FooterButtonOptions = osu.Game.Screens.SelectV2.FooterButtonOptions; using FooterButtonOptions = osu.Game.Screens.SelectV2.FooterButtonOptions;
using FooterButtonRandom = osu.Game.Screens.SelectV2.FooterButtonRandom; using FooterButtonRandom = osu.Game.Screens.SelectV2.FooterButtonRandom;
@@ -302,6 +304,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}); });
} }
/// <summary>
/// Last played and rank achieved may have changed, so we want to make sure filtering runs on resume to song select.
/// </summary>
[Test]
public void TestFilteringRunsAfterReturningFromGameplay()
{
AddStep("import actual beatmap", () => Beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()));
LoadSongSelect();
AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType<BeatmapCarousel>().Single().FilterCount, () => Is.EqualTo(1));
AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
AddUntilStep("wait for fail", () => ((Player)Stack.CurrentScreen).GameplayState.HasFailed);
AddStep("exit gameplay", () => InputManager.Key(Key.Escape));
AddStep("exit gameplay", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType<BeatmapCarousel>().Single().FilterCount, () => Is.EqualTo(2));
}
[Test] [Test]
public void TestAutoplayShortcut() public void TestAutoplayShortcut()
{ {

View File

@@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("no-collection group present", () => AddAssert("no-collection group present", () =>
{ {
var group = grouping.GroupItems.Single(g => g.Key.Title == "Not in collection"); var group = grouping.GroupItems.Single(g => g.Key.Title == "Not in collection");
return group.Value.Select(i => i.Model).OfType<BeatmapSetInfo>().Single().Equals(beatmapSet); return group.Value.Select(i => i.Model).OfType<GroupedBeatmapSet>().Single().BeatmapSet.Equals(beatmapSet);
}); });
AddStep("add beatmap to collection", () => AddStep("add beatmap to collection", () =>

View File

@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Screens.Gameplay; using osu.Game.Tournament.Screens.Gameplay;
@@ -66,6 +67,6 @@ namespace osu.Game.Tournament.Tests.Screens
() => this.ChildrenOfType<TeamScore>().All(score => score.Alpha == (visible ? 1 : 0))); () => this.ChildrenOfType<TeamScore>().All(score => score.Alpha == (visible ? 1 : 0)));
private void toggleWarmup() private void toggleWarmup()
=> AddStep("toggle warmup", () => this.ChildrenOfType<TourneyButton>().First().TriggerClick()); => AddStep("toggle warmup", () => this.ChildrenOfType<LabelledSwitchButton>().First().ChildrenOfType<SwitchButton>().First().TriggerClick());
} }
} }

View File

@@ -7,7 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
@@ -24,7 +24,6 @@ namespace osu.Game.Tournament.Screens.Gameplay
private readonly BindableBool warmup = new BindableBool(); private readonly BindableBool warmup = new BindableBool();
public readonly Bindable<TourneyState> State = new Bindable<TourneyState>(); public readonly Bindable<TourneyState> State = new Bindable<TourneyState>();
private OsuButton warmupButton = null!;
private MatchIPCInfo ipc = null!; private MatchIPCInfo ipc = null!;
[Resolved] [Resolved]
@@ -40,6 +39,8 @@ namespace osu.Game.Tournament.Screens.Gameplay
{ {
this.ipc = ipc; this.ipc = ipc;
LabelledSwitchButton chatToggle;
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
new TourneyVideo("gameplay") new TourneyVideo("gameplay")
@@ -95,17 +96,14 @@ namespace osu.Game.Tournament.Screens.Gameplay
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
warmupButton = new TourneyButton new LabelledSwitchButton
{ {
RelativeSizeAxes = Axes.X, Label = "Warmup",
Text = "Toggle warmup", Current = warmup,
Action = () => warmup.Toggle()
}, },
new TourneyButton chatToggle = new LabelledSwitchButton
{ {
RelativeSizeAxes = Axes.X, Label = "Show chat",
Text = "Toggle chat",
Action = () => { State.Value = State.Value == TourneyState.Idle ? TourneyState.Playing : TourneyState.Idle; }
}, },
new SettingsSlider<int> new SettingsSlider<int>
{ {
@@ -123,13 +121,12 @@ namespace osu.Game.Tournament.Screens.Gameplay
} }
}); });
State.BindValueChanged(state => chatToggle.Current.Value = State.Value == TourneyState.Idle, true);
chatToggle.Current.BindValueChanged(v => State.Value = v.NewValue ? TourneyState.Idle : TourneyState.Playing);
LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true);
warmup.BindValueChanged(w => warmup.BindValueChanged(w => header.ShowScores = !w.NewValue, true);
{
warmupButton.Alpha = !w.NewValue ? 0.5f : 1;
header.ShowScores = !w.NewValue;
}, true);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@@ -13,6 +13,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Configuration;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
@@ -36,8 +37,8 @@ namespace osu.Game.Beatmaps
public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; } public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public BeatmapImporter(Storage storage, RealmAccess realm) public BeatmapImporter(Storage storage, RealmAccess realm, OsuConfigManager? config)
: base(storage, realm) : base(storage, realm, config)
{ {
} }

View File

@@ -17,6 +17,7 @@ using osu.Framework.IO.Stores;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
@@ -59,7 +60,7 @@ namespace osu.Game.Beatmaps
} }
public BeatmapManager(Storage storage, RealmAccess realm, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null, public BeatmapManager(Storage storage, RealmAccess realm, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null,
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false) WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false, OsuConfigManager? config = null)
: base(storage, realm) : base(storage, realm)
{ {
if (performOnlineLookups) if (performOnlineLookups)
@@ -75,7 +76,7 @@ namespace osu.Game.Beatmaps
BeatmapTrackStore = audioManager.GetTrackStore(userResources); BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapImporter = CreateBeatmapImporter(storage, realm); beatmapImporter = CreateBeatmapImporter(storage, realm, config);
beatmapImporter.ProcessBeatmap = (beatmapSet, scope) => ProcessBeatmap?.Invoke(beatmapSet, scope); beatmapImporter.ProcessBeatmap = (beatmapSet, scope) => ProcessBeatmap?.Invoke(beatmapSet, scope);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj); beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
@@ -98,7 +99,7 @@ namespace osu.Game.Beatmaps
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host); return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
} }
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm); protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, OsuConfigManager? config = null) => new BeatmapImporter(storage, realm, config);
/// <summary> /// <summary>
/// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model, /// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model,

View File

@@ -59,7 +59,10 @@ namespace osu.Game.Beatmaps
// An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting // An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
interpolatedTrack = new InterpolatingFramedClock(decoupledTrack); interpolatedTrack = new InterpolatingFramedClock(decoupledTrack)
{
DriftRecoveryHalfLife = 80,
};
if (applyOffsets) if (applyOffsets)
{ {

View File

@@ -202,6 +202,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full); SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
SetDefault(OsuSetting.DeleteImportedArchives, true);
SetDefault(OsuSetting.EditorDim, 0.25f, 0f, 0.75f, 0.25f); SetDefault(OsuSetting.EditorDim, 0.25f, 0f, 0.75f, 0.25f);
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f); SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f);
SetDefault(OsuSetting.EditorShowHitMarkers, true); SetDefault(OsuSetting.EditorShowHitMarkers, true);
@@ -449,6 +451,7 @@ namespace osu.Game.Configuration
EditorShowHitMarkers, EditorShowHitMarkers,
EditorAutoSeekOnPlacement, EditorAutoSeekOnPlacement,
DiscordRichPresence, DiscordRichPresence,
DeleteImportedArchives,
ShowOnlineExplicitContent, ShowOnlineExplicitContent,
LastProcessedMetadataId, LastProcessedMetadataId,

View File

@@ -8,11 +8,14 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Humanizer; using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Models; using osu.Game.Models;
@@ -77,11 +80,19 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public Action<Notification>? PostNotification { get; set; } public Action<Notification>? PostNotification { get; set; }
protected RealmArchiveModelImporter(Storage storage, RealmAccess realm) private readonly OsuConfigManager? config;
private Bindable<bool>? deleteImportedArchives;
protected RealmArchiveModelImporter(Storage storage, RealmAccess realm, OsuConfigManager? config = null)
{ {
Realm = realm; Realm = realm;
Files = new RealmFileStore(realm, storage); Files = new RealmFileStore(realm, storage);
deleteImportedArchives = config?.GetBindable<bool>(OsuSetting.DeleteImportedArchives);
this.config = config;
} }
public Task Import(params string[] paths) => Import(paths.Select(p => new ImportTask(p)).ToArray()); public Task Import(params string[] paths) => Import(paths.Select(p => new ImportTask(p)).ToArray());
@@ -241,9 +252,10 @@ namespace osu.Game.Database
// e.g. reconstructing/repairing database with items from default storage. // e.g. reconstructing/repairing database with items from default storage.
// Also, not always a single file, i.e. for LegacyFilesystemReader // Also, not always a single file, i.e. for LegacyFilesystemReader
// TODO: Add a check to prevent files from storage to be deleted. // TODO: Add a check to prevent files from storage to be deleted.
bool allowDelete = deleteImportedArchives?.Value ?? true;
try try
{ {
if (import != null && ShouldDeleteArchive(task.Path)) if (import != null && ShouldDeleteArchive(task.Path) && allowDelete)
task.DeleteFile(); task.DeleteFile();
} }
catch (Exception e) catch (Exception e)

View File

@@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -79,7 +80,7 @@ namespace osu.Game.Graphics.Carousel
/// <summary> /// <summary>
/// The number of times filter operations have been triggered. /// The number of times filter operations have been triggered.
/// </summary> /// </summary>
internal int FilterCount { get; private set; } public int FilterCount { get; private set; }
/// <summary> /// <summary>
/// The number of displayable items currently being tracked (before filtering). /// The number of displayable items currently being tracked (before filtering).
@@ -210,6 +211,12 @@ namespace osu.Game.Graphics.Carousel
return filterTask; return filterTask;
} }
/// <summary>
/// Called when <see cref="Items"/> changes in any way.
/// </summary>
/// <returns>Whether a re-filter is required.</returns>
protected virtual bool HandleItemsChanged(NotifyCollectionChangedEventArgs args) => true;
/// <summary> /// <summary>
/// Fired after a filter operation completed. /// Fired after a filter operation completed.
/// </summary> /// </summary>
@@ -301,7 +308,11 @@ namespace osu.Game.Graphics.Carousel
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}; };
Items.BindCollectionChanged((_, _) => filterAfterItemsChanged.Invalidate()); Items.BindCollectionChanged((_, args) =>
{
if (HandleItemsChanged(args))
filterAfterItemsChanged.Invalidate();
});
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@@ -298,7 +298,7 @@ namespace osu.Game
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler)); dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler, LocalConfig));
dependencies.CacheAs<ISkinSource>(SkinManager); dependencies.CacheAs<ISkinSource>(SkinManager);
EndpointConfiguration endpoints = CreateEndpoints(); EndpointConfiguration endpoints = CreateEndpoints();
@@ -322,7 +322,7 @@ namespace osu.Game
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig)); dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true, config: LocalConfig));
dependencies.CacheAs<IWorkingBeatmapCache>(BeatmapManager); dependencies.CacheAs<IWorkingBeatmapCache>(BeatmapManager);
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API)); dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));

View File

@@ -180,7 +180,7 @@ namespace osu.Game.Overlays
notification.Closed += () => notificationClosed(notification); notification.Closed += () => notificationClosed(notification);
if (notification is IHasCompletionTarget hasCompletionTarget) if (notification is IHasCompletionTarget hasCompletionTarget)
hasCompletionTarget.CompletionTarget = Post; hasCompletionTarget.CompletionTarget ??= Post;
playDebouncedSample(notification.PopInSampleName); playDebouncedSample(notification.PopInSampleName);

View File

@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Configuration;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Import; using osu.Game.Screens.Import;
@@ -22,13 +23,19 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private ISystemFileSelector? selector; private ISystemFileSelector? selector;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuGameBase game, GameHost host, IPerformFromScreenRunner? performer) private void load(OsuGameBase game, GameHost host, IPerformFromScreenRunner? performer, OsuConfigManager config)
{ {
if ((selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray())) != null) if ((selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray())) != null)
selector.Selected += f => Task.Run(() => game.Import(f.FullName)); selector.Selected += f => Task.Run(() => game.Import(f.FullName));
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
new SettingsCheckbox
{
LabelText = "Delete archives on import",
Current = config.GetBindable<bool>(OsuSetting.DeleteImportedArchives),
ClassicDefault = true
},
new SettingsButton new SettingsButton
{ {
Text = DebugSettingsStrings.ImportFiles, Text = DebugSettingsStrings.ImportFiles,
@@ -44,7 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
Text = DebugSettingsStrings.RunLatencyCertifier, Text = DebugSettingsStrings.RunLatencyCertifier,
Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen())) Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen()))
} },
}); });
} }
} }

View File

@@ -56,6 +56,9 @@ namespace osu.Game.Rulesets.Mods
{ {
var bindable = (IBindable)property.GetValue(this)!; var bindable = (IBindable)property.GetValue(this)!;
if (bindable.IsDefault)
continue;
string valueText; string valueText;
switch (bindable) switch (bindable)
@@ -69,8 +72,7 @@ namespace osu.Game.Rulesets.Mods
break; break;
} }
if (!bindable.IsDefault) yield return (attr.Label, valueText);
yield return (attr.Label, valueText);
} }
} }
} }

View File

@@ -46,8 +46,10 @@ namespace osu.Game.Rulesets.Mods
{ {
get get
{ {
yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm"); if (!SpinSpeed.IsDefault)
yield return ("Direction", Direction.Value.GetDescription()); yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm");
if (!Direction.IsDefault)
yield return ("Direction", Direction.Value.GetDescription());
} }
} }
@@ -55,7 +57,8 @@ namespace osu.Game.Rulesets.Mods
public virtual void Update(Playfield playfield) public virtual void Update(Playfield playfield)
{ {
playfieldAdjustmentContainer.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); playfieldAdjustmentContainer.Rotation =
CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
} }
public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset)

View File

@@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Mods
{ {
get get
{ {
yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"); if (!InitialRate.IsDefault || !FinalRate.IsDefault)
yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x");
if (!AdjustPitch.IsDefault) if (!AdjustPitch.IsDefault)
yield return ("Adjust pitch", AdjustPitch.Value ? "On" : "Off"); yield return ("Adjust pitch", AdjustPitch.Value ? "On" : "Off");

View File

@@ -160,8 +160,8 @@ namespace osu.Game.Rulesets.Objects.Pooling
if (!IsPresent) if (!IsPresent)
return false; return false;
bool aliveChanged = base.CheckChildrenLife(); bool aliveChanged = lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension);
aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); aliveChanged |= base.CheckChildrenLife();
return aliveChanged; return aliveChanged;
} }
} }

View File

@@ -59,11 +59,7 @@ namespace osu.Game.Rulesets.Scoring
protected override void RevertResultInternal(JudgementResult result) protected override void RevertResultInternal(JudgementResult result)
{ {
// TODO: this is rudimentary as to make rewinding failed replays work, HasFailed = result.FailedAtJudgement;
// but it also acts up (sometimes rewinding a replay several times around the fail boundary moves the point of fail forward).
// needs further investigation.
if (result.FailedAtJudgement)
HasFailed = false;
if (HasFailed) if (HasFailed)
return; return;

View File

@@ -1402,9 +1402,36 @@ namespace osu.Game.Screens.Edit
private void anonymizeBeatmap() private void anonymizeBeatmap()
{ {
dialogOverlay.Push(new ConfirmDialog( dialogOverlay.Push(new ConfirmDialog(
"Really remove online IDs?", "Really remove online IDs?", () => attemptAsyncMutationOperation(anonymizeMaps))
() => playableBeatmap.BeatmapInfo.ResetOnlineInfo(true) );
));
Task anonymizeMaps()
{
var maps = editorBeatmap.BeatmapInfo.BeatmapSet.Beatmaps;
foreach (BeatmapInfo map in maps)
{
try
{
map.OnlineID = -1;
map.BeatmapSet.OnlineID = -1;
map.ResetOnlineInfo(true);
beatmapManager.Save(
map,
beatmapManager.GetWorkingBeatmap(map, true).Beatmap,
editorBeatmap.BeatmapSkin
);
}
catch (Exception ex)
{
Logger.Error(ex, ex.Message);
notifications?.Post(new SimpleErrorNotification { Text = "Failed to update beatmap difficulty!\nCheck logs for details" });
throw; // we don't want to handle it further, task will do it for us
}
}
updateLastSavedHash();
onScreenDisplay?.Display(new BeatmapEditorToast("Online IDs removed", editorBeatmap.BeatmapInfo.GetDisplayTitle()));
return Task.CompletedTask;
}
} }
private void exportBeatmap(bool legacy) private void exportBeatmap(bool legacy)

View File

@@ -178,17 +178,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{ {
base.Update(); base.Update();
if (!isCandidateAudioSource(currentAudioSource?.SpectatorPlayerClock)) checkAudioSource();
{ }
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.SpectatorPlayerClock)).MinBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime));
// Only bind adjustments if there's actually a valid source, else just use the previous ones to ensure no sudden changes to audio. private void checkAudioSource()
if (currentAudioSource != null) {
bindAudioAdjustments(currentAudioSource); // always use the maximised player instance as the current audio source if there is one
if (grid.MaximisedCell?.Content is PlayerArea maximisedPlayer && maximisedPlayer == currentAudioSource)
return;
foreach (var instance in instances) // if there is no maximised player instance and the previous audio source is still good to use, keep using it
instance.Mute = instance != currentAudioSource; if (grid.MaximisedCell == null && isCandidateAudioSource(currentAudioSource?.SpectatorPlayerClock))
} return;
// at this point we're in one of the following scenarios:
// - the maximised player instance is not the current audio source => we want to switch to the maximised player instance
// - there is no maximised player instance, and the previous audio source is stopped => find another running audio source
currentAudioSource = grid.MaximisedCell?.Content as PlayerArea
?? instances.Where(i => isCandidateAudioSource(i.SpectatorPlayerClock)).MinBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime));
// Only bind adjustments if there's actually a valid source, else just use the previous ones to ensure no sudden changes to audio.
if (currentAudioSource != null)
bindAudioAdjustments(currentAudioSource);
foreach (var instance in instances)
instance.Mute = instance != currentAudioSource;
} }
private void bindAudioAdjustments(PlayerArea first) private void bindAudioAdjustments(PlayerArea first)

View File

@@ -31,6 +31,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary> /// </summary>
public Facade MaximisedFacade { get; } public Facade MaximisedFacade { get; }
/// <summary>
/// The currently-maximised cell.
/// </summary>
public Cell? MaximisedCell { get; private set; }
private readonly Container paddingContainer; private readonly Container paddingContainer;
private readonly FillFlowContainer<Facade> facadeContainer; private readonly FillFlowContainer<Facade> facadeContainer;
private readonly Container<Cell> cellContainer; private readonly Container<Cell> cellContainer;
@@ -99,7 +104,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private void toggleMaximisationState(Cell target) private void toggleMaximisationState(Cell target)
{ {
// in the case the target is the already maximised cell (or there is only one cell), no cell should be maximised. // in the case the target is the already maximised cell (or there is only one cell), no cell should be maximised.
bool hasMaximised = !target.IsMaximised && cellContainer.Count > 1; bool hasMaximised = target != MaximisedCell && cellContainer.Count > 1;
MaximisedCell = hasMaximised ? target : null;
// Iterate through all cells to ensure only one is maximised at any time. // Iterate through all cells to ensure only one is maximised at any time.
foreach (var cell in cellContainer.ToList()) foreach (var cell in cellContainer.ToList())

View File

@@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <summary> /// <summary>
/// A cell of the grid. Contains the content and tracks to the linked facade. /// A cell of the grid. Contains the content and tracks to the linked facade.
/// </summary> /// </summary>
private partial class Cell : CompositeDrawable public partial class Cell : CompositeDrawable
{ {
/// <summary> /// <summary>
/// The index of the original facade of this cell. /// The index of the original facade of this cell.
@@ -33,11 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary> /// </summary>
public Action<Cell>? ToggleMaximisationState; public Action<Cell>? ToggleMaximisationState;
/// <summary>
/// Whether this cell is currently maximised.
/// </summary>
public bool IsMaximised { get; private set; }
private Facade facade; private Facade facade;
private bool isAnimating; private bool isAnimating;
@@ -83,7 +78,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public void SetFacade(Facade newFacade, bool isMaximised) public void SetFacade(Facade newFacade, bool isMaximised)
{ {
facade = newFacade; facade = newFacade;
IsMaximised = isMaximised;
isAnimating = true; isAnimating = true;
TweenEdgeEffectTo(new EdgeEffectParameters TweenEdgeEffectTo(new EdgeEffectParameters

View File

@@ -8,6 +8,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@@ -69,11 +70,11 @@ namespace osu.Game.Screens.SelectV2
if (grouping.BeatmapSetsGroupedTogether) if (grouping.BeatmapSetsGroupedTogether)
{ {
// Give some space around the expanded beatmap set, at the top.. // Give some space around the expanded beatmap set, at the top..
if (bottom.Model is BeatmapSetInfo && bottom.IsExpanded) if (bottom.Model is GroupedBeatmapSet && bottom.IsExpanded)
return SPACING * 2; return SPACING * 2;
// ..and the bottom. // ..and the bottom.
if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetInfo) if (top.Model is BeatmapInfo && bottom.Model is GroupedBeatmapSet)
return SPACING * 2; return SPACING * 2;
// Beatmap difficulty panels do not overlap with themselves or any other panel. // Beatmap difficulty panels do not overlap with themselves or any other panel.
@@ -143,6 +144,9 @@ namespace osu.Game.Screens.SelectV2
switch (changed.Action) switch (changed.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
if (!newItems!.Any())
return;
Items.AddRange(newItems!.SelectMany(s => s.Beatmaps)); Items.AddRange(newItems!.SelectMany(s => s.Beatmaps));
break; break;
@@ -206,12 +210,12 @@ namespace osu.Game.Screens.SelectV2
return true; return true;
} }
if (item.Model is BeatmapSetInfo beatmapSetInfo) if (item.Model is GroupedBeatmapSet groupedSet)
{ {
if (oldItems.Contains(beatmapSetInfo)) if (oldItems.Contains(groupedSet.BeatmapSet))
return false; return false;
RequestRecommendedSelection(beatmapSetInfo.Beatmaps); RequestRecommendedSelection(groupedSet.BeatmapSet.Beatmaps);
return true; return true;
} }
} }
@@ -282,7 +286,7 @@ namespace osu.Game.Screens.SelectV2
protected GroupDefinition? ExpandedGroup { get; private set; } protected GroupDefinition? ExpandedGroup { get; private set; }
protected BeatmapSetInfo? ExpandedBeatmapSet { get; private set; } protected GroupedBeatmapSet? ExpandedBeatmapSet { get; private set; }
protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) =>
grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo; grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo;
@@ -310,8 +314,8 @@ namespace osu.Game.Screens.SelectV2
return; return;
case BeatmapSetInfo setInfo: case GroupedBeatmapSet groupedSet:
selectRecommendedDifficultyForBeatmapSet(setInfo); selectRecommendedDifficultyForBeatmapSet(groupedSet);
return; return;
case BeatmapInfo beatmapInfo: case BeatmapInfo beatmapInfo:
@@ -337,7 +341,7 @@ namespace osu.Game.Screens.SelectV2
switch (model) switch (model)
{ {
case BeatmapSetInfo: case GroupedBeatmapSet:
case GroupDefinition: case GroupDefinition:
throw new InvalidOperationException("Groups should never become selected"); throw new InvalidOperationException("Groups should never become selected");
@@ -348,11 +352,62 @@ namespace osu.Game.Screens.SelectV2
setExpandedGroup(containingGroup); setExpandedGroup(containingGroup);
if (grouping.BeatmapSetsGroupedTogether) if (grouping.BeatmapSetsGroupedTogether)
setExpandedSet(beatmapInfo); setExpandedSet(new GroupedBeatmapSet(containingGroup, beatmapInfo.BeatmapSet!));
break; break;
} }
} }
protected override bool HandleItemsChanged(NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Reset:
return true;
case NotifyCollectionChangedAction.Replace:
var oldBeatmaps = args.OldItems!.OfType<BeatmapInfo>().ToList();
var newBeatmaps = args.NewItems!.OfType<BeatmapInfo>().ToList();
for (int i = 0; i < oldBeatmaps.Count; i++)
{
var oldBeatmap = oldBeatmaps[i];
var newBeatmap = newBeatmaps[i];
// Ignore changes which don't concern us.
//
// Here are some examples of things that can go wrong:
// - Background difficulty calculation runs and causes a realm update.
// We use `BeatmapDifficultyCache` and don't want to know about these.
// - Background user tag population runs and causes a realm update.
// We don't display user tags so want to ignore this.
bool equalForDisplayPurposes =
// covers metadata changes
oldBeatmap.Hash == newBeatmap.Hash &&
// sanity check
oldBeatmap.OnlineID == newBeatmap.OnlineID &&
// displayed on panel
oldBeatmap.Status == newBeatmap.Status &&
// displayed on panel
oldBeatmap.DifficultyName == newBeatmap.DifficultyName &&
// hidden changed, needs re-filter
oldBeatmap.Hidden == newBeatmap.Hidden &&
// might be used for grouping, returning from gameplay
oldBeatmap.LastPlayed == newBeatmap.LastPlayed;
if (equalForDisplayPurposes)
return false;
}
return true;
default:
throw new ArgumentOutOfRangeException();
}
}
protected override void HandleFilterCompleted() protected override void HandleFilterCompleted()
{ {
base.HandleFilterCompleted(); base.HandleFilterCompleted();
@@ -372,10 +427,10 @@ namespace osu.Game.Screens.SelectV2
setExpandedGroup(groupForReselection); setExpandedGroup(groupForReselection);
} }
private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetInfo beatmapSet) private void selectRecommendedDifficultyForBeatmapSet(GroupedBeatmapSet set)
{ {
// Selecting a set isn't valid let's re-select the first visible difficulty. // Selecting a set isn't valid let's re-select the first visible difficulty.
if (grouping.SetItems.TryGetValue(beatmapSet, out var items)) if (grouping.SetItems.TryGetValue(set, out var items))
{ {
var beatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>(); var beatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>();
RequestRecommendedSelection(beatmaps); RequestRecommendedSelection(beatmaps);
@@ -423,7 +478,7 @@ namespace osu.Game.Screens.SelectV2
{ {
switch (item.Model) switch (item.Model)
{ {
case BeatmapSetInfo: case GroupedBeatmapSet:
return true; return true;
case BeatmapInfo: case BeatmapInfo:
@@ -462,11 +517,11 @@ namespace osu.Game.Screens.SelectV2
i.IsExpanded = true; i.IsExpanded = true;
break; break;
case BeatmapSetInfo set: case GroupedBeatmapSet groupedSet:
// Case where there are set headers, header should be visible // Case where there are set headers, header should be visible
// and items should use the set's expanded state. // and items should use the set's expanded state.
i.IsVisible = true; i.IsVisible = true;
setExpansionStateOfSetItems(set, i.IsExpanded); setExpansionStateOfSetItems(groupedSet, i.IsExpanded);
break; break;
default: default:
@@ -496,21 +551,21 @@ namespace osu.Game.Screens.SelectV2
} }
} }
private void setExpandedSet(BeatmapInfo beatmapInfo) private void setExpandedSet(GroupedBeatmapSet set)
{ {
if (ExpandedBeatmapSet != null) if (ExpandedBeatmapSet != null)
setExpansionStateOfSetItems(ExpandedBeatmapSet, false); setExpansionStateOfSetItems(ExpandedBeatmapSet, false);
ExpandedBeatmapSet = beatmapInfo.BeatmapSet!; ExpandedBeatmapSet = set;
setExpansionStateOfSetItems(ExpandedBeatmapSet, true); setExpansionStateOfSetItems(ExpandedBeatmapSet, true);
} }
private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded) private void setExpansionStateOfSetItems(GroupedBeatmapSet set, bool expanded)
{ {
if (grouping.SetItems.TryGetValue(set, out var items)) if (grouping.SetItems.TryGetValue(set, out var items))
{ {
foreach (var i in items) foreach (var i in items)
{ {
if (i.Model is BeatmapSetInfo) if (i.Model is GroupedBeatmapSet)
i.IsExpanded = expanded; i.IsExpanded = expanded;
else else
i.IsVisible = expanded; i.IsVisible = expanded;
@@ -548,7 +603,7 @@ namespace osu.Game.Screens.SelectV2
sampleToggleGroup?.Play(); sampleToggleGroup?.Play();
return; return;
case BeatmapSetInfo: case GroupedBeatmapSet:
sampleChangeSet?.Play(); sampleChangeSet?.Play();
return; return;
@@ -687,8 +742,8 @@ namespace osu.Game.Screens.SelectV2
// it is doing a Replace operation on the list. If it is, then check the local handling in beatmapSetsChanged // it is doing a Replace operation on the list. If it is, then check the local handling in beatmapSetsChanged
// before changing matching requirements here. // before changing matching requirements here.
if (x is BeatmapSetInfo beatmapSetX && y is BeatmapSetInfo beatmapSetY) if (x is GroupedBeatmapSet groupedSetX && y is GroupedBeatmapSet groupedSetY)
return beatmapSetX.Equals(beatmapSetY); return groupedSetX.Equals(groupedSetY);
if (x is BeatmapInfo beatmapX && y is BeatmapInfo beatmapY) if (x is BeatmapInfo beatmapX && y is BeatmapInfo beatmapY)
return beatmapX.Equals(beatmapY); return beatmapX.Equals(beatmapY);
@@ -718,7 +773,7 @@ namespace osu.Game.Screens.SelectV2
return beatmapPanelPool.Get(); return beatmapPanelPool.Get();
case BeatmapSetInfo: case GroupedBeatmapSet:
return setPanelPool.Get(); return setPanelPool.Get();
} }
@@ -828,30 +883,31 @@ namespace osu.Game.Screens.SelectV2
private bool nextRandomSet() private bool nextRandomSet()
{ {
ICollection<BeatmapSetInfo> visibleSets = ExpandedGroup != null ICollection<GroupedBeatmapSet> visibleSetsUnderGrouping = ExpandedGroup != null
// In the case of grouping, users expect random to only operate on the expanded group. // In the case of grouping, users expect random to only operate on the expanded group.
// This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently.
// //
// If this becomes an issue, we could either store a mapping, or run the random algorithm many times // If this becomes an issue, we could either store a mapping, or run the random algorithm many times
// using the `SetItems` method until we get a group HIT. // using the `SetItems` method until we get a group HIT.
? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType<BeatmapSetInfo>().ToArray() ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType<GroupedBeatmapSet>().ToArray()
// This is the fastest way to retrieve sets for randomisation. // This is the fastest way to retrieve sets for randomisation.
: grouping.SetItems.Keys; : grouping.SetItems.Keys;
BeatmapSetInfo set; GroupedBeatmapSet set;
switch (randomAlgorithm.Value) switch (randomAlgorithm.Value)
{ {
case RandomSelectAlgorithm.RandomPermutation: case RandomSelectAlgorithm.RandomPermutation:
{ {
ICollection<BeatmapSetInfo> notYetVisitedSets = visibleSets.Except(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!)).ToList(); ICollection<GroupedBeatmapSet> notYetVisitedSets =
visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), groupedSet => groupedSet.BeatmapSet).ToList();
if (!notYetVisitedSets.Any()) if (!notYetVisitedSets.Any())
{ {
previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSets.Contains(b.BeatmapSet!)); previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(groupedSet => groupedSet.BeatmapSet.Equals(b.BeatmapSet!)));
notYetVisitedSets = visibleSets; notYetVisitedSets = visibleSetsUnderGrouping;
if (CurrentSelection is BeatmapInfo beatmapInfo) if (CurrentSelection is BeatmapInfo beatmapInfo)
notYetVisitedSets = notYetVisitedSets.Except([beatmapInfo.BeatmapSet!]).ToList(); notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], groupedSet => groupedSet.BeatmapSet).ToList();
} }
if (notYetVisitedSets.Count == 0) if (notYetVisitedSets.Count == 0)
@@ -862,7 +918,7 @@ namespace osu.Game.Screens.SelectV2
} }
case RandomSelectAlgorithm.Random: case RandomSelectAlgorithm.Random:
set = visibleSets.ElementAt(RNG.Next(visibleSets.Count)); set = visibleSetsUnderGrouping.ElementAt(RNG.Next(visibleSetsUnderGrouping.Count));
break; break;
default: default:
@@ -959,4 +1015,10 @@ namespace osu.Game.Screens.SelectV2
/// Defines a grouping header for a set of carousel items grouped by star difficulty. /// Defines a grouping header for a set of carousel items grouped by star difficulty.
/// </summary> /// </summary>
public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title);
/// <summary>
/// Used to represent a portion of a <see cref="BeatmapSetInfo"/> under a <see cref="GroupDefinition"/>.
/// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it.
/// </summary>
public record GroupedBeatmapSet([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet);
} }

View File

@@ -29,21 +29,22 @@ namespace osu.Game.Screens.SelectV2
/// <summary> /// <summary>
/// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection.
/// </summary> /// </summary>
public IDictionary<BeatmapSetInfo, HashSet<CarouselItem>> SetItems => setMap; public IDictionary<GroupedBeatmapSet, HashSet<CarouselItem>> SetItems => setMap;
/// <summary> /// <summary>
/// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection. /// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection.
/// </summary> /// </summary>
public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupMap; public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupMap;
private Dictionary<BeatmapSetInfo, HashSet<CarouselItem>> setMap = new Dictionary<BeatmapSetInfo, HashSet<CarouselItem>>(); private Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>> setMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>();
private Dictionary<GroupDefinition, HashSet<CarouselItem>> groupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>(); private Dictionary<GroupDefinition, HashSet<CarouselItem>> groupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>();
private readonly Func<FilterCriteria> getCriteria; private readonly Func<FilterCriteria> getCriteria;
private readonly Func<List<BeatmapCollection>> getCollections; private readonly Func<List<BeatmapCollection>> getCollections;
private readonly Func<FilterCriteria, IReadOnlyDictionary<Guid, ScoreRank>> getLocalUserTopRanks; private readonly Func<FilterCriteria, IReadOnlyDictionary<Guid, ScoreRank>> getLocalUserTopRanks;
public BeatmapCarouselFilterGrouping(Func<FilterCriteria> getCriteria, Func<List<BeatmapCollection>> getCollections, Func<FilterCriteria, IReadOnlyDictionary<Guid, ScoreRank>> getLocalUserTopRanks) public BeatmapCarouselFilterGrouping(Func<FilterCriteria> getCriteria, Func<List<BeatmapCollection>> getCollections,
Func<FilterCriteria, IReadOnlyDictionary<Guid, ScoreRank>> getLocalUserTopRanks)
{ {
this.getCriteria = getCriteria; this.getCriteria = getCriteria;
this.getCollections = getCollections; this.getCollections = getCollections;
@@ -55,7 +56,7 @@ namespace osu.Game.Screens.SelectV2
return await Task.Run(() => return await Task.Run(() =>
{ {
// preallocate space for the new mappings using last known estimates // preallocate space for the new mappings using last known estimates
var newSetMap = new Dictionary<BeatmapSetInfo, HashSet<CarouselItem>>(setMap.Count); var newSetMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>(setMap.Count);
var newGroupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>(groupMap.Count); var newGroupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>(groupMap.Count);
var criteria = getCriteria(); var criteria = getCriteria();
@@ -93,11 +94,12 @@ namespace osu.Game.Screens.SelectV2
var beatmap = (BeatmapInfo)item.Model; var beatmap = (BeatmapInfo)item.Model;
bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID;
var groupedBeatmapSet = new GroupedBeatmapSet(group, beatmap.BeatmapSet!);
if (newBeatmapSet) if (newBeatmapSet)
{ {
if (!newSetMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems)) if (!newSetMap.TryGetValue(groupedBeatmapSet, out currentSetItems))
newSetMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet<CarouselItem>(); newSetMap[groupedBeatmapSet] = currentSetItems = new HashSet<CarouselItem>();
} }
if (BeatmapSetsGroupedTogether) if (BeatmapSetsGroupedTogether)
@@ -107,7 +109,7 @@ namespace osu.Game.Screens.SelectV2
if (groupItem != null) if (groupItem != null)
groupItem.NestedItemCount++; groupItem.NestedItemCount++;
addItem(new CarouselItem(beatmap.BeatmapSet!) addItem(new CarouselItem(groupedBeatmapSet)
{ {
DrawHeight = PanelBeatmapSet.HEIGHT, DrawHeight = PanelBeatmapSet.HEIGHT,
DepthLayer = -1 DepthLayer = -1
@@ -134,7 +136,7 @@ namespace osu.Game.Screens.SelectV2
currentGroupItems?.Add(i); currentGroupItems?.Add(i);
currentSetItems?.Add(i); currentSetItems?.Add(i);
i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || !BeatmapSetsGroupedTogether)); i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is GroupedBeatmapSet || !BeatmapSetsGroupedTogether));
} }
} }
@@ -189,9 +191,6 @@ namespace osu.Game.Screens.SelectV2
{ {
var date = b.LastPlayed; var date = b.LastPlayed;
if (BeatmapSetsGroupedTogether)
date = aggregateMax(b, static b => b.LastPlayed ?? DateTimeOffset.MinValue);
if (date == null || date == DateTimeOffset.MinValue) if (date == null || date == DateTimeOffset.MinValue)
return new GroupDefinition(int.MaxValue, "Never"); return new GroupDefinition(int.MaxValue, "Never");
@@ -202,29 +201,13 @@ namespace osu.Game.Screens.SelectV2
return getGroupsBy(b => defineGroupByStatus(b.BeatmapSet!.Status), items); return getGroupsBy(b => defineGroupByStatus(b.BeatmapSet!.Status), items);
case GroupMode.BPM: case GroupMode.BPM:
return getGroupsBy(b => return getGroupsBy(b => defineGroupByBPM(FormatUtils.RoundBPM(b.BPM)), items);
{
double bpm = FormatUtils.RoundBPM(b.BPM);
if (BeatmapSetsGroupedTogether)
bpm = aggregateMax(b, bb => FormatUtils.RoundBPM(bb.BPM));
return defineGroupByBPM(bpm);
}, items);
case GroupMode.Difficulty: case GroupMode.Difficulty:
return getGroupsBy(b => defineGroupByStars(b.StarRating), items); return getGroupsBy(b => defineGroupByStars(b.StarRating), items);
case GroupMode.Length: case GroupMode.Length:
return getGroupsBy(b => return getGroupsBy(b => defineGroupByLength(b.Length), items);
{
double length = b.Length;
if (BeatmapSetsGroupedTogether)
length = aggregateMax(b, bb => bb.Length);
return defineGroupByLength(length);
}, items);
case GroupMode.Source: case GroupMode.Source:
return getGroupsBy(b => defineGroupBySource(b.BeatmapSet!.Metadata.Source), items); return getGroupsBy(b => defineGroupBySource(b.BeatmapSet!.Metadata.Source), items);
@@ -433,12 +416,6 @@ namespace osu.Game.Screens.SelectV2
return new GroupDefinition(int.MaxValue, "Unplayed"); return new GroupDefinition(int.MaxValue, "Unplayed");
} }
private static T? aggregateMax<T>(BeatmapInfo b, Func<BeatmapInfo, T> func)
{
var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden);
return beatmaps.Max(func);
}
private record GroupMapping(GroupDefinition? Group, List<CarouselItem> ItemsInGroup); private record GroupMapping(GroupDefinition? Group, List<CarouselItem> ItemsInGroup);
} }
} }

View File

@@ -199,17 +199,8 @@ namespace osu.Game.Screens.SelectV2
{ {
base.LoadComplete(); base.LoadComplete();
ruleset.BindValueChanged(_ => ruleset.BindValueChanged(_ => updateKeyCount());
{ mods.BindValueChanged(_ => updateKeyCount(), true);
computeStarRating();
updateKeyCount();
});
mods.BindValueChanged(_ =>
{
computeStarRating();
updateKeyCount();
}, true);
} }
protected override void PrepareForUse() protected override void PrepareForUse()

View File

@@ -67,6 +67,15 @@ namespace osu.Game.Screens.SelectV2
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!; private IBindable<RulesetInfo> ruleset { get; set; } = null!;
private GroupedBeatmapSet groupedBeatmapSet
{
get
{
Debug.Assert(Item != null);
return (GroupedBeatmapSet)Item!.Model;
}
}
public PanelBeatmapSet() public PanelBeatmapSet()
{ {
PanelXOffset = 20f; PanelXOffset = 20f;
@@ -179,9 +188,7 @@ namespace osu.Game.Screens.SelectV2
{ {
base.PrepareForUse(); base.PrepareForUse();
Debug.Assert(Item != null); var beatmapSet = groupedBeatmapSet.BeatmapSet;
var beatmapSet = (BeatmapSetInfo)Item.Model;
// Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set).
setBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); setBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID));
@@ -215,7 +222,7 @@ namespace osu.Game.Screens.SelectV2
if (Item == null) if (Item == null)
return Array.Empty<MenuItem>(); return Array.Empty<MenuItem>();
var beatmapSet = (BeatmapSetInfo)Item.Model; var beatmapSet = groupedBeatmapSet.BeatmapSet;
List<MenuItem> items = new List<MenuItem>(); List<MenuItem> items = new List<MenuItem>();
@@ -268,9 +275,7 @@ namespace osu.Game.Screens.SelectV2
private MenuItem createCollectionMenuItem(BeatmapCollection collection) private MenuItem createCollectionMenuItem(BeatmapCollection collection)
{ {
var beatmapSet = (BeatmapSetInfo)Item!.Model; var beatmapSet = groupedBeatmapSet.BeatmapSet;
Debug.Assert(beatmapSet != null);
TernaryState state; TernaryState state;

View File

@@ -209,17 +209,8 @@ namespace osu.Game.Screens.SelectV2
{ {
base.LoadComplete(); base.LoadComplete();
ruleset.BindValueChanged(_ => ruleset.BindValueChanged(_ => updateKeyCount());
{ mods.BindValueChanged(_ => updateKeyCount(), true);
computeStarRating();
updateKeyCount();
});
mods.BindValueChanged(_ =>
{
computeStarRating();
updateKeyCount();
}, true);
Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true);
} }

View File

@@ -653,6 +653,7 @@ namespace osu.Game.Screens.SelectV2
ensurePlayingSelected(); ensurePlayingSelected();
updateBackgroundDim(); updateBackgroundDim();
fetchOnlineInfo();
} }
private void onLeavingScreen() private void onLeavingScreen()

View File

@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO; using osu.Game.IO;
@@ -28,8 +29,8 @@ namespace osu.Game.Skinning
private readonly ModelManager<SkinInfo> modelManager; private readonly ModelManager<SkinInfo> modelManager;
public SkinImporter(Storage storage, RealmAccess realm, IStorageResourceProvider skinResources) public SkinImporter(Storage storage, RealmAccess realm, IStorageResourceProvider skinResources, OsuConfigManager? config = null)
: base(storage, realm) : base(storage, realm, config)
{ {
this.skinResources = skinResources; this.skinResources = skinResources;

View File

@@ -21,6 +21,7 @@ using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
@@ -74,7 +75,7 @@ namespace osu.Game.Skinning
} }
} }
public SkinManager(Storage storage, RealmAccess realm, GameHost host, IResourceStore<byte[]> resources, AudioManager audio, Scheduler scheduler) public SkinManager(Storage storage, RealmAccess realm, GameHost host, IResourceStore<byte[]> resources, AudioManager audio, Scheduler scheduler, OsuConfigManager config)
: base(storage, realm) : base(storage, realm)
{ {
this.audio = audio; this.audio = audio;
@@ -84,7 +85,7 @@ namespace osu.Game.Skinning
userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files")); userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files"));
skinImporter = new SkinImporter(storage, realm, this) skinImporter = new SkinImporter(storage, realm, this, config)
{ {
PostNotification = obj => PostNotification?.Invoke(obj), PostNotification = obj => PostNotification?.Invoke(obj),
}; };

View File

@@ -7,7 +7,9 @@ using osu.Framework.Bindables;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Overlays; using osu.Game.Overlays;
@@ -32,7 +34,7 @@ namespace osu.Game.Tests.Visual
protected DialogOverlay DialogOverlay { get; private set; } protected DialogOverlay DialogOverlay { get; private set; }
[Cached] [Cached]
private ScreenFooter footer; protected ScreenFooter Footer { get; private set; }
protected ScreenTestScene() protected ScreenTestScene()
{ {
@@ -43,17 +45,32 @@ namespace osu.Game.Tests.Visual
Name = nameof(ScreenTestScene), Name = nameof(ScreenTestScene),
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
content = new Container { RelativeSizeAxes = Axes.Both }, new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
content = new Container { RelativeSizeAxes = Axes.Both },
Footer = new ScreenFooter(),
}
},
overlayContent = new Container overlayContent = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = DialogOverlay = new DialogOverlay() Child = DialogOverlay = new DialogOverlay()
}, },
footer = new ScreenFooter(),
}); });
Stack.ScreenPushed += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); Stack.ScreenPushed += (oldScreen, newScreen) =>
Stack.ScreenExited += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); {
updateFooter(oldScreen, newScreen);
Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}");
};
Stack.ScreenExited += (oldScreen, newScreen) =>
{
updateFooter(oldScreen, newScreen);
Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}");
};
} }
protected void LoadScreen(OsuScreen screen) => Stack.Push(screen); protected void LoadScreen(OsuScreen screen) => Stack.Push(screen);
@@ -79,6 +96,39 @@ namespace osu.Game.Tests.Visual
}); });
} }
private void updateFooter(IScreen? _, IScreen? newScreen)
{
if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter)
{
Footer.Show();
if (osuScreen.IsLoaded)
updateFooterButtons();
else
{
// ensure the current buttons are immediately disabled on screen change (so they can't be pressed).
Footer.SetButtons(Array.Empty<ScreenFooterButton>());
osuScreen.OnLoadComplete += _ => updateFooterButtons();
}
void updateFooterButtons()
{
var buttons = osuScreen.CreateFooterButtons();
osuScreen.LoadComponentsAgainstScreenDependencies(buttons);
Footer.SetButtons(buttons);
Footer.Show();
}
}
else
{
Footer.Hide();
Footer.SetButtons(Array.Empty<ScreenFooterButton>());
}
}
#region IOverlayManager #region IOverlayManager
IBindable<OverlayActivation> IOverlayManager.OverlayActivationMode { get; } = new Bindable<OverlayActivation>(OverlayActivation.All); IBindable<OverlayActivation> IOverlayManager.OverlayActivationMode { get; } = new Bindable<OverlayActivation>(OverlayActivation.All);

View File

@@ -5,7 +5,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Nuget"> <PropertyGroup Label="Nuget">
<Title>osu!</Title> <Title>jvnkosu!</Title>
<PackageId>ppy.osu.Game</PackageId> <PackageId>ppy.osu.Game</PackageId>
<Version>0.0.0</Version> <Version>0.0.0</Version>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
@@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="20.1.0" /> <PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.808.0" /> <PackageReference Include="ppy.osu.Framework" Version="2025.829.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.821.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2025.821.0" />
<PackageReference Include="Sentry" Version="5.1.1" /> <PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. --> <!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter> <MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.808.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2025.829.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>