mirror of
https://github.com/ppy/osu.git
synced 2026-05-17 08:42:35 +08:00
Compare commits
274 Commits
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Benchmarks" type="DotNetProject" factoryName=".NET Project">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Debug/net6.0/osu.Game.Benchmarks.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Debug/net8.0/osu.Game.Benchmarks.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="CatchRuleset (Tests)" type="DotNetProject" factoryName=".NET Project" folderName="Ruleset" activateToolWindowBeforeRun="false">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Rulesets.Catch.Tests/bin/Debug/net6.0/osu.Game.Rulesets.Catch.Tests.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Rulesets.Catch.Tests/bin/Debug/net8.0/osu.Game.Rulesets.Catch.Tests.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Rulesets.Catch.Tests/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Rulesets.Catch.Tests/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<browser url="http://localhost:5000" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="ManiaRuleset (Tests)" type="DotNetProject" factoryName=".NET Project" folderName="Ruleset" activateToolWindowBeforeRun="false">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Rulesets.Mania.Tests/bin/Debug/net6.0/osu.Game.Rulesets.Mania.Tests.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Rulesets.Mania.Tests/bin/Debug/net8.0/osu.Game.Rulesets.Mania.Tests.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Rulesets.Mania.Tests/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Rulesets.Mania.Tests/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<browser url="http://localhost:5000" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="OsuRuleset (Tests)" type="DotNetProject" factoryName=".NET Project" folderName="Ruleset" activateToolWindowBeforeRun="false">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Rulesets.Osu.Tests/bin/Debug/net6.0/osu.Game.Rulesets.Osu.Tests.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Rulesets.Osu.Tests/bin/Debug/net8.0/osu.Game.Rulesets.Osu.Tests.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Rulesets.Osu.Tests/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Rulesets.Osu.Tests/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<browser url="http://localhost:5000" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="TaikoRuleset (Tests)" type="DotNetProject" factoryName=".NET Project" folderName="Ruleset" activateToolWindowBeforeRun="false">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Rulesets.Taiko.Tests/bin/Debug/net6.0/osu.Game.Rulesets.Taiko.Tests.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Rulesets.Taiko.Tests/bin/Debug/net8.0/osu.Game.Rulesets.Taiko.Tests.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Rulesets.Taiko.Tests/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Rulesets.Taiko.Tests/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<browser url="http://localhost:5000" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Tournament" type="DotNetProject" factoryName=".NET Project" folderName="Tournament" activateToolWindowBeforeRun="false">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net6.0/osu!.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net8.0/osu!.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--tournament" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Tournament (Tests)" type="DotNetProject" factoryName=".NET Project" folderName="Tournament" activateToolWindowBeforeRun="false">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Tournament.Tests/bin/Debug/net6.0/osu.Game.Tournament.Tests.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Tournament.Tests/bin/Debug/net8.0/osu.Game.Tournament.Tests.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Tournament.Tests/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Tournament.Tests/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<browser url="http://localhost:5000" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="osu!" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net6.0/osu!.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net8.0/osu!.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="osu! (Tests)" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Tests/bin/Debug/net6.0/osu.Game.Tests.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Tests/bin/Debug/net8.0/osu.Game.Tests.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Tests/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Tests/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="osu! (Second Client)" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net6.0/osu!.dll" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net8.0/osu!.dll" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--debug-client-id=1" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net6.0" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net8.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net6.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
||||
Vendored
+9
-9
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/osu.Desktop/bin/Debug/net6.0/osu!.dll"
|
||||
"${workspaceRoot}/osu.Desktop/bin/Debug/net8.0/osu!.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build osu! (Debug)",
|
||||
@@ -19,7 +19,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/osu.Desktop/bin/Release/net6.0/osu!.dll"
|
||||
"${workspaceRoot}/osu.Desktop/bin/Release/net8.0/osu!.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build osu! (Release)",
|
||||
@@ -31,7 +31,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/osu.Game.Tests/bin/Debug/net6.0/osu.Game.Tests.dll"
|
||||
"${workspaceRoot}/osu.Game.Tests/bin/Debug/net8.0/osu.Game.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build tests (Debug)",
|
||||
@@ -43,7 +43,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/osu.Game.Tests/bin/Release/net6.0/osu.Game.Tests.dll"
|
||||
"${workspaceRoot}/osu.Game.Tests/bin/Release/net8.0/osu.Game.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build tests (Release)",
|
||||
@@ -55,7 +55,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/osu.Desktop/bin/Debug/net6.0/osu!.dll",
|
||||
"${workspaceRoot}/osu.Desktop/bin/Debug/net8.0/osu!.dll",
|
||||
"--tournament"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
@@ -68,7 +68,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/osu.Desktop/bin/Release/net6.0/osu!.dll",
|
||||
"${workspaceRoot}/osu.Desktop/bin/Release/net8.0/osu!.dll",
|
||||
"--tournament"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
@@ -81,7 +81,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net6.0/osu.Game.Tournament.Tests.dll",
|
||||
"${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net8.0/osu.Game.Tournament.Tests.dll",
|
||||
"--tournament"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
@@ -94,7 +94,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net6.0/osu.Game.Tournament.Tests.dll",
|
||||
"${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net8.0/osu.Game.Tournament.Tests.dll",
|
||||
"--tournament"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
@@ -105,7 +105,7 @@
|
||||
"name": "Benchmark",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/net6.0/osu.Game.Benchmarks.dll",
|
||||
"program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/net8.0/osu.Game.Benchmarks.dll",
|
||||
"args": [
|
||||
"--filter",
|
||||
"*"
|
||||
|
||||
@@ -68,6 +68,7 @@ Aside from the above, below is a brief checklist of things to watch out when you
|
||||
- Please do not make code changes via the GitHub web interface.
|
||||
- Please add tests for your changes. We expect most new features and bugfixes to have test coverage, unless the effort of adding them is prohibitive. The visual testing methodology we use is described in more detail [here](https://github.com/ppy/osu-framework/wiki/Development-and-Testing).
|
||||
- Please run tests and code style analysis (via `InspectCode.{ps1,sh}` scripts in the root of this repository) before opening the PR. This is particularly important if you're a first-time contributor, as CI will not run for your PR until we allow it to do so.
|
||||
- **Do not run the game in release configuration at any point during your testing** (the sole exception to this being benchmarks). Using release is an unnecessary and harmful practice, and can even lead to you losing your local realm database if you start making changes to the schema. The debug configuration has a completely separated full-stack environment, including a development website instance at https://dev.ppy.sh/. It is permitted to register an account on that development instance for testing purposes and not worry about multi-accounting infractions.
|
||||
|
||||
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ If you are just looking to give the game a whirl, you can grab the latest releas
|
||||
|
||||
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 not listed above, there is still a chance you can manually build it by following the instructions below.
|
||||
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 in early 2024.
|
||||
|
||||
@@ -51,7 +51,7 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir
|
||||
|
||||
Please make sure you have the following prerequisites:
|
||||
|
||||
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
|
||||
- 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#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin installed.
|
||||
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
|
||||
"${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Debug)",
|
||||
@@ -20,7 +20,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
|
||||
"${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Release)",
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll"
|
||||
"${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.Pippidon.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Debug)",
|
||||
@@ -20,7 +20,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll"
|
||||
"${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.Pippidon.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Release)",
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
|
||||
"${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Debug)",
|
||||
@@ -20,7 +20,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
|
||||
"${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Release)",
|
||||
|
||||
Vendored
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll"
|
||||
"${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.Pippidon.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Debug)",
|
||||
@@ -20,7 +20,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll"
|
||||
"${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.Pippidon.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Release)",
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.205.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.223.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace osu.Desktop
|
||||
{
|
||||
var windowsVersion = Environment.OSVersion.Version;
|
||||
|
||||
// While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
|
||||
// See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
|
||||
// While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher.
|
||||
// See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
|
||||
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
|
||||
{
|
||||
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 75 KiB |
@@ -0,0 +1,48 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkStringComparison
|
||||
{
|
||||
private string[] strings = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetUp()
|
||||
{
|
||||
strings = new string[10000];
|
||||
|
||||
for (int i = 0; i < strings.Length; ++i)
|
||||
strings[i] = Guid.NewGuid().ToString();
|
||||
|
||||
for (int i = 0; i < strings.Length; ++i)
|
||||
{
|
||||
if (i % 2 == 0)
|
||||
strings[i] = strings[i].ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void OrdinalIgnoreCase() => compare(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
[Benchmark]
|
||||
public void OrdinalSortByCase() => compare(OrdinalSortByCaseStringComparer.DEFAULT);
|
||||
|
||||
[Benchmark]
|
||||
public void InvariantCulture() => compare(StringComparer.InvariantCulture);
|
||||
|
||||
private void compare(IComparer<string> comparer)
|
||||
{
|
||||
for (int i = 0; i < strings.Length; ++i)
|
||||
{
|
||||
for (int j = i + 1; j < strings.Length; ++j)
|
||||
_ = comparer.Compare(strings[i], strings[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Catch.Tests.dll"
|
||||
"${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.Catch.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Debug)",
|
||||
@@ -20,7 +20,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Catch.Tests.dll"
|
||||
"${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.Catch.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Release)",
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
using Direction = osu.Game.Rulesets.Catch.UI.Direction;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public partial class TestSceneCatchSkinConfiguration : OsuTestScene
|
||||
{
|
||||
private Catcher catcher;
|
||||
|
||||
private readonly Container container;
|
||||
|
||||
public TestSceneCatchSkinConfiguration()
|
||||
{
|
||||
Add(container = new Container { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestCatcherPlateFlipping(bool flip)
|
||||
{
|
||||
AddStep("setup catcher", () =>
|
||||
{
|
||||
var skin = new TestSkin { FlipCatcherPlate = flip };
|
||||
container.Child = new SkinProvidingContainer(skin)
|
||||
{
|
||||
Child = catcher = new Catcher(new DroppedObjectContainer())
|
||||
{
|
||||
Anchor = Anchor.Centre
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Fruit fruit = new Fruit();
|
||||
|
||||
AddStep("catch fruit", () => catchFruit(fruit, 20));
|
||||
|
||||
float position = 0;
|
||||
|
||||
AddStep("record fruit position", () => position = getCaughtObjectPosition(fruit));
|
||||
|
||||
AddStep("face left", () => catcher.VisualDirection = Direction.Left);
|
||||
|
||||
if (flip)
|
||||
AddAssert("fruit position changed", () => !Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
|
||||
else
|
||||
AddAssert("fruit position unchanged", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
|
||||
|
||||
AddStep("face right", () => catcher.VisualDirection = Direction.Right);
|
||||
|
||||
AddAssert("fruit position restored", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
|
||||
}
|
||||
|
||||
private float getCaughtObjectPosition(Fruit fruit)
|
||||
{
|
||||
var caughtObject = catcher.ChildrenOfType<CaughtObject>().Single(c => c.HitObject == fruit);
|
||||
return caughtObject.Parent!.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
|
||||
}
|
||||
|
||||
private void catchFruit(Fruit fruit, float x)
|
||||
{
|
||||
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
var drawableFruit = new DrawableFruit(fruit) { X = x };
|
||||
var judgement = fruit.CreateJudgement();
|
||||
catcher.OnNewResult(drawableFruit, new CatchJudgementResult(fruit, judgement)
|
||||
{
|
||||
Type = judgement.MaxResult
|
||||
});
|
||||
}
|
||||
|
||||
private class TestSkin : TrianglesSkin
|
||||
{
|
||||
public bool FlipCatcherPlate { get; set; }
|
||||
|
||||
public TestSkin()
|
||||
: base(null!)
|
||||
{
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
if (lookup is CatchSkinConfiguration config)
|
||||
{
|
||||
if (config == CatchSkinConfiguration.FlipCatcherPlate)
|
||||
return SkinUtils.As<TValue>(new Bindable<bool>(FlipCatcherPlate));
|
||||
}
|
||||
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private JudgementResult createResult(CatchHitObject hitObject)
|
||||
{
|
||||
return new CatchJudgementResult(hitObject, hitObject.CreateJudgement())
|
||||
return new CatchJudgementResult(hitObject, hitObject.Judgement)
|
||||
{
|
||||
Type = catcher.CanCatch(hitObject) ? HitResult.Great : HitResult.Miss
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.PlayfieldAdjustmentContainer.Scale = new Vector2(1, -1);
|
||||
drawableRuleset.PlayfieldAdjustmentContainer.Y = 1 - drawableRuleset.PlayfieldAdjustmentContainer.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
public enum CatchSkinConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
|
||||
/// </summary>
|
||||
FlipCatcherPlate
|
||||
}
|
||||
}
|
||||
@@ -122,19 +122,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
|
||||
return (IBindable<TValue>)result;
|
||||
|
||||
case CatchSkinConfiguration config:
|
||||
switch (config)
|
||||
{
|
||||
case CatchSkinConfiguration.FlipCatcherPlate:
|
||||
// Don't flip catcher plate contents if the catcher is provided by this legacy skin.
|
||||
if (GetDrawableComponent(new CatchSkinComponentLookup(CatchSkinComponents.Catcher)) != null)
|
||||
return (IBindable<TValue>)new Bindable<bool>();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
|
||||
@@ -17,24 +17,36 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public CatchPlayfieldAdjustmentContainer()
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
const float base_game_width = 1024f;
|
||||
const float base_game_height = 768f;
|
||||
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
// we can match that in lazer by using relative coordinates for Y and considering window height to be 1, and playfield height to be 0.8.
|
||||
RelativePositionAxes = Axes.Y;
|
||||
Y = (1 - playfield_size_adjust) / 4 * 3;
|
||||
// extra bottom space for the catcher to not get cut off at tall resolutions lower than 4:3 (e.g. 5:4). number chosen based on testing with maximum catcher scale (i.e. CS 0).
|
||||
const float extra_bottom_space = 200f;
|
||||
|
||||
Size = new Vector2(playfield_size_adjust);
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = new Container
|
||||
{
|
||||
// This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits).
|
||||
// Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off.
|
||||
Name = "Visible area",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
FillAspectRatio = 4f / 3,
|
||||
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both, }
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = base_game_height + extra_bottom_space,
|
||||
Y = extra_bottom_space / 2,
|
||||
Masking = true,
|
||||
Child = new Container
|
||||
{
|
||||
Name = "Playable area",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3),
|
||||
Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust,
|
||||
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -112,11 +112,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public Vector2 BodyScale => Scale * body.Scale;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
|
||||
/// </summary>
|
||||
private bool flipCatcherPlate;
|
||||
|
||||
/// <summary>
|
||||
/// Width of the area that can be used to attempt catches during gameplay.
|
||||
/// </summary>
|
||||
@@ -339,8 +334,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
|
||||
DEFAULT_HYPER_DASH_COLOUR;
|
||||
|
||||
flipCatcherPlate = skin.GetConfig<CatchSkinConfiguration, bool>(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
|
||||
|
||||
runHyperDashStateTransition(HyperDashing);
|
||||
}
|
||||
|
||||
@@ -352,8 +345,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
body.Scale = scaleFromDirection;
|
||||
// Inverse of catcher scale is applied here, as catcher gets scaled by circle size and so do the incoming fruit.
|
||||
caughtObjectContainer.Scale = (1 / Scale.X) * (flipCatcherPlate ? scaleFromDirection : Vector2.One);
|
||||
hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
|
||||
caughtObjectContainer.Scale = new Vector2(1 / Scale.X);
|
||||
|
||||
// Correct overshooting.
|
||||
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Mania.Tests.dll"
|
||||
"${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.Mania.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Debug)",
|
||||
@@ -20,7 +20,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Mania.Tests.dll"
|
||||
"${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.Mania.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Release)",
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
@@ -11,9 +21,80 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[TestCase(0.5f)]
|
||||
[TestCase(0.1f)]
|
||||
[TestCase(0.7f)]
|
||||
public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModFadeIn { Coverage = { Value = coverage } }, PassCondition = () => true });
|
||||
[Test]
|
||||
public void TestMinCoverageFullWidth()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMinCoverageHalfWidth()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
|
||||
AddStep("set playfield width to 0.5", () => Player.Width = 0.5f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaxCoverageFullWidth()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
AddStep("set combo to 480", () => Player.ScoreProcessor.Combo.Value = 480);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaxCoverageHalfWidth()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
AddStep("set combo to 480", () => Player.ScoreProcessor.Combo.Value = 480);
|
||||
AddStep("set playfield width to 0.5", () => Player.Width = 0.5f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoCoverageDuringBreak()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
|
||||
Breaks = { new BreakPeriod(2000, 28000) }
|
||||
},
|
||||
PassCondition = () => Player.IsBreakTime.Value && checkCoverage(0)
|
||||
});
|
||||
}
|
||||
|
||||
private bool checkCoverage(float expected)
|
||||
{
|
||||
Drawable? cover = this.ChildrenOfType<PlayfieldCoveringWrapper>().FirstOrDefault();
|
||||
Drawable? filledArea = cover?.ChildrenOfType<Box>().LastOrDefault();
|
||||
|
||||
if (filledArea == null)
|
||||
return false;
|
||||
|
||||
float scale = cover!.DrawHeight / (768 - Stage.HIT_TARGET_POSITION);
|
||||
|
||||
// A bit of lenience because the test may end up hitting hitobjects before any assertions.
|
||||
return Precision.AlmostEquals(filledArea.DrawHeight / scale, expected, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
@@ -11,9 +21,80 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[TestCase(0.5f)]
|
||||
[TestCase(0.2f)]
|
||||
[TestCase(0.8f)]
|
||||
public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModHidden { Coverage = { Value = coverage } }, PassCondition = () => true });
|
||||
[Test]
|
||||
public void TestMinCoverageFullWidth()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMinCoverageHalfWidth()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
|
||||
AddStep("set playfield width to 0.5", () => Player.Width = 0.5f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaxCoverageFullWidth()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
AddStep("set combo to 480", () => Player.ScoreProcessor.Combo.Value = 480);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaxCoverageHalfWidth()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
AddStep("set combo to 480", () => Player.ScoreProcessor.Combo.Value = 480);
|
||||
AddStep("set playfield width to 0.5", () => Player.Width = 0.5f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoCoverageDuringBreak()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
|
||||
Breaks = { new BreakPeriod(2000, 28000) }
|
||||
},
|
||||
PassCondition = () => Player.IsBreakTime.Value && checkCoverage(0)
|
||||
});
|
||||
}
|
||||
|
||||
private bool checkCoverage(float expected)
|
||||
{
|
||||
Drawable? cover = this.ChildrenOfType<PlayfieldCoveringWrapper>().FirstOrDefault();
|
||||
Drawable? filledArea = cover?.ChildrenOfType<Box>().LastOrDefault();
|
||||
|
||||
if (filledArea == null)
|
||||
return false;
|
||||
|
||||
float scale = cover!.DrawHeight / (768 - Stage.HIT_TARGET_POSITION);
|
||||
|
||||
// A bit of lenience because the test may end up hitting hitobjects before any assertions.
|
||||
return Precision.AlmostEquals(filledArea.DrawHeight / scale, expected, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,18 +39,18 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
public void TestScrollingDownwards()
|
||||
{
|
||||
AddStep("set down scroll", () => scrollingContainer.Direction = ScrollingDirection.Down);
|
||||
AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
|
||||
AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
|
||||
AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
|
||||
AddStep("set coverage = 0.5", () => cover.Coverage.Value = 0.5f);
|
||||
AddStep("set coverage = 0.8f", () => cover.Coverage.Value = 0.8f);
|
||||
AddStep("set coverage = 0.2f", () => cover.Coverage.Value = 0.2f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollingUpwards()
|
||||
{
|
||||
AddStep("set up scroll", () => scrollingContainer.Direction = ScrollingDirection.Up);
|
||||
AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
|
||||
AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
|
||||
AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
|
||||
AddStep("set coverage = 0.5", () => cover.Coverage.Value = 0.5f);
|
||||
AddStep("set coverage = 0.8f", () => cover.Coverage.Value = 0.8f);
|
||||
AddStep("set coverage = 0.2f", () => cover.Coverage.Value = 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,16 +34,21 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("setup hierarchy", () => Child = new Container
|
||||
AddStep("setup hierarchy", () =>
|
||||
{
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
Child = new Container
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
};
|
||||
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
});
|
||||
AddStep("retrieve config bindable", () =>
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
@@ -17,9 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||
|
||||
protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
|
||||
|
||||
protected ManiaSelectionBlueprint(T hitObject)
|
||||
@@ -28,14 +26,31 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
RelativeSizeAxes = Axes.None;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
private readonly IBindable<ScrollingDirection> directionBindable = new Bindable<ScrollingDirection>();
|
||||
|
||||
var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
directionBindable.BindTo(scrollingInfo.Direction);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
directionBindable.BindValueChanged(onDirectionChanged, true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
var anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Anchor = Origin = anchor;
|
||||
foreach (var child in InternalChildren)
|
||||
child.Anchor = child.Origin = anchor;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Position = Parent!.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
|
||||
Width = HitObjectContainer.DrawWidth;
|
||||
|
||||
@@ -247,7 +247,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
new ManiaModHardRock(),
|
||||
new MultiMod(new ManiaModSuddenDeath(), new ManiaModPerfect()),
|
||||
new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()),
|
||||
new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()),
|
||||
new MultiMod(new ManiaModFadeIn(), new ManiaModHidden(), new ManiaModCover()),
|
||||
new ManiaModFlashlight(),
|
||||
new ModAccuracyChallenge(),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModCover : ManiaModWithPlayfieldCover
|
||||
{
|
||||
public override string Name => "Cover";
|
||||
public override string Acronym => "CO";
|
||||
|
||||
public override LocalisableString Description => @"Decrease the playfield's viewing area.";
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
protected override CoverExpandDirection ExpandDirection => Direction.Value;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||
{
|
||||
typeof(ManiaModHidden),
|
||||
typeof(ManiaModFadeIn)
|
||||
}).ToArray();
|
||||
|
||||
public override bool Ranked => false;
|
||||
|
||||
[SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")]
|
||||
public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f)
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0.2f,
|
||||
MaxValue = 0.8f,
|
||||
Default = 0.5f,
|
||||
};
|
||||
|
||||
[SettingSource("Direction", "The direction on which the cover is applied")]
|
||||
public Bindable<CoverExpandDirection> Direction { get; } = new Bindable<CoverExpandDirection>();
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,24 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModFadeIn : ManiaModPlayfieldCover
|
||||
public class ManiaModFadeIn : ManiaModHidden
|
||||
{
|
||||
public override string Name => "Fade In";
|
||||
public override string Acronym => "FI";
|
||||
public override LocalisableString Description => @"Keys appear out of nowhere!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||
{
|
||||
typeof(ManiaModHidden),
|
||||
typeof(ManiaModCover)
|
||||
}).ToArray();
|
||||
|
||||
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
|
||||
|
||||
public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f)
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0.1f,
|
||||
MaxValue = 0.7f,
|
||||
Default = 0.5f,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,27 +3,104 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModHidden : ManiaModPlayfieldCover
|
||||
public partial class ManiaModHidden : ManiaModWithPlayfieldCover, IApplicableToPlayer, IUpdatableByPlayfield
|
||||
{
|
||||
/// <summary>
|
||||
/// osu!stable is referenced to 768px.
|
||||
/// </summary>
|
||||
private const float reference_playfield_height = 768;
|
||||
|
||||
public const float MIN_COVERAGE = 160f;
|
||||
public const float MAX_COVERAGE = 400f;
|
||||
private const float coverage_increase_per_combo = 0.5f;
|
||||
|
||||
public override LocalisableString Description => @"Keys fade out before you hit them!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f)
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0.2f,
|
||||
MaxValue = 0.8f,
|
||||
Default = 0.5f,
|
||||
};
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
|
||||
typeof(ManiaModFadeIn),
|
||||
typeof(ManiaModCover)
|
||||
}).ToArray();
|
||||
|
||||
public override BindableNumber<float> Coverage { get; } = new BindableFloat(MIN_COVERAGE);
|
||||
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
|
||||
|
||||
private readonly IBindable<bool> isBreakTime = new Bindable<bool>();
|
||||
private readonly BindableInt combo = new BindableInt();
|
||||
|
||||
public override void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
base.ApplyToScoreProcessor(scoreProcessor);
|
||||
|
||||
combo.UnbindAll();
|
||||
combo.BindTo(scoreProcessor.Combo);
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
isBreakTime.UnbindAll();
|
||||
isBreakTime.BindTo(player.IsBreakTime);
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
Coverage.Value = isBreakTime.Value
|
||||
? 0
|
||||
: Math.Min(MAX_COVERAGE, MIN_COVERAGE + combo.Value * coverage_increase_per_combo) / reference_playfield_height;
|
||||
}
|
||||
|
||||
protected override PlayfieldCoveringWrapper CreateCover(Drawable content) => new LegacyPlayfieldCover(content);
|
||||
|
||||
private partial class LegacyPlayfieldCover : PlayfieldCoveringWrapper
|
||||
{
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
private IBindable<float>? hitPosition;
|
||||
|
||||
public LegacyPlayfieldCover(Drawable content)
|
||||
: base(content)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
skin.SourceChanged += onSkinChanged;
|
||||
onSkinChanged();
|
||||
}
|
||||
|
||||
private void onSkinChanged()
|
||||
{
|
||||
hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition);
|
||||
}
|
||||
|
||||
protected override float GetHeight(float coverage)
|
||||
{
|
||||
// In osu!stable, the cover is applied in absolute (x768) coordinates from the hit position.
|
||||
float availablePlayfieldHeight = Math.Abs(reference_playfield_height - (hitPosition?.Value ?? Stage.HIT_TARGET_POSITION));
|
||||
|
||||
if (availablePlayfieldHeight == 0)
|
||||
return base.GetHeight(coverage);
|
||||
|
||||
return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -6,7 +6,6 @@ using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -15,7 +14,7 @@ using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public abstract class ManiaModPlayfieldCover : ModHidden, IApplicableToDrawableRuleset<ManiaHitObject>
|
||||
public abstract class ManiaModWithPlayfieldCover : ModHidden, IApplicableToDrawableRuleset<ManiaHitObject>
|
||||
{
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<ManiaHitObject>) };
|
||||
|
||||
@@ -24,7 +23,9 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
/// </summary>
|
||||
protected abstract CoverExpandDirection ExpandDirection { get; }
|
||||
|
||||
[SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")]
|
||||
/// <summary>
|
||||
/// The relative area that should be completely covered. This does not include the fade.
|
||||
/// </summary>
|
||||
public abstract BindableNumber<float> Coverage { get; }
|
||||
|
||||
public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
|
||||
@@ -37,15 +38,17 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
Container hocParent = (Container)hoc.Parent!;
|
||||
|
||||
hocParent.Remove(hoc, false);
|
||||
hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
|
||||
hocParent.Add(CreateCover(hoc).With(c =>
|
||||
{
|
||||
c.RelativeSizeAxes = Axes.Both;
|
||||
c.Direction = ExpandDirection;
|
||||
c.Coverage = Coverage.Value;
|
||||
c.Coverage.BindTo(Coverage);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual PlayfieldCoveringWrapper CreateCover(Drawable content) => new PlayfieldCoveringWrapper(content);
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
}
|
||||
@@ -243,7 +243,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
bodySprite.FillMode = FillMode.Stretch;
|
||||
// i dunno this looks about right??
|
||||
bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight);
|
||||
// the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild.
|
||||
if (sprite.DrawHeight > 0)
|
||||
bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -7,7 +7,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@@ -149,7 +148,18 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <summary>
|
||||
/// Retrieves the total amount of columns across all stages in this playfield.
|
||||
/// </summary>
|
||||
public int TotalColumns => stages.Sum(s => s.Columns.Length);
|
||||
public int TotalColumns
|
||||
{
|
||||
get
|
||||
{
|
||||
int sum = 0;
|
||||
|
||||
foreach (var stage in stages)
|
||||
sum += stage.Columns.Length;
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
private Stage getStageByColumn(int column)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@@ -8,17 +10,24 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Container = osu.Framework.Graphics.Containers.Container;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="Container"/> that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield.
|
||||
/// A <see cref="Framework.Graphics.Containers.Container"/> that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield.
|
||||
/// </summary>
|
||||
public partial class PlayfieldCoveringWrapper : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The relative area that should be completely covered. This does not include the fade.
|
||||
/// </summary>
|
||||
public readonly BindableFloat Coverage = new BindableFloat();
|
||||
|
||||
/// <summary>
|
||||
/// The complete cover, including gradient and fill.
|
||||
/// </summary>
|
||||
@@ -36,6 +45,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly IBindable<ScrollingDirection> scrollDirection = new Bindable<ScrollingDirection>();
|
||||
|
||||
private float currentCoverageHeight;
|
||||
|
||||
public PlayfieldCoveringWrapper(Drawable content)
|
||||
{
|
||||
InternalChild = new BufferedContainer
|
||||
@@ -94,21 +105,46 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
scrollDirection.BindValueChanged(onScrollDirectionChanged, true);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateCoverSize(true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
updateCoverSize(false);
|
||||
}
|
||||
|
||||
private void updateCoverSize(bool instant)
|
||||
{
|
||||
float targetCoverage;
|
||||
float targetAlpha;
|
||||
|
||||
if (instant)
|
||||
{
|
||||
targetCoverage = Coverage.Value;
|
||||
targetAlpha = Coverage.Value > 0 ? 1 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetCoverage = (float)Interpolation.DampContinuously(currentCoverageHeight, Coverage.Value, 25, Math.Abs(Time.Elapsed));
|
||||
targetAlpha = (float)Interpolation.DampContinuously(gradient.Alpha, Coverage.Value > 0 ? 1 : 0, 25, Math.Abs(Time.Elapsed));
|
||||
}
|
||||
|
||||
filled.Height = GetHeight(targetCoverage);
|
||||
gradient.Y = -GetHeight(targetCoverage);
|
||||
gradient.Alpha = targetAlpha;
|
||||
|
||||
currentCoverageHeight = targetCoverage;
|
||||
}
|
||||
|
||||
protected virtual float GetHeight(float coverage) => coverage;
|
||||
|
||||
private void onScrollDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
=> cover.Rotation = direction.NewValue == ScrollingDirection.Up ? 0 : 180f;
|
||||
|
||||
/// <summary>
|
||||
/// The relative area that should be completely covered. This does not include the fade.
|
||||
/// </summary>
|
||||
public float Coverage
|
||||
{
|
||||
set
|
||||
{
|
||||
filled.Height = value;
|
||||
gradient.Y = -value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The direction in which the cover expands.
|
||||
/// </summary>
|
||||
@@ -123,11 +159,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <summary>
|
||||
/// The cover expands along the scrolling direction.
|
||||
/// </summary>
|
||||
[Description("Along scroll")]
|
||||
AlongScroll,
|
||||
|
||||
/// <summary>
|
||||
/// The cover expands against the scrolling direction.
|
||||
/// </summary>
|
||||
[Description("Against scroll")]
|
||||
AgainstScroll
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Osu.Tests.dll"
|
||||
"${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.Osu.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Debug)",
|
||||
@@ -20,7 +20,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Osu.Tests.dll"
|
||||
"${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.Osu.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Release)",
|
||||
|
||||
@@ -124,6 +124,113 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestControlClickAddsControlPointsIfSingleSliderSelected()
|
||||
{
|
||||
var firstSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
var secondSlider = new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(200, 200),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100, -100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange(new HitObject[] { secondSlider }));
|
||||
|
||||
AddStep("move mouse to middle of slider", () =>
|
||||
{
|
||||
var pos = blueprintContainer.SelectionBlueprints
|
||||
.First(s => s.Item == secondSlider)
|
||||
.ChildrenOfType<SliderBodyPiece>().First()
|
||||
.ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
InputManager.MoveMouseTo(pos);
|
||||
});
|
||||
AddStep("control-click left mouse", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
|
||||
AddAssert("slider has 3 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestControlClickDoesNotAddSliderControlPointsIfMultipleObjectsSelected()
|
||||
{
|
||||
var firstSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
var secondSlider = new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(200, 200),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100, -100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||
|
||||
AddStep("move mouse to middle of slider", () =>
|
||||
{
|
||||
var pos = blueprintContainer.SelectionBlueprints
|
||||
.First(s => s.Item == secondSlider)
|
||||
.ChildrenOfType<SliderBodyPiece>().First()
|
||||
.ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
InputManager.MoveMouseTo(pos);
|
||||
});
|
||||
AddStep("control-click left mouse", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddAssert("selection not preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
|
||||
AddAssert("second slider not selected",
|
||||
() => blueprintContainer.SelectionBlueprints.First(s => s.Item == secondSlider).IsSelected,
|
||||
() => Is.False);
|
||||
AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
private ComposeBlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
|
||||
long totalScore = scoreProcessor.TotalScore.Value * 2;
|
||||
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * scoreProcessor.GetBaseScoreForResult(new SpinnerTick().CreateJudgement().MaxResult);
|
||||
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * scoreProcessor.GetBaseScoreForResult(new SpinnerTick().Judgement.MaxResult);
|
||||
});
|
||||
|
||||
addSeekStep(0);
|
||||
|
||||
@@ -40,7 +40,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
|
||||
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
|
||||
|
||||
double flashlightRating = 0.0;
|
||||
|
||||
if (mods.Any(h => h is OsuModFlashlight))
|
||||
flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
|
||||
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
|
||||
@@ -126,13 +130,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
return new Skill[]
|
||||
var skills = new List<Skill>
|
||||
{
|
||||
new Aim(mods, true),
|
||||
new Aim(mods, false),
|
||||
new Speed(mods),
|
||||
new Flashlight(mods)
|
||||
new Speed(mods)
|
||||
};
|
||||
|
||||
if (mods.Any(h => h is OsuModFlashlight))
|
||||
skills.Add(new Flashlight(mods));
|
||||
|
||||
return skills.ToArray();
|
||||
}
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||
|
||||
@@ -232,7 +232,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
IList<HitObject> nestedObjects = slider.NestedHitObjects;
|
||||
|
||||
SliderTick? lastRealTick = slider.NestedHitObjects.OfType<SliderTick>().LastOrDefault();
|
||||
SliderTick? lastRealTick = null;
|
||||
|
||||
foreach (var hitobject in slider.NestedHitObjects)
|
||||
{
|
||||
if (hitobject is SliderTick tick)
|
||||
lastRealTick = tick;
|
||||
}
|
||||
|
||||
if (lastRealTick?.StartTime > trackingEndTime)
|
||||
{
|
||||
|
||||
+4
-3
@@ -78,9 +78,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
||||
|
||||
Scale = new Vector2(hitObject.Scale);
|
||||
|
||||
if (hitObject is IHasComboInformation combo)
|
||||
ring.BorderColour = combo.GetComboColour(skin);
|
||||
|
||||
double editorTime = editorClock.CurrentTime;
|
||||
double hitObjectTime = hitObject.StartTime;
|
||||
bool hasReachedObject = editorTime >= hitObjectTime;
|
||||
@@ -92,6 +89,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
||||
|
||||
ring.Scale = new Vector2(1 + 0.1f * ringScale);
|
||||
content.Alpha = 0.9f * (1 - alpha);
|
||||
|
||||
// TODO: should only update colour on skin/combo/object change.
|
||||
if (hitObject is IHasComboInformation combo && content.Alpha > 0)
|
||||
ring.BorderColour = combo.GetComboColour(skin);
|
||||
}
|
||||
else
|
||||
content.Alpha = 0;
|
||||
|
||||
@@ -171,7 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return false; // Allow right click to be handled by context menu
|
||||
|
||||
case MouseButton.Left:
|
||||
if (e.ControlPressed && IsSelected)
|
||||
// If there's more than two objects selected, ctrl+click should deselect
|
||||
if (e.ControlPressed && IsSelected && selectedObjects.Count < 2)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
placementControlPoint = addControlPoint(e.MousePosition);
|
||||
@@ -415,8 +416,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
|
||||
};
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
|
||||
return true;
|
||||
|
||||
if (ControlPointVisualiser == null)
|
||||
return false;
|
||||
|
||||
foreach (var p in ControlPointVisualiser.Pieces)
|
||||
{
|
||||
if (p.ReceivePositionalInputAt(screenSpacePos))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new SliderCircleOverlay(slider, position);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 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.
|
||||
|
||||
using System;
|
||||
@@ -38,12 +38,18 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
private ReplayState<OsuAction> state = null!;
|
||||
private double lastStateChangeTime;
|
||||
|
||||
private DrawableOsuRuleset ruleset = null!;
|
||||
private IPressHandler pressHandler = null!;
|
||||
|
||||
private bool hasReplay;
|
||||
private bool legacyReplay;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
ruleset = (DrawableOsuRuleset)drawableRuleset;
|
||||
|
||||
// grab the input manager for future use.
|
||||
osuInputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager;
|
||||
osuInputManager = ruleset.KeyBindingInputManager;
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
@@ -51,15 +57,22 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
if (osuInputManager.ReplayInputHandler != null)
|
||||
{
|
||||
hasReplay = true;
|
||||
|
||||
Debug.Assert(ruleset.ReplayScore != null);
|
||||
legacyReplay = ruleset.ReplayScore.ScoreInfo.IsLegacyScore;
|
||||
|
||||
pressHandler = legacyReplay ? new LegacyReplayPressHandler(this) : new PressHandler(this);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
pressHandler = new PressHandler(this);
|
||||
osuInputManager.AllowGameplayInputs = false;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
if (hasReplay)
|
||||
if (hasReplay && !legacyReplay)
|
||||
return;
|
||||
|
||||
bool requiresHold = false;
|
||||
@@ -132,11 +145,62 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
if (down)
|
||||
{
|
||||
state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton);
|
||||
pressHandler.HandlePress(wasLeft);
|
||||
wasLeft = !wasLeft;
|
||||
}
|
||||
else
|
||||
{
|
||||
pressHandler.HandleRelease(wasLeft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.Apply(osuInputManager.CurrentState, osuInputManager);
|
||||
private interface IPressHandler
|
||||
{
|
||||
void HandlePress(bool wasLeft);
|
||||
void HandleRelease(bool wasLeft);
|
||||
}
|
||||
|
||||
private class PressHandler : IPressHandler
|
||||
{
|
||||
private readonly OsuModRelax mod;
|
||||
|
||||
public PressHandler(OsuModRelax mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
public void HandlePress(bool wasLeft)
|
||||
{
|
||||
mod.state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton);
|
||||
mod.state.Apply(mod.osuInputManager.CurrentState, mod.osuInputManager);
|
||||
}
|
||||
|
||||
public void HandleRelease(bool wasLeft)
|
||||
{
|
||||
mod.state.Apply(mod.osuInputManager.CurrentState, mod.osuInputManager);
|
||||
}
|
||||
}
|
||||
|
||||
// legacy replays do not contain key-presses with Relax mod, so they need to be triggered by themselves.
|
||||
private class LegacyReplayPressHandler : IPressHandler
|
||||
{
|
||||
private readonly OsuModRelax mod;
|
||||
|
||||
public LegacyReplayPressHandler(OsuModRelax mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
public void HandlePress(bool wasLeft)
|
||||
{
|
||||
mod.osuInputManager.KeyBindingContainer.TriggerPressed(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton);
|
||||
}
|
||||
|
||||
public void HandleRelease(bool wasLeft)
|
||||
{
|
||||
// this intentionally releases right when `wasLeft` is true because `wasLeft` is set at point of press and not at point of release
|
||||
mod.osuInputManager.KeyBindingContainer.TriggerReleased(wasLeft ? OsuAction.RightButton : OsuAction.LeftButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -149,8 +148,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
StackHeightBindable.BindValueChanged(height =>
|
||||
{
|
||||
foreach (var nested in NestedHitObjects.OfType<OsuHitObject>())
|
||||
nested.StackHeight = height.NewValue;
|
||||
foreach (var nested in NestedHitObjects)
|
||||
{
|
||||
if (nested is OsuHitObject osuHitObject)
|
||||
osuHitObject.StackHeight = height.NewValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -252,18 +252,25 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
protected void UpdateNestedSamples()
|
||||
{
|
||||
var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
|
||||
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
|
||||
var sampleList = new List<HitSampleInfo>();
|
||||
// TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
|
||||
HitSampleInfo tickSample = (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault())?.With("slidertick");
|
||||
|
||||
if (firstSample != null)
|
||||
sampleList.Add(firstSample.With("slidertick"));
|
||||
foreach (var nested in NestedHitObjects)
|
||||
{
|
||||
switch (nested)
|
||||
{
|
||||
case SliderTick tick:
|
||||
tick.SamplesBindable.Clear();
|
||||
|
||||
foreach (var tick in NestedHitObjects.OfType<SliderTick>())
|
||||
tick.Samples = sampleList;
|
||||
if (tickSample != null)
|
||||
tick.SamplesBindable.Add(tickSample);
|
||||
break;
|
||||
|
||||
foreach (var repeat in NestedHitObjects.OfType<SliderRepeat>())
|
||||
repeat.Samples = this.GetNodeSamples(repeat.RepeatIndex + 1);
|
||||
case SliderRepeat repeat:
|
||||
repeat.Samples = this.GetNodeSamples(repeat.RepeatIndex + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (HeadCircle != null)
|
||||
HeadCircle.Samples = this.GetNodeSamples(0);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
public partial class OsuInputManager : RulesetInputManager<OsuAction>
|
||||
{
|
||||
public IEnumerable<OsuAction> PressedActions => KeyBindingContainer.PressedActions;
|
||||
public SlimReadOnlyListWrapper<OsuAction> PressedActions => KeyBindingContainer.PressedActions;
|
||||
|
||||
/// <summary>
|
||||
/// Whether gameplay input buttons should be allowed.
|
||||
|
||||
@@ -25,8 +25,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
Texture = textures.Get(@"Gameplay/osu/approachcircle").WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2);
|
||||
|
||||
// account for the sprite being used for the default approach circle being taken from stable,
|
||||
// when hitcircles have 5px padding on each size. this should be removed if we update the sprite.
|
||||
// In triangles and argon skins, we expanded hitcircles to take up the full 128 px which are clickable,
|
||||
// but still use the old approach circle sprite. To make it feel correct (ie. disappear as it collides
|
||||
// with the hitcircle, *not when it overlaps the border*) we need to expand it slightly.
|
||||
Scale = new Vector2(128 / 118f);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
@@ -26,10 +25,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
var texture = skin.GetTexture(@"approachcircle");
|
||||
Debug.Assert(texture != null);
|
||||
Texture = texture.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2);
|
||||
|
||||
// account for the sprite being used for the default approach circle being taken from stable,
|
||||
// when hitcircles have 5px padding on each size. this should be removed if we update the sprite.
|
||||
Scale = new Vector2(128 / 118f);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
|
||||
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
|
||||
this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
|
||||
this.ScaleTo(1f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
|
||||
.FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@@ -39,6 +40,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
{
|
||||
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||
return false;
|
||||
|
||||
if (showRipples.Value)
|
||||
{
|
||||
AddInternal(ripplePool.Get(r =>
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Taiko.Tests.dll"
|
||||
"${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.Taiko.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Debug)",
|
||||
@@ -20,7 +20,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Taiko.Tests.dll"
|
||||
"${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.Taiko.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Release)",
|
||||
|
||||
@@ -126,11 +126,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[0].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
var nestedJudgement = nested.Judgement;
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[0].CreateJudgement();
|
||||
var judgement = beatmap.HitObjects[0].Judgement;
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
@@ -159,11 +159,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[0].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
var nestedJudgement = nested.Judgement;
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[0].CreateJudgement();
|
||||
var judgement = beatmap.HitObjects[0].Judgement;
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
|
||||
@@ -31,4 +31,22 @@
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
</ItemGroup>
|
||||
<!-- osu.Framework.Android depends on https://www.nuget.org/packages/Xamarin.AndroidX.Window,
|
||||
which - via a chain of transitive dependencies - also includes https://www.nuget.org/packages/Xamarin.Jetbrains.Annotations,
|
||||
which causes compile failures such as:
|
||||
|
||||
The type 'NotNullAttribute' exists in both 'JetBrains.Annotations, Version=4242.42.42.42, Culture=neutral, PublicKeyToken=1010a0d8d6380325'
|
||||
and 'Xamarin.Jetbrains.Annotations, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' [D:\a\osu\osu\osu.Game.Tests.Android\osu.Game.Tests.Android.csproj]
|
||||
|
||||
We cannot easily change the source files, because of how this project works
|
||||
(all source files are basically symlinked from the desktop test project,
|
||||
so changing anything there just for the sake of mobile would be strange).
|
||||
Thus, apply the following "interesting" workaround as borrowed from https://stackoverflow.com/a/65127159 instead. -->
|
||||
<Target Name="AddPackageAliases" BeforeTargets="FindReferenceAssembliesForReferences;ResolveReferences">
|
||||
<ItemGroup>
|
||||
<ReferencePath Condition="%(Filename) == 'Xamarin.Jetbrains.Annotations'">
|
||||
<Aliases>XamarinJetbrainsAnnotations</Aliases>
|
||||
</ReferencePath>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
// 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 Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
[TestFixture]
|
||||
public class BeatmapUpdaterMetadataLookupTest
|
||||
{
|
||||
private Mock<IOnlineBeatmapMetadataSource> apiMetadataSourceMock = null!;
|
||||
private Mock<IOnlineBeatmapMetadataSource> localCachedMetadataSourceMock = null!;
|
||||
|
||||
private BeatmapUpdaterMetadataLookup metadataLookup = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
apiMetadataSourceMock = new Mock<IOnlineBeatmapMetadataSource>();
|
||||
localCachedMetadataSourceMock = new Mock<IOnlineBeatmapMetadataSource>();
|
||||
|
||||
metadataLookup = new BeatmapUpdaterMetadataLookup(apiMetadataSourceMock.Object, localCachedMetadataSourceMock.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalCacheQueriedFirst()
|
||||
{
|
||||
var localLookupResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 123456,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
||||
};
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: false);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata>.IsAny!), Times.Once);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out It.Ref<OnlineBeatmapMetadata>.IsAny!), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAPIQueriedSecond()
|
||||
{
|
||||
OnlineBeatmapMetadata? localLookupResult = null;
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(false);
|
||||
|
||||
var onlineLookupResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 123456,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
||||
};
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out onlineLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: false);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPreferOnlineFetch()
|
||||
{
|
||||
var localLookupResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 123456,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
||||
};
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var onlineLookupResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 123456,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Graveyard,
|
||||
BeatmapSetStatus = BeatmapOnlineStatus.Graveyard,
|
||||
};
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out onlineLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: true);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Graveyard));
|
||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.Graveyard));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Never);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPreferOnlineFetchFallsBackToLocalCacheIfOnlineSourceUnavailable()
|
||||
{
|
||||
var localLookupResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 123456,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
||||
};
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(false);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: true);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMetadataLookupFailed()
|
||||
{
|
||||
OnlineBeatmapMetadata? lookupResult = null;
|
||||
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(false);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: false);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// For the time being, if we fail to find a match in the local cache but online retrieval is not available, we trust the incoming beatmap verbatim wrt online ID.
|
||||
/// While this is suboptimal as it implicitly trusts the contents of the beatmap,
|
||||
/// throwing away the online data would be anti-user as it would make all beatmaps imported offline stop working in online.
|
||||
/// TODO: revisit if/when we have a better flow of queueing metadata retrieval.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void TestLocalMetadataLookupReturnedNoMatchAndOnlineLookupIsUnavailable([Values] bool preferOnlineFetch)
|
||||
{
|
||||
OnlineBeatmapMetadata? localLookupResult = null;
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(false);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(false);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(123456));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// For the time being, if there are no available metadata lookup sources, we trust the incoming beatmap verbatim wrt online ID.
|
||||
/// While this is suboptimal as it implicitly trusts the contents of the beatmap,
|
||||
/// throwing away the online data would be anti-user as it would make all beatmaps imported offline stop working in online.
|
||||
/// TODO: revisit if/when we have a better flow of queueing metadata retrieval.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void TestNoAvailableSources([Values] bool preferOnlineFetch)
|
||||
{
|
||||
OnlineBeatmapMetadata? lookupResult = null;
|
||||
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(false);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(false);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(false);
|
||||
apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(false);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(123456));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReturnedMetadataHasDifferentOnlineID([Values] bool preferOnlineFetch)
|
||||
{
|
||||
var lookupResult = new OnlineBeatmapMetadata { BeatmapID = 654321, BeatmapStatus = BeatmapOnlineStatus.Ranked };
|
||||
|
||||
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
|
||||
targetMock.Setup(src => src.Available).Returns(true);
|
||||
targetMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMetadataLookupForBeatmapWithoutPopulatedIDAndCorrectHash([Values] bool preferOnlineFetch)
|
||||
{
|
||||
var lookupResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 654321,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
MD5Hash = @"deadbeef",
|
||||
};
|
||||
|
||||
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
|
||||
targetMock.Setup(src => src.Available).Returns(true);
|
||||
targetMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo
|
||||
{
|
||||
MD5Hash = @"deadbeef"
|
||||
};
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(654321));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMetadataLookupForBeatmapWithoutPopulatedIDAndIncorrectHash([Values] bool preferOnlineFetch)
|
||||
{
|
||||
var lookupResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 654321,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
MD5Hash = @"cafebabe",
|
||||
};
|
||||
|
||||
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
|
||||
targetMock.Setup(src => src.Available).Returns(true);
|
||||
targetMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo
|
||||
{
|
||||
MD5Hash = @"deadbeef"
|
||||
};
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReturnedMetadataHasDifferentHash([Values] bool preferOnlineFetch)
|
||||
{
|
||||
var lookupResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 654321,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
MD5Hash = @"deadbeef"
|
||||
};
|
||||
|
||||
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
|
||||
targetMock.Setup(src => src.Available).Returns(true);
|
||||
targetMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo
|
||||
{
|
||||
OnlineID = 654321,
|
||||
MD5Hash = @"cafebabe",
|
||||
};
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(654321));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPartiallyModifiedSet([Values] bool preferOnlineFetch)
|
||||
{
|
||||
var firstResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 654321,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
||||
MD5Hash = @"cafebabe"
|
||||
};
|
||||
var secondResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 666666,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
||||
MD5Hash = @"dededede"
|
||||
};
|
||||
|
||||
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
|
||||
targetMock.Setup(src => src.Available).Returns(true);
|
||||
targetMock.Setup(src => src.TryLookup(It.Is<BeatmapInfo>(bi => bi.OnlineID == 654321), out firstResult))
|
||||
.Returns(true);
|
||||
targetMock.Setup(src => src.TryLookup(It.Is<BeatmapInfo>(bi => bi.OnlineID == 666666), out secondResult))
|
||||
.Returns(true);
|
||||
|
||||
var firstBeatmap = new BeatmapInfo
|
||||
{
|
||||
OnlineID = 654321,
|
||||
MD5Hash = @"cafebabe",
|
||||
};
|
||||
var secondBeatmap = new BeatmapInfo
|
||||
{
|
||||
OnlineID = 666666,
|
||||
MD5Hash = @"deadbeef"
|
||||
};
|
||||
var beatmapSet = new BeatmapSetInfo(new[]
|
||||
{
|
||||
firstBeatmap,
|
||||
secondBeatmap
|
||||
});
|
||||
firstBeatmap.BeatmapSet = beatmapSet;
|
||||
secondBeatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(firstBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
Assert.That(firstBeatmap.OnlineID, Is.EqualTo(654321));
|
||||
|
||||
Assert.That(secondBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(secondBeatmap.OnlineID, Is.EqualTo(666666));
|
||||
|
||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPartiallyMaliciousSet([Values] bool preferOnlineFetch)
|
||||
{
|
||||
var firstResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = 654321,
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
||||
MD5Hash = @"cafebabe"
|
||||
};
|
||||
var secondResult = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
||||
MD5Hash = @"dededede"
|
||||
};
|
||||
|
||||
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
|
||||
targetMock.Setup(src => src.Available).Returns(true);
|
||||
targetMock.Setup(src => src.TryLookup(It.Is<BeatmapInfo>(bi => bi.OnlineID == 654321), out firstResult))
|
||||
.Returns(true);
|
||||
targetMock.Setup(src => src.TryLookup(It.Is<BeatmapInfo>(bi => bi.OnlineID == 666666), out secondResult))
|
||||
.Returns(true);
|
||||
|
||||
var firstBeatmap = new BeatmapInfo
|
||||
{
|
||||
OnlineID = 654321,
|
||||
MD5Hash = @"cafebabe",
|
||||
};
|
||||
var secondBeatmap = new BeatmapInfo
|
||||
{
|
||||
OnlineID = 666666,
|
||||
MD5Hash = @"deadbeef"
|
||||
};
|
||||
var beatmapSet = new BeatmapSetInfo(new[]
|
||||
{
|
||||
firstBeatmap,
|
||||
secondBeatmap
|
||||
});
|
||||
firstBeatmap.BeatmapSet = beatmapSet;
|
||||
secondBeatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(firstBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
Assert.That(firstBeatmap.OnlineID, Is.EqualTo(654321));
|
||||
|
||||
Assert.That(secondBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(secondBeatmap.OnlineID, Is.EqualTo(-1));
|
||||
|
||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
var hitObject = new HitObject { StartTime = Time.Current };
|
||||
lifetimeEntry = new HitObjectLifetimeEntry(hitObject)
|
||||
{
|
||||
Result = new JudgementResult(hitObject, hitObject.CreateJudgement())
|
||||
Result = new JudgementResult(hitObject, hitObject.Judgement)
|
||||
{
|
||||
Type = HitResult.Great
|
||||
}
|
||||
|
||||
@@ -129,10 +129,10 @@ namespace osu.Game.Tests.Gameplay
|
||||
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||
scoreProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.LargeTickHit });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], beatmap.HitObjects[2].CreateJudgement()) { Type = HitResult.SmallTickMiss });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], beatmap.HitObjects[3].CreateJudgement()) { Type = HitResult.SmallBonus });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].Judgement) { Type = HitResult.Ok });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].Judgement) { Type = HitResult.LargeTickHit });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], beatmap.HitObjects[2].Judgement) { Type = HitResult.SmallTickMiss });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], beatmap.HitObjects[3].Judgement) { Type = HitResult.SmallBonus });
|
||||
|
||||
var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
|
||||
scoreProcessor.FailScore(score);
|
||||
@@ -169,8 +169,8 @@ namespace osu.Game.Tests.Gameplay
|
||||
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo(0));
|
||||
Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo(1));
|
||||
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.Great });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].Judgement) { Type = HitResult.Ok });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].Judgement) { Type = HitResult.Great });
|
||||
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo((double)(100 + 300) / (2 * 300)).Within(Precision.DOUBLE_EPSILON));
|
||||
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo((double)(100 + 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
|
||||
|
||||
@@ -274,10 +274,12 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyCreatorQueries()
|
||||
[TestCase("creator")]
|
||||
[TestCase("author")]
|
||||
[TestCase("mapper")]
|
||||
public void TestApplyCreatorQueries(string keyword)
|
||||
{
|
||||
const string query = "beatmap specifically by creator=my_fav";
|
||||
string query = $"beatmap specifically by {keyword}=my_fav";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("beatmap specifically by", filterCriteria.SearchText.Trim());
|
||||
@@ -452,5 +454,111 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly object[] correct_date_query_examples =
|
||||
{
|
||||
new object[] { "600" },
|
||||
new object[] { "0.5s" },
|
||||
new object[] { "120m" },
|
||||
new object[] { "48h120s" },
|
||||
new object[] { "10y24M" },
|
||||
new object[] { "10y60d120s" },
|
||||
new object[] { "0y0M2d" },
|
||||
new object[] { "1y1M2d" }
|
||||
};
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(correct_date_query_examples))]
|
||||
public void TestValidDateQueries(string dateQuery)
|
||||
{
|
||||
string query = $"played<{dateQuery} time";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
|
||||
}
|
||||
|
||||
private static readonly object[] incorrect_date_query_examples =
|
||||
{
|
||||
new object[] { ".5s" },
|
||||
new object[] { "7m27" },
|
||||
new object[] { "7m7m7m" },
|
||||
new object[] { "5s6m" },
|
||||
new object[] { "7d7y" },
|
||||
new object[] { "0:3:6" },
|
||||
new object[] { "0:3:" },
|
||||
new object[] { "\"three days\"" },
|
||||
new object[] { "0.1y0.1M2d" },
|
||||
new object[] { "0.99y0.99M2d" },
|
||||
new object[] { string.Empty }
|
||||
};
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(incorrect_date_query_examples))]
|
||||
public void TestInvalidDateQueries(string dateQuery)
|
||||
{
|
||||
string query = $"played<{dateQuery} time";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(false, filterCriteria.LastPlayed.HasFilter);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGreaterDateQuery()
|
||||
{
|
||||
const string query = "played>50";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.That(filterCriteria.LastPlayed.Max, Is.Not.Null);
|
||||
Assert.That(filterCriteria.LastPlayed.Min, Is.Null);
|
||||
// the parser internally references `DateTimeOffset.Now`, so to not make things too annoying for tests, just assume some tolerance
|
||||
// (irrelevant in proportion to the actual filter proscribed).
|
||||
Assert.That(filterCriteria.LastPlayed.Max, Is.EqualTo(DateTimeOffset.Now.AddDays(-50)).Within(TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLowerDateQuery()
|
||||
{
|
||||
const string query = "played<50";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.That(filterCriteria.LastPlayed.Max, Is.Null);
|
||||
Assert.That(filterCriteria.LastPlayed.Min, Is.Not.Null);
|
||||
// the parser internally references `DateTimeOffset.Now`, so to not make things too annoying for tests, just assume some tolerance
|
||||
// (irrelevant in proportion to the actual filter proscribed).
|
||||
Assert.That(filterCriteria.LastPlayed.Min, Is.EqualTo(DateTimeOffset.Now.AddDays(-50)).Within(TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBothSidesDateQuery()
|
||||
{
|
||||
const string query = "played>3M played<1y6M";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.That(filterCriteria.LastPlayed.Min, Is.Not.Null);
|
||||
Assert.That(filterCriteria.LastPlayed.Max, Is.Not.Null);
|
||||
// the parser internally references `DateTimeOffset.Now`, so to not make things too annoying for tests, just assume some tolerance
|
||||
// (irrelevant in proportion to the actual filter proscribed).
|
||||
Assert.That(filterCriteria.LastPlayed.Min, Is.EqualTo(DateTimeOffset.Now.AddYears(-1).AddMonths(-6)).Within(TimeSpan.FromSeconds(5)));
|
||||
Assert.That(filterCriteria.LastPlayed.Max, Is.EqualTo(DateTimeOffset.Now.AddMonths(-3)).Within(TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEqualDateQuery()
|
||||
{
|
||||
const string query = "played=50";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(false, filterCriteria.LastPlayed.HasFilter);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOutOfRangeDateQuery()
|
||||
{
|
||||
const string query = "played<10000y";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
|
||||
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
public override Container FrameStableComponents { get; }
|
||||
public override IFrameStableClock FrameStableClock { get; }
|
||||
internal override bool FrameStablePlayback { get; set; }
|
||||
public override bool AllowBackwardsSeeks { get; set; }
|
||||
public override IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
public override double GameplayStartTime { get; }
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], fourObjectBeatmap.HitObjects[i].CreateJudgement())
|
||||
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], fourObjectBeatmap.HitObjects[i].Judgement)
|
||||
{
|
||||
Type = i == 2 ? minResult : hitResult
|
||||
};
|
||||
@@ -141,7 +141,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
|
||||
for (int i = 0; i < object_count; ++i)
|
||||
{
|
||||
var judgementResult = new JudgementResult(largeBeatmap.HitObjects[i], largeBeatmap.HitObjects[i].CreateJudgement())
|
||||
var judgementResult = new JudgementResult(largeBeatmap.HitObjects[i], largeBeatmap.HitObjects[i].Judgement)
|
||||
{
|
||||
Type = HitResult.Great
|
||||
};
|
||||
@@ -325,11 +325,11 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
scoreProcessor = new TestScoreProcessor();
|
||||
scoreProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Great });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].Judgement) { Type = HitResult.Great });
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
|
||||
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.ComboBreak });
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].Judgement) { Type = HitResult.ComboBreak });
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
|
||||
}
|
||||
@@ -350,7 +350,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
|
||||
for (int i = 0; i < beatmap.HitObjects.Count; i++)
|
||||
{
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], beatmap.HitObjects[i].CreateJudgement())
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], beatmap.HitObjects[i].Judgement)
|
||||
{
|
||||
Type = i == 0 ? HitResult.Miss : HitResult.Great
|
||||
});
|
||||
@@ -441,10 +441,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
private readonly HitResult maxResult;
|
||||
private readonly HitResult? minResult;
|
||||
|
||||
public override Judgement CreateJudgement()
|
||||
{
|
||||
return new TestJudgement(maxResult, minResult);
|
||||
}
|
||||
public override Judgement CreateJudgement() => new TestJudgement(maxResult, minResult);
|
||||
|
||||
public TestHitObject(HitResult maxResult, HitResult? minResult = null)
|
||||
{
|
||||
|
||||
@@ -43,13 +43,13 @@ namespace osu.Game.Tests.Rulesets
|
||||
|
||||
AddStep("setup provider", () =>
|
||||
{
|
||||
var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin);
|
||||
|
||||
rulesetSkinProvider.Add(requester = new SkinRequester());
|
||||
|
||||
requester = new SkinRequester();
|
||||
requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("test-image");
|
||||
|
||||
Child = rulesetSkinProvider;
|
||||
Child = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin)
|
||||
{
|
||||
Child = requester
|
||||
};
|
||||
});
|
||||
|
||||
AddAssert("requester got correct initial texture", () => textureOnLoad != null);
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override bool AllowFail => false;
|
||||
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
|
||||
@@ -130,8 +130,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
|
||||
{
|
||||
mainContainer.Child = new FrameStabilityContainer(gameplayStartTime)
|
||||
.WithChild(consumer = new ClockConsumingChild()));
|
||||
{
|
||||
AllowBackwardsSeeks = true,
|
||||
}.WithChild(consumer = new ClockConsumingChild());
|
||||
});
|
||||
|
||||
private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time);
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneGameplaySamplePlayback : PlayerTestScene
|
||||
{
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
[Test]
|
||||
public void TestAllSamplesStopDuringSeek()
|
||||
{
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
|
||||
{
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
|
||||
@@ -288,6 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public override Container FrameStableComponents { get; }
|
||||
public override IFrameStableClock FrameStableClock { get; }
|
||||
internal override bool FrameStablePlayback { get; set; }
|
||||
public override bool AllowBackwardsSeeks { get; set; }
|
||||
public override IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
public override double GameplayStartTime { get; }
|
||||
|
||||
@@ -16,6 +16,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@@ -31,6 +32,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private bool gameplayClockAlwaysGoingForward = true;
|
||||
private double lastForwardCheckTime;
|
||||
|
||||
public TestScenePause()
|
||||
{
|
||||
base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both });
|
||||
@@ -67,12 +71,20 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
confirmPausedWithNoOverlay();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestForwardPlaybackGuarantee()
|
||||
{
|
||||
hookForwardPlaybackCheck();
|
||||
|
||||
AddUntilStep("wait for forward playback", () => Player.GameplayClockContainer.CurrentTime > 1000);
|
||||
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
|
||||
|
||||
checkForwardPlayback();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseWithLargeOffset()
|
||||
{
|
||||
double lastStopTime;
|
||||
bool alwaysGoingForward = true;
|
||||
|
||||
AddStep("force large offset", () =>
|
||||
{
|
||||
var offset = (BindableDouble)LocalConfig.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
@@ -82,25 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
offset.Value = -5000;
|
||||
});
|
||||
|
||||
AddStep("add time forward check hook", () =>
|
||||
{
|
||||
lastStopTime = double.MinValue;
|
||||
alwaysGoingForward = true;
|
||||
|
||||
Player.OnUpdate += _ =>
|
||||
{
|
||||
var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer;
|
||||
|
||||
double currentTime = masterClock.CurrentTime;
|
||||
|
||||
bool goingForward = currentTime >= lastStopTime;
|
||||
|
||||
alwaysGoingForward &= goingForward;
|
||||
|
||||
if (!goingForward)
|
||||
Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})");
|
||||
};
|
||||
});
|
||||
hookForwardPlaybackCheck();
|
||||
|
||||
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||
|
||||
@@ -108,11 +102,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
resumeAndConfirm();
|
||||
|
||||
AddAssert("time didn't go too far backwards", () => alwaysGoingForward);
|
||||
checkForwardPlayback();
|
||||
|
||||
AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0));
|
||||
}
|
||||
|
||||
private void checkForwardPlayback() => AddAssert("time didn't go too far backwards", () => gameplayClockAlwaysGoingForward);
|
||||
|
||||
private void hookForwardPlaybackCheck()
|
||||
{
|
||||
AddStep("add time forward check hook", () =>
|
||||
{
|
||||
lastForwardCheckTime = double.MinValue;
|
||||
gameplayClockAlwaysGoingForward = true;
|
||||
|
||||
Player.OnUpdate += _ =>
|
||||
{
|
||||
var frameStableClock = Player.ChildrenOfType<FrameStabilityContainer>().Single().Clock;
|
||||
|
||||
double currentTime = frameStableClock.CurrentTime;
|
||||
|
||||
bool goingForward = currentTime >= lastForwardCheckTime;
|
||||
lastForwardCheckTime = currentTime;
|
||||
|
||||
gameplayClockAlwaysGoingForward &= goingForward;
|
||||
|
||||
if (!goingForward)
|
||||
Logger.Log($"Went too far backwards (last stop: {lastForwardCheckTime:N1} current: {currentTime:N1})");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseResume()
|
||||
{
|
||||
|
||||
@@ -264,15 +264,23 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedNotificationMasterVolume()
|
||||
public void TestMutedNotificationLowMusicVolume()
|
||||
{
|
||||
addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.Value == 0.5);
|
||||
addVolumeSteps("master and music volumes", () =>
|
||||
{
|
||||
audioManager.Volume.Value = 0.6;
|
||||
audioManager.VolumeTrack.Value = 0.01;
|
||||
}, () => Precision.AlmostEquals(audioManager.Volume.Value, 0.6) && Precision.AlmostEquals(audioManager.VolumeTrack.Value, 0.5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedNotificationTrackVolume()
|
||||
public void TestMutedNotificationLowMasterVolume()
|
||||
{
|
||||
addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.Value == 0.5);
|
||||
addVolumeSteps("master and music volumes", () =>
|
||||
{
|
||||
audioManager.Volume.Value = 0.01;
|
||||
audioManager.VolumeTrack.Value = 0.6;
|
||||
}, () => Precision.AlmostEquals(audioManager.Volume.Value, 0.5) && Precision.AlmostEquals(audioManager.VolumeTrack.Value, 0.6));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -281,9 +289,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
addVolumeSteps("mute button", () =>
|
||||
{
|
||||
// Importantly, in the case the volume is muted but the user has a volume level set, it should be retained.
|
||||
audioManager.VolumeTrack.Value = 0.5f;
|
||||
audioManager.Volume.Value = 0.5;
|
||||
audioManager.VolumeTrack.Value = 0.5;
|
||||
volumeOverlay.IsMuted.Value = true;
|
||||
}, () => !volumeOverlay.IsMuted.Value && audioManager.VolumeTrack.Value == 0.5f);
|
||||
}, () => !volumeOverlay.IsMuted.Value && audioManager.Volume.Value == 0.5 && audioManager.VolumeTrack.Value == 0.5);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
|
||||
@@ -269,6 +269,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo));
|
||||
drawableRuleset.FrameStablePlayback = true;
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
drawableRuleset.PoolSize = poolSize;
|
||||
|
||||
Child = new Container
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -14,39 +15,47 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public partial class TestSceneStarCounter : OsuTestScene
|
||||
{
|
||||
private readonly StarCounter starCounter;
|
||||
private readonly StarCounter twentyStarCounter;
|
||||
private readonly OsuSpriteText starsLabel;
|
||||
|
||||
public TestSceneStarCounter()
|
||||
{
|
||||
starCounter = new StarCounter
|
||||
Add(new FillFlowContainer
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
};
|
||||
|
||||
Add(starCounter);
|
||||
|
||||
starsLabel = new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Scale = new Vector2(2),
|
||||
Y = 50,
|
||||
};
|
||||
|
||||
Add(starsLabel);
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
starCounter = new StarCounter(),
|
||||
twentyStarCounter = new StarCounter(20),
|
||||
starsLabel = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Scale = new Vector2(2),
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
setStars(5);
|
||||
|
||||
AddRepeatStep("random value", () => setStars(RNG.NextSingle() * (starCounter.StarCount + 1)), 10);
|
||||
AddSliderStep("exact value", 0f, 10f, 5f, setStars);
|
||||
AddStep("stop animation", () => starCounter.StopAnimation());
|
||||
AddRepeatStep("random value", () => setStars(RNG.NextSingle() * (twentyStarCounter.StarCount + 1)), 10);
|
||||
AddSliderStep("exact value", 0f, 20f, 5f, setStars);
|
||||
AddStep("stop animation", () =>
|
||||
{
|
||||
starCounter.StopAnimation();
|
||||
twentyStarCounter.StopAnimation();
|
||||
});
|
||||
AddStep("reset", () => setStars(0));
|
||||
}
|
||||
|
||||
private void setStars(float stars)
|
||||
{
|
||||
starCounter.Current = stars;
|
||||
twentyStarCounter.Current = stars;
|
||||
starsLabel.Text = starCounter.Current.ToString("0.00");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
protected new OutroPlayer Player => (OutroPlayer)base.Player;
|
||||
|
||||
private double currentBeatmapDuration;
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@@ -87,5 +92,91 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
AddStep($"Change state to {state}", () => dummyAPI.SetState(state));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTransientUserStatisticsDisplay()
|
||||
{
|
||||
AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
|
||||
AddStep("Gain", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 123_456,
|
||||
PP = 1234
|
||||
},
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357
|
||||
});
|
||||
});
|
||||
AddStep("Loss", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357
|
||||
},
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 123_456,
|
||||
PP = 1234
|
||||
});
|
||||
});
|
||||
AddStep("No change", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357
|
||||
},
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357
|
||||
});
|
||||
});
|
||||
AddStep("Was null", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = null,
|
||||
PP = null
|
||||
},
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357
|
||||
});
|
||||
});
|
||||
AddStep("Became null", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357
|
||||
},
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = null,
|
||||
PP = null
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -18,6 +19,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
@@ -221,6 +223,67 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAttemptPlayBeatmapWrongHashFails()
|
||||
{
|
||||
Screens.Select.SongSelect songSelect = null;
|
||||
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("change beatmap files", () =>
|
||||
{
|
||||
foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu"))
|
||||
{
|
||||
using (var stream = Game.Storage.GetStream(Path.Combine("files", file.File.GetStoragePath()), FileAccess.ReadWrite))
|
||||
stream.WriteByte(0);
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("invalidate cache", () =>
|
||||
{
|
||||
((IWorkingBeatmapCache)Game.BeatmapManager).Invalidate(Game.Beatmap.Value.BeatmapSetInfo);
|
||||
});
|
||||
|
||||
AddStep("select next difficulty", () => InputManager.Key(Key.Down));
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
|
||||
AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAttemptPlayBeatmapMissingFails()
|
||||
{
|
||||
Screens.Select.SongSelect songSelect = null;
|
||||
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("delete beatmap files", () =>
|
||||
{
|
||||
foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu"))
|
||||
Game.Storage.Delete(Path.Combine("files", file.File.GetStoragePath()));
|
||||
});
|
||||
|
||||
AddStep("invalidate cache", () =>
|
||||
{
|
||||
((IWorkingBeatmapCache)Game.BeatmapManager).Invalidate(Game.Beatmap.Value.BeatmapSetInfo);
|
||||
});
|
||||
|
||||
AddStep("select next difficulty", () => InputManager.Key(Key.Down));
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
|
||||
AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRetryCountIncrements()
|
||||
{
|
||||
@@ -986,6 +1049,29 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPresentBeatmapAfterDeletion()
|
||||
{
|
||||
BeatmapSetInfo beatmap = null;
|
||||
|
||||
Screens.Select.SongSelect songSelect = null;
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("delete selected beatmap", () =>
|
||||
{
|
||||
beatmap = Game.Beatmap.Value.BeatmapSetInfo;
|
||||
Game.BeatmapManager.Delete(Game.Beatmap.Value.BeatmapSetInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("nothing selected", () => Game.Beatmap.IsDefault);
|
||||
AddStep("present deleted beatmap", () => Game.PresentBeatmap(beatmap));
|
||||
AddAssert("still nothing selected", () => Game.Beatmap.IsDefault);
|
||||
}
|
||||
|
||||
private Func<Player> playToResults()
|
||||
{
|
||||
var player = playToCompletion();
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
@@ -301,6 +302,25 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
switchToGameplayScene();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesetInputDisabledWhenSkinEditorOpen()
|
||||
{
|
||||
advanceToSongSelect();
|
||||
openSkinEditor();
|
||||
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
switchToGameplayScene();
|
||||
AddUntilStep("nested input disabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().All(manager => !manager.UseParentInput));
|
||||
|
||||
toggleSkinEditor();
|
||||
AddUntilStep("nested input enabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().Any(manager => manager.UseParentInput));
|
||||
|
||||
toggleSkinEditor();
|
||||
AddUntilStep("nested input disabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().All(manager => !manager.UseParentInput));
|
||||
}
|
||||
|
||||
private void advanceToSongSelect()
|
||||
{
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
|
||||
@@ -170,6 +170,24 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPostAsOwner()
|
||||
{
|
||||
setUpCommentsResponse(getExampleComments());
|
||||
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
|
||||
|
||||
setUpPostResponse(true);
|
||||
AddStep("enter text", () => editorTextBox.Current.Value = "comm");
|
||||
AddStep("submit", () => commentsContainer.ChildrenOfType<CommentEditor>().Single().ChildrenOfType<RoundedButton>().First().TriggerClick());
|
||||
|
||||
AddUntilStep("comment sent", () =>
|
||||
{
|
||||
string writtenText = editorTextBox.Current.Value;
|
||||
var comment = commentsContainer.ChildrenOfType<DrawableComment>().LastOrDefault();
|
||||
return comment != null && comment.ChildrenOfType<SpriteText>().Any(y => y.Text == writtenText) && comment.ChildrenOfType<SpriteText>().Any(y => y.Text == "MAPPER");
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpCommentsResponse(CommentBundle commentBundle)
|
||||
=> AddStep("set up response", () =>
|
||||
{
|
||||
@@ -183,7 +201,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
};
|
||||
});
|
||||
|
||||
private void setUpPostResponse()
|
||||
private void setUpPostResponse(bool asOwner = false)
|
||||
=> AddStep("set up response", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = request =>
|
||||
@@ -191,7 +209,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
if (!(request is CommentPostRequest req))
|
||||
return false;
|
||||
|
||||
req.TriggerSuccess(new CommentBundle
|
||||
var bundle = new CommentBundle
|
||||
{
|
||||
Comments = new List<Comment>
|
||||
{
|
||||
@@ -202,9 +220,26 @@ namespace osu.Game.Tests.Visual.Online
|
||||
LegacyName = "FirstUser",
|
||||
CreatedAt = DateTimeOffset.Now,
|
||||
VotesCount = 98,
|
||||
CommentableId = 2001,
|
||||
CommentableType = "test",
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (asOwner)
|
||||
{
|
||||
bundle.Comments[0].UserId = 1001;
|
||||
bundle.Comments[0].User = new APIUser { Id = 1001, Username = "FirstUser" };
|
||||
bundle.CommentableMeta.Add(new CommentableMeta
|
||||
{
|
||||
Id = 2001,
|
||||
OwnerId = 1001,
|
||||
OwnerTitle = "MAPPER",
|
||||
Type = "test",
|
||||
});
|
||||
}
|
||||
|
||||
req.TriggerSuccess(bundle);
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,62 +4,66 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Comments;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public partial class TestSceneDrawableComment : OsuTestScene
|
||||
public partial class TestSceneDrawableComment : ThemeComparisonTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
private Container container;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
public TestSceneDrawableComment()
|
||||
: base(false)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
container = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
[TestCaseSource(nameof(comments))]
|
||||
public void TestComment(string description, string text)
|
||||
{
|
||||
AddStep(description, () =>
|
||||
{
|
||||
comment.Pinned = description == "Pinned";
|
||||
comment.Message = text;
|
||||
container.Add(new DrawableComment(comment));
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly Comment comment = new Comment
|
||||
protected override Drawable CreateContent() => new OsuScrollContainer(Direction.Vertical)
|
||||
{
|
||||
Id = 1,
|
||||
LegacyName = "Test User",
|
||||
CreatedAt = DateTimeOffset.Now,
|
||||
VotesCount = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
ChildrenEnumerable = comments.Select(info =>
|
||||
{
|
||||
var comment = new Comment
|
||||
{
|
||||
Id = 1,
|
||||
UserId = 1000,
|
||||
User = new APIUser { Id = 1000, Username = "Someone" },
|
||||
CreatedAt = DateTimeOffset.Now,
|
||||
VotesCount = 0,
|
||||
Pinned = info[0] == "Pinned",
|
||||
Message = info[1],
|
||||
CommentableId = 2001,
|
||||
CommentableType = "test"
|
||||
};
|
||||
|
||||
return new[]
|
||||
{
|
||||
new DrawableComment(comment, Array.Empty<CommentableMeta>()),
|
||||
new DrawableComment(comment, new[]
|
||||
{
|
||||
new CommentableMeta
|
||||
{
|
||||
Id = 2001,
|
||||
OwnerId = comment.UserId,
|
||||
OwnerTitle = "MAPPER",
|
||||
Type = "test",
|
||||
},
|
||||
new CommentableMeta { Title = "Other Meta" },
|
||||
}),
|
||||
};
|
||||
}).SelectMany(c => c)
|
||||
}
|
||||
};
|
||||
|
||||
private static object[] comments =
|
||||
private static readonly string[][] comments =
|
||||
{
|
||||
new[] { "Plain", "This is plain comment" },
|
||||
new[] { "Pinned", "This is pinned comment" },
|
||||
@@ -77,16 +81,17 @@ namespace osu.Game.Tests.Visual.Online
|
||||
},
|
||||
|
||||
// Taken from https://github.com/ppy/osu/issues/13993#issuecomment-885994077
|
||||
new[] { "Problematic", @"My tablet doesn't work :( It's a Huion 420 and it's apparently incompatible with OpenTablet Driver. The warning I get is: ""DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"" and it repeats 4 times on the notification before logging subsequent warnings. Checking the logs, it looks for other Huion tablets before sending the notification (e.g. ""2021-07-23 03:52:33 [verbose]: Detect: Searching for tablet 'Huion WH1409 V2' 20 2021-07-23 03:52:33 [error]: DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"") I use an Arch based installation of Linux and the tablet runs perfectly with Digimend kernel driver, with area configuration, pen pressure, etc. On osu!lazer the cursor disappears until I set it to ""Borderless"" instead of ""Fullscreen"" and even after it shows up, it goes to the bottom left corner as soon as a map starts. I have honestly 0 idea of whats going on at this point.", },
|
||||
new[]
|
||||
{
|
||||
"Problematic", @"My tablet doesn't work :(
|
||||
It's a Huion 420 and it's apparently incompatible with OpenTablet Driver. The warning I get is: ""DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"" and it repeats 4 times on the notification before logging subsequent warnings.
|
||||
Checking the logs, it looks for other Huion tablets before sending the notification (e.g.
|
||||
""2021-07-23 03:52:33 [verbose]: Detect: Searching for tablet 'Huion WH1409 V2'
|
||||
20 2021-07-23 03:52:33 [error]: DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"")
|
||||
I use an Arch based installation of Linux and the tablet runs perfectly with Digimend kernel driver, with area configuration, pen pressure, etc. On osu!lazer the cursor disappears until I set it to ""Borderless"" instead of ""Fullscreen"" and even after it shows up, it goes to the bottom left corner as soon as a map starts.
|
||||
I have honestly 0 idea of whats going on at this point."
|
||||
}
|
||||
"Code Block", @"User not found! ;_;
|
||||
|
||||
There are a few possible reasons for this:
|
||||
|
||||
They may have changed their username.
|
||||
The account may be temporarily unavailable due to security or abuse issues.
|
||||
You may have made a typo!"
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,19 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnrankedPP()
|
||||
{
|
||||
AddStep("Load scores with unranked PP", () =>
|
||||
{
|
||||
var allScores = createScores();
|
||||
allScores.Scores[0].Ranked = false;
|
||||
allScores.UserScore = createUserBest();
|
||||
allScores.UserScore.Score.Ranked = false;
|
||||
scoresContainer.Scores = allScores;
|
||||
});
|
||||
}
|
||||
|
||||
private ulong onlineID = 1;
|
||||
|
||||
private APIScoresCollection createScores()
|
||||
@@ -184,6 +197,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
MaxCombo = 1234,
|
||||
TotalScore = 1234567890,
|
||||
Accuracy = 1,
|
||||
Ranked = true,
|
||||
},
|
||||
new SoloScoreInfo
|
||||
{
|
||||
@@ -206,6 +220,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
MaxCombo = 1234,
|
||||
TotalScore = 1234789,
|
||||
Accuracy = 0.9997,
|
||||
Ranked = true,
|
||||
},
|
||||
new SoloScoreInfo
|
||||
{
|
||||
@@ -227,6 +242,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
MaxCombo = 1234,
|
||||
TotalScore = 12345678,
|
||||
Accuracy = 0.9854,
|
||||
Ranked = true,
|
||||
},
|
||||
new SoloScoreInfo
|
||||
{
|
||||
@@ -247,6 +263,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
MaxCombo = 1234,
|
||||
TotalScore = 1234567,
|
||||
Accuracy = 0.8765,
|
||||
Ranked = true,
|
||||
},
|
||||
new SoloScoreInfo
|
||||
{
|
||||
@@ -263,6 +280,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
MaxCombo = 1234,
|
||||
TotalScore = 123456,
|
||||
Accuracy = 0.6543,
|
||||
Ranked = true,
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -309,6 +327,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
MaxCombo = 1234,
|
||||
TotalScore = 123456,
|
||||
Accuracy = 0.6543,
|
||||
Ranked = true,
|
||||
},
|
||||
Position = 1337,
|
||||
};
|
||||
|
||||
@@ -35,8 +35,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
private Action<GetUsersRequest>? handleGetUsersRequest;
|
||||
private Action<GetUserRequest>? handleGetUserRequest;
|
||||
|
||||
private IDisposable? subscription;
|
||||
|
||||
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
|
||||
|
||||
[SetUpSteps]
|
||||
@@ -252,26 +250,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStatisticsUpdateNotFiredAfterSubscriptionDisposal()
|
||||
{
|
||||
int userId = getUserId();
|
||||
setUpUser(userId);
|
||||
|
||||
long scoreId = getScoreId();
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
AddStep("unsubscribe", () => subscription!.Dispose());
|
||||
|
||||
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||
|
||||
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
|
||||
AddWaitStep("wait a bit", 5);
|
||||
AddAssert("update not received", () => update == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGlobalStatisticsUpdatedAfterRegistrationAddedAndScoreProcessed()
|
||||
{
|
||||
@@ -312,13 +290,20 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
|
||||
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> onUpdateReady) =>
|
||||
AddStep("register for updates", () => subscription = watcher.RegisterForStatisticsUpdateAfter(
|
||||
new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
|
||||
AddStep("register for updates", () =>
|
||||
{
|
||||
watcher.RegisterForStatisticsUpdateAfter(
|
||||
new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
|
||||
{
|
||||
Ruleset = rulesetInfo,
|
||||
OnlineID = scoreId
|
||||
});
|
||||
watcher.LatestUpdate.BindValueChanged(update =>
|
||||
{
|
||||
Ruleset = rulesetInfo,
|
||||
OnlineID = scoreId
|
||||
},
|
||||
onUpdateReady));
|
||||
if (update.NewValue?.Score.OnlineID == scoreId)
|
||||
onUpdateReady.Invoke(update.NewValue);
|
||||
});
|
||||
});
|
||||
|
||||
private void feignScoreProcessing(int userId, RulesetInfo rulesetInfo, long newTotalScore)
|
||||
=> AddStep("feign score processing", () => serverSideStatistics[(userId, rulesetInfo.ShortName)] = new UserStatistics { TotalScore = newTotalScore });
|
||||
|
||||
@@ -40,7 +40,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
new APIMod { Acronym = new OsuModHardRock().Acronym },
|
||||
new APIMod { Acronym = new OsuModDoubleTime().Acronym },
|
||||
},
|
||||
Accuracy = 0.9813
|
||||
Accuracy = 0.9813,
|
||||
Ranked = true,
|
||||
};
|
||||
|
||||
var secondScore = new SoloScoreInfo
|
||||
@@ -62,7 +63,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
new APIMod { Acronym = new OsuModHardRock().Acronym },
|
||||
new APIMod { Acronym = new OsuModDoubleTime().Acronym },
|
||||
},
|
||||
Accuracy = 0.998546
|
||||
Accuracy = 0.998546,
|
||||
Ranked = true,
|
||||
};
|
||||
|
||||
var thirdScore = new SoloScoreInfo
|
||||
@@ -79,7 +81,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
DifficultyName = "Insane"
|
||||
},
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Accuracy = 0.9726
|
||||
Accuracy = 0.9726,
|
||||
Ranked = true,
|
||||
};
|
||||
|
||||
var noPPScore = new SoloScoreInfo
|
||||
@@ -95,7 +98,26 @@ namespace osu.Game.Tests.Visual.Online
|
||||
DifficultyName = "[4K] Cataclysmic Hypernova"
|
||||
},
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Accuracy = 0.55879
|
||||
Accuracy = 0.55879,
|
||||
Ranked = true,
|
||||
};
|
||||
|
||||
var lovedScore = new SoloScoreInfo
|
||||
{
|
||||
Rank = ScoreRank.B,
|
||||
Beatmap = new APIBeatmap
|
||||
{
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Title = "C18H27NO3(extend)",
|
||||
Artist = "Team Grimoire",
|
||||
},
|
||||
DifficultyName = "[4K] Cataclysmic Hypernova",
|
||||
Status = BeatmapOnlineStatus.Loved,
|
||||
},
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Accuracy = 0.55879,
|
||||
Ranked = true,
|
||||
};
|
||||
|
||||
var unprocessedPPScore = new SoloScoreInfo
|
||||
@@ -112,7 +134,26 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Status = BeatmapOnlineStatus.Ranked,
|
||||
},
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Accuracy = 0.55879
|
||||
Accuracy = 0.55879,
|
||||
Ranked = true,
|
||||
};
|
||||
|
||||
var unrankedPPScore = new SoloScoreInfo
|
||||
{
|
||||
Rank = ScoreRank.B,
|
||||
Beatmap = new APIBeatmap
|
||||
{
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Title = "C18H27NO3(extend)",
|
||||
Artist = "Team Grimoire",
|
||||
},
|
||||
DifficultyName = "[4K] Cataclysmic Hypernova",
|
||||
Status = BeatmapOnlineStatus.Ranked,
|
||||
},
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Accuracy = 0.55879,
|
||||
Ranked = false,
|
||||
};
|
||||
|
||||
Add(new FillFlowContainer
|
||||
@@ -128,7 +169,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
new ColourProvidedContainer(OverlayColourScheme.Green, new DrawableProfileScore(firstScore)),
|
||||
new ColourProvidedContainer(OverlayColourScheme.Green, new DrawableProfileScore(secondScore)),
|
||||
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileScore(noPPScore)),
|
||||
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileScore(lovedScore)),
|
||||
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileScore(unprocessedPPScore)),
|
||||
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileScore(unrankedPPScore)),
|
||||
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(firstScore, 0.97)),
|
||||
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(secondScore, 0.85)),
|
||||
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(thirdScore, 0.66)),
|
||||
|
||||
@@ -629,7 +629,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
var sets = new List<BeatmapSetInfo>();
|
||||
|
||||
const string zzz_string = "zzzzz";
|
||||
const string zzz_lowercase = "zzzzz";
|
||||
const string zzz_uppercase = "ZZZZZ";
|
||||
|
||||
AddStep("Populuate beatmap sets", () =>
|
||||
{
|
||||
@@ -640,10 +641,16 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
var set = TestResources.CreateTestBeatmapSetInfo();
|
||||
|
||||
if (i == 4)
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_uppercase);
|
||||
|
||||
if (i == 8)
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_lowercase);
|
||||
|
||||
if (i == 12)
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_uppercase);
|
||||
|
||||
if (i == 16)
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_string);
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_lowercase);
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
@@ -652,9 +659,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
|
||||
AddAssert($"Check {zzz_string} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_string);
|
||||
AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_uppercase);
|
||||
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Author.Username == zzz_lowercase);
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
AddAssert($"Check {zzz_string} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_string);
|
||||
AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_uppercase);
|
||||
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Artist == zzz_lowercase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -282,7 +282,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
/// <summary>
|
||||
/// Check that the logo is tracking the position of the facade, with an acceptable precision lenience.
|
||||
/// </summary>
|
||||
public bool IsLogoTracking => Precision.AlmostEquals(Logo.Position, ComputeLogoTrackingPosition());
|
||||
public bool IsLogoTracking => Precision.AlmostEquals(Logo!.Position, ComputeLogoTrackingPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.States;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
@@ -18,13 +17,22 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
public partial class TestSceneOsuDropdown : ThemeComparisonTestScene
|
||||
{
|
||||
protected override Drawable CreateContent() =>
|
||||
new OsuEnumDropdown<BeatmapOnlineStatus>
|
||||
new OsuEnumDropdown<TestEnum>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 150
|
||||
};
|
||||
|
||||
private enum TestEnum
|
||||
{
|
||||
[System.ComponentModel.Description("Option")]
|
||||
Option,
|
||||
|
||||
[System.ComponentModel.Description("Really lonnnnnnng option")]
|
||||
ReallyLongOption,
|
||||
}
|
||||
|
||||
[Test]
|
||||
// todo: this can be written much better if ThemeComparisonTestScene has a manual input manager
|
||||
public void TestBackAction()
|
||||
@@ -43,7 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
|
||||
AddAssert("closed", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Closed);
|
||||
|
||||
OsuEnumDropdown<BeatmapOnlineStatus> dropdown() => this.ChildrenOfType<OsuEnumDropdown<BeatmapOnlineStatus>>().First();
|
||||
OsuEnumDropdown<TestEnum> dropdown() => this.ChildrenOfType<OsuEnumDropdown<TestEnum>>().First();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,31 +14,39 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public abstract partial class ThemeComparisonTestScene : OsuGridTestScene
|
||||
{
|
||||
protected ThemeComparisonTestScene()
|
||||
: base(1, 2)
|
||||
private readonly bool showWithoutColourProvider;
|
||||
|
||||
protected ThemeComparisonTestScene(bool showWithoutColourProvider = true)
|
||||
: base(1, showWithoutColourProvider ? 2 : 1)
|
||||
{
|
||||
this.showWithoutColourProvider = showWithoutColourProvider;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Cell(0, 0).AddRange(new[]
|
||||
if (showWithoutColourProvider)
|
||||
{
|
||||
new Box
|
||||
Cell(0, 0).AddRange(new[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.GreySeaFoam
|
||||
},
|
||||
CreateContent()
|
||||
});
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.GreySeaFoam
|
||||
},
|
||||
CreateContent()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected void CreateThemedContent(OverlayColourScheme colourScheme)
|
||||
{
|
||||
var colourProvider = new OverlayColourProvider(colourScheme);
|
||||
|
||||
Cell(0, 1).Clear();
|
||||
Cell(0, 1).Add(new DependencyProvidingContainer
|
||||
int col = showWithoutColourProvider ? 1 : 0;
|
||||
|
||||
Cell(0, col).Clear();
|
||||
Cell(0, col).Add(new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[]
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Debug/net6.0/osu.Game.Tournament.Tests.dll"
|
||||
"${workspaceRoot}/bin/Debug/net8.0/osu.Game.Tournament.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Debug)",
|
||||
@@ -20,7 +20,7 @@
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/bin/Release/net6.0/osu.Game.Tournament.Tests.dll"
|
||||
"${workspaceRoot}/bin/Release/net8.0/osu.Game.Tournament.Tests.dll"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build (Release)",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Diagnostics;
|
||||
using ManagedBass.Fx;
|
||||
using osu.Framework.Audio.Mixing;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
namespace osu.Game.Audio.Effects
|
||||
@@ -22,6 +23,8 @@ namespace osu.Game.Audio.Effects
|
||||
|
||||
private bool isAttached;
|
||||
|
||||
private readonly Cached filterApplication = new Cached();
|
||||
|
||||
private int cutoff;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,7 +39,7 @@ namespace osu.Game.Audio.Effects
|
||||
return;
|
||||
|
||||
cutoff = value;
|
||||
updateFilter(cutoff);
|
||||
filterApplication.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +64,17 @@ namespace osu.Game.Audio.Effects
|
||||
Cutoff = getInitialCutoff(type);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!filterApplication.IsValid)
|
||||
{
|
||||
updateFilter(cutoff);
|
||||
filterApplication.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private int getInitialCutoff(BQFType type)
|
||||
{
|
||||
switch (type)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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;
|
||||
using System.Diagnostics;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs online metadata lookups using the osu-web API.
|
||||
/// </summary>
|
||||
public class APIBeatmapMetadataSource : IOnlineBeatmapMetadataSource
|
||||
{
|
||||
private readonly IAPIProvider api;
|
||||
|
||||
public APIBeatmapMetadataSource(IAPIProvider api)
|
||||
{
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
public bool Available => api.State.Value == APIState.Online;
|
||||
|
||||
public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
if (!Available)
|
||||
{
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
var req = new GetBeatmapRequest(beatmapInfo);
|
||||
|
||||
try
|
||||
{
|
||||
// intentionally blocking to limit web request concurrency
|
||||
api.Perform(req);
|
||||
|
||||
if (req.CompletionState == APIRequestCompletionState.Failed)
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Online retrieval failed for {beatmapInfo}");
|
||||
onlineMetadata = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
var res = req.Response;
|
||||
|
||||
if (res != null)
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = res.OnlineID,
|
||||
BeatmapSetID = res.OnlineBeatmapSetID,
|
||||
AuthorID = res.AuthorID,
|
||||
BeatmapStatus = res.Status,
|
||||
BeatmapSetStatus = res.BeatmapSet?.Status,
|
||||
DateRanked = res.BeatmapSet?.Ranked,
|
||||
DateSubmitted = res.BeatmapSet?.Submitted,
|
||||
MD5Hash = res.MD5Hash,
|
||||
LastUpdated = res.LastUpdated
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Online retrieval failed for {beatmapInfo} ({e.Message})");
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void logForModel(BeatmapSetInfo set, string message) =>
|
||||
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $@"[{nameof(APIBeatmapMetadataSource)}] {message}");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
private TextureUpload limitTextureUploadSize(TextureUpload textureUpload)
|
||||
{
|
||||
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
|
||||
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
||||
@@ -1,62 +1,32 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SQLitePCL;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// A component which handles population of online IDs for beatmaps using a two part lookup procedure.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to <see cref="cache_database_name"/>) will be downloaded if not already present locally.
|
||||
/// This will always be checked before doing a second online query to get required metadata.
|
||||
/// </remarks>
|
||||
public class BeatmapUpdaterMetadataLookup : IDisposable
|
||||
{
|
||||
private readonly IAPIProvider api;
|
||||
private readonly Storage storage;
|
||||
|
||||
private FileWebRequest cacheDownloadRequest;
|
||||
|
||||
private const string cache_database_name = "online.db";
|
||||
private readonly IOnlineBeatmapMetadataSource apiMetadataSource;
|
||||
private readonly IOnlineBeatmapMetadataSource localCachedMetadataSource;
|
||||
|
||||
public BeatmapUpdaterMetadataLookup(IAPIProvider api, Storage storage)
|
||||
: this(new APIBeatmapMetadataSource(api), new LocalCachedBeatmapMetadataSource(storage))
|
||||
{
|
||||
try
|
||||
{
|
||||
// required to initialise native SQLite libraries on some platforms.
|
||||
Batteries_V2.Init();
|
||||
raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// may fail if platform not supported.
|
||||
}
|
||||
}
|
||||
|
||||
this.api = api;
|
||||
this.storage = storage;
|
||||
|
||||
// avoid downloading / using cache for unit tests.
|
||||
if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name))
|
||||
prepareLocalCache();
|
||||
internal BeatmapUpdaterMetadataLookup(IOnlineBeatmapMetadataSource apiMetadataSource, IOnlineBeatmapMetadataSource localCachedMetadataSource)
|
||||
{
|
||||
this.apiMetadataSource = apiMetadataSource;
|
||||
this.localCachedMetadataSource = localCachedMetadataSource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,205 +39,96 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param>
|
||||
public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch)
|
||||
{
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
lookup(beatmapSet, b, preferOnlineFetch);
|
||||
}
|
||||
var lookupResults = new List<OnlineBeatmapMetadata?>();
|
||||
|
||||
private void lookup(BeatmapSetInfo set, BeatmapInfo beatmapInfo, bool preferOnlineFetch)
|
||||
{
|
||||
bool apiAvailable = api?.State.Value == APIState.Online;
|
||||
|
||||
bool useLocalCache = !apiAvailable || !preferOnlineFetch;
|
||||
|
||||
if (useLocalCache && checkLocalCache(set, beatmapInfo))
|
||||
return;
|
||||
|
||||
if (!apiAvailable)
|
||||
return;
|
||||
|
||||
var req = new GetBeatmapRequest(beatmapInfo);
|
||||
|
||||
try
|
||||
foreach (var beatmapInfo in beatmapSet.Beatmaps)
|
||||
{
|
||||
// intentionally blocking to limit web request concurrency
|
||||
api.Perform(req);
|
||||
if (!tryLookup(beatmapInfo, preferOnlineFetch, out var res))
|
||||
continue;
|
||||
|
||||
if (req.CompletionState == APIRequestCompletionState.Failed)
|
||||
if (res == null || shouldDiscardLookupResult(res, beatmapInfo))
|
||||
{
|
||||
logForModel(set, $"Online retrieval failed for {beatmapInfo}");
|
||||
beatmapInfo.ResetOnlineInfo();
|
||||
return;
|
||||
lookupResults.Add(null); // mark lookup failure
|
||||
continue;
|
||||
}
|
||||
|
||||
var res = req.Response;
|
||||
lookupResults.Add(res);
|
||||
|
||||
if (res != null)
|
||||
beatmapInfo.OnlineID = res.BeatmapID;
|
||||
beatmapInfo.OnlineMD5Hash = res.MD5Hash;
|
||||
beatmapInfo.LastOnlineUpdate = res.LastUpdated;
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
beatmapInfo.BeatmapSet.OnlineID = res.BeatmapSetID;
|
||||
|
||||
// Some metadata should only be applied if there's no local changes.
|
||||
if (beatmapInfo.MatchesOnlineVersion)
|
||||
{
|
||||
beatmapInfo.OnlineID = res.OnlineID;
|
||||
beatmapInfo.OnlineMD5Hash = res.MD5Hash;
|
||||
beatmapInfo.LastOnlineUpdate = res.LastUpdated;
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
|
||||
|
||||
// Some metadata should only be applied if there's no local changes.
|
||||
if (shouldSaveOnlineMetadata(beatmapInfo))
|
||||
{
|
||||
beatmapInfo.Status = res.Status;
|
||||
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
|
||||
}
|
||||
|
||||
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
|
||||
{
|
||||
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
|
||||
beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked;
|
||||
beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted;
|
||||
}
|
||||
|
||||
logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
|
||||
beatmapInfo.Status = res.BeatmapStatus;
|
||||
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
if (beatmapSet.Beatmaps.All(b => b.MatchesOnlineVersion)
|
||||
&& lookupResults.All(r => r != null)
|
||||
&& lookupResults.Select(r => r!.BeatmapSetID).Distinct().Count() == 1)
|
||||
{
|
||||
logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})");
|
||||
beatmapInfo.ResetOnlineInfo();
|
||||
var representative = lookupResults.First()!;
|
||||
|
||||
beatmapSet.Status = representative.BeatmapSetStatus ?? BeatmapOnlineStatus.None;
|
||||
beatmapSet.DateRanked = representative.DateRanked;
|
||||
beatmapSet.DateSubmitted = representative.DateSubmitted;
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareLocalCache()
|
||||
private bool shouldDiscardLookupResult(OnlineBeatmapMetadata result, BeatmapInfo beatmapInfo)
|
||||
{
|
||||
string cacheFilePath = storage.GetFullPath(cache_database_name);
|
||||
string compressedCacheFilePath = $"{cacheFilePath}.bz2";
|
||||
if (beatmapInfo.OnlineID > 0 && result.BeatmapID != beatmapInfo.OnlineID)
|
||||
return true;
|
||||
|
||||
cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}");
|
||||
|
||||
cacheDownloadRequest.Failed += ex =>
|
||||
{
|
||||
File.Delete(compressedCacheFilePath);
|
||||
File.Delete(cacheFilePath);
|
||||
|
||||
Logger.Log($"{nameof(BeatmapUpdaterMetadataLookup)}'s online cache download failed: {ex}", LoggingTarget.Database);
|
||||
};
|
||||
|
||||
cacheDownloadRequest.Finished += () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = File.OpenRead(cacheDownloadRequest.Filename))
|
||||
using (var outStream = File.OpenWrite(cacheFilePath))
|
||||
using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false))
|
||||
bz2.CopyTo(outStream);
|
||||
|
||||
// set to null on completion to allow lookups to begin using the new source
|
||||
cacheDownloadRequest = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"{nameof(BeatmapUpdaterMetadataLookup)}'s online cache extraction failed: {ex}", LoggingTarget.Database);
|
||||
File.Delete(cacheFilePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(compressedCacheFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await cacheDownloadRequest.PerformAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Prevent throwing unobserved exceptions, as they will be logged from the network request to the log file anyway.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo)
|
||||
{
|
||||
// download is in progress (or was, and failed).
|
||||
if (cacheDownloadRequest != null)
|
||||
return false;
|
||||
|
||||
// database is unavailable.
|
||||
if (!storage.Exists(cache_database_name))
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
|
||||
&& string.IsNullOrEmpty(beatmapInfo.Path)
|
||||
&& beatmapInfo.OnlineID <= 0)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = new SqliteConnection(string.Concat("Data Source=", storage.GetFullPath($@"{"online.db"}", true))))
|
||||
{
|
||||
db.Open();
|
||||
|
||||
using (var cmd = db.CreateCommand())
|
||||
{
|
||||
cmd.CommandText =
|
||||
"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter("@Path", beatmapInfo.Path));
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
var status = (BeatmapOnlineStatus)reader.GetByte(2);
|
||||
|
||||
// Some metadata should only be applied if there's no local changes.
|
||||
if (shouldSaveOnlineMetadata(beatmapInfo))
|
||||
{
|
||||
beatmapInfo.Status = status;
|
||||
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
|
||||
}
|
||||
|
||||
// TODO: DateSubmitted and DateRanked are not provided by local cache.
|
||||
beatmapInfo.OnlineID = reader.GetInt32(1);
|
||||
beatmapInfo.OnlineMD5Hash = reader.GetString(4);
|
||||
beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5);
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
|
||||
|
||||
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
|
||||
{
|
||||
beatmapInfo.BeatmapSet.Status = status;
|
||||
}
|
||||
|
||||
logForModel(set, $"Cached local retrieval for {beatmapInfo}.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logForModel(set, $"Cached local retrieval for {beatmapInfo} failed with {ex}.");
|
||||
}
|
||||
if (beatmapInfo.OnlineID == -1 && result.MD5Hash != beatmapInfo.MD5Hash)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void logForModel(BeatmapSetInfo set, string message) =>
|
||||
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $"[{nameof(BeatmapUpdaterMetadataLookup)}] {message}");
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the provided beatmap is in a state where online "ranked" status metadata should be saved against it.
|
||||
/// Handles the case where a user may have locally modified a beatmap in the editor and expects the local status to stick.
|
||||
/// Attempts to retrieve the <see cref="OnlineBeatmapMetadata"/> for the given <paramref name="beatmapInfo"/>.
|
||||
/// </summary>
|
||||
private static bool shouldSaveOnlineMetadata(BeatmapInfo beatmapInfo) => beatmapInfo.MatchesOnlineVersion || beatmapInfo.Status != BeatmapOnlineStatus.LocallyModified;
|
||||
/// <param name="beatmapInfo">The beatmap to perform the online lookup for.</param>
|
||||
/// <param name="preferOnlineFetch">Whether online sources should be preferred for the lookup.</param>
|
||||
/// <param name="result">The result of the lookup. Can be <see langword="null"/> if no matching beatmap was found (or the lookup failed).</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if any of the metadata sources were available and returned a valid <paramref name="result"/>.
|
||||
/// <see langword="false"/> if none of the metadata sources were available, or if there was insufficient data to return a valid <paramref name="result"/>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// There are two cases wherein this method will return <see langword="false"/>:
|
||||
/// <list type="bullet">
|
||||
/// <item>If neither the local cache or the API are available to query.</item>
|
||||
/// <item>If the API is not available to query, and a positive match was not made in the local cache.</item>
|
||||
/// </list>
|
||||
/// In either case, the online ID read from the .osu file will be preserved, which may not necessarily be what we want.
|
||||
/// TODO: reconsider this if/when a better flow for queueing online retrieval is implemented.
|
||||
/// </remarks>
|
||||
private bool tryLookup(BeatmapInfo beatmapInfo, bool preferOnlineFetch, out OnlineBeatmapMetadata? result)
|
||||
{
|
||||
bool useLocalCache = !apiMetadataSource.Available || !preferOnlineFetch;
|
||||
if (useLocalCache && localCachedMetadataSource.TryLookup(beatmapInfo, out result))
|
||||
return true;
|
||||
|
||||
if (apiMetadataSource.TryLookup(beatmapInfo, out result))
|
||||
return true;
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
cacheDownloadRequest?.Dispose();
|
||||
apiMetadataSource.Dispose();
|
||||
localCachedMetadataSource.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
private readonly OffsetCorrectionClock? userGlobalOffsetClock;
|
||||
private readonly OffsetCorrectionClock? platformOffsetClock;
|
||||
private readonly OffsetCorrectionClock? userBeatmapOffsetClock;
|
||||
private readonly FramedOffsetClock? userBeatmapOffsetClock;
|
||||
|
||||
private readonly IFrameBasedClock finalClockSource;
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps
|
||||
private IDisposable? beatmapOffsetSubscription;
|
||||
|
||||
private readonly DecouplingFramedClock decoupledTrack;
|
||||
private readonly InterpolatingFramedClock interpolatedTrack;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
@@ -58,7 +59,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
// 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).
|
||||
var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
|
||||
interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
|
||||
|
||||
if (applyOffsets)
|
||||
{
|
||||
@@ -70,7 +71,7 @@ namespace osu.Game.Beatmaps
|
||||
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock);
|
||||
|
||||
// User per-beatmap offset will be applied to this final clock.
|
||||
finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock);
|
||||
finalClockSource = userBeatmapOffsetClock = new FramedOffsetClock(userGlobalOffsetClock);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -122,7 +123,7 @@ namespace osu.Game.Beatmaps
|
||||
Debug.Assert(userBeatmapOffsetClock != null);
|
||||
Debug.Assert(platformOffsetClock != null);
|
||||
|
||||
return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset;
|
||||
return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.Offset + platformOffsetClock.RateAdjustedOffset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,5 +191,28 @@ namespace osu.Game.Beatmaps
|
||||
base.Dispose(isDisposing);
|
||||
beatmapOffsetSubscription?.Dispose();
|
||||
}
|
||||
|
||||
public string GetSnapshot()
|
||||
{
|
||||
return
|
||||
$"originalSource: {output(Source)}\n" +
|
||||
$"userGlobalOffsetClock: {output(userGlobalOffsetClock)}\n" +
|
||||
$"platformOffsetClock: {output(platformOffsetClock)}\n" +
|
||||
$"userBeatmapOffsetClock: {output(userBeatmapOffsetClock)}\n" +
|
||||
$"interpolatedTrack: {output(interpolatedTrack)}\n" +
|
||||
$"decoupledTrack: {output(decoupledTrack)}\n" +
|
||||
$"finalClockSource: {output(finalClockSource)}\n";
|
||||
|
||||
string output(IClock? clock)
|
||||
{
|
||||
if (clock == null)
|
||||
return "null";
|
||||
|
||||
if (clock is IFrameBasedClock framed)
|
||||
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate} elapsed: {framed.ElapsedFrameTime:N2}";
|
||||
|
||||
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
static void addCombo(HitObject hitObject, ref int combo)
|
||||
{
|
||||
if (hitObject.CreateJudgement().MaxResult.AffectsCombo())
|
||||
if (hitObject.Judgement.MaxResult.AffectsCombo())
|
||||
combo++;
|
||||
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Unifying interface for sources of <see cref="OnlineBeatmapMetadata"/>.
|
||||
/// </summary>
|
||||
public interface IOnlineBeatmapMetadataSource : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this source can currently service lookups.
|
||||
/// </summary>
|
||||
bool Available { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Looks up the online metadata for the supplied <paramref name="beatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to look up.</param>
|
||||
/// <param name="onlineMetadata">
|
||||
/// An <see cref="OnlineBeatmapMetadata"/> instance if the lookup is successful.
|
||||
/// <see langword="null"/> if a mismatch between the local instance and the looked-up data was detected.
|
||||
/// The returned value is only valid if the return value of the method is <see langword="true"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Whether the lookup was performed.
|
||||
/// </returns>
|
||||
bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// 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;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SQLitePCL;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs online metadata lookups using a copy of a database containing metadata for a large subset of beatmaps (stored to <see cref="cache_database_name"/>).
|
||||
/// The database will be asynchronously downloaded - if not already present locally - when this component is constructed.
|
||||
/// </summary>
|
||||
public class LocalCachedBeatmapMetadataSource : IOnlineBeatmapMetadataSource
|
||||
{
|
||||
private readonly Storage storage;
|
||||
|
||||
private FileWebRequest? cacheDownloadRequest;
|
||||
|
||||
private const string cache_database_name = @"online.db";
|
||||
|
||||
public LocalCachedBeatmapMetadataSource(Storage storage)
|
||||
{
|
||||
try
|
||||
{
|
||||
// required to initialise native SQLite libraries on some platforms.
|
||||
Batteries_V2.Init();
|
||||
raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// may fail if platform not supported.
|
||||
}
|
||||
|
||||
this.storage = storage;
|
||||
|
||||
// avoid downloading / using cache for unit tests.
|
||||
if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name))
|
||||
prepareLocalCache();
|
||||
}
|
||||
|
||||
public bool Available =>
|
||||
// no download in progress.
|
||||
cacheDownloadRequest == null
|
||||
// cached database exists on disk.
|
||||
&& storage.Exists(cache_database_name);
|
||||
|
||||
public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
if (!Available)
|
||||
{
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
|
||||
&& string.IsNullOrEmpty(beatmapInfo.Path)
|
||||
&& beatmapInfo.OnlineID <= 0)
|
||||
{
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))))
|
||||
{
|
||||
db.Open();
|
||||
|
||||
using (var cmd = db.CreateCommand())
|
||||
{
|
||||
cmd.CommandText =
|
||||
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo}.");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapSetID = reader.GetInt32(0),
|
||||
BeatmapID = reader.GetInt32(1),
|
||||
BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
AuthorID = reader.GetInt32(3),
|
||||
MD5Hash = reader.GetString(4),
|
||||
LastUpdated = reader.GetDateTimeOffset(5),
|
||||
// TODO: DateSubmitted and DateRanked are not provided by local cache.
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with {ex}.");
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void prepareLocalCache()
|
||||
{
|
||||
string cacheFilePath = storage.GetFullPath(cache_database_name);
|
||||
string compressedCacheFilePath = $@"{cacheFilePath}.bz2";
|
||||
|
||||
cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $@"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}");
|
||||
|
||||
cacheDownloadRequest.Failed += ex =>
|
||||
{
|
||||
File.Delete(compressedCacheFilePath);
|
||||
File.Delete(cacheFilePath);
|
||||
|
||||
Logger.Log($@"{nameof(BeatmapUpdaterMetadataLookup)}'s online cache download failed: {ex}", LoggingTarget.Database);
|
||||
};
|
||||
|
||||
cacheDownloadRequest.Finished += () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = File.OpenRead(cacheDownloadRequest.Filename))
|
||||
using (var outStream = File.OpenWrite(cacheFilePath))
|
||||
using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false))
|
||||
bz2.CopyTo(outStream);
|
||||
|
||||
// set to null on completion to allow lookups to begin using the new source
|
||||
cacheDownloadRequest = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($@"{nameof(LocalCachedBeatmapMetadataSource)}'s online cache extraction failed: {ex}", LoggingTarget.Database);
|
||||
File.Delete(cacheFilePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(compressedCacheFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await cacheDownloadRequest.PerformAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Prevent throwing unobserved exceptions, as they will be logged from the network request to the log file anyway.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void logForModel(BeatmapSetInfo set, string message) =>
|
||||
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
cacheDownloadRequest?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user