1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-13 14:37:52 +08:00

Merge branch 'master' into catch-fail-on-banana

This commit is contained in:
Dan Balasescu 2024-03-04 13:13:26 +09:00 committed by GitHub
commit 5bd037fe8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
287 changed files with 4585 additions and 1898 deletions

View File

@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
@ -27,7 +27,7 @@ jobs:
run: dotnet restore osu.Desktop.slnf
- name: Restore inspectcode cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}
@ -70,10 +70,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
@ -99,16 +99,16 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup JDK 11
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: microsoft
java-version: 11
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
@ -126,10 +126,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"

View File

@ -140,7 +140,7 @@ jobs:
GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }}
steps:
- name: Checkout diffcalc-sheet-generator
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: ${{ env.EXECUTION_ID }}
repository: 'smoogipoo/diffcalc-sheet-generator'

View File

@ -28,7 +28,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.6.0
uses: dorny/test-reporter@v1.8.0
with:
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@ -13,23 +13,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Checkout ppy/osu
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: osu
- name: Checkout ppy/osu-tools
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: ppy/osu-tools
path: osu-tools
- name: Checkout ppy/osu-web
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: ppy/osu-web
path: osu-web
@ -43,7 +43,7 @@ jobs:
working-directory: ./osu-tools
- name: Create pull request with changes
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v6
with:
title: Update mod definitions
body: "This PR has been auto-generated to update the mod definitions to match ppy/osu@${{ github.ref_name }}."

View File

@ -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>

View File

@ -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" />

View File

@ -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" />

View File

@ -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" />

View File

@ -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" />

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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>

18
.vscode/launch.json vendored
View File

@ -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",
"*"

View File

@ -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.

View File

@ -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)",

View File

@ -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)",

View File

@ -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)",

View File

@ -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)",

View File

@ -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.

View File

@ -92,9 +92,10 @@ namespace osu.Desktop
return;
}
if (status.Value == UserStatus.Online && activity.Value != null)
if (activity.Value != null)
{
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited;
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
using Microsoft.Win32;
using osu.Desktop.Performance;
using osu.Desktop.Security;
using osu.Framework.Platform;
using osu.Game;
@ -15,9 +16,11 @@ using osu.Framework;
using osu.Framework.Logging;
using osu.Game.Updater;
using osu.Desktop.Windows;
using osu.Framework.Allocation;
using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Online.Multiplayer;
using osu.Game.Performance;
using osu.Game.Utils;
using SDL2;
@ -28,6 +31,9 @@ namespace osu.Desktop
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
private ArchiveImportIPCChannel? archiveImportIPCChannel;
[Cached(typeof(IHighPerformanceSessionManager))]
private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
public OsuGameDesktop(string[]? args = null)
: base(args)
{
@ -86,8 +92,8 @@ namespace osu.Desktop
[SupportedOSPlatform("windows")]
private string? getStableInstallPathFromRegistry()
{
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
}
protected override UpdateManager CreateUpdateManager()

View File

@ -0,0 +1,43 @@
// 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.Runtime;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Performance;
namespace osu.Desktop.Performance
{
public class HighPerformanceSessionManager : IHighPerformanceSessionManager
{
private GCLatencyMode originalGCMode;
public IDisposable BeginSession()
{
enableHighPerformanceSession();
return new InvokeOnDisposal<HighPerformanceSessionManager>(this, static m => m.disableHighPerformanceSession());
}
private void enableHighPerformanceSession()
{
Logger.Log("Starting high performance session");
originalGCMode = GCSettings.LatencyMode;
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
// Without doing this, the new GC mode won't kick in until the next GC, which could be at a more noticeable point in time.
GC.Collect(0);
}
private void disableHighPerformanceSession()
{
Logger.Log("Ending high performance session");
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
GCSettings.LatencyMode = originalGCMode;
// No GC.Collect() as we were already collecting at a higher frequency in the old mode.
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Runtime.Versioning;
using osu.Desktop.LegacyIpc;
using osu.Desktop.Windows;
using osu.Framework;
using osu.Framework.Development;
using osu.Framework.Logging;
@ -47,8 +48,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
@ -173,13 +174,16 @@ namespace osu.Desktop
{
tools.CreateShortcutForThisExe();
tools.CreateUninstallerRegistryEntry();
WindowsAssociationManager.InstallAssociations();
}, onAppUpdate: (_, tools) =>
{
tools.CreateUninstallerRegistryEntry();
WindowsAssociationManager.UpdateAssociations();
}, onAppUninstall: (_, tools) =>
{
tools.RemoveShortcutForThisExe();
tools.RemoveUninstallerRegistryEntry();
WindowsAssociationManager.UninstallAssociations();
}, onEveryRun: (_, _, _) =>
{
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently

View File

@ -0,0 +1,17 @@
// 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.IO;
namespace osu.Desktop.Windows
{
public static class Icons
{
/// <summary>
/// Fully qualified path to the directory that contains icons (in the installation folder).
/// </summary>
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
}
}

View File

@ -0,0 +1,292 @@
// 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.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Microsoft.Win32;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Localisation;
namespace osu.Desktop.Windows
{
[SupportedOSPlatform("windows")]
public static class WindowsAssociationManager
{
private const string software_classes = @"Software\Classes";
/// <summary>
/// Sub key for setting the icon.
/// https://learn.microsoft.com/en-us/windows/win32/com/defaulticon
/// </summary>
private const string default_icon = @"DefaultIcon";
/// <summary>
/// Sub key for setting the command line that the shell invokes.
/// https://learn.microsoft.com/en-us/windows/win32/com/shell
/// </summary>
internal const string SHELL_OPEN_COMMAND = @"Shell\Open\Command";
private static readonly string exe_path = Path.ChangeExtension(typeof(WindowsAssociationManager).Assembly.Location, ".exe").Replace('/', '\\');
/// <summary>
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
/// </summary>
private const string program_id_prefix = "osu.File";
private static readonly FileAssociation[] file_associations =
{
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
};
private static readonly UriAssociation[] uri_associations =
{
new UriAssociation(@"osu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
new UriAssociation(@"osump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
};
/// <summary>
/// Installs file and URI associations.
/// </summary>
/// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks>
public static void InstallAssociations()
{
try
{
updateAssociations();
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
NotifyShellUpdate();
}
catch (Exception e)
{
Logger.Error(e, @$"Failed to install file and URI associations: {e.Message}");
}
}
/// <summary>
/// Updates associations with latest definitions.
/// </summary>
/// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks>
public static void UpdateAssociations()
{
try
{
updateAssociations();
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
NotifyShellUpdate();
}
catch (Exception e)
{
Logger.Error(e, @"Failed to update file and URI associations.");
}
}
public static void UpdateDescriptions(LocalisationManager localisationManager)
{
try
{
updateDescriptions(localisationManager);
NotifyShellUpdate();
}
catch (Exception e)
{
Logger.Error(e, @"Failed to update file and URI association descriptions.");
}
}
public static void UninstallAssociations()
{
try
{
foreach (var association in file_associations)
association.Uninstall();
foreach (var association in uri_associations)
association.Uninstall();
NotifyShellUpdate();
}
catch (Exception e)
{
Logger.Error(e, @"Failed to uninstall file and URI associations.");
}
}
public static void NotifyShellUpdate() => SHChangeNotify(EventId.SHCNE_ASSOCCHANGED, Flags.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
/// <summary>
/// Installs or updates associations.
/// </summary>
private static void updateAssociations()
{
foreach (var association in file_associations)
association.Install();
foreach (var association in uri_associations)
association.Install();
}
private static void updateDescriptions(LocalisationManager? localisation)
{
foreach (var association in file_associations)
association.UpdateDescription(getLocalisedString(association.Description));
foreach (var association in uri_associations)
association.UpdateDescription(getLocalisedString(association.Description));
string getLocalisedString(LocalisableString s)
{
if (localisation == null)
return s.ToString();
var b = localisation.GetLocalisedBindableString(s);
b.UnbindAll();
return b.Value;
}
}
#region Native interop
[DllImport("Shell32.dll")]
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
private enum EventId
{
/// <summary>
/// A file type association has changed. <see cref="Flags.SHCNF_IDLIST"/> must be specified in the uFlags parameter.
/// dwItem1 and dwItem2 are not used and must be <see cref="IntPtr.Zero"/>. This event should also be sent for registered protocols.
/// </summary>
SHCNE_ASSOCCHANGED = 0x08000000
}
private enum Flags : uint
{
SHCNF_IDLIST = 0x0000
}
#endregion
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
{
private string programId => $@"{program_id_prefix}{Extension}";
/// <summary>
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
/// </summary>
public void Install()
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
// register a program id for the given extension
using (var programKey = classes.CreateSubKey(programId))
{
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, IconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
}
using (var extensionKey = classes.CreateSubKey(Extension))
{
// set ourselves as the default program
extensionKey.SetValue(null, programId);
// add to the open with dialog
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
openWithKey.SetValue(programId, string.Empty);
}
}
public void UpdateDescription(string description)
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
using (var programKey = classes.OpenSubKey(programId, true))
programKey?.SetValue(null, description);
}
/// <summary>
/// Uninstalls the file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation
/// </summary>
public void Uninstall()
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
using (var extensionKey = classes.OpenSubKey(Extension, true))
{
// clear our default association so that Explorer doesn't show the raw programId to users
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
if (extensionKey?.GetValue(null) is string s && s == programId)
extensionKey.SetValue(null, string.Empty);
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
}
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
}
}
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
{
/// <summary>
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
/// </summary>
public const string URL_PROTOCOL = @"URL Protocol";
/// <summary>
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
/// </summary>
public void Install()
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
using (var protocolKey = classes.CreateSubKey(Protocol))
{
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, IconPath);
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
}
}
public void UpdateDescription(string description)
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
using (var protocolKey = classes.OpenSubKey(Protocol, true))
protocolKey?.SetValue(null, $@"URL:{description}");
}
public void Uninstall()
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -31,4 +31,7 @@
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />
</ItemGroup>
<ItemGroup Label="Windows Icons">
<Content Include="*.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@ -20,6 +20,7 @@
<file src="**.dll" target="lib\net45\"/>
<file src="**.config" target="lib\net45\"/>
<file src="**.json" target="lib\net45\"/>
<file src="**.ico" target="lib\net45\"/>
<file src="icon.png" target=""/>
</files>
</package>

View File

@ -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]);
}
}
}
}

View File

@ -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)",

View File

@ -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);
}
}
}
}

View File

@ -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
};

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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);

View File

@ -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 }
},
};
}

View File

@ -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) ||

View File

@ -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)",

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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", () =>
{

View File

@ -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;

View File

@ -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(),
};

View File

@ -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>();
}
}

View File

@ -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,
};
}
}

View File

@ -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;
}
}
}
}

View File

@ -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)
{
}

View File

@ -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;

View File

@ -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)
{

View File

@ -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
}
}

View File

@ -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)",

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
@ -32,6 +33,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
[Test]
public void TestPlayfieldBasedSize()
{
ModFlashlight mod = new OsuModFlashlight();
CreateModTest(new ModTestData
{
Mod = mod,
PassCondition = () =>
{
var flashlightOverlay = Player.DrawableRuleset.Overlays
.OfType<ModFlashlight<OsuHitObject>.Flashlight>()
.First();
return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize());
}
});
AddStep("adjust playfield scale", () =>
Player.DrawableRuleset.Playfield.Scale = new Vector2(.5f));
}
[Test]
public void TestSliderDimsOnlyAfterStartTime()
{

View File

@ -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;

View File

@ -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);

View File

@ -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)
{

View File

@ -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;

View File

@ -416,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);
}

View File

@ -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);
}
}
}

View File

@ -80,11 +80,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.UpdateInitialTransforms();
foreach (var piece in DimmablePieces)
{
// if the specified dimmable piece is a DHO, it is generally not safe to tack transforms onto it directly
// as they may be cleared via the `updateState()` DHO flow,
// so use `ApplyCustomUpdateState` instead. which does not have this pitfall.
if (piece is DrawableHitObject drawableObjectPiece)
{
// this method can be called multiple times, and we don't want to subscribe to the event more than once,
// so this is what it is going to have to be...
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
}
else
applyDim(piece);
}
void applyDim(Drawable piece)
{
piece.FadeColour(new Color4(195, 195, 195, 255));
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
piece.FadeColour(Color4.White, 100);
}
void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
}
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override IEnumerable<Drawable> DimmablePieces => new Drawable[]
{
HeadCircle,
// HeadCircle should not be added to this list, as it handles dimming itself
TailCircle,
repeatContainer,
Body,

View File

@ -279,10 +279,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (HandleUserInput)
{
bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime;
bool correctButtonPressed = (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
RotationTracker.Tracking = !Result.HasResult
&& correctButtonPressed
&& correctButtonPressed()
&& isValidSpinningTime;
}
@ -292,11 +291,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// Ticks can theoretically be judged at any point in the spinner's duration.
// A tick must be alive to correctly play back samples,
// but for performance reasons, we only want to keep the next tick alive.
var next = NestedHitObjects.FirstOrDefault(h => !h.Judged);
DrawableHitObject nextTick = null;
foreach (var nested in NestedHitObjects)
{
if (!nested.Judged)
{
nextTick = nested;
break;
}
}
// See default `LifetimeStart` as set in `DrawableSpinnerTick`.
if (next?.LifetimeStart == double.MaxValue)
next.LifetimeStart = HitObject.StartTime;
if (nextTick?.LifetimeStart == double.MaxValue)
nextTick.LifetimeStart = HitObject.StartTime;
}
private bool correctButtonPressed()
{
if (OsuActionInputManager == null)
return false;
foreach (var action in OsuActionInputManager.PressedActions)
{
if (action == OsuAction.LeftButton || action == OsuAction.RightButton)
return true;
}
return false;
}
protected override void UpdateAfterChildren()

View File

@ -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;
}
});
}

View File

@ -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);

View File

@ -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.

View File

@ -5,7 +5,6 @@ using System;
using System.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
@ -111,42 +110,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
}, true);
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
{
base.Update();
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
fadeCounterOnTimeStart();
updateSpmAlpha();
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSpinner))
return;
fadeCounterOnTimeStart();
}
private void fadeCounterOnTimeStart()
private void updateSpmAlpha()
{
if (drawableSpinner.Result?.TimeStarted is double startTime)
{
using (BeginAbsoluteSequence(startTime))
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner.IsNotNull())
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
spmContainer.Alpha = (float)Math.Clamp((Clock.CurrentTime - startTime) / drawableSpinner.HitObject.TimeFadeIn, 0, 1);
else
spmContainer.Alpha = 0;
}
}
}

View File

@ -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);
}

View File

@ -5,7 +5,6 @@ using System;
using System.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
@ -117,42 +116,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
}, true);
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
{
base.Update();
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
fadeCounterOnTimeStart();
updateSpmAlpha();
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSpinner))
return;
fadeCounterOnTimeStart();
}
private void fadeCounterOnTimeStart()
private void updateSpmAlpha()
{
if (drawableSpinner.Result?.TimeStarted is double startTime)
{
using (BeginAbsoluteSequence(startTime))
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner.IsNotNull())
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
spmContainer.Alpha = (float)Math.Clamp((Clock.CurrentTime - startTime) / drawableSpinner.HitObject.TimeFadeIn, 0, 1);
else
spmContainer.Alpha = 0;
}
}
}

View File

@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
SnakingOut.BindTo(configSnakingOut);
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
@ -33,14 +32,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
drawableSpinner.HitObjectApplied += resetState;
}
private RotationRecord lastRecord;
public void SetRotation(float currentRotation)
{
// If we've gone back in time, it's fine to work with a fresh set of records for now
if (records.Count > 0 && Time.Current < records.Last().Time)
if (records.Count > 0 && Time.Current < lastRecord.Time)
records.Clear();
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
if (records.Count > 0 && Precision.AlmostEquals(Time.Current, records.Last().Time))
if (records.Count > 0 && Precision.AlmostEquals(Time.Current, lastRecord.Time))
return;
if (records.Count > 0)
@ -52,11 +53,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
}
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
records.Enqueue(lastRecord = new RotationRecord { Rotation = currentRotation, Time = Time.Current });
}
private void resetState(DrawableHitObject hitObject)
{
lastRecord = default;
result.Value = 0;
records.Clear();
}

View File

@ -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()

View File

@ -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));
}

View File

@ -92,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn))
this.FadeInFromZero(spinner.TimeFadeIn);
}
protected override void Update()

View File

@ -23,29 +23,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private partial class LegacyDrawableSliderPath : DrawableSliderPath
{
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
protected new float CalculatedBorderPortion
// Roughly matches osu!stable's slider border portions.
=> base.CalculatedBorderPortion * 0.77f;
protected override Color4 ColourAt(float position)
{
float realBorderPortion = shadow_portion + CalculatedBorderPortion;
float realGradientPortion = 1 - realBorderPortion;
if (position <= shadow_portion)
return new Color4(0f, 0f, 0f, 0.25f * position / shadow_portion);
if (position <= realBorderPortion)
return BorderColour;
position -= realBorderPortion;
// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Renderers/MmSliderRendererGL.cs#L99
// float aaWidth = Math.Min(Math.Max(0.5f / PathRadius, 3.0f / 256.0f), 1.0f / 16.0f);
// applying the aa_width constant from stable makes sliders blurry, especially on CS>5. set to zero for now.
// this might be related to SmoothPath applying AA internally, but disabling that does not seem to have much of an effect.
const float aa_width = 0f;
Color4 shadow = new Color4(0, 0, 0, 0.25f);
Color4 outerColour = AccentColour.Darken(0.1f);
Color4 innerColour = lighten(AccentColour, 0.5f);
return LegacyUtils.InterpolateNonLinear(position / realGradientPortion, outerColour, innerColour, 0, 1);
// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Renderers/MmSliderRendererGL.cs#L59-L70
const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
const float border_portion = 0.1875f;
if (position <= shadow_portion - aa_width)
return LegacyUtils.InterpolateNonLinear(position, Color4.Black.Opacity(0f), shadow, 0, shadow_portion - aa_width);
if (position <= shadow_portion + aa_width)
return LegacyUtils.InterpolateNonLinear(position, shadow, BorderColour, shadow_portion - aa_width, shadow_portion + aa_width);
if (position <= border_portion - aa_width)
return BorderColour;
if (position <= border_portion + aa_width)
return LegacyUtils.InterpolateNonLinear(position, BorderColour, outerColour, border_portion - aa_width, border_portion + aa_width);
return LegacyUtils.InterpolateNonLinear(position, outerColour, innerColour, border_portion + aa_width, 1);
}
/// <summary>

View File

@ -5,7 +5,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public enum OsuSkinConfiguration
{
SliderBorderSize,
SliderPathRadius,
CursorCentre,
CursorExpand,

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -228,7 +227,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer());
points.Clear();
points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
for (int i = firstVisiblePointIndex; i < futurePointIndex; i++)
points.Add(Source.SmokePoints[i]);
}
protected sealed override void Draw(IRenderer renderer)

View File

@ -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 =>

View File

@ -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)",

View File

@ -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(() =>

View File

@ -0,0 +1,57 @@
// 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.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests
{
public partial class TestSceneTaikoPlayerScroller : LegacySkinPlayerTestScene
{
private Storyboard? currentStoryboard;
protected override bool HasCustomSteps => true;
[Test]
public void TestForegroundSpritesHidesScroller()
{
AddStep("load storyboard", () =>
{
currentStoryboard = new Storyboard();
for (int i = 0; i < 10; i++)
currentStoryboard.GetLayer("Foreground").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
});
CreateTest();
AddAssert("taiko scroller not present", () => !this.ChildrenOfType<LegacyTaikoScroller>().Any());
}
[Test]
public void TestOverlaySpritesKeepsScroller()
{
AddStep("load storyboard", () =>
{
currentStoryboard = new Storyboard();
for (int i = 0; i < 10; i++)
currentStoryboard.GetLayer("Overlay").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
});
CreateTest();
AddAssert("taiko scroller present", () => this.ChildrenOfType<LegacyTaikoScroller>().Single().IsPresent);
}
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> base.CreateWorkingBeatmap(beatmap, currentStoryboard ?? storyboard);
}
}

View File

@ -5,6 +5,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -22,7 +24,9 @@ using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
@ -39,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override bool UserScrollSpeedAdjustment => false;
[CanBeNull]
private SkinnableDrawable scroller;
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
@ -48,16 +53,24 @@ namespace osu.Game.Rulesets.Taiko.UI
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
}
[BackgroundDependencyLoader]
private void load()
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] GameplayState gameplayState)
{
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar));
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
var spriteElements = gameplayState?.Storyboard.Layers.Where(l => l.Name != @"Overlay")
.SelectMany(l => l.Elements)
.OfType<StoryboardSprite>()
.DistinctBy(e => e.Path) ?? Enumerable.Empty<StoryboardSprite>();
if (spriteElements.Count() < 10)
{
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue
});
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
{
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue,
});
}
KeyBindingInputManager.Add(new DrumTouchInputArea());
}
@ -76,7 +89,9 @@ namespace osu.Game.Rulesets.Taiko.UI
base.UpdateAfterChildren();
var playfieldScreen = Playfield.ScreenSpaceDrawQuad;
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
if (scroller != null)
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
}
public MultiplierControlPoint ControlPointAt(double time)

View File

@ -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>

View File

@ -1,6 +1,7 @@
// 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.Allocation;
using osu.Framework.Graphics;
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Tests.Editing
{
@ -228,6 +230,28 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(400, 400);
}
[Test]
public void TestUseCurrentSnap()
{
AddStep("add objects to beatmap", () =>
{
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
});
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(composer.ChildrenOfType<ExpandableButton>().Single()));
AddUntilStep("use current snap expanded", () => composer.ChildrenOfType<ExpandableButton>().Single().Expanded.Value, () => Is.True);
AddStep("seek before first object", () => EditorClock.Seek(0));
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
AddStep("seek to between objects", () => EditorClock.Seek(1500));
AddUntilStep("use current snap available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.True);
AddStep("seek after last object", () => EditorClock.Seek(2500));
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
}
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));

View File

@ -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
}

View File

@ -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));

View File

@ -454,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.AddMonths(-6).AddYears(-1)).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);
}
}
}

View File

@ -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; }

View File

@ -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)
{

View File

@ -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);

View File

@ -349,8 +349,9 @@ namespace osu.Game.Tests.Visual.Background
private partial class FadeAccessibleResults : ResultsScreen
{
public FadeAccessibleResults(ScoreInfo score)
: base(score, true)
: base(score)
{
AllowRetry = true;
}
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);

View File

@ -0,0 +1,60 @@
// 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 NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Visual.Beatmaps
{
public partial class TestSceneDifficultyIcon : OsuTestScene
{
private FillFlowContainer<DifficultyIcon> fill = null!;
protected override void LoadComplete()
{
base.LoadComplete();
Child = fill = new FillFlowContainer<DifficultyIcon>
{
AutoSizeAxes = Axes.Y,
Width = 300,
Direction = FillDirection.Full,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
[Test]
public void CreateDifficultyIcon()
{
AddRepeatStep("create difficulty icon", () =>
{
var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = new TestBeatmap(rulesetInfo).BeatmapInfo;
beatmapInfo.Difficulty.ApproachRate = RNG.Next(0, 10);
beatmapInfo.Difficulty.CircleSize = RNG.Next(0, 10);
beatmapInfo.Difficulty.OverallDifficulty = RNG.Next(0, 10);
beatmapInfo.Difficulty.DrainRate = RNG.Next(0, 10);
beatmapInfo.StarRating = RNG.NextSingle(0, 10);
beatmapInfo.BPM = RNG.Next(60, 300);
fill.Add(new DifficultyIcon(beatmapInfo, rulesetInfo)
{
Scale = new Vector2(2),
});
}, 10);
AddStep("no tooltip", () => fill.ForEach(icon => icon.TooltipType = DifficultyIconTooltipType.None));
AddStep("basic tooltip", () => fill.ForEach(icon => icon.TooltipType = DifficultyIconTooltipType.StarRating));
AddStep("extended tooltip", () => fill.ForEach(icon => icon.TooltipType = DifficultyIconTooltipType.Extended));
}
}
}

View File

@ -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()
{

View File

@ -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);

View File

@ -1,9 +1,6 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -21,10 +18,12 @@ namespace osu.Game.Tests.Visual.Gameplay
[Description("player pause/fail screens")]
public partial class TestSceneGameplayMenuOverlay : OsuManualInputManagerTestScene
{
private FailOverlay failOverlay;
private PauseOverlay pauseOverlay;
private FailOverlay failOverlay = null!;
private PauseOverlay pauseOverlay = null!;
private GlobalActionContainer globalActionContainer;
private GlobalActionContainer globalActionContainer = null!;
private bool triggeredRetryButton;
[BackgroundDependencyLoader]
private void load(OsuGameBase game)
@ -35,12 +34,18 @@ namespace osu.Game.Tests.Visual.Gameplay
[SetUp]
public void SetUp() => Schedule(() =>
{
triggeredRetryButton = false;
globalActionContainer.Children = new Drawable[]
{
pauseOverlay = new PauseOverlay
{
OnResume = () => Logger.Log(@"Resume"),
OnRetry = () => Logger.Log(@"Retry"),
OnRetry = () =>
{
Logger.Log(@"Retry");
triggeredRetryButton = true;
},
OnQuit = () => Logger.Log(@"Quit"),
},
failOverlay = new FailOverlay
@ -224,17 +229,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{
showOverlay();
bool triggered = false;
AddStep("Click retry button", () =>
{
var lastAction = pauseOverlay.OnRetry;
pauseOverlay.OnRetry = () => triggered = true;
AddStep("Click retry button", () => getButton(1).TriggerClick());
getButton(1).TriggerClick();
pauseOverlay.OnRetry = lastAction;
});
AddAssert("Action was triggered", () => triggered);
AddAssert("Retry was triggered", () => triggeredRetryButton);
AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
}
@ -252,25 +249,9 @@ namespace osu.Game.Tests.Visual.Gameplay
InputManager.Key(Key.Down);
});
bool triggered = false;
Action lastAction = null;
AddStep("Press enter", () =>
{
lastAction = pauseOverlay.OnRetry;
pauseOverlay.OnRetry = () => triggered = true;
InputManager.Key(Key.Enter);
});
AddStep("Press enter", () => InputManager.Key(Key.Enter));
AddAssert("Action was triggered", () =>
{
if (lastAction != null)
{
pauseOverlay.OnRetry = lastAction;
lastAction = null;
}
return triggered;
});
AddAssert("Retry was triggered", () => triggeredRetryButton);
AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
}

View File

@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneGameplaySamplePlayback : PlayerTestScene
{
protected override bool AllowBackwardsSeeks => true;
[Test]
public void TestAllSamplesStopDuringSeek()
{

View File

@ -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();

View File

@ -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; }

Some files were not shown because too many files have changed in this diff Show More