diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 99906f0895..c4ba6e5143 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
- "version": "2023.1117.0",
+ "version": "2024.802.0",
"commands": [
"localisation"
]
diff --git a/.editorconfig b/.editorconfig
index c249e5e9b3..7aecde95ee 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -196,6 +196,9 @@ csharp_style_prefer_switch_expression = false:none
csharp_style_namespace_declarations = block_scoped:warning
+#Style - C# 12 features
+csharp_style_prefer_primary_constructors = false
+
[*.{yaml,yml}]
insert_final_newline = true
indent_style = space
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index de902df93f..4abd55e3f4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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') }}
@@ -64,16 +64,17 @@ jobs:
matrix:
os:
- { prettyname: Windows, fullname: windows-latest }
- - { prettyname: macOS, fullname: macos-latest }
+ # macOS runner performance has gotten unbearably slow so let's turn them off temporarily.
+ # - { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded']
- timeout-minutes: 60
+ timeout-minutes: 120
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 +100,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"
@@ -120,24 +121,19 @@ jobs:
build-only-ios:
name: Build only (iOS)
- # `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3.
- # TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images)
- runs-on: macos-13
+ runs-on: macos-latest
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"
- name: Install .NET Workloads
run: dotnet workload install maui-ios
- - name: Select Xcode 15.2
- run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
-
- name: Build
run: dotnet build -c Debug osu.iOS
diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml
index 5f16e09040..9f129a697c 100644
--- a/.github/workflows/diffcalc.yml
+++ b/.github/workflows/diffcalc.yml
@@ -110,10 +110,14 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }}
steps:
- name: Check permissions
- if: ${{ github.event_name != 'workflow_dispatch' }}
- uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819 # v2.2.0
- with:
- require: 'write'
+ run: |
+ ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte)
+ for i in "${ALLOWED_USERS[@]}"; do
+ if [[ "${{ github.actor }}" == "$i" ]]; then
+ exit 0
+ fi
+ done
+ exit 1
create-comment:
name: Create PR comment
@@ -122,7 +126,7 @@ jobs:
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
steps:
- name: Create comment
- uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
+ uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
message: |
@@ -140,7 +144,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'
@@ -249,7 +253,7 @@ jobs:
- name: Restore cache
id: restore-cache
- uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
+ uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
with:
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
key: ${{ steps.query.outputs.DATA_NAME }}
@@ -280,7 +284,7 @@ jobs:
- name: Restore cache
id: restore-cache
- uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
+ uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
with:
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
key: ${{ steps.query.outputs.DATA_NAME }}
@@ -354,7 +358,7 @@ jobs:
steps:
- name: Update comment on success
if: ${{ needs.generator.result == 'success' }}
- uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
+ uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: upsert
@@ -365,7 +369,7 @@ jobs:
- name: Update comment on failure
if: ${{ needs.generator.result == 'failure' }}
- uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
+ uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: upsert
@@ -375,7 +379,7 @@ jobs:
- name: Update comment on cancellation
if: ${{ needs.generator.result == 'cancelled' }}
- uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
+ uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: delete
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index 99e39f6f56..c44f46d70a 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -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}})
diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml
index ff4165c414..be104d0fd3 100644
--- a/.github/workflows/sentry-release.yml
+++ b/.github/workflows/sentry-release.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
diff --git a/.github/workflows/update-web-mod-definitions.yml b/.github/workflows/update-web-mod-definitions.yml
index 5827a6cdbf..b19f03ad7d 100644
--- a/.github/workflows/update-web-mod-definitions.yml
+++ b/.github/workflows/update-web-mod-definitions.yml
@@ -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 }}."
diff --git a/.gitignore b/.gitignore
index 525b3418cd..1fec94d82b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -265,6 +265,8 @@ __pycache__/
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
+.idea/*/.idea/projectSettingsUpdater.xml
+.idea/*/.idea/encodings.xml
# Generated files
.idea/**/contentModel.xml
@@ -340,4 +342,5 @@ inspectcode
# Fody (pulled in by Realm) - schema file
FodyWeavers.xsd
-.idea/.idea.osu.Desktop/.idea/misc.xml
\ No newline at end of file
+.idea/.idea.osu.Desktop/.idea/misc.xml
+.idea/.idea.osu.Android/.idea/deploymentTargetDropDown.xml
diff --git a/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml
deleted file mode 100644
index 4bb9f4d2a0..0000000000
--- a/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/encodings.xml b/.idea/.idea.osu.Desktop/.idea/encodings.xml
deleted file mode 100644
index 15a15b218a..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/encodings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
deleted file mode 100644
index 4bb9f4d2a0..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
index d500c595c0..a7a6649a4f 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
index 6da760dead..39e6cb10c5 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
index 741679707a..88367387ae 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
index 104b1266ca..cdc980e019 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
index f58f9d4ae2..d91cf01479 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
index 0f2c390328..daafff7dfb 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
index 898aec880c..62f587410f 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
index dae6e032b1..f6e2c26d19 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
index 519107b5e3..c437dcf0f0 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml
deleted file mode 100644
index 4bb9f4d2a0..0000000000
--- a/.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu/.idea/projectSettingsUpdater.xml
deleted file mode 100644
index 4bb9f4d2a0..0000000000
--- a/.idea/.idea.osu/.idea/projectSettingsUpdater.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.run/osu! (Second Client).run.xml b/.run/osu! (Second Client).run.xml
index 9a471df902..d72e7abee2 100644
--- a/.run/osu! (Second Client).run.xml
+++ b/.run/osu! (Second Client).run.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,7 +12,7 @@
-
+
diff --git a/.vscode/launch.json b/.vscode/launch.json
index d93fddf42d..7c5225cff7 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -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",
"*"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4106641adb..ebe1e08074 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -55,7 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
-The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
+The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
@@ -68,6 +68,7 @@ Aside from the above, below is a brief checklist of things to watch out when you
- Please do not make code changes via the GitHub web interface.
- Please add tests for your changes. We expect most new features and bugfixes to have test coverage, unless the effort of adding them is prohibitive. The visual testing methodology we use is described in more detail [here](https://github.com/ppy/osu-framework/wiki/Development-and-Testing).
- Please run tests and code style analysis (via `InspectCode.{ps1,sh}` scripts in the root of this repository) before opening the PR. This is particularly important if you're a first-time contributor, as CI will not run for your PR until we allow it to do so.
+- **Do not run the game in release configuration at any point during your testing** (the sole exception to this being benchmarks). Using release is an unnecessary and harmful practice, and can even lead to you losing your local realm database if you start making changes to the schema. The debug configuration has a completely separated full-stack environment, including a development website instance at https://dev.ppy.sh/. It is permitted to register an account on that development instance for testing purposes and not worry about multi-accounting infractions.
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 03fd21829d..3c60b28765 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -7,7 +7,6 @@ T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable ins
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
-M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead.
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead.
M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead.
diff --git a/Directory.Build.props b/Directory.Build.props
index 2d289d0f22..5ba12b845b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -2,7 +2,6 @@
12.0
- true
enable
diff --git a/README.md b/README.md
index dc5809d46b..cb722e5df3 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json
index b433819346..0d72037393 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json
@@ -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)",
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs
index 744e207b57..e53fe01157 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects.Drawables
{
if (timeOffset >= 0)
// todo: implement judgement logic
- ApplyResult(r => r.Type = HitResult.Perfect);
+ ApplyResult(HitResult.Perfect);
}
protected override void UpdateHitStateTransforms(ArmedState state)
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
index d60bc2571d..ec832d9a72 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
@@ -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)",
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
index c5ada4288d..b1be25727f 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
@@ -49,7 +48,12 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (timeOffset >= 0)
- ApplyResult(r => r.Type = IsHovered ? HitResult.Perfect : HitResult.Miss);
+ {
+ if (IsHovered)
+ ApplyMaxResult();
+ else
+ ApplyMinResult();
+ }
}
protected override double InitialLifetimeOffset => time_preempt;
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json
index f1f37f6363..a60979073b 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json
@@ -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)",
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs
index a3c3b89105..adcbd36485 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs
@@ -3,7 +3,6 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
@@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Objects.Drawables
{
if (timeOffset >= 0)
// todo: implement judgement logic
- ApplyResult(r => r.Type = HitResult.Perfect);
+ ApplyMaxResult();
}
protected override void UpdateHitStateTransforms(ArmedState state)
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
index d60bc2571d..ec832d9a72 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json
@@ -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)",
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
index d198fa81cb..3ad636a601 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs
@@ -10,7 +10,6 @@ using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Pippidon.UI;
-using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
@@ -49,7 +48,12 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (timeOffset >= 0)
- ApplyResult(r => r.Type = currentLane.Value == HitObject.Lane ? HitResult.Perfect : HitResult.Miss);
+ {
+ if (currentLane.Value == HitObject.Lane)
+ ApplyMaxResult();
+ else
+ ApplyMinResult();
+ }
}
protected override void UpdateHitStateTransforms(ArmedState state)
diff --git a/assets/lazer-nuget.png b/assets/lazer-nuget.png
index fed2f45149..fabfcc223e 100644
Binary files a/assets/lazer-nuget.png and b/assets/lazer-nuget.png differ
diff --git a/assets/lazer.png b/assets/lazer.png
index 2ee44225bf..f564b93d6f 100644
Binary files a/assets/lazer.png and b/assets/lazer.png differ
diff --git a/osu.Android.props b/osu.Android.props
index d7f29beeb3..2609fd42c3 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
+
+
+
+ XamarinJetbrainsAnnotations
+
+
+
diff --git a/osu.Game.Tests/Beatmaps/BeatmapUpdaterMetadataLookupTest.cs b/osu.Game.Tests/Beatmaps/BeatmapUpdaterMetadataLookupTest.cs
new file mode 100644
index 0000000000..11c4c54ea6
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/BeatmapUpdaterMetadataLookupTest.cs
@@ -0,0 +1,440 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Moq;
+using NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Tests.Beatmaps
+{
+ [TestFixture]
+ public class BeatmapUpdaterMetadataLookupTest
+ {
+ private Mock apiMetadataSourceMock = null!;
+ private Mock localCachedMetadataSourceMock = null!;
+
+ private BeatmapUpdaterMetadataLookup metadataLookup = null!;
+
+ [SetUp]
+ public void SetUp()
+ {
+ apiMetadataSourceMock = new Mock();
+ localCachedMetadataSourceMock = new Mock();
+
+ metadataLookup = new BeatmapUpdaterMetadataLookup(apiMetadataSourceMock.Object, localCachedMetadataSourceMock.Object);
+ }
+
+ [Test]
+ public void TestLocalCacheQueriedFirst()
+ {
+ var localLookupResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 123456,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
+ };
+ localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
+ localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out localLookupResult))
+ .Returns(true);
+
+ apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
+
+ var beatmap = new BeatmapInfo { OnlineID = 123456 };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch: false);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
+ Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
+ localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref.IsAny!), Times.Once);
+ apiMetadataSourceMock.Verify(src => src.TryLookup(It.IsAny(), out It.Ref.IsAny!), Times.Never);
+ }
+
+ [Test]
+ public void TestAPIQueriedSecond()
+ {
+ OnlineBeatmapMetadata? localLookupResult = null;
+ localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
+ localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out localLookupResult))
+ .Returns(false);
+
+ var onlineLookupResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 123456,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
+ };
+ apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
+ apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out onlineLookupResult))
+ .Returns(true);
+
+ var beatmap = new BeatmapInfo { OnlineID = 123456 };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch: false);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
+ Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
+ localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref.IsAny!), Times.Once);
+ apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref.IsAny!), Times.Once);
+ }
+
+ [Test]
+ public void TestPreferOnlineFetch()
+ {
+ var localLookupResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 123456,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
+ };
+ localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
+ localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out localLookupResult))
+ .Returns(true);
+
+ var onlineLookupResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 123456,
+ BeatmapStatus = BeatmapOnlineStatus.Graveyard,
+ BeatmapSetStatus = BeatmapOnlineStatus.Graveyard,
+ };
+ apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
+ apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out onlineLookupResult))
+ .Returns(true);
+
+ var beatmap = new BeatmapInfo { OnlineID = 123456 };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch: true);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Graveyard));
+ Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.Graveyard));
+ localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref.IsAny!), Times.Never);
+ apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref.IsAny!), Times.Once);
+ }
+
+ [Test]
+ public void TestPreferOnlineFetchFallsBackToLocalCacheIfOnlineSourceUnavailable()
+ {
+ var localLookupResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 123456,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
+ };
+ localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
+ localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out localLookupResult))
+ .Returns(true);
+
+ apiMetadataSourceMock.Setup(src => src.Available).Returns(false);
+
+ var beatmap = new BeatmapInfo { OnlineID = 123456 };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch: true);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
+ Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
+ localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref.IsAny!), Times.Once);
+ apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref.IsAny!), Times.Never);
+ }
+
+ [Test]
+ public void TestMetadataLookupFailed()
+ {
+ OnlineBeatmapMetadata? lookupResult = null;
+
+ localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
+ localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out lookupResult))
+ .Returns(false);
+
+ apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
+ apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out lookupResult))
+ .Returns(true);
+
+ var beatmap = new BeatmapInfo { OnlineID = 123456 };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch: false);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
+ localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref.IsAny!), Times.Once);
+ apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref.IsAny!), Times.Once);
+ }
+
+ ///
+ /// For the time being, if we fail to find a match in the local cache but online retrieval is not available, we trust the incoming beatmap verbatim wrt online ID.
+ /// While this is suboptimal as it implicitly trusts the contents of the beatmap,
+ /// throwing away the online data would be anti-user as it would make all beatmaps imported offline stop working in online.
+ /// TODO: revisit if/when we have a better flow of queueing metadata retrieval.
+ ///
+ [Test]
+ public void TestLocalMetadataLookupReturnedNoMatchAndOnlineLookupIsUnavailable([Values] bool preferOnlineFetch)
+ {
+ OnlineBeatmapMetadata? localLookupResult = null;
+ localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
+ localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out localLookupResult))
+ .Returns(false);
+
+ apiMetadataSourceMock.Setup(src => src.Available).Returns(false);
+
+ var beatmap = new BeatmapInfo { OnlineID = 123456 };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ Assert.That(beatmap.OnlineID, Is.EqualTo(123456));
+ }
+
+ ///
+ /// For the time being, if there are no available metadata lookup sources, we trust the incoming beatmap verbatim wrt online ID.
+ /// While this is suboptimal as it implicitly trusts the contents of the beatmap,
+ /// throwing away the online data would be anti-user as it would make all beatmaps imported offline stop working in online.
+ /// TODO: revisit if/when we have a better flow of queueing metadata retrieval.
+ ///
+ [Test]
+ public void TestNoAvailableSources([Values] bool preferOnlineFetch)
+ {
+ OnlineBeatmapMetadata? lookupResult = null;
+
+ localCachedMetadataSourceMock.Setup(src => src.Available).Returns(false);
+ localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out lookupResult))
+ .Returns(false);
+
+ apiMetadataSourceMock.Setup(src => src.Available).Returns(false);
+ apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny(), out lookupResult))
+ .Returns(false);
+
+ var beatmap = new BeatmapInfo { OnlineID = 123456 };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch);
+
+ Assert.That(beatmap.OnlineID, Is.EqualTo(123456));
+ }
+
+ [Test]
+ public void TestReturnedMetadataHasDifferentOnlineID([Values] bool preferOnlineFetch)
+ {
+ var lookupResult = new OnlineBeatmapMetadata { BeatmapID = 654321, BeatmapStatus = BeatmapOnlineStatus.Ranked };
+
+ var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
+ targetMock.Setup(src => src.Available).Returns(true);
+ targetMock.Setup(src => src.TryLookup(It.IsAny(), out lookupResult))
+ .Returns(true);
+
+ var beatmap = new BeatmapInfo { OnlineID = 123456 };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
+ }
+
+ [Test]
+ public void TestMetadataLookupForBeatmapWithoutPopulatedIDAndCorrectHash([Values] bool preferOnlineFetch)
+ {
+ var lookupResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 654321,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ MD5Hash = @"deadbeef",
+ };
+
+ var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
+ targetMock.Setup(src => src.Available).Returns(true);
+ targetMock.Setup(src => src.TryLookup(It.IsAny(), out lookupResult))
+ .Returns(true);
+
+ var beatmap = new BeatmapInfo
+ {
+ MD5Hash = @"deadbeef"
+ };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
+ Assert.That(beatmap.OnlineID, Is.EqualTo(654321));
+ }
+
+ [Test]
+ public void TestMetadataLookupForBeatmapWithoutPopulatedIDAndIncorrectHash([Values] bool preferOnlineFetch)
+ {
+ var lookupResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 654321,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ MD5Hash = @"cafebabe",
+ };
+
+ var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
+ targetMock.Setup(src => src.Available).Returns(true);
+ targetMock.Setup(src => src.TryLookup(It.IsAny(), out lookupResult))
+ .Returns(true);
+
+ var beatmap = new BeatmapInfo
+ {
+ MD5Hash = @"deadbeef"
+ };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
+ }
+
+ [Test]
+ public void TestReturnedMetadataHasDifferentHash([Values] bool preferOnlineFetch)
+ {
+ var lookupResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 654321,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ MD5Hash = @"deadbeef"
+ };
+
+ var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
+ targetMock.Setup(src => src.Available).Returns(true);
+ targetMock.Setup(src => src.TryLookup(It.IsAny(), out lookupResult))
+ .Returns(true);
+
+ var beatmap = new BeatmapInfo
+ {
+ OnlineID = 654321,
+ MD5Hash = @"cafebabe",
+ };
+ var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
+ beatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch);
+
+ Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ Assert.That(beatmap.OnlineID, Is.EqualTo(654321));
+ }
+
+ [Test]
+ public void TestPartiallyModifiedSet([Values] bool preferOnlineFetch)
+ {
+ var firstResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 654321,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
+ MD5Hash = @"cafebabe"
+ };
+ var secondResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 666666,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
+ MD5Hash = @"dededede"
+ };
+
+ var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
+ targetMock.Setup(src => src.Available).Returns(true);
+ targetMock.Setup(src => src.TryLookup(It.Is(bi => bi.OnlineID == 654321), out firstResult))
+ .Returns(true);
+ targetMock.Setup(src => src.TryLookup(It.Is(bi => bi.OnlineID == 666666), out secondResult))
+ .Returns(true);
+
+ var firstBeatmap = new BeatmapInfo
+ {
+ OnlineID = 654321,
+ MD5Hash = @"cafebabe",
+ };
+ var secondBeatmap = new BeatmapInfo
+ {
+ OnlineID = 666666,
+ MD5Hash = @"deadbeef"
+ };
+ var beatmapSet = new BeatmapSetInfo(new[]
+ {
+ firstBeatmap,
+ secondBeatmap
+ });
+ firstBeatmap.BeatmapSet = beatmapSet;
+ secondBeatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch);
+
+ Assert.That(firstBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
+ Assert.That(firstBeatmap.OnlineID, Is.EqualTo(654321));
+
+ Assert.That(secondBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ Assert.That(secondBeatmap.OnlineID, Is.EqualTo(666666));
+
+ Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ }
+
+ [Test]
+ public void TestPartiallyMaliciousSet([Values] bool preferOnlineFetch)
+ {
+ var firstResult = new OnlineBeatmapMetadata
+ {
+ BeatmapID = 654321,
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
+ MD5Hash = @"cafebabe"
+ };
+ var secondResult = new OnlineBeatmapMetadata
+ {
+ BeatmapStatus = BeatmapOnlineStatus.Ranked,
+ BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
+ MD5Hash = @"dededede"
+ };
+
+ var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
+ targetMock.Setup(src => src.Available).Returns(true);
+ targetMock.Setup(src => src.TryLookup(It.Is(bi => bi.OnlineID == 654321), out firstResult))
+ .Returns(true);
+ targetMock.Setup(src => src.TryLookup(It.Is(bi => bi.OnlineID == 666666), out secondResult))
+ .Returns(true);
+
+ var firstBeatmap = new BeatmapInfo
+ {
+ OnlineID = 654321,
+ MD5Hash = @"cafebabe",
+ };
+ var secondBeatmap = new BeatmapInfo
+ {
+ OnlineID = 666666,
+ MD5Hash = @"deadbeef"
+ };
+ var beatmapSet = new BeatmapSetInfo(new[]
+ {
+ firstBeatmap,
+ secondBeatmap
+ });
+ firstBeatmap.BeatmapSet = beatmapSet;
+ secondBeatmap.BeatmapSet = beatmapSet;
+
+ metadataLookup.Update(beatmapSet, preferOnlineFetch);
+
+ Assert.That(firstBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
+ Assert.That(firstBeatmap.OnlineID, Is.EqualTo(654321));
+
+ Assert.That(secondBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ Assert.That(secondBeatmap.OnlineID, Is.EqualTo(-1));
+
+ Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 02432a1935..54ebebeb7b 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -468,6 +468,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeBeatmapHitObjectCoordinatesLegacy()
+ {
+ var decoder = new LegacyBeatmapDecoder();
+
+ using (var resStream = TestResources.OpenResource("hitobject-coordinates-legacy.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var hitObjects = decoder.Decode(stream).HitObjects;
+
+ var positionData = hitObjects[0] as IHasPosition;
+
+ Assert.IsNotNull(positionData);
+ Assert.AreEqual(new Vector2(256, 256), positionData!.Position);
+ }
+ }
+
+ [Test]
+ public void TestDecodeBeatmapHitObjectCoordinatesLazer()
+ {
+ var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION);
+
+ using (var resStream = TestResources.OpenResource("hitobject-coordinates-lazer.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var hitObjects = decoder.Decode(stream).HitObjects;
+
+ var positionData = hitObjects[0] as IHasPosition;
+
+ Assert.IsNotNull(positionData);
+ Assert.AreEqual(new Vector2(256.99853f, 256.001f), positionData!.Position);
+ }
+ }
+
[Test]
public void TestDecodeBeatmapHitObjects()
{
@@ -528,8 +562,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First());
- // The control point at the end time of the slider should be applied
- Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
+ // The fourth object is a slider.
+ // `Samples` of a slider are presumed to control the volume of sounds that last the entire duration of the slider
+ // (such as ticks, slider slide sounds, etc.)
+ // Thus, the point of query of control points used for `Samples` is just beyond the start time of the slider.
+ Assert.AreEqual("Gameplay/soft-hitnormal11", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
+
+ // That said, the `NodeSamples` of the slider are responsible for the sounds of the slider's head / tail / repeats / large ticks etc.
+ // Therefore, they should be read at the time instant correspondent to the given node.
+ // This means that the tail should use bank 8 rather than 11.
+ Assert.AreEqual("Gameplay/soft-hitnormal11", ((ConvertSlider)hitObjects[4]).NodeSamples[0][0].LookupNames.First());
+ Assert.AreEqual("Gameplay/soft-hitnormal8", ((ConvertSlider)hitObjects[4]).NodeSamples[1][0].LookupNames.First());
}
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
@@ -1188,5 +1231,36 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153));
}
}
+
+ [Test]
+ public void TestBeatmapDifficultyIsClamped()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("out-of-range-difficulties.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var decoded = decoder.Decode(stream).Difficulty;
+ Assert.That(decoded.DrainRate, Is.EqualTo(10));
+ Assert.That(decoded.CircleSize, Is.EqualTo(10));
+ Assert.That(decoded.OverallDifficulty, Is.EqualTo(10));
+ Assert.That(decoded.ApproachRate, Is.EqualTo(10));
+ Assert.That(decoded.SliderMultiplier, Is.EqualTo(3.6));
+ Assert.That(decoded.SliderTickRate, Is.EqualTo(8));
+ }
+ }
+
+ [Test]
+ public void TestManiaBeatmapDifficultyCircleSizeClamp()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("out-of-range-difficulties-mania.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var decoded = decoder.Decode(stream).Difficulty;
+ Assert.That(decoded.CircleSize, Is.EqualTo(14));
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index e847b61fbe..b931896898 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning;
+using osu.Game.Storyboards;
using osu.Game.Tests.Resources;
using osuTK;
@@ -37,6 +38,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
private static IEnumerable allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
+ [Test]
+ public void TestUnsupportedStoryboardEvents()
+ {
+ const string name = "Resources/storyboard_only_video.osu";
+
+ var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
+ Assert.That(decoded.beatmap.UnhandledEventLines.Count, Is.EqualTo(1));
+ Assert.That(decoded.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\""));
+
+ var memoryStream = encodeToLegacy(decoded);
+
+ var storyboard = new LegacyStoryboardDecoder().Decode(new LineBufferedReader(memoryStream));
+ StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.EqualTo(1));
+ }
+
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStability(string name)
{
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
index 7e3967dc95..713f2f3fb1 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
@@ -14,11 +14,12 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO.Legacy;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
-using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@@ -31,6 +32,8 @@ using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources;
+using osu.Game.Users;
+using osuTK;
namespace osu.Game.Tests.Beatmaps.Formats
{
@@ -62,14 +65,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(829_931, score.ScoreInfo.LegacyTotalScore);
Assert.AreEqual(3, score.ScoreInfo.MaxCombo);
- Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic));
- Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL"));
- Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL"));
+ Assert.That(score.ScoreInfo.APIMods.Select(m => m.Acronym), Is.EquivalentTo(new[] { "CL", "9K", "DS" }));
Assert.That((2 * 300d + 1 * 200) / (3 * 305d), Is.EqualTo(score.ScoreInfo.Accuracy).Within(0.0001));
Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank);
- Assert.That(score.Replay.Frames, Is.Not.Empty);
+ Assert.That(score.Replay.Frames, Has.One.Matches(frame =>
+ frame.Time == 414 && frame.Actions.SequenceEqual(new[] { ManiaAction.Key1, ManiaAction.Key18 })));
}
}
@@ -124,17 +126,20 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
{
- const double first_frame_time = 48;
- const double second_frame_time = 65;
+ const double first_frame_time = 31;
+ const double second_frame_time = 48;
+ const double third_frame_time = 65;
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
{
var score = decoder.Parse(resourceStream);
+ int offset = offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
- Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
- Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
+ Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + offset));
+ Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + offset));
+ Assert.That(score.Replay.Frames[2].Time, Is.EqualTo(third_frame_time + offset));
}
}
@@ -175,6 +180,94 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
}
+ [Test]
+ public void TestNegativeFrameSkipped()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ var beatmap = new TestBeatmap(ruleset);
+
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(0, new Vector2()),
+ new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE),
+ new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2),
+ new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE),
+ }
+ }
+ };
+
+ var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap);
+
+ Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3));
+ Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0));
+ Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(1000));
+ Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(2000));
+ }
+
+ [Test]
+ public void FirstTwoFramesSwappedIfInWrongOrder()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ var beatmap = new TestBeatmap(ruleset);
+
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(100, new Vector2()),
+ new OsuReplayFrame(50, OsuPlayfield.BASE_SIZE / 2),
+ new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE),
+ }
+ }
+ };
+
+ var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap);
+
+ Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3));
+ Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0));
+ Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(100));
+ Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(1000));
+ }
+
+ [Test]
+ public void FirstTwoFramesPulledTowardThirdIfTheyAreAfterIt()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ var beatmap = new TestBeatmap(ruleset);
+
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(0, new Vector2()),
+ new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2),
+ new OsuReplayFrame(-1500, OsuPlayfield.BASE_SIZE),
+ }
+ }
+ };
+
+ var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap);
+
+ Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3));
+ Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(-1500));
+ Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(-1500));
+ Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(-1500));
+ }
+
[Test]
public void TestCultureInvariance()
{
@@ -224,6 +317,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
+ scoreInfo.User = new APIUser
+ {
+ Username = "spaceman_atlas",
+ Id = 3035836,
+ CountryCode = CountryCode.PL
+ };
scoreInfo.ClientVersion = "2023.1221.0";
var beatmap = new TestBeatmap(ruleset);
@@ -248,6 +347,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0"));
+ Assert.That(decodedAfterEncode.ScoreInfo.RealmUser.OnlineID, Is.EqualTo(3035836));
});
}
@@ -352,6 +452,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[HitResult.Great] = 200,
[HitResult.LargeTickHit] = 1,
};
+ scoreInfo.Rank = ScoreRank.A;
var beatmap = new TestBeatmap(ruleset);
var score = new Score
@@ -412,6 +513,80 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
+ [Test]
+ public void TestTotalScoreWithoutModsReadIfPresent()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ scoreInfo.Mods = new Mod[]
+ {
+ new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
+ };
+ scoreInfo.OnlineID = 123123;
+ scoreInfo.ClientVersion = "2023.1221.0";
+ scoreInfo.TotalScoreWithoutMods = 1_000_000;
+ scoreInfo.TotalScore = 1_020_000;
+
+ var beatmap = new TestBeatmap(ruleset);
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
+ }
+ }
+ };
+
+ var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
+ Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
+ });
+ }
+
+ [Test]
+ public void TestTotalScoreWithoutModsBackwardsPopulatedIfMissing()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ scoreInfo.Mods = new Mod[]
+ {
+ new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
+ };
+ scoreInfo.OnlineID = 123123;
+ scoreInfo.ClientVersion = "2023.1221.0";
+ scoreInfo.TotalScoreWithoutMods = 0;
+ scoreInfo.TotalScore = 1_020_000;
+
+ var beatmap = new TestBeatmap(ruleset);
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
+ }
+ }
+ };
+
+ var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
+ Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
+ });
+ }
+
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();
@@ -432,7 +607,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
CultureInfo.CurrentCulture = originalCulture;
}
- private class TestLegacyScoreDecoder : LegacyScoreDecoder
+ public class TestLegacyScoreDecoder : LegacyScoreDecoder
{
private readonly int beatmapVersion;
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs
new file mode 100644
index 0000000000..1e57bd76cf
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs
@@ -0,0 +1,122 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using NUnit.Framework;
+using osu.Game.Beatmaps.Formats;
+using osu.Game.IO.Legacy;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Scoring.Legacy;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Beatmaps.Formats
+{
+ public class LegacyScoreEncoderTest
+ {
+ [TestCase(1, 3)]
+ [TestCase(1, 0)]
+ [TestCase(0, 3)]
+ public void TestCatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
+ {
+ var ruleset = new CatchRuleset().RulesetInfo;
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ var beatmap = new TestBeatmap(ruleset);
+
+ scoreInfo.Statistics = new Dictionary
+ {
+ [HitResult.Great] = 50,
+ [HitResult.LargeTickHit] = 5,
+ [HitResult.Miss] = missCount,
+ [HitResult.LargeTickMiss] = largeTickMissCount
+ };
+
+ var score = new Score { ScoreInfo = scoreInfo };
+ var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
+
+ Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount));
+ }
+
+ [Test]
+ public void TestFailPreserved()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+ var scoreInfo = TestResources.CreateTestScoreInfo();
+ var beatmap = new TestBeatmap(ruleset);
+
+ scoreInfo.Rank = ScoreRank.F;
+
+ var score = new Score { ScoreInfo = scoreInfo };
+ var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
+
+ Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.F));
+ }
+
+ [Test]
+ public void TestScoreWithMissIsNotPerfect()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ var beatmap = new TestBeatmap(ruleset);
+
+ scoreInfo.Statistics = new Dictionary
+ {
+ [HitResult.Great] = 2,
+ [HitResult.Miss] = 1,
+ };
+
+ scoreInfo.MaximumStatistics = new Dictionary
+ {
+ [HitResult.Great] = 3
+ };
+
+ // Hit -> Miss -> Hit
+ scoreInfo.Combo = 1;
+ scoreInfo.MaxCombo = 1;
+
+ using (var ms = new MemoryStream())
+ {
+ new LegacyScoreEncoder(new Score { ScoreInfo = scoreInfo }, beatmap).Encode(ms, true);
+
+ ms.Seek(0, SeekOrigin.Begin);
+
+ using (var sr = new SerializationReader(ms))
+ {
+ sr.ReadByte(); // ruleset id
+ sr.ReadInt32(); // version
+ sr.ReadString(); // beatmap hash
+ sr.ReadString(); // username
+ sr.ReadString(); // score hash
+ sr.ReadInt16(); // count300
+ sr.ReadInt16(); // count100
+ sr.ReadInt16(); // count50
+ sr.ReadInt16(); // countGeki
+ sr.ReadInt16(); // countKatu
+ sr.ReadInt16(); // countMiss
+ sr.ReadInt32(); // total score
+ sr.ReadInt16(); // max combo
+ bool isPerfect = sr.ReadBoolean(); // full combo
+
+ Assert.That(isPerfect, Is.False);
+ }
+ }
+ }
+
+ private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
+ {
+ var encodeStream = new MemoryStream();
+
+ var encoder = new LegacyScoreEncoder(score, beatmap);
+ encoder.Encode(encodeStream);
+
+ var decodeStream = new MemoryStream(encodeStream.GetBuffer());
+
+ var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion);
+ var decodedAfterEncode = decoder.Parse(decodeStream);
+ return decodedAfterEncode;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
index 95fd2669e5..ef4d4f683a 100644
--- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs
+++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.Chat
return true;
case ChatAckRequest ack:
- ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToList() });
+ ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToArray() });
silencedUserIds.Clear();
return true;
diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs
index e960995c45..f9f9fa2622 100644
--- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs
+++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs
@@ -157,8 +157,9 @@ namespace osu.Game.Tests.Database
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
}
- [Test]
- public void TestScoreUpgradeFailed()
+ [TestCase(30000002)]
+ [TestCase(30000013)]
+ public void TestScoreUpgradeFailed(int scoreVersion)
{
ScoreInfo scoreInfo = null!;
@@ -172,16 +173,18 @@ namespace osu.Game.Tests.Database
Ruleset = r.All().First(),
})
{
- TotalScoreVersion = 30000002,
+ TotalScoreVersion = scoreVersion,
IsLegacyScore = true,
});
});
});
- AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
+ TestBackgroundDataStoreProcessor processor = null!;
+ AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
+ AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
- AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
+ AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion));
}
[Test]
diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs
index d30b3c089e..3f1bc58147 100644
--- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs
@@ -168,12 +168,12 @@ namespace osu.Game.Tests.Database
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
+ realm.Run(r => r.Refresh());
+
// should only contain the modified beatmap (others purged).
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(1));
Assert.That(importAfterUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
- realm.Run(r => r.Refresh());
-
checkCount(realm, count_beatmaps + 1);
checkCount(realm, count_beatmaps + 1);
@@ -259,6 +259,44 @@ namespace osu.Game.Tests.Database
});
}
+ [Test]
+ public void TestNoChangesAfterDelete()
+ {
+ RunTestWithRealmAsync(async (realm, storage) =>
+ {
+ var importer = new BeatmapImporter(storage, realm);
+ using var rulesets = new RealmRulesetStore(realm, storage);
+
+ using var __ = getBeatmapArchive(out string pathOriginal);
+ using var _ = getBeatmapArchive(out string pathOriginalSecond);
+
+ var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
+
+ importBeforeUpdate!.PerformWrite(s => s.DeletePending = true);
+
+ var dateBefore = importBeforeUpdate.Value.DateAdded;
+
+ Assert.That(importBeforeUpdate, Is.Not.Null);
+ Debug.Assert(importBeforeUpdate != null);
+
+ var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
+
+ realm.Run(r => r.Refresh());
+
+ Assert.That(importAfterUpdate, Is.Not.Null);
+ Debug.Assert(importAfterUpdate != null);
+
+ checkCount(realm, 1);
+ checkCount(realm, count_beatmaps);
+ checkCount(realm, count_beatmaps);
+
+ Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
+ Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
+ Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
+ Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
+ });
+ }
+
[Test]
public void TestNoChanges()
{
@@ -272,21 +310,25 @@ namespace osu.Game.Tests.Database
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
+ var dateBefore = importBeforeUpdate!.Value.DateAdded;
+
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
+ realm.Run(r => r.Refresh());
+
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
- realm.Run(r => r.Refresh());
-
checkCount(realm, 1);
checkCount(realm, count_beatmaps);
checkCount(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
+ Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
+ Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
});
}
@@ -479,6 +521,7 @@ namespace osu.Game.Tests.Database
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
+
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// arbitrary beatmap removal
@@ -496,7 +539,7 @@ namespace osu.Game.Tests.Database
Debug.Assert(importAfterUpdate != null);
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
- Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded));
+ Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded).Within(TimeSpan.FromSeconds(1)));
});
}
diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
index 5f722e381c..016928c6d6 100644
--- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
+++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
@@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
+using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Database
@@ -77,6 +78,7 @@ namespace osu.Game.Tests.Database
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
+ using (new RealmRulesetStore(realm, storage))
{
var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);
diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
index 45842a952a..e5be4d665b 100644
--- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
+++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
@@ -71,6 +71,35 @@ namespace osu.Game.Tests.Database
}
}
+ [Test]
+ public void TestSubscriptionInitialChangeSetNull()
+ {
+ ChangeSet? firstChanges = null;
+ int receivedChangesCount = 0;
+
+ RunTestWithRealm((realm, _) =>
+ {
+ var registration = realm.RegisterForNotifications(r => r.All(), onChanged);
+
+ realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
+
+ realm.Run(r => r.Refresh());
+
+ Assert.That(receivedChangesCount, Is.EqualTo(1));
+ Assert.That(firstChanges, Is.Null);
+
+ registration.Dispose();
+ });
+
+ void onChanged(IRealmCollection sender, ChangeSet? changes)
+ {
+ if (receivedChangesCount == 0)
+ firstChanges = changes;
+
+ receivedChangesCount++;
+ }
+ }
+
[Test]
public void TestSubscriptionWithAsyncWrite()
{
diff --git a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs
index 28556566ba..f53dd9a62a 100644
--- a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
@@ -29,7 +28,7 @@ namespace osu.Game.Tests.Editing.Checks
{
var beatmap = new Beatmap
{
- Breaks = new List
+ Breaks =
{
new BreakPeriod(0, 649)
}
@@ -52,7 +51,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_200 }
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(100, 751)
}
@@ -75,7 +74,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_298 }
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(200, 850)
}
@@ -98,7 +97,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1200 }
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(1398, 2300)
}
@@ -121,7 +120,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 1500 }
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(0, 652)
}
@@ -145,7 +144,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1_297 },
new HitCircle { StartTime = 1_298 }
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(200, 850)
}
@@ -168,7 +167,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_300 }
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(200, 850)
}
diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs
index 1b5c5c398f..be9aa711cb 100644
--- a/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 40_000 }
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(10_000, 21_000)
}
diff --git a/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs
new file mode 100644
index 0000000000..cb1cf21734
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs
@@ -0,0 +1,128 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using System.Linq;
+using ManagedBass;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using osuTK.Audio;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckHitsoundsFormatTest
+ {
+ private CheckHitsoundsFormat check = null!;
+
+ private IBeatmap beatmap = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckHitsoundsFormat();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = { CheckTestHelpers.CreateMockFile("wav") }
+ }
+ }
+ };
+
+ // 0 = No output device. This still allows decoding.
+ if (!Bass.Init(0) && Bass.LastError != Errors.Already)
+ throw new AudioException("Could not initialize Bass.");
+ }
+
+ [Test]
+ public void TestMp3Audio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateIncorrectFormat);
+ }
+ }
+
+ [Test]
+ public void TestOggAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+ }
+
+ [Test]
+ public void TestWavAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+ }
+
+ [Test]
+ public void TestWebmAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported);
+ }
+ }
+
+ [Test]
+ public void TestNotAnAudioFile()
+ {
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = { CheckTestHelpers.CreateMockFile("png") }
+ }
+ }
+ };
+
+ using (var resourceStream = TestResources.OpenResource("Textures/test-image.png"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+ }
+
+ [Test]
+ public void TestCorruptAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported);
+ }
+ }
+
+ private BeatmapVerifierContext getContext(Stream? resourceStream)
+ {
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs b/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs
new file mode 100644
index 0000000000..98a4e1f9e9
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs
@@ -0,0 +1,112 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using System.Linq;
+using ManagedBass;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using osuTK.Audio;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public partial class CheckSongFormatTest
+ {
+ private CheckSongFormat check = null!;
+
+ private IBeatmap beatmap = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckSongFormat();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = { CheckTestHelpers.CreateMockFile("mp3") }
+ }
+ }
+ };
+
+ // 0 = No output device. This still allows decoding.
+ if (!Bass.Init(0) && Bass.LastError != Errors.Already)
+ throw new AudioException("Could not initialize Bass.");
+ }
+
+ [Test]
+ public void TestMp3Audio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
+ {
+ beatmap.Metadata.AudioFile = "abc123.mp3";
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+ }
+
+ [Test]
+ public void TestOggAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg"))
+ {
+ beatmap.Metadata.AudioFile = "abc123.mp3";
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+ }
+
+ [Test]
+ public void TestWavAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav"))
+ {
+ beatmap.Metadata.AudioFile = "abc123.mp3";
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateIncorrectFormat);
+ }
+ }
+
+ [Test]
+ public void TestWebmAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm"))
+ {
+ beatmap.Metadata.AudioFile = "abc123.mp3";
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported);
+ }
+ }
+
+ [Test]
+ public void TestCorruptAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
+ {
+ beatmap.Metadata.AudioFile = "abc123.mp3";
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported);
+ }
+ }
+
+ private BeatmapVerifierContext getContext(Stream? resourceStream)
+ {
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs
new file mode 100644
index 0000000000..a8f86a6d45
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs
@@ -0,0 +1,235 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckTitleMarkersTest
+ {
+ private CheckTitleMarkers check = null!;
+
+ private IBeatmap beatmap = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckTitleMarkers();
+
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Egao no Kanata",
+ TitleUnicode = "エガオノカナタ"
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestNoTitleMarkers()
+ {
+ var issues = check.Run(getContext(beatmap)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestTvSizeMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (TV Size)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (TV Size)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestMalformedTvSizeMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (tv size)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (tv size)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(2));
+ Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
+ }
+
+ [Test]
+ public void TestGameVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (Game Ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Game Ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestMalformedGameVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (game ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (game ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(2));
+ Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
+ }
+
+ [Test]
+ public void TestShortVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (Short Ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Short Ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestMalformedShortVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (short ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (short ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(2));
+ Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
+ }
+
+ [Test]
+ public void TestCutVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (Cut Ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Cut Ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestMalformedCutVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (cut ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (cut ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(2));
+ Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
+ }
+
+ [Test]
+ public void TestSpedUpVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (Sped Up Ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up Ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestMalformedSpedUpVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (sped up ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(2));
+ Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
+ }
+
+ [Test]
+ public void TestNightcoreMixMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (Nightcore Mix)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore Mix)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestMalformedNightcoreMixMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (nightcore mix)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore mix)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(2));
+ Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
+ }
+
+ [Test]
+ public void TestSpedUpCutVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (Sped Up & Cut Ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up & Cut Ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestMalformedSpedUpCutVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (sped up & cut ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up & cut ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(2));
+ Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
+ }
+
+ [Test]
+ public void TestNightcoreCutVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (Nightcore & Cut Ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore & Cut Ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestMalformedNightcoreCutVerMarker()
+ {
+ beatmap.BeatmapInfo.Metadata.Title += " (nightcore & cut ver.)";
+ beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore & cut ver.)";
+
+ var issues = check.Run(getContext(beatmap)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(2));
+ Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
+ }
+
+ private BeatmapVerifierContext getContext(IBeatmap beatmap)
+ {
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
\ No newline at end of file
diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
index 4918369460..b646e63955 100644
--- a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
@@ -95,18 +95,6 @@ namespace osu.Game.Tests.Editing.Checks
}
}
- [Test]
- public void TestCorruptAudioFile()
- {
- using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
- {
- var issues = check.Run(getContext(resourceStream)).ToList();
-
- Assert.That(issues, Has.Count.EqualTo(1));
- Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
- }
- }
-
private BeatmapVerifierContext getContext(Stream? resourceStream)
{
var mockWorkingBeatmap = new Mock(beatmap, null, null);
diff --git a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs
new file mode 100644
index 0000000000..bf996b06ea
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs
@@ -0,0 +1,145 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Storyboards;
+using osuTK;
+using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ public class CheckUnusedAudioAtEndTest
+ {
+ private CheckUnusedAudioAtEnd check = null!;
+
+ private IBeatmap beatmapNotFullyMapped = null!;
+
+ private IBeatmap beatmapFullyMapped = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckUnusedAudioAtEnd();
+ beatmapNotFullyMapped = new Beatmap
+ {
+ HitObjects =
+ {
+ new HitCircle { StartTime = 0 },
+ new HitCircle { StartTime = 1_298 },
+ },
+ BeatmapInfo = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }
+ }
+ };
+ beatmapFullyMapped = new Beatmap
+ {
+ HitObjects =
+ {
+ new HitCircle { StartTime = 0 },
+ new HitCircle { StartTime = 9000 },
+ },
+ BeatmapInfo = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" },
+ }
+ };
+ }
+
+ [Test]
+ public void TestEmptyBeatmap()
+ {
+ var context = getContext(new Beatmap());
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd);
+ }
+
+ [Test]
+ public void TestAudioNotFullyUsed()
+ {
+ var context = getContext(beatmapNotFullyMapped);
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd);
+ }
+
+ [Test]
+ public void TestAudioNotFullyUsedWithVideo()
+ {
+ var storyboard = new Storyboard();
+
+ var video = new StoryboardVideo("abc123.mp4", 0);
+
+ storyboard.GetLayer("Video").Add(video);
+
+ var mockWorkingBeatmap = getMockWorkingBeatmap(beatmapNotFullyMapped, storyboard);
+
+ var context = getContext(beatmapNotFullyMapped, mockWorkingBeatmap);
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEndStoryboardOrVideo);
+ }
+
+ [Test]
+ public void TestAudioNotFullyUsedWithStoryboardElement()
+ {
+ var storyboard = new Storyboard();
+
+ var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
+
+ storyboard.GetLayer("Background").Add(sprite);
+
+ var mockWorkingBeatmap = getMockWorkingBeatmap(beatmapNotFullyMapped, storyboard);
+
+ var context = getContext(beatmapNotFullyMapped, mockWorkingBeatmap);
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEndStoryboardOrVideo);
+ }
+
+ [Test]
+ public void TestAudioFullyUsed()
+ {
+ var context = getContext(beatmapFullyMapped);
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ private BeatmapVerifierContext getContext(IBeatmap beatmap)
+ {
+ return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(beatmap, new Storyboard()).Object);
+ }
+
+ private BeatmapVerifierContext getContext(IBeatmap beatmap, Mock workingBeatmap)
+ {
+ return new BeatmapVerifierContext(beatmap, workingBeatmap.Object);
+ }
+
+ private Mock getMockWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard)
+ {
+ var mockTrack = new TrackVirtualStore(new FramedClock()).GetVirtual(10000, "virtual");
+
+ var mockWorkingBeatmap = new Mock();
+ mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
+ mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack);
+ mockWorkingBeatmap.SetupGet(w => w.Storyboard).Returns(storyboard);
+
+ return mockWorkingBeatmap;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs
new file mode 100644
index 0000000000..1e16c67aab
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs
@@ -0,0 +1,89 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckVideoResolutionTest
+ {
+ private CheckVideoResolution check = null!;
+
+ private IBeatmap beatmap = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckVideoResolution();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files =
+ {
+ CheckTestHelpers.CreateMockFile("mp4"),
+ }
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestNoVideo()
+ {
+ beatmap.BeatmapInfo.BeatmapSet?.Files.Clear();
+
+ var issues = check.Run(getContext(null)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestVideoAcceptableResolution()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+ }
+
+ [Test]
+ public void TestVideoHighResolution()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video-resolution-high.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckVideoResolution.IssueTemplateHighResolution);
+ }
+ }
+
+ private BeatmapVerifierContext getContext(Stream? resourceStream)
+ {
+ var storyboard = new Storyboard();
+ var layer = storyboard.GetLayer("Video");
+ layer.Add(new StoryboardVideo("abc123.mp4", 0));
+
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+ mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs
new file mode 100644
index 0000000000..49154f1cbb
--- /dev/null
+++ b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Game.Rulesets.Edit;
+
+namespace osu.Game.Tests.Editing
+{
+ [TestFixture]
+ public class EditorTimestampParserTest
+ {
+ private static readonly object?[][] test_cases =
+ {
+ new object?[] { ":", false, null, null },
+ new object?[] { "1", true, TimeSpan.FromMilliseconds(1), null },
+ new object?[] { "99", true, TimeSpan.FromMilliseconds(99), null },
+ new object?[] { "320000", true, TimeSpan.FromMilliseconds(320000), null },
+ new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null },
+ new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null },
+ new object?[] { "1:92", false, null, null },
+ new object?[] { "1:002", false, null, null },
+ new object?[] { "1:02:3", true, new TimeSpan(0, 0, 1, 2, 3), null },
+ new object?[] { "1:02:300", true, new TimeSpan(0, 0, 1, 2, 300), null },
+ new object?[] { "1:02:3000", false, null, null },
+ new object?[] { "1:02:300 ()", false, null, null },
+ new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
+ new object?[] { "1:02:300 (1,2,3) - ", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
+ new object?[] { "1:02:300 (1,2,3) - following mod", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
+ new object?[] { "1:02:300 (1,2,3) - following mod\nwith newlines", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
+ };
+
+ [TestCaseSource(nameof(test_cases))]
+ public void TestTryParse(string timestamp, bool expectedSuccess, TimeSpan? expectedParsedTime, string? expectedSelection)
+ {
+ bool actualSuccess = EditorTimestampParser.TryParse(timestamp, out var actualParsedTime, out string? actualSelection);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(actualSuccess, Is.EqualTo(expectedSuccess));
+ Assert.That(actualParsedTime, Is.EqualTo(expectedParsedTime));
+ Assert.That(actualSelection, Is.EqualTo(expectedSelection));
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs
new file mode 100644
index 0000000000..bbcf6aac2c
--- /dev/null
+++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs
@@ -0,0 +1,543 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+
+namespace osu.Game.Tests.Editing
+{
+ [TestFixture]
+ public class TestSceneEditorBeatmapProcessor
+ {
+ [Test]
+ public void TestEmptyBeatmap()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Is.Empty);
+ }
+
+ [Test]
+ public void TestSingleObjectBeatmap()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Is.Empty);
+ }
+
+ [Test]
+ public void TestTwoObjectsCloseTogether()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ new Note { StartTime = 2000 },
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Is.Empty);
+ }
+
+ [Test]
+ public void TestHoldNote()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new HoldNote { StartTime = 1000, Duration = 10000 },
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestHoldNoteWithOverlappingNote()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new HoldNote { StartTime = 1000, Duration = 10000 },
+ new Note { StartTime = 2000 },
+ new Note { StartTime = 12000 },
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestTwoObjectsFarApart()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ new Note { StartTime = 5000 },
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
+ Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
+ Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000));
+ });
+ }
+
+ [Test]
+ public void TestBreaksAreFused()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ new Note { StartTime = 9000 },
+ },
+ Breaks =
+ {
+ new BreakPeriod(1200, 4000),
+ new BreakPeriod(5200, 8000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
+ Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
+ Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8000));
+ });
+ }
+
+ [Test]
+ public void TestBreaksAreSplit()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ new Note { StartTime = 5000 },
+ new Note { StartTime = 9000 },
+ },
+ Breaks =
+ {
+ new BreakPeriod(1200, 8000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(2));
+ Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
+ Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000));
+ Assert.That(beatmap.Breaks[1].StartTime, Is.EqualTo(5200));
+ Assert.That(beatmap.Breaks[1].EndTime, Is.EqualTo(8000));
+ });
+ }
+
+ [Test]
+ public void TestBreaksAreNudged()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1100 },
+ new Note { StartTime = 9000 },
+ },
+ Breaks =
+ {
+ new BreakPeriod(1200, 8000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
+ Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1300));
+ Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8000));
+ });
+ }
+
+ [Test]
+ public void TestManualBreaksAreNotFused()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ new Note { StartTime = 9000 },
+ },
+ Breaks =
+ {
+ new ManualBreakPeriod(1200, 4000),
+ new ManualBreakPeriod(5200, 8000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(2));
+ Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
+ Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000));
+ Assert.That(beatmap.Breaks[1].StartTime, Is.EqualTo(5200));
+ Assert.That(beatmap.Breaks[1].EndTime, Is.EqualTo(8000));
+ });
+ }
+
+ [Test]
+ public void TestManualBreaksAreSplit()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ new Note { StartTime = 5000 },
+ new Note { StartTime = 9000 },
+ },
+ Breaks =
+ {
+ new ManualBreakPeriod(1200, 8000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(2));
+ Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
+ Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000));
+ Assert.That(beatmap.Breaks[1].StartTime, Is.EqualTo(5200));
+ Assert.That(beatmap.Breaks[1].EndTime, Is.EqualTo(8000));
+ });
+ }
+
+ [Test]
+ public void TestManualBreaksAreNotNudged()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ new Note { StartTime = 9000 },
+ },
+ Breaks =
+ {
+ new ManualBreakPeriod(1200, 8800),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
+ Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
+ Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8800));
+ });
+ }
+
+ [Test]
+ public void TestBreaksAtEndOfBeatmapAreRemoved()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ new Note { StartTime = 2000 },
+ },
+ Breaks =
+ {
+ new BreakPeriod(10000, 15000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Is.Empty);
+ }
+
+ [Test]
+ public void TestManualBreaksAtEndOfBeatmapAreRemoved()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 1000 },
+ new Note { StartTime = 2000 },
+ },
+ Breaks =
+ {
+ new ManualBreakPeriod(10000, 15000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Is.Empty);
+ }
+
+ [Test]
+ public void TestManualBreaksAtEndOfBeatmapAreRemovedCorrectlyEvenWithConcurrentObjects()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new HoldNote { StartTime = 1000, EndTime = 20000 },
+ new HoldNote { StartTime = 2000, EndTime = 3000 },
+ },
+ Breaks =
+ {
+ new ManualBreakPeriod(10000, 15000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Is.Empty);
+ }
+
+ [Test]
+ public void TestBreaksAtStartOfBeatmapAreRemoved()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 10000 },
+ new Note { StartTime = 11000 },
+ },
+ Breaks =
+ {
+ new BreakPeriod(0, 9000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Is.Empty);
+ }
+
+ [Test]
+ public void TestManualBreaksAtStartOfBeatmapAreRemoved()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Note { StartTime = 10000 },
+ new Note { StartTime = 11000 },
+ },
+ Breaks =
+ {
+ new ManualBreakPeriod(0, 9000),
+ }
+ });
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.That(beatmap.Breaks, Is.Empty);
+ }
+
+ [Test]
+ public void TestTimePreemptIsRespected()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ Difficulty =
+ {
+ ApproachRate = 10,
+ },
+ HitObjects =
+ {
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 5000 },
+ }
+ });
+
+ foreach (var ho in beatmap.HitObjects)
+ ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
+ Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
+ Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MIN));
+ });
+
+ beatmap.Difficulty.ApproachRate = 0;
+
+ foreach (var ho in beatmap.HitObjects)
+ ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
+
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
+ Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
+ Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX));
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index 463287fb35..cf8c3c6ef1 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.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
{
@@ -50,10 +52,7 @@ namespace osu.Game.Tests.Editing
[SetUp]
public void Setup() => Schedule(() =>
{
- Children = new Drawable[]
- {
- composer = new TestHitObjectComposer()
- };
+ Child = composer = new TestHitObjectComposer();
BeatDivisor.Value = 1;
@@ -228,6 +227,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().Single()));
+ AddUntilStep("use current snap expanded", () => composer.ChildrenOfType().Single().Expanded.Value, () => Is.True);
+
+ AddStep("seek before first object", () => EditorClock.Seek(0));
+ AddUntilStep("use current snap not available", () => composer.ChildrenOfType().Single().Enabled.Value, () => Is.False);
+
+ AddStep("seek to between objects", () => EditorClock.Seek(1500));
+ AddUntilStep("use current snap available", () => composer.ChildrenOfType().Single().Enabled.Value, () => Is.True);
+
+ AddStep("seek after last object", () => EditorClock.Seek(2500));
+ AddUntilStep("use current snap not available", () => composer.ChildrenOfType().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));
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
index 10dbede2e0..22643feebb 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
@@ -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
}
@@ -216,7 +216,7 @@ namespace osu.Game.Tests.Gameplay
LifetimeStart = LIFETIME_ON_APPLY;
}
- public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
+ public void MissForcefully() => ApplyResult(HitResult.Miss);
protected override void UpdateHitStateTransforms(ArmedState state)
{
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
index 1a644ad600..8ec18377f4 100644
--- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -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));
diff --git a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
index 4101652c49..e31a3dbdf0 100644
--- a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
+++ b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
@@ -8,6 +8,8 @@ using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Mods
@@ -105,9 +107,6 @@ namespace osu.Game.Tests.Mods
testMod.ResetSettingsToDefaults();
Assert.That(testMod.DrainRate.Value, Is.Null);
-
- // ReSharper disable once HeuristicUnreachableCode
- // see https://youtrack.jetbrains.com/issue/RIDER-70159.
Assert.That(testMod.OverallDifficulty.Value, Is.Null);
var applied = applyDifficulty(new BeatmapDifficulty
@@ -119,6 +118,48 @@ namespace osu.Game.Tests.Mods
Assert.That(applied.OverallDifficulty, Is.EqualTo(10));
}
+ [Test]
+ public void TestDeserializeIncorrectRange()
+ {
+ var apiMod = new APIMod
+ {
+ Acronym = @"DA",
+ Settings = new Dictionary
+ {
+ [@"circle_size"] = -727,
+ [@"approach_rate"] = -727,
+ }
+ };
+ var ruleset = new OsuRuleset();
+
+ var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(mod.CircleSize.Value, Is.GreaterThanOrEqualTo(0).And.LessThanOrEqualTo(11));
+ Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11));
+ });
+ }
+
+ [Test]
+ public void TestDeserializeNegativeApproachRate()
+ {
+ var apiMod = new APIMod
+ {
+ Acronym = @"DA",
+ Settings = new Dictionary
+ {
+ [@"approach_rate"] = -9,
+ }
+ };
+ var ruleset = new OsuRuleset();
+
+ var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset);
+
+ Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11));
+ Assert.That(mod.ApproachRate.Value, Is.EqualTo(-9));
+ }
+
///
/// Applies a to the mod and returns a new
/// representing the result if the mod were applied to a fresh instance.
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index 2d5d425ee8..d7df3d318d 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
@@ -286,5 +287,62 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.TimingPoints[0].BeatLength, Is.Not.EqualTo(cpiCopy.TimingPoints[0].BeatLength));
}
+
+ [Test]
+ public void TestBinarySearchEmptyList()
+ {
+ Assert.That(ControlPointInfo.BinarySearch(Array.Empty(), 0, EqualitySelection.FirstFound), Is.EqualTo(-1));
+ Assert.That(ControlPointInfo.BinarySearch(Array.Empty(), 0, EqualitySelection.Leftmost), Is.EqualTo(-1));
+ Assert.That(ControlPointInfo.BinarySearch(Array.Empty(), 0, EqualitySelection.Rightmost), Is.EqualTo(-1));
+ }
+
+ [TestCase(new[] { 1 }, 0, -1)]
+ [TestCase(new[] { 1 }, 1, 0)]
+ [TestCase(new[] { 1 }, 2, -2)]
+ [TestCase(new[] { 1, 3 }, 0, -1)]
+ [TestCase(new[] { 1, 3 }, 1, 0)]
+ [TestCase(new[] { 1, 3 }, 2, -2)]
+ [TestCase(new[] { 1, 3 }, 3, 1)]
+ [TestCase(new[] { 1, 3 }, 4, -3)]
+ public void TestBinarySearchUniqueScenarios(int[] values, int search, int expectedIndex)
+ {
+ var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
+ Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
+ Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
+ Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
+ }
+
+ [TestCase(new[] { 1, 1 }, 1, 0)]
+ [TestCase(new[] { 1, 2, 2 }, 2, 1)]
+ [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
+ [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)]
+ [TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
+ public void TestBinarySearchFirstFoundDuplicateScenarios(int[] values, int search, int expectedIndex)
+ {
+ var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
+ Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
+ }
+
+ [TestCase(new[] { 1, 1 }, 1, 0)]
+ [TestCase(new[] { 1, 2, 2 }, 2, 1)]
+ [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
+ [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)]
+ [TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
+ public void TestBinarySearchLeftMostDuplicateScenarios(int[] values, int search, int expectedIndex)
+ {
+ var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
+ Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
+ }
+
+ [TestCase(new[] { 1, 1 }, 1, 1)]
+ [TestCase(new[] { 1, 2, 2 }, 2, 2)]
+ [TestCase(new[] { 1, 2, 2, 2 }, 2, 3)]
+ [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)]
+ [TestCase(new[] { 1, 2, 2, 3 }, 2, 2)]
+ public void TestBinarySearchRightMostDuplicateScenarios(int[] values, int search, int expectedIndex)
+ {
+ var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
+ Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
+ }
}
}
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
index c7a32ebbc4..10e0e46f4c 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
@@ -1,10 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using NUnit.Framework;
+using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Filter;
+using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;
@@ -309,8 +312,10 @@ namespace osu.Game.Tests.NonVisual.Filtering
match = shouldMatch;
}
- public bool Matches(BeatmapInfo beatmapInfo) => match;
+ public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) => match;
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false;
+
+ public bool FilterMayChangeFromMods(ValueChangedEvent> mods) => false;
}
}
}
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
index 739a72df08..e6006b7fd2 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter;
+using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;
@@ -256,8 +259,8 @@ namespace osu.Game.Tests.NonVisual.Filtering
const string query = "status=r";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
- Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
- Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
+ Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values);
+ Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked));
}
[Test]
@@ -268,16 +271,79 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim());
Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
- Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
- Assert.IsTrue(filterCriteria.OnlineStatus.IsLowerInclusive);
- Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
- Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive);
+ Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values);
+ Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked));
}
[Test]
- public void TestApplyCreatorQueries()
+ public void TestApplyMultipleEqualityStatusQueries()
{
- const string query = "beatmap specifically by creator=my_fav";
+ const string query = "status=ranked status=loved";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.That(filterCriteria.OnlineStatus.Values, Is.Empty);
+ }
+
+ [Test]
+ public void TestApplyEqualStatusQueryWithMultipleValues()
+ {
+ const string query = "status=ranked,loved";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.That(filterCriteria.OnlineStatus.Values, Is.Not.Empty);
+ Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked));
+ Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved));
+ }
+
+ [Test]
+ public void TestApplyRangeStatusMatches()
+ {
+ const string query = "status>=r";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.That(filterCriteria.OnlineStatus.Values, Has.Count.EqualTo(4));
+ Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked));
+ Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Approved));
+ Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Qualified));
+ Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved));
+ }
+
+ [Test]
+ public void TestApplyRangeStatusWithMultipleMatchesQuery()
+ {
+ const string query = "status>=r,l";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.That(filterCriteria.OnlineStatus.Values, Is.EquivalentTo(Enum.GetValues()));
+ }
+
+ [Test]
+ public void TestApplyTwoRangeStatusQuery()
+ {
+ const string query = "status>r status true;
+ public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) => true;
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
{
@@ -451,6 +517,137 @@ namespace osu.Game.Tests.NonVisual.Filtering
return false;
}
+
+ public bool FilterMayChangeFromMods(ValueChangedEvent> mods) => 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 = $"lastplayed<{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 = "lastplayed>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 = "lastplayed<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 = "lastplayed>3M lastplayed<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 = "lastplayed=50";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual(false, filterCriteria.LastPlayed.HasFilter);
+ }
+
+ [Test]
+ public void TestOutOfRangeDateQuery()
+ {
+ const string query = "lastplayed<10000y";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
+ Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
+ }
+
+ private static readonly object[] played_query_tests =
+ {
+ new object[] { "0", DateTimeOffset.MinValue, true },
+ new object[] { "0", DateTimeOffset.Now, false },
+ new object[] { "false", DateTimeOffset.MinValue, true },
+ new object[] { "false", DateTimeOffset.Now, false },
+
+ new object[] { "1", DateTimeOffset.MinValue, false },
+ new object[] { "1", DateTimeOffset.Now, true },
+ new object[] { "true", DateTimeOffset.MinValue, false },
+ new object[] { "true", DateTimeOffset.Now, true },
+ };
+
+ [Test]
+ [TestCaseSource(nameof(played_query_tests))]
+ public void TestPlayedQuery(string query, DateTimeOffset reference, bool matched)
+ {
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, $"played={query}");
+ Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
+ Assert.AreEqual(matched, filterCriteria.LastPlayed.IsInRange(reference));
}
}
}
diff --git a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
index 0bdd0ceae6..d4b69c1be2 100644
--- a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
+++ b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
@@ -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 Mods { get; }
public override double GameplayStartTime { get; }
diff --git a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs
new file mode 100644
index 0000000000..f860cd097a
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs
@@ -0,0 +1,216 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.UI;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [TestFixture]
+ public class TestSceneTimedDifficultyCalculation
+ {
+ [Test]
+ public void TestAttributesGeneratedForEachObjectOnce()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new TestHitObject { StartTime = 1 },
+ new TestHitObject
+ {
+ StartTime = 2,
+ Nested = 1
+ },
+ new TestHitObject { StartTime = 3 },
+ }
+ };
+
+ List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
+
+ Assert.That(attribs.Count, Is.EqualTo(3));
+ assertEquals(attribs[0], beatmap.HitObjects[0]);
+ assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
+ assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
+ }
+
+ [Test]
+ public void TestAttributesGeneratedForSkippedObjects()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ // The first object is usually skipped in all implementations
+ new TestHitObject
+ {
+ StartTime = 1,
+ Skip = true
+ },
+ // An intermediate skipped object.
+ new TestHitObject
+ {
+ StartTime = 2,
+ Skip = true
+ },
+ new TestHitObject { StartTime = 3 },
+ }
+ };
+
+ List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
+
+ Assert.That(attribs.Count, Is.EqualTo(3));
+ assertEquals(attribs[0], beatmap.HitObjects[0]);
+ assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
+ assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
+ }
+
+ [Test]
+ public void TestAttributesGeneratedOnceForSkippedObjects()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new TestHitObject { StartTime = 1 },
+ new TestHitObject
+ {
+ StartTime = 2,
+ Nested = 5,
+ Skip = true
+ },
+ new TestHitObject
+ {
+ StartTime = 3,
+ Skip = true
+ },
+ }
+ };
+
+ List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
+
+ Assert.That(attribs.Count, Is.EqualTo(3));
+ assertEquals(attribs[0], beatmap.HitObjects[0]);
+ assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
+ assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
+ }
+
+ private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected)
+ {
+ Assert.That(((TestDifficultyAttributes)attribs.Attributes).Objects, Is.EquivalentTo(expected));
+ }
+
+ private class TestHitObject : HitObject
+ {
+ ///
+ /// Whether to skip generating a difficulty representation for this object.
+ ///
+ public bool Skip { get; set; }
+
+ ///
+ /// Whether to generate nested difficulty representations for this object, and if so, how many.
+ ///
+ public int Nested { get; set; }
+
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ for (int i = 0; i < Nested; i++)
+ AddNested(new TestHitObject { StartTime = StartTime + 0.1 * i });
+ }
+ }
+
+ private class TestRuleset : Ruleset
+ {
+ public override IEnumerable GetModsFor(ModType type) => Enumerable.Empty();
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => throw new NotImplementedException();
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new PassThroughBeatmapConverter(beatmap);
+
+ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TestDifficultyCalculator(beatmap);
+
+ public override string Description => string.Empty;
+ public override string ShortName => string.Empty;
+
+ private class PassThroughBeatmapConverter : IBeatmapConverter
+ {
+ public event Action>? ObjectConverted
+ {
+ add { }
+ remove { }
+ }
+
+ public IBeatmap Beatmap { get; }
+
+ public PassThroughBeatmapConverter(IBeatmap beatmap)
+ {
+ Beatmap = beatmap;
+ }
+
+ public bool CanConvert() => true;
+
+ public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap;
+ }
+ }
+
+ private class TestDifficultyCalculator : DifficultyCalculator
+ {
+ public TestDifficultyCalculator(IWorkingBeatmap beatmap)
+ : base(new TestRuleset().RulesetInfo, beatmap)
+ {
+ }
+
+ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+ => new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() };
+
+ protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
+ {
+ List objects = new List();
+
+ foreach (var obj in beatmap.HitObjects.OfType())
+ {
+ if (!obj.Skip)
+ objects.Add(new DifficultyHitObject(obj, obj, clockRate, objects, objects.Count));
+
+ foreach (var nested in obj.NestedHitObjects)
+ objects.Add(new DifficultyHitObject(nested, nested, clockRate, objects, objects.Count));
+ }
+
+ return objects;
+ }
+
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new PassThroughSkill(mods) };
+
+ private class PassThroughSkill : Skill
+ {
+ public PassThroughSkill(Mod[] mods)
+ : base(mods)
+ {
+ }
+
+ public override void Process(DifficultyHitObject current)
+ {
+ }
+
+ public override double DifficultyValue() => 1;
+ }
+ }
+
+ private class TestDifficultyAttributes : DifficultyAttributes
+ {
+ public HitObject[] Objects = Array.Empty();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 585fd516bd..ae3451c3e0 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -257,7 +257,7 @@ namespace osu.Game.Tests.Online
{
}
- protected override string Target => null;
+ protected override string Target => string.Empty;
}
}
}
diff --git a/osu.Game.Tests/Resources/Archives/argon-invalid-drawable.osk b/osu.Game.Tests/Resources/Archives/argon-invalid-drawable.osk
new file mode 100644
index 0000000000..23c318149c
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/argon-invalid-drawable.osk differ
diff --git a/osu.Game.Tests/Resources/Archives/argon-layout-version-0.osk b/osu.Game.Tests/Resources/Archives/argon-layout-version-0.osk
new file mode 100644
index 0000000000..f767033eb1
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/argon-layout-version-0.osk differ
diff --git a/osu.Game.Tests/Resources/Archives/classic-layout-version-0.osk b/osu.Game.Tests/Resources/Archives/classic-layout-version-0.osk
new file mode 100644
index 0000000000..8240510f7c
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/classic-layout-version-0.osk differ
diff --git a/osu.Game.Tests/Resources/Archives/japanese-filename.osz b/osu.Game.Tests/Resources/Archives/japanese-filename.osz
new file mode 100644
index 0000000000..4825c88179
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/japanese-filename.osz differ
diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20240305.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20240305.osk
new file mode 100644
index 0000000000..6db24352a5
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-argon-20240305.osk differ
diff --git a/osu.Game.Tests/Resources/Archives/modified-classic-20230809.osk b/osu.Game.Tests/Resources/Archives/modified-classic-20230809.osk
new file mode 100644
index 0000000000..b200ab1261
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-classic-20230809.osk differ
diff --git a/osu.Game.Tests/Resources/Archives/modified-classic-20240724.osk b/osu.Game.Tests/Resources/Archives/modified-classic-20240724.osk
new file mode 100644
index 0000000000..29a06abf1d
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-classic-20240724.osk differ
diff --git a/osu.Game.Tests/Resources/Archives/modified-default-20230809.osk b/osu.Game.Tests/Resources/Archives/modified-default-20230809.osk
new file mode 100644
index 0000000000..a46c20d2b8
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-default-20230809.osk differ
diff --git a/osu.Game.Tests/Resources/Archives/skin-with-subfolder-zip-entries.osk b/osu.Game.Tests/Resources/Archives/skin-with-subfolder-zip-entries.osk
new file mode 100644
index 0000000000..013bca3801
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/skin-with-subfolder-zip-entries.osk differ
diff --git a/osu.Game.Tests/Resources/Archives/triangles-layout-version-0.osk b/osu.Game.Tests/Resources/Archives/triangles-layout-version-0.osk
new file mode 100644
index 0000000000..5601eb279b
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/triangles-layout-version-0.osk differ
diff --git a/osu.Game.Tests/Resources/Replays/mania-replay.osr b/osu.Game.Tests/Resources/Replays/mania-replay.osr
index da1a7bdd28..ad55a5a318 100644
Binary files a/osu.Game.Tests/Resources/Replays/mania-replay.osr and b/osu.Game.Tests/Resources/Replays/mania-replay.osr differ
diff --git a/osu.Game.Tests/Resources/Samples/test-sample.ogg b/osu.Game.Tests/Resources/Samples/test-sample.ogg
new file mode 100644
index 0000000000..b33119cfaf
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/test-sample.ogg differ
diff --git a/osu.Game.Tests/Resources/Samples/test-sample.webm b/osu.Game.Tests/Resources/Samples/test-sample.webm
new file mode 100644
index 0000000000..3964d248f4
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/test-sample.webm differ
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index a77dc8d49b..e0572e604c 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -73,7 +73,12 @@ namespace osu.Game.Tests.Resources
private static string getTempFilename() => temp_storage.GetFullPath(Guid.NewGuid() + ".osz");
- private static int importId;
+ private static int testId = 1;
+
+ ///
+ /// Get a unique int value which is incremented each call.
+ ///
+ public static int GetNextTestID() => Interlocked.Increment(ref testId);
///
/// Create a test beatmap set model.
@@ -88,7 +93,7 @@ namespace osu.Game.Tests.Resources
RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length];
- int setId = Interlocked.Increment(ref importId);
+ int setId = GetNextTestID();
var metadata = new BeatmapMetadata
{
diff --git a/osu.Game.Tests/Resources/Videos/test-video-resolution-high.mp4 b/osu.Game.Tests/Resources/Videos/test-video-resolution-high.mp4
new file mode 100644
index 0000000000..fbdb00d3ad
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-resolution-high.mp4 differ
diff --git a/osu.Game.Tests/Resources/hitobject-coordinates-lazer.osu b/osu.Game.Tests/Resources/hitobject-coordinates-lazer.osu
new file mode 100644
index 0000000000..bb898a1521
--- /dev/null
+++ b/osu.Game.Tests/Resources/hitobject-coordinates-lazer.osu
@@ -0,0 +1,6 @@
+osu file format v128
+
+[HitObjects]
+// Coordinates should be preserves in lazer beatmaps.
+
+256.99853,256.001,1000,49,0,0:0:0:0:
diff --git a/osu.Game.Tests/Resources/hitobject-coordinates-legacy.osu b/osu.Game.Tests/Resources/hitobject-coordinates-legacy.osu
new file mode 100644
index 0000000000..e914c2fb36
--- /dev/null
+++ b/osu.Game.Tests/Resources/hitobject-coordinates-legacy.osu
@@ -0,0 +1,5 @@
+osu file format v14
+
+[HitObjects]
+// Coordinates should be truncated to int values in legacy beatmaps.
+256.99853,256.001,1000,49,0,0:0:0:0:
diff --git a/osu.Game.Tests/Resources/out-of-range-difficulties-mania.osu b/osu.Game.Tests/Resources/out-of-range-difficulties-mania.osu
new file mode 100644
index 0000000000..7dc2e51ad9
--- /dev/null
+++ b/osu.Game.Tests/Resources/out-of-range-difficulties-mania.osu
@@ -0,0 +1,5 @@
+[General]
+Mode: 3
+
+[Difficulty]
+CircleSize:14
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/out-of-range-difficulties.osu b/osu.Game.Tests/Resources/out-of-range-difficulties.osu
new file mode 100644
index 0000000000..5029395614
--- /dev/null
+++ b/osu.Game.Tests/Resources/out-of-range-difficulties.osu
@@ -0,0 +1,10 @@
+[General]
+Mode: 0
+
+[Difficulty]
+HPDrainRate:25
+CircleSize:25
+OverallDifficulty:25
+ApproachRate:30
+SliderMultiplier:30
+SliderTickRate:30
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu
new file mode 100644
index 0000000000..8b10f21f52
--- /dev/null
+++ b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu
@@ -0,0 +1,22 @@
+osu file format v128
+
+[General]
+SampleSet: Normal
+
+[TimingPoints]
+15,1000,4,1,0,100,1,0
+2271,-100,4,1,0,5,0,0
+6021,-100,4,1,0,100,0,0
+8515,-100,4,1,0,5,0,0
+12765,-100,4,1,0,100,0,0
+14764,-100,4,1,0,5,0,0
+14770,-100,4,1,0,50,0,0
+17264,-100,4,1,0,5,0,0
+17270,-100,4,1,0,50,0,0
+22264,-100,4,1,0,100,0,0
+
+[HitObjects]
+113,54,2265,6,0,L|422:55,1,300,0|0,1:0|1:0,1:0:0:0:
+82,206,6015,2,0,L|457:204,1,350,0|0,2:0|2:0,2:0:0:0:
+75,310,10265,2,0,L|435:312,1,350,0|0,3:0|3:0,3:0:0:0:
+75,310,14764,2,0,L|435:312,3,350,0|0|0|0,3:0|3:0|3:0|3:0,3:0:0:0:
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
index a3f91fffba..1647fbee42 100644
--- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -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)
{
diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
index 11f3fe660d..b089144233 100644
--- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
@@ -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);
diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
index ebbc329b9d..9c72804a6b 100644
--- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
+++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
@@ -15,6 +15,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@@ -23,6 +24,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Resources;
+using osu.Game.Users;
namespace osu.Game.Tests.Scores.IO
{
@@ -284,6 +286,272 @@ namespace osu.Game.Tests.Scores.IO
}
}
+ [Test]
+ public void TestUserLookedUpByUsernameForOnlineScoreIfUserIDMissing()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
+ {
+ try
+ {
+ var osu = LoadOsuIntoHost(host, true);
+
+ var api = (DummyAPIAccess)osu.API;
+ api.HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case GetUserRequest userRequest:
+ if (userRequest.Lookup != "Test user")
+ return false;
+
+ userRequest.TriggerSuccess(new APIUser
+ {
+ Username = "Test user",
+ CountryCode = CountryCode.JP,
+ Id = 1234
+ });
+ return true;
+
+ default:
+ return false;
+ }
+ };
+
+ var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
+
+ var toImport = new ScoreInfo
+ {
+ Rank = ScoreRank.B,
+ TotalScore = 987654,
+ Accuracy = 0.8,
+ MaxCombo = 500,
+ Combo = 250,
+ User = new APIUser { Username = "Test user" },
+ Date = DateTimeOffset.Now,
+ OnlineID = 12345,
+ Ruleset = new OsuRuleset().RulesetInfo,
+ BeatmapInfo = beatmap.Beatmaps.First()
+ };
+
+ var imported = LoadScoreIntoOsu(osu, toImport);
+
+ Assert.AreEqual(toImport.Rank, imported.Rank);
+ Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
+ Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
+ Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
+ Assert.AreEqual(toImport.User.Username, imported.User.Username);
+ Assert.AreEqual(toImport.Date, imported.Date);
+ Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
+ Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
+ Assert.AreEqual(1234, imported.RealmUser.OnlineID);
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public void TestUserLookedUpByUsernameForLegacyOnlineScore()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
+ {
+ try
+ {
+ var osu = LoadOsuIntoHost(host, true);
+
+ var api = (DummyAPIAccess)osu.API;
+ api.HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case GetUserRequest userRequest:
+ if (userRequest.Lookup != "Test user")
+ return false;
+
+ userRequest.TriggerSuccess(new APIUser
+ {
+ Username = "Test user",
+ CountryCode = CountryCode.JP,
+ Id = 1234
+ });
+ return true;
+
+ default:
+ return false;
+ }
+ };
+
+ var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
+
+ var toImport = new ScoreInfo
+ {
+ Rank = ScoreRank.B,
+ TotalScore = 987654,
+ Accuracy = 0.8,
+ MaxCombo = 500,
+ Combo = 250,
+ User = new APIUser { Username = "Test user" },
+ Date = DateTimeOffset.Now,
+ LegacyOnlineID = 12345,
+ Ruleset = new OsuRuleset().RulesetInfo,
+ BeatmapInfo = beatmap.Beatmaps.First()
+ };
+
+ var imported = LoadScoreIntoOsu(osu, toImport);
+
+ Assert.AreEqual(toImport.Rank, imported.Rank);
+ Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
+ Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
+ Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
+ Assert.AreEqual(toImport.User.Username, imported.User.Username);
+ Assert.AreEqual(toImport.Date, imported.Date);
+ Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
+ Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
+ Assert.AreEqual(1234, imported.RealmUser.OnlineID);
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public void TestUserNotLookedUpForOfflineScoreIfUserIDMissing()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
+ {
+ try
+ {
+ var osu = LoadOsuIntoHost(host, true);
+
+ var api = (DummyAPIAccess)osu.API;
+ api.HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case GetUserRequest userRequest:
+ if (userRequest.Lookup != "Test user")
+ return false;
+
+ userRequest.TriggerSuccess(new APIUser
+ {
+ Username = "Test user",
+ CountryCode = CountryCode.JP,
+ Id = 1234
+ });
+ return true;
+
+ default:
+ return false;
+ }
+ };
+
+ var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
+
+ var toImport = new ScoreInfo
+ {
+ Rank = ScoreRank.B,
+ TotalScore = 987654,
+ Accuracy = 0.8,
+ MaxCombo = 500,
+ Combo = 250,
+ User = new APIUser { Username = "Test user" },
+ Date = DateTimeOffset.Now,
+ OnlineID = -1,
+ LegacyOnlineID = -1,
+ Ruleset = new OsuRuleset().RulesetInfo,
+ BeatmapInfo = beatmap.Beatmaps.First()
+ };
+
+ var imported = LoadScoreIntoOsu(osu, toImport);
+
+ Assert.AreEqual(toImport.Rank, imported.Rank);
+ Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
+ Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
+ Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
+ Assert.AreEqual(toImport.User.Username, imported.User.Username);
+ Assert.AreEqual(toImport.Date, imported.Date);
+ Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
+ Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
+ Assert.That(imported.RealmUser.OnlineID, Is.LessThanOrEqualTo(1));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public void TestUserLookedUpByOnlineIDIfPresent([Values] bool isOnlineScore)
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
+ {
+ try
+ {
+ var osu = LoadOsuIntoHost(host, true);
+
+ var api = (DummyAPIAccess)osu.API;
+ api.HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case GetUserRequest userRequest:
+ if (userRequest.Lookup != "5555")
+ return false;
+
+ userRequest.TriggerSuccess(new APIUser
+ {
+ Username = "Some other guy",
+ CountryCode = CountryCode.DE,
+ Id = 5555
+ });
+ return true;
+
+ default:
+ return false;
+ }
+ };
+
+ var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
+
+ var toImport = new ScoreInfo
+ {
+ Rank = ScoreRank.B,
+ TotalScore = 987654,
+ Accuracy = 0.8,
+ MaxCombo = 500,
+ Combo = 250,
+ User = new APIUser { Id = 5555 },
+ Date = DateTimeOffset.Now,
+ Ruleset = new OsuRuleset().RulesetInfo,
+ BeatmapInfo = beatmap.Beatmaps.First()
+ };
+ if (isOnlineScore)
+ toImport.OnlineID = 12345;
+
+ var imported = LoadScoreIntoOsu(osu, toImport);
+
+ Assert.AreEqual(toImport.Rank, imported.Rank);
+ Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
+ Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
+ Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
+ Assert.AreEqual(toImport.Date, imported.Date);
+ Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
+ Assert.AreEqual("Some other guy", imported.RealmUser.Username);
+ Assert.AreEqual(5555, imported.RealmUser.OnlineID);
+ Assert.AreEqual(CountryCode.DE, imported.RealmUser.CountryCode);
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
{
// clone to avoid attaching the input score to realm.
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index 606a5afac2..62e7a80435 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -173,6 +173,16 @@ namespace osu.Game.Tests.Skins.IO
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", 1.0m, osu);
});
+ [Test]
+ public Task TestImportWithSubfolder() => runSkinTest(async osu =>
+ {
+ const string filename = "Archives/skin-with-subfolder-zip-entries.osk";
+ var import = await loadSkinIntoOsu(osu, new ImportTask(TestResources.OpenResource(filename), filename));
+
+ assertCorrectMetadata(import, $"Totally fully features skin [Real Skin with Real Features] [{filename[..^4]}]", "Unknown", 2.7m, osu);
+ Assert.That(import.PerformRead(r => r.Files.Count), Is.EqualTo(3));
+ });
+
#endregion
#region Cases where imports should be uniquely imported
diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
index 6423e061c5..7372557161 100644
--- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
+++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
@@ -12,6 +12,7 @@ using osu.Framework.IO.Stores;
using osu.Game.Audio;
using osu.Game.IO;
using osu.Game.IO.Archives;
+using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
@@ -60,6 +61,14 @@ namespace osu.Game.Tests.Skins
"Archives/modified-argon-20231106.osk",
// Covers "Argon" accuracy/score/combo counters, and wedges
"Archives/modified-argon-20231108.osk",
+ // Covers "Argon" performance points counter
+ "Archives/modified-argon-20240305.osk",
+ // Covers default rank display
+ "Archives/modified-default-20230809.osk",
+ // Covers legacy rank display
+ "Archives/modified-classic-20230809.osk",
+ // Covers legacy key counter
+ "Archives/modified-classic-20240724.osk"
};
///
@@ -99,7 +108,7 @@ namespace osu.Game.Tests.Skins
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
- Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
+ Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
}
}
@@ -112,8 +121,20 @@ namespace osu.Game.Tests.Skins
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
- Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
- Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
+ Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
+ Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
+ }
+ }
+
+ [Test]
+ public void TestDeserialiseInvalidDrawables()
+ {
+ using (var stream = TestResources.OpenResource("Archives/argon-invalid-drawable.osk"))
+ using (var storage = new ZipArchiveReader(stream))
+ {
+ var skin = new TestSkin(new SkinInfo(), null, storage);
+
+ Assert.That(skin.LayoutInfos.Any(kvp => kvp.Value.AllDrawables.Any(d => d.Type == typeof(StarFountain))), Is.False);
}
}
@@ -126,10 +147,10 @@ namespace osu.Game.Tests.Skins
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
- Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
- Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
+ Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
+ Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
- var skinnableInfo = skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.First();
+ var skinnableInfo = skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.First();
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
@@ -140,10 +161,10 @@ namespace osu.Game.Tests.Skins
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
- Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
- Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
- Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
- Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
+ Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
+ Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
+ Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
+ Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
}
}
diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
index d9212386c3..5086b64433 100644
--- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
+++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.IO;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
@@ -12,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
+using MemoryStream = System.IO.MemoryStream;
namespace osu.Game.Tests.Skins
{
@@ -21,6 +23,52 @@ namespace osu.Game.Tests.Skins
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
+ [Test]
+ public void TestRetrieveAndLegacyExportJapaneseFilename()
+ {
+ IWorkingBeatmap beatmap = null!;
+ MemoryStream outStream = null!;
+
+ // Ensure importer encoding is correct
+ AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz"));
+ AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
+
+ // Ensure exporter encoding is correct (round trip)
+ AddStep("export", () =>
+ {
+ outStream = new MemoryStream();
+
+ new LegacyBeatmapExporter(LocalStorage)
+ .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
+ });
+
+ AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
+ AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
+ }
+
+ [Test]
+ public void TestRetrieveAndNonLegacyExportJapaneseFilename()
+ {
+ IWorkingBeatmap beatmap = null!;
+ MemoryStream outStream = null!;
+
+ // Ensure importer encoding is correct
+ AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz"));
+ AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
+
+ // Ensure exporter encoding is correct (round trip)
+ AddStep("export", () =>
+ {
+ outStream = new MemoryStream();
+
+ new BeatmapExporter(LocalStorage)
+ .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
+ });
+
+ AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
+ AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
+ }
+
[Test]
public void TestRetrieveOggAudio()
{
@@ -45,6 +93,12 @@ namespace osu.Game.Tests.Skins
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null);
}
+ private IWorkingBeatmap importBeatmapFromStream(Stream stream)
+ {
+ var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely();
+ return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
+ }
+
private IWorkingBeatmap importBeatmapFromArchives(string filename)
{
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
index 37f2ee0b3f..7865d8fef7 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -304,11 +304,6 @@ namespace osu.Game.Tests.Visual.Background
{
private bool? lastLoadTriggerCausedChange;
- public TestBackgroundScreenDefault()
- : base(false)
- {
- }
-
public override bool Next()
{
bool didChange = base.Next();
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 566ccd6bd5..d8be57382f 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -18,6 +18,7 @@ using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -48,13 +49,18 @@ namespace osu.Game.Tests.Visual.Background
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
+ DetachedBeatmapStore detachedBeatmapStore;
+
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage));
+ Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
Dependencies.Cache(Realm);
manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
+ Add(detachedBeatmapStore);
+
Beatmap.SetDefault();
}
@@ -89,13 +95,18 @@ namespace osu.Game.Tests.Visual.Background
setupUserSettings();
AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true })));
AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false);
- AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
- AddStep("Trigger background preview", () =>
+ AddAssert("Background retained from song select", () =>
{
- InputManager.MoveMouseTo(playerLoader.ScreenPos);
- InputManager.MoveMouseTo(playerLoader.VisualSettingsPos);
+ InputManager.MoveMouseTo(playerLoader);
+ return songSelect.IsBackgroundCurrent();
});
- AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+
+ AddUntilStep("Screen is dimmed and blur applied", () =>
+ {
+ InputManager.MoveMouseTo(playerLoader.VisualSettingsPos);
+ return songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied();
+ });
+
AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.CheckBackgroundBlur(playerLoader.ExpectedBackgroundBlur));
}
@@ -349,8 +360,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);
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs
index f44fe2b90c..f5f9d121cc 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value);
beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online.
- Child = thumbnail = new BeatmapCardThumbnail(beatmapSet)
+ Child = thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultyIcon.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultyIcon.cs
new file mode 100644
index 0000000000..fb6bebe50d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultyIcon.cs
@@ -0,0 +1,60 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using 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 fill = null!;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Child = fill = new FillFlowContainer
+ {
+ 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));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs
new file mode 100644
index 0000000000..e10b3f76e6
--- /dev/null
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Screens;
+using osu.Game.Online.API;
+using osu.Game.Online.Metadata;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Tests.Resources;
+using osu.Game.Tests.Visual.Metadata;
+using osu.Game.Tests.Visual.OnlinePlay;
+
+namespace osu.Game.Tests.Visual.DailyChallenge
+{
+ public partial class TestSceneDailyChallenge : OnlinePlayTestScene
+ {
+ [Cached(typeof(MetadataClient))]
+ private TestMetadataClient metadataClient = new TestMetadataClient();
+
+ [Cached(typeof(INotificationOverlay))]
+ private NotificationOverlay notificationOverlay = new NotificationOverlay();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ base.Content.Add(notificationOverlay);
+ base.Content.Add(metadataClient);
+ }
+
+ [Test]
+ public void TestDailyChallenge()
+ {
+ var room = new Room
+ {
+ RoomID = { Value = 1234 },
+ Name = { Value = "Daily Challenge: June 4, 2024" },
+ Playlist =
+ {
+ new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
+ {
+ RequiredMods = [new APIMod(new OsuModTraceable())],
+ AllowedMods = [new APIMod(new OsuModDoubleTime())]
+ }
+ },
+ EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
+ Category = { Value = RoomCategory.DailyChallenge }
+ };
+
+ AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
+ AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
+ }
+
+ [Test]
+ public void TestNotifications()
+ {
+ var room = new Room
+ {
+ RoomID = { Value = 1234 },
+ Name = { Value = "Daily Challenge: June 4, 2024" },
+ Playlist =
+ {
+ new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
+ {
+ RequiredMods = [new APIMod(new OsuModTraceable())],
+ AllowedMods = [new APIMod(new OsuModDoubleTime())]
+ }
+ },
+ EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
+ Category = { Value = RoomCategory.DailyChallenge }
+ };
+
+ AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
+ AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
+
+ Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
+ AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
+ AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
+ AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs
new file mode 100644
index 0000000000..d53e386ad4
--- /dev/null
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs
@@ -0,0 +1,185 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
+using osu.Game.Screens.OnlinePlay.DailyChallenge;
+using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
+
+namespace osu.Game.Tests.Visual.DailyChallenge
+{
+ public partial class TestSceneDailyChallengeCarousel : OsuTestScene
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
+
+ private readonly Bindable room = new Bindable(new Room());
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new CachedModelDependencyContainer(base.CreateChildDependencies(parent))
+ {
+ Model = { BindTarget = room }
+ };
+
+ [Test]
+ public void TestBasicAppearance()
+ {
+ DailyChallengeCarousel carousel = null!;
+
+ AddStep("create content", () => Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4,
+ },
+ carousel = new DailyChallengeCarousel
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ });
+ AddSliderStep("adjust width", 0.1f, 1, 1, width =>
+ {
+ if (carousel.IsNotNull())
+ carousel.Width = width;
+ });
+ AddSliderStep("adjust height", 0.1f, 1, 1, height =>
+ {
+ if (carousel.IsNotNull())
+ carousel.Height = height;
+ });
+ AddRepeatStep("add content", () => carousel.Add(new FakeContent()), 3);
+ }
+
+ [Test]
+ public void TestIntegration()
+ {
+ GridContainer grid = null!;
+ DailyChallengeEventFeed feed = null!;
+ DailyChallengeScoreBreakdown breakdown = null!;
+
+ AddStep("create content", () => Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4,
+ },
+ grid = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RowDimensions =
+ [
+ new Dimension(),
+ new Dimension()
+ ],
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new DailyChallengeCarousel
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new DailyChallengeTimeRemainingRing(),
+ breakdown = new DailyChallengeScoreBreakdown(),
+ }
+ }
+ },
+ [
+ feed = new DailyChallengeEventFeed
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ ],
+ }
+ },
+ });
+ AddSliderStep("adjust width", 0.1f, 1, 1, width =>
+ {
+ if (grid.IsNotNull())
+ grid.Width = width;
+ });
+ AddSliderStep("adjust height", 0.1f, 1, 1, height =>
+ {
+ if (grid.IsNotNull())
+ grid.Height = height;
+ });
+ AddSliderStep("update time remaining", 0f, 1f, 0f, progress =>
+ {
+ var startedTimeAgo = TimeSpan.FromHours(24) * progress;
+ room.Value.StartDate.Value = DateTimeOffset.Now - startedTimeAgo;
+ room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1);
+ });
+ AddStep("add normal score", () =>
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), null);
+
+ feed.AddNewScore(ev);
+ breakdown.AddNewScore(ev);
+ });
+ AddStep("add new user best", () =>
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), RNG.Next(1, 1000));
+
+ feed.AddNewScore(ev);
+ breakdown.AddNewScore(ev);
+ });
+ }
+
+ private partial class FakeContent : CompositeDrawable
+ {
+ private OsuSpriteText text = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1),
+ },
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "Fake Content " + (char)('A' + RNG.Next(26)),
+ },
+ };
+
+ text.FadeOut(500, Easing.OutQuint)
+ .Then().FadeIn(500, Easing.OutQuint)
+ .Loop();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs
new file mode 100644
index 0000000000..4b784f661d
--- /dev/null
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs
@@ -0,0 +1,119 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays;
+using osu.Game.Screens.OnlinePlay.DailyChallenge;
+using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.DailyChallenge
+{
+ public partial class TestSceneDailyChallengeEventFeed : OsuTestScene
+ {
+ private DailyChallengeEventFeed feed = null!;
+
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create content", () => Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4,
+ },
+ feed = new DailyChallengeEventFeed
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.3f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ });
+
+ AddSliderStep("adjust width", 0.1f, 1, 1, width =>
+ {
+ if (feed.IsNotNull())
+ feed.Width = width;
+ });
+ AddSliderStep("adjust height", 0.1f, 1, 0.3f, height =>
+ {
+ if (feed.IsNotNull())
+ feed.Height = height;
+ });
+ }
+
+ [Test]
+ public void TestBasicAppearance()
+ {
+ AddRepeatStep("add normal score", () =>
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), null);
+
+ feed.AddNewScore(ev);
+ }, 50);
+
+ AddRepeatStep("add new user best", () =>
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), RNG.Next(11, 1000));
+
+ var testScore = TestResources.CreateTestScoreInfo();
+ testScore.TotalScore = RNG.Next(1_000_000);
+
+ feed.AddNewScore(ev);
+ }, 50);
+
+ AddRepeatStep("add top 10 score", () =>
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), RNG.Next(1, 10));
+
+ feed.AddNewScore(ev);
+ }, 50);
+ }
+
+ [Test]
+ public void TestMassAdd()
+ {
+ AddStep("add 1000 scores at once", () =>
+ {
+ for (int i = 0; i < 1000; i++)
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), null);
+
+ feed.AddNewScore(ev);
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs
new file mode 100644
index 0000000000..f1a2d6b5f2
--- /dev/null
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs
@@ -0,0 +1,89 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Configuration;
+using osu.Game.Online.API;
+using osu.Game.Online.Metadata;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.Menu;
+using osu.Game.Screens.OnlinePlay.DailyChallenge;
+using osu.Game.Tests.Visual.Metadata;
+using osu.Game.Tests.Visual.OnlinePlay;
+using osuTK.Graphics;
+using osuTK.Input;
+using CreateRoomRequest = osu.Game.Online.Rooms.CreateRoomRequest;
+
+namespace osu.Game.Tests.Visual.DailyChallenge
+{
+ public partial class TestSceneDailyChallengeIntro : OnlinePlayTestScene
+ {
+ [Cached(typeof(MetadataClient))]
+ private TestMetadataClient metadataClient = new TestMetadataClient();
+
+ [Cached(typeof(INotificationOverlay))]
+ private NotificationOverlay notificationOverlay = new NotificationOverlay();
+
+ private Room room = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(notificationOverlay);
+ Add(metadataClient);
+
+ // add button to observe for daily challenge changes and perform its logic.
+ Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D));
+ }
+
+ [Test]
+ public void TestDailyChallenge()
+ {
+ startChallenge(1234);
+ AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
+ }
+
+ [Test]
+ public void TestPlayIntroOnceFlag()
+ {
+ startChallenge(1234);
+ AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true));
+
+ startChallenge(1235);
+
+ AddAssert("intro played flag reset", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.False);
+
+ AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
+ AddUntilStep("intro played flag set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.True);
+ }
+
+ private void startChallenge(int roomId)
+ {
+ AddStep("add room", () =>
+ {
+ API.Perform(new CreateRoomRequest(room = new Room
+ {
+ RoomID = { Value = roomId },
+ Name = { Value = "Daily Challenge: June 4, 2024" },
+ Playlist =
+ {
+ new PlaylistItem(CreateAPIBeatmap(new OsuRuleset().RulesetInfo))
+ {
+ RequiredMods = [new APIMod(new OsuModTraceable())],
+ AllowedMods = [new APIMod(new OsuModDoubleTime())]
+ }
+ },
+ StartDate = { Value = DateTimeOffset.Now },
+ EndDate = { Value = DateTimeOffset.Now.AddHours(24) },
+ Category = { Value = RoomCategory.DailyChallenge }
+ }));
+ });
+ AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs
new file mode 100644
index 0000000000..5fff6bb010
--- /dev/null
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs
@@ -0,0 +1,142 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Utils;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.OnlinePlay.DailyChallenge;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.DailyChallenge
+{
+ public partial class TestSceneDailyChallengeLeaderboard : OsuTestScene
+ {
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
+
+ [Test]
+ public void TestBasicBehaviour()
+ {
+ DailyChallengeLeaderboard leaderboard = null!;
+
+ AddStep("set up response without user best", () =>
+ {
+ dummyAPI.HandleRequest = req =>
+ {
+ if (req is IndexPlaylistScoresRequest indexRequest)
+ {
+ indexRequest.TriggerSuccess(createResponse(50, false));
+ return true;
+ }
+
+ return false;
+ };
+ });
+ AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo))
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(0.8f),
+ });
+
+ AddStep("set up response with user best", () =>
+ {
+ dummyAPI.HandleRequest = req =>
+ {
+ if (req is IndexPlaylistScoresRequest indexRequest)
+ {
+ indexRequest.TriggerSuccess(createResponse(50, true));
+ return true;
+ }
+
+ return false;
+ };
+ });
+ AddStep("force refetch", () => leaderboard.RefetchScores());
+ }
+
+ [Test]
+ public void TestLoadingBehaviour()
+ {
+ IndexPlaylistScoresRequest pendingRequest = null!;
+ DailyChallengeLeaderboard leaderboard = null!;
+
+ AddStep("set up requests handler", () =>
+ {
+ dummyAPI.HandleRequest = req =>
+ {
+ if (req is IndexPlaylistScoresRequest indexRequest)
+ {
+ pendingRequest = indexRequest;
+ return true;
+ }
+
+ return false;
+ };
+ });
+ AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo))
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(0.8f),
+ });
+ AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(3, true)));
+ AddStep("force refetch", () => leaderboard.RefetchScores());
+ AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(4, true)));
+ }
+
+ private IndexedMultiplayerScores createResponse(int scoreCount, bool returnUserBest)
+ {
+ var result = new IndexedMultiplayerScores();
+
+ for (int i = 0; i < scoreCount; ++i)
+ {
+ result.Scores.Add(new MultiplayerScore
+ {
+ ID = i,
+ Accuracy = 1 - (float)i / (2 * scoreCount),
+ Position = i + 1,
+ EndedAt = DateTimeOffset.Now,
+ Passed = true,
+ Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH),
+ MaxCombo = 1000 - i,
+ TotalScore = (long)(1_000_000 * (1 - (float)i / (2 * scoreCount))),
+ User = new APIUser { Username = $"user {i}" },
+ Statistics = new Dictionary()
+ });
+ }
+
+ if (returnUserBest)
+ {
+ result.UserScore = new MultiplayerScore
+ {
+ ID = 99999,
+ Accuracy = 0.91,
+ Position = 4,
+ EndedAt = DateTimeOffset.Now,
+ Passed = true,
+ Rank = ScoreRank.A,
+ MaxCombo = 100,
+ TotalScore = 800000,
+ User = dummyAPI.LocalUser.Value,
+ Statistics = new Dictionary()
+ };
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs
new file mode 100644
index 0000000000..b04696aded
--- /dev/null
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs
@@ -0,0 +1,96 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
+using osu.Game.Screens.OnlinePlay.DailyChallenge;
+using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
+
+namespace osu.Game.Tests.Visual.DailyChallenge
+{
+ public partial class TestSceneDailyChallengeScoreBreakdown : OsuTestScene
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
+
+ private DailyChallengeScoreBreakdown breakdown = null!;
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create content", () => Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4,
+ },
+ breakdown = new DailyChallengeScoreBreakdown
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ });
+ AddSliderStep("adjust width", 0.1f, 1, 1, width =>
+ {
+ if (breakdown.IsNotNull())
+ breakdown.Width = width;
+ });
+ AddSliderStep("adjust height", 0.1f, 1, 1, height =>
+ {
+ if (breakdown.IsNotNull())
+ breakdown.Height = height;
+ });
+
+ AddToggleStep("toggle visible", v => breakdown.Alpha = v ? 1 : 0);
+
+ AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1]));
+ }
+
+ [Test]
+ public void TestBasicAppearance()
+ {
+ AddStep("add new score", () =>
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), null);
+
+ breakdown.AddNewScore(ev);
+ });
+ AddStep("set user score", () => breakdown.UserBestScore.Value = new MultiplayerScore { TotalScore = RNG.Next(1_000_000) });
+ AddStep("unset user score", () => breakdown.UserBestScore.Value = null);
+ }
+
+ [Test]
+ public void TestMassAdd()
+ {
+ AddStep("add 1000 scores at once", () =>
+ {
+ for (int i = 0; i < 1000; i++)
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), null);
+
+ breakdown.AddNewScore(ev);
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs
new file mode 100644
index 0000000000..baa1eb8318
--- /dev/null
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs
@@ -0,0 +1,88 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
+using osu.Game.Screens.OnlinePlay.DailyChallenge;
+
+namespace osu.Game.Tests.Visual.DailyChallenge
+{
+ public partial class TestSceneDailyChallengeTimeRemainingRing : OsuTestScene
+ {
+ private readonly Bindable room = new Bindable(new Room());
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new CachedModelDependencyContainer(base.CreateChildDependencies(parent))
+ {
+ Model = { BindTarget = room }
+ };
+
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
+
+ [Test]
+ public void TestBasicAppearance()
+ {
+ DailyChallengeTimeRemainingRing ring = null!;
+
+ AddStep("create content", () => Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4,
+ },
+ ring = new DailyChallengeTimeRemainingRing
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ });
+ AddSliderStep("adjust width", 0.1f, 1, 1, width =>
+ {
+ if (ring.IsNotNull())
+ ring.Width = width;
+ });
+ AddSliderStep("adjust height", 0.1f, 1, 1, height =>
+ {
+ if (ring.IsNotNull())
+ ring.Height = height;
+ });
+ AddToggleStep("toggle visible", v => ring.Alpha = v ? 1 : 0);
+
+ AddStep("just started", () =>
+ {
+ room.Value.StartDate.Value = DateTimeOffset.Now.AddMinutes(-1);
+ room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1);
+ });
+ AddStep("midway through", () =>
+ {
+ room.Value.StartDate.Value = DateTimeOffset.Now.AddHours(-12);
+ room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1);
+ });
+ AddStep("nearing end", () =>
+ {
+ room.Value.StartDate.Value = DateTimeOffset.Now.AddDays(-1).AddMinutes(8);
+ room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1);
+ });
+ AddStep("already ended", () =>
+ {
+ room.Value.StartDate.Value = DateTimeOffset.Now.AddDays(-2);
+ room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1);
+ });
+ AddSliderStep("manual progress", 0f, 1f, 0f, progress =>
+ {
+ var startedTimeAgo = TimeSpan.FromHours(24) * progress;
+ room.Value.StartDate.Value = DateTimeOffset.Now - startedTimeAgo;
+ room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs
new file mode 100644
index 0000000000..ae212f5212
--- /dev/null
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs
@@ -0,0 +1,87 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays;
+using osu.Game.Screens.OnlinePlay.DailyChallenge;
+using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.DailyChallenge
+{
+ public partial class TestSceneDailyChallengeTotalsDisplay : OsuTestScene
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
+
+ [Test]
+ public void TestBasicAppearance()
+ {
+ DailyChallengeTotalsDisplay totals = null!;
+
+ AddStep("create content", () => Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4,
+ },
+ totals = new DailyChallengeTotalsDisplay
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ });
+ AddSliderStep("adjust width", 0.1f, 1, 1, width =>
+ {
+ if (totals.IsNotNull())
+ totals.Width = width;
+ });
+ AddSliderStep("adjust height", 0.1f, 1, 1, height =>
+ {
+ if (totals.IsNotNull())
+ totals.Height = height;
+ });
+ AddToggleStep("toggle visible", v => totals.Alpha = v ? 1 : 0);
+
+ AddStep("set counts", () => totals.SetInitialCounts(totalPassCount: 9650, cumulativeTotalScore: 10_000_000_000));
+
+ AddStep("add normal score", () =>
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), null);
+
+ totals.AddNewScore(ev);
+ });
+
+ AddStep("spam scores", () =>
+ {
+ for (int i = 0; i < 1000; ++i)
+ {
+ var ev = new NewScoreEvent(1, new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }, RNG.Next(1_000_000), RNG.Next(11, 1000));
+
+ var testScore = TestResources.CreateTestScoreInfo();
+ testScore.TotalScore = RNG.Next(1_000_000);
+
+ totals.AddNewScore(ev);
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs
index f6637d0e80..28763051e3 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs
@@ -10,9 +10,11 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Utils;
using osuTK;
using osuTK.Input;
@@ -26,9 +28,13 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(SelectionRotationHandler))]
private TestSelectionRotationHandler rotationHandler;
+ [Cached(typeof(SelectionScaleHandler))]
+ private TestSelectionScaleHandler scaleHandler;
+
public TestSceneComposeSelectBox()
{
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
+ scaleHandler = new TestSelectionScaleHandler(() => selectionArea);
}
[SetUp]
@@ -45,13 +51,8 @@ namespace osu.Game.Tests.Visual.Editing
{
RelativeSizeAxes = Axes.Both,
- CanScaleX = true,
- CanScaleY = true,
- CanScaleDiagonally = true,
CanFlipX = true,
CanFlipY = true,
-
- OnScale = handleScale
}
}
};
@@ -60,27 +61,6 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseButton(MouseButton.Left);
});
- private bool handleScale(Vector2 amount, Anchor reference)
- {
- if ((reference & Anchor.y1) == 0)
- {
- int directionY = (reference & Anchor.y0) > 0 ? -1 : 1;
- if (directionY < 0)
- selectionArea.Y += amount.Y;
- selectionArea.Height += directionY * amount.Y;
- }
-
- if ((reference & Anchor.x1) == 0)
- {
- int directionX = (reference & Anchor.x0) > 0 ? -1 : 1;
- if (directionX < 0)
- selectionArea.X += amount.X;
- selectionArea.Width += directionX * amount.X;
- }
-
- return true;
- }
-
private partial class TestSelectionRotationHandler : SelectionRotationHandler
{
private readonly Func getTargetContainer;
@@ -89,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing
{
this.getTargetContainer = getTargetContainer;
- CanRotate.Value = true;
+ CanRotateAroundSelectionOrigin.Value = true;
}
[CanBeNull]
@@ -104,6 +84,8 @@ namespace osu.Game.Tests.Visual.Editing
targetContainer = getTargetContainer();
initialRotation = targetContainer!.Rotation;
+
+ base.Begin();
}
public override void Update(float rotation, Vector2? origin = null)
@@ -122,6 +104,53 @@ namespace osu.Game.Tests.Visual.Editing
targetContainer = null;
initialRotation = null;
+
+ base.Commit();
+ }
+ }
+
+ private partial class TestSelectionScaleHandler : SelectionScaleHandler
+ {
+ private readonly Func getTargetContainer;
+
+ public TestSelectionScaleHandler(Func getTargetContainer)
+ {
+ this.getTargetContainer = getTargetContainer;
+
+ CanScaleX.Value = true;
+ CanScaleY.Value = true;
+ CanScaleDiagonally.Value = true;
+ }
+
+ [CanBeNull]
+ private Container targetContainer;
+
+ public override void Begin()
+ {
+ if (targetContainer != null)
+ throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
+
+ targetContainer = getTargetContainer();
+ OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
+ }
+
+ public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
+ {
+ if (targetContainer == null)
+ throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
+
+ Vector2 actualOrigin = origin ?? Vector2.Zero;
+
+ targetContainer.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft);
+ targetContainer.Size = OriginalSurroundingQuad!.Value.Size * scale;
+ }
+
+ public override void Commit()
+ {
+ if (targetContainer == null)
+ throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!");
+
+ targetContainer = null;
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs
index 12e00c4485..d4bd77642c 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
@@ -83,6 +84,49 @@ namespace osu.Game.Tests.Visual.Editing
}
}
+ [Test]
+ public void TestDeleteDifficultyWithPendingChanges()
+ {
+ Guid deletedDifficultyID = Guid.Empty;
+ int countBeforeDeletion = 0;
+ string beatmapSetHashBefore = string.Empty;
+
+ AddUntilStep("wait for editor to load", () => Editor?.ReadyForUse == true);
+
+ AddStep("store selected difficulty", () =>
+ {
+ deletedDifficultyID = EditorBeatmap.BeatmapInfo.ID;
+ countBeforeDeletion = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count;
+ beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
+ });
+
+ AddStep("make change to difficulty", () =>
+ {
+ EditorBeatmap.BeginChange();
+ EditorBeatmap.BeatmapInfo.DifficultyName = "changin' things";
+ EditorBeatmap.EndChange();
+ });
+
+ AddStep("click File", () => this.ChildrenOfType().First().TriggerClick());
+
+ AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
+ AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
+ AddAssert("dialog is deletion confirmation dialog", () => DialogOverlay.CurrentDialog, Is.InstanceOf);
+ AddStep("confirm", () => InputManager.Key(Key.Number1));
+
+ AddUntilStep("no next dialog", () => DialogOverlay.CurrentDialog == null);
+ AddUntilStep("switched to different difficulty",
+ () => this.ChildrenOfType().SingleOrDefault() != null && EditorBeatmap.BeatmapInfo.ID != deletedDifficultyID);
+
+ AddAssert("difficulty is unattached from set",
+ () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID));
+ AddAssert("beatmap set difficulty count decreased by one",
+ () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1));
+ AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore));
+ AddAssert("difficulty is deleted from realm",
+ () => Realm.Run(r => r.Find(deletedDifficultyID)), () => Is.Null);
+ }
+
private DrawableOsuMenuItem getDeleteMenuItem() => this.ChildrenOfType()
.Single(item => item.ChildrenOfType().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal)));
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
index 76ed5063b0..457d4cee34 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
@@ -12,7 +12,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
using osu.Game.Storyboards;
@@ -169,6 +171,24 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
+ [Test]
+ public void TestSwitchToDifficultyOfAnotherRuleset()
+ {
+ BeatmapInfo targetDifficulty = null;
+
+ AddAssert("ruleset is catch", () => Ruleset.Value.CreateInstance() is CatchRuleset);
+
+ AddStep("set taiko difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1));
+ switchToDifficulty(() => targetDifficulty);
+ confirmEditingBeatmap(() => targetDifficulty);
+
+ AddAssert("ruleset switched to taiko", () => Ruleset.Value.CreateInstance() is TaikoRuleset);
+
+ AddStep("exit editor forcefully", () => Stack.Exit());
+ // ensure editor loader didn't resume.
+ AddAssert("stack empty", () => Stack.CurrentScreen == null);
+ }
+
private void switchToDifficulty(Func difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke()));
private void confirmEditingBeatmap(Func targetDifficulty)
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
index 278b6e9626..7827347b1f 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
@@ -4,10 +4,12 @@
#nullable disable
using NUnit.Framework;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Editing
{
@@ -15,6 +17,8 @@ namespace osu.Game.Tests.Visual.Editing
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
[Test]
public void TestSelectedObjects()
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
index ed58c59ff0..bfc8af7283 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
@@ -1,10 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
@@ -19,9 +22,10 @@ namespace osu.Game.Tests.Visual.Editing
[Cached]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo));
- public TestSceneEditorClock()
+ [SetUpSteps]
+ public void SetUpSteps()
{
- Add(new FillFlowContainer
+ AddStep("create content", () => Add(new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
@@ -39,19 +43,17 @@ namespace osu.Game.Tests.Visual.Editing
Size = new Vector2(200, 100)
}
}
+ }));
+ AddStep("set working beatmap", () =>
+ {
+ Beatmap.Disabled = false;
+ Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ // ensure that music controller does not change this beatmap due to it
+ // completing naturally as part of the test.
+ Beatmap.Disabled = true;
});
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
- // ensure that music controller does not change this beatmap due to it
- // completing naturally as part of the test.
- Beatmap.Disabled = true;
- }
-
[Test]
public void TestStopAtTrackEnd()
{
@@ -102,6 +104,29 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("time is clamped to track length", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength));
}
+ [Test]
+ public void TestCurrentTimeDoubleTransform()
+ {
+ AddAssert("seek smoothly twice and current time is accurate", () =>
+ {
+ EditorClock.SeekSmoothlyTo(1000);
+ EditorClock.SeekSmoothlyTo(2000);
+ return 2000 == EditorClock.CurrentTimeAccurate;
+ });
+ }
+
+ [Test]
+ public void TestAdjustmentsRemovedOnDisposal()
+ {
+ AddStep("reset clock", () => EditorClock.Seek(0));
+
+ AddStep("set 0.25x speed", () => this.ChildrenOfType>().First().Current.Value = 0.25);
+ AddAssert("track has 0.25x tempo", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25));
+
+ AddStep("dispose playback control", () => Clear(disposeChildren: true));
+ AddAssert("track has 1x tempo", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(1));
+ }
+
protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index 64c48e74cf..b487fa3cec 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -193,5 +193,20 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save);
}
+
+ [Test]
+ public void TestBeatDivisor()
+ {
+ AddStep("Set custom beat divisor", () => Editor.Dependencies.Get().SetArbitraryDivisor(7));
+
+ SaveEditor();
+ AddAssert("Hash updated", () => !string.IsNullOrEmpty(EditorBeatmap.BeatmapInfo.BeatmapSet?.Hash));
+ AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7));
+
+ ReloadEditorToSameBeatmap();
+
+ AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7));
+ AddAssert("Correct beat divisor actually active", () => Editor.BeatDivisor, () => Is.EqualTo(7));
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
index ddca2f8553..677d3135ba 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
@@ -24,7 +24,10 @@ namespace osu.Game.Tests.Visual.Editing
beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = 100 });
beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
+ beatmap.ControlPointInfo.Add(80000, new EffectControlPoint { KiaiMode = true });
+ beatmap.ControlPointInfo.Add(110000, new EffectControlPoint { KiaiMode = false });
beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 };
+ beatmap.Breaks.Add(new ManualBreakPeriod(90000, 120000));
editorBeatmap = new EditorBeatmap(beatmap);
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
index ca5e89c8ed..23efb40d3f 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.UI;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
@@ -126,6 +127,24 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
}
+ [TestCase(2000)] // chosen to be after last object in the map
+ [TestCase(22000)] // chosen to be in the middle of the last spinner
+ public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd)
+ {
+ AddStep($"seek to end minus {offsetFromEnd}ms", () => EditorClock.Seek(importedBeatmapSet.MaxLength - offsetFromEnd));
+ AddStep("click test gameplay button", () =>
+ {
+ var button = Editor.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer);
+
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ }
+
[Test]
public void TestCancelGameplayTestWithUnsavedChanges()
{
@@ -206,6 +225,116 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
}
+ [Test]
+ public void TestAutoplayToggle()
+ {
+ AddStep("click test gameplay button", () =>
+ {
+ var button = Editor.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ EditorPlayer editorPlayer = null;
+ AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
+ AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType().Single().ReplayScore, () => Is.Null);
+ AddStep("press Tab", () => InputManager.Key(Key.Tab));
+ AddUntilStep("replay active", () => editorPlayer.ChildrenOfType().Single().ReplayScore, () => Is.Not.Null);
+ AddStep("press Tab", () => InputManager.Key(Key.Tab));
+ AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType().Single().ReplayScore, () => Is.Null);
+ AddStep("exit player", () => editorPlayer.Exit());
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ }
+
+ [Test]
+ public void TestQuickPause()
+ {
+ AddStep("click test gameplay button", () =>
+ {
+ var button = Editor.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ EditorPlayer editorPlayer = null;
+ AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
+ AddUntilStep("clock running", () => editorPlayer.ChildrenOfType().Single().IsPaused.Value, () => Is.False);
+ AddStep("press Ctrl-P", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.P);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddUntilStep("clock not running", () => editorPlayer.ChildrenOfType().Single().IsPaused.Value, () => Is.True);
+ AddStep("press Ctrl-P", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.P);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddUntilStep("clock running", () => editorPlayer.ChildrenOfType().Single().IsPaused.Value, () => Is.False);
+ AddStep("exit player", () => editorPlayer.Exit());
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ }
+
+ [Test]
+ public void TestQuickExitAtInitialPosition()
+ {
+ AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
+ AddStep("click test gameplay button", () =>
+ {
+ var button = Editor.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ EditorPlayer editorPlayer = null;
+ AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
+
+ GameplayClockContainer gameplayClockContainer = null;
+ AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType().First());
+ AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
+ // when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
+ AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
+
+ AddWaitStep("wait some", 5);
+
+ AddStep("exit player", () => InputManager.PressKey(Key.F1));
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
+ }
+
+ [Test]
+ public void TestQuickExitAtCurrentPosition()
+ {
+ AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
+ AddStep("click test gameplay button", () =>
+ {
+ var button = Editor.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ EditorPlayer editorPlayer = null;
+ AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
+
+ GameplayClockContainer gameplayClockContainer = null;
+ AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType().First());
+ AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
+ // when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
+ AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
+
+ AddWaitStep("wait some", 5);
+
+ AddStep("exit player", () => InputManager.PressKey(Key.F2));
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ AddAssert("time moved forward", () => EditorClock.CurrentTime, () => Is.GreaterThan(60_000));
+ }
+
public override void TearDownSteps()
{
base.TearDownSteps();
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs
index 1415ff4b0f..3c5277a4d9 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs
@@ -5,16 +5,21 @@ using System.Linq;
using System.Collections.Generic;
using Humanizer;
using NUnit.Framework;
+using osu.Framework.Input;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Beatmaps;
@@ -78,10 +83,10 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
- public void TestPopoverHasFocus()
+ public void TestPopoverHasNoFocus()
{
clickSamplePiece(0);
- samplePopoverHasFocus();
+ samplePopoverHasNoFocus();
}
[Test]
@@ -225,6 +230,124 @@ namespace osu.Game.Tests.Visual.Editing
samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
}
+ [Test]
+ public void TestPopoverAddSampleAddition()
+ {
+ clickSamplePiece(0);
+
+ setBankViaPopover(HitSampleInfo.BANK_SOFT);
+ hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
+
+ toggleAdditionViaPopover(0);
+
+ hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+
+ setAdditionBankViaPopover(HitSampleInfo.BANK_DRUM);
+
+ hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
+ hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
+
+ toggleAdditionViaPopover(0);
+
+ hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
+ }
+
+ [Test]
+ public void TestNodeSamplePopover()
+ {
+ AddStep("add slider", () =>
+ {
+ EditorBeatmap.Clear();
+ EditorBeatmap.Add(new Slider
+ {
+ Position = new Vector2(256, 256),
+ StartTime = 0,
+ Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
+ Samples =
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
+ },
+ NodeSamples =
+ {
+ new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
+ new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
+ }
+ });
+ });
+
+ clickNodeSamplePiece(0, 1);
+
+ setBankViaPopover(HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_NORMAL);
+ hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
+
+ toggleAdditionViaPopover(0);
+
+ hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_NORMAL);
+ hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL);
+ hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+
+ setAdditionBankViaPopover(HitSampleInfo.BANK_DRUM);
+
+ hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_NORMAL);
+ hitObjectNodeHasSampleNormalBank(0, 1, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSampleAdditionBank(0, 1, HitSampleInfo.BANK_DRUM);
+
+ toggleAdditionViaPopover(0);
+
+ hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL);
+ hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL);
+
+ setVolumeViaPopover(10);
+
+ hitObjectNodeHasSampleVolume(0, 0, 100);
+ hitObjectNodeHasSampleVolume(0, 1, 10);
+ }
+
+ [Test]
+ public void TestSamplePointSeek()
+ {
+ AddStep("add slider", () =>
+ {
+ EditorBeatmap.Clear();
+ EditorBeatmap.Add(new Slider
+ {
+ Position = new Vector2(256, 256),
+ StartTime = 0,
+ Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
+ Samples =
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
+ },
+ NodeSamples =
+ {
+ new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
+ new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
+ },
+ RepeatCount = 1
+ });
+ });
+
+ seekSamplePiece(-1);
+ editorTimeIs(0);
+ samplePopoverIsOpen();
+ seekSamplePiece(-1);
+ editorTimeIs(0);
+ samplePopoverIsOpen();
+ seekSamplePiece(1);
+ editorTimeIs(406);
+ seekSamplePiece(1);
+ editorTimeIs(813);
+ seekSamplePiece(1);
+ editorTimeIs(1627);
+ seekSamplePiece(1);
+ editorTimeIs(1627);
+ }
+
[Test]
public void TestHotkeysMultipleSelectionWithSameSampleBank()
{
@@ -320,21 +443,266 @@ namespace osu.Game.Tests.Visual.Editing
void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected));
}
+ [Test]
+ public void PopoverForMultipleSelectionChangesAllSamples()
+ {
+ AddStep("add slider", () =>
+ {
+ EditorBeatmap.Add(new Slider
+ {
+ Position = new Vector2(256, 256),
+ StartTime = 1000,
+ Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
+ Samples =
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
+ },
+ NodeSamples = new List>
+ {
+ new List
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
+ new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
+ },
+ new List
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
+ new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
+ },
+ }
+ });
+ });
+ AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+
+ clickSamplePiece(0);
+
+ setBankViaPopover(HitSampleInfo.BANK_DRUM);
+ samplePopoverHasSingleBank(HitSampleInfo.BANK_DRUM);
+ hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
+ hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
+ hitObjectHasSampleNormalBank(2, HitSampleInfo.BANK_DRUM);
+ hitObjectNodeHasSampleNormalBank(2, 0, HitSampleInfo.BANK_DRUM);
+ hitObjectNodeHasSampleNormalBank(2, 1, HitSampleInfo.BANK_DRUM);
+
+ setVolumeViaPopover(30);
+ samplePopoverHasSingleVolume(30);
+ hitObjectHasSampleVolume(0, 30);
+ hitObjectHasSampleVolume(1, 30);
+ hitObjectHasSampleVolume(2, 30);
+ hitObjectNodeHasSampleVolume(2, 0, 30);
+ hitObjectNodeHasSampleVolume(2, 1, 30);
+
+ toggleAdditionViaPopover(0);
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+ hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+ hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+ hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
+ hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+
+ setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT);
+ hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
+ hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
+ hitObjectHasSampleAdditionBank(2, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSampleAdditionBank(2, 0, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSampleAdditionBank(2, 1, HitSampleInfo.BANK_SOFT);
+ }
+
+ [Test]
+ public void TestHotkeysAffectNodeSamples()
+ {
+ AddStep("add slider", () =>
+ {
+ EditorBeatmap.Add(new Slider
+ {
+ Position = new Vector2(256, 256),
+ StartTime = 1000,
+ Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
+ Samples =
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
+ },
+ NodeSamples = new List>
+ {
+ new List
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
+ new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
+ },
+ new List
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
+ new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
+ },
+ }
+ });
+ });
+ AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+
+ AddStep("add clap addition", () => InputManager.Key(Key.R));
+
+ hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
+
+ hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
+ hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
+
+ hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL);
+ hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
+ hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
+ hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
+ hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP);
+
+ AddStep("remove clap addition", () => InputManager.Key(Key.R));
+
+ hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
+
+ hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
+ hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
+
+ hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL);
+ hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
+ hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
+ hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
+ hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+
+ AddStep("set drum bank", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.R);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+
+ hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
+
+ hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
+ hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
+
+ hitObjectHasSampleBank(2, HitSampleInfo.BANK_DRUM);
+ hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
+ hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
+ hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
+ hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_DRUM);
+ hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+ }
+
+ [Test]
+ public void TestHotkeysUnifySliderSamplesAndNodeSamples()
+ {
+ AddStep("add slider", () =>
+ {
+ EditorBeatmap.Clear();
+ EditorBeatmap.Add(new Slider
+ {
+ Position = new Vector2(256, 256),
+ StartTime = 1000,
+ Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
+ Samples =
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT),
+ new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_DRUM),
+ },
+ NodeSamples = new List>
+ {
+ new List
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
+ new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
+ },
+ new List
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
+ new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
+ },
+ }
+ });
+ });
+ AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+
+ AddStep("set soft bank", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.E);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+
+ hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+ hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
+ hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+
+ AddStep("unify whistle addition", () => InputManager.Key(Key.W));
+
+ hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+ hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
+ hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
+ hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
+ }
+
+ [Test]
+ public void TestSelectingObjectDoesNotMutateSamples()
+ {
+ clickSamplePiece(0);
+ toggleAdditionViaPopover(1);
+ setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT);
+ dismissPopover();
+
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
+ hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
+ hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
+
+ AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]));
+
+ hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
+ hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
+ hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
+ }
+
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
{
- var samplePiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
+ var samplePiece = this.ChildrenOfType().Single(piece => piece is not NodeSamplePointPiece && piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
InputManager.MoveMouseTo(samplePiece);
InputManager.Click(MouseButton.Left);
});
- private void samplePopoverHasFocus() => AddUntilStep("sample popover textbox focused", () =>
+ private void clickNodeSamplePiece(int objectIndex, int nodeIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node sample piece", () =>
+ {
+ var samplePiece = this.ChildrenOfType().Where(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)).ToArray()[nodeIndex];
+
+ InputManager.MoveMouseTo(samplePiece);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ private void seekSamplePiece(int direction) => AddStep($"seek sample piece {direction}", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.PressKey(Key.ShiftLeft);
+ InputManager.Key(direction < 1 ? Key.Left : Key.Right);
+ InputManager.ReleaseKey(Key.ShiftLeft);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ private void samplePopoverIsOpen() => AddUntilStep("sample popover is open", () =>
+ {
+ var popover = this.ChildrenOfType().SingleOrDefault(o => o.IsPresent);
+ return popover != null;
+ });
+
+ private void samplePopoverHasNoFocus() => AddUntilStep("sample popover textbox not focused", () =>
{
var popover = this.ChildrenOfType().SingleOrDefault();
var slider = popover?.ChildrenOfType>().Single();
var textbox = slider?.ChildrenOfType().Single();
- return textbox?.HasFocus == true;
+ return textbox?.HasFocus == false;
});
private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () =>
@@ -371,7 +739,6 @@ namespace osu.Game.Tests.Visual.Editing
private void dismissPopover()
{
- AddStep("unfocus textbox", () => InputManager.Key(Key.Escape));
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any(popover => popover.IsPresent));
}
@@ -389,6 +756,12 @@ namespace osu.Game.Tests.Visual.Editing
return h.Samples.All(o => o.Volume == volume);
});
+ private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
+ return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume);
+ });
+
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
{
var popover = this.ChildrenOfType().Single();
@@ -396,10 +769,30 @@ namespace osu.Game.Tests.Visual.Editing
textBox.Current.Value = bank;
// force a commit via keyboard.
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
- InputManager.ChangeFocus(textBox);
+ ((IFocusManager)InputManager).ChangeFocus(textBox);
InputManager.Key(Key.Enter);
});
+ private void setAdditionBankViaPopover(string bank) => AddStep($"set addition bank {bank} via popover", () =>
+ {
+ var popover = this.ChildrenOfType().Single();
+ var textBox = popover.ChildrenOfType().ToArray()[1];
+ textBox.Current.Value = bank;
+ // force a commit via keyboard.
+ // this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
+ ((IFocusManager)InputManager).ChangeFocus(textBox);
+ InputManager.Key(Key.Enter);
+ });
+
+ private void toggleAdditionViaPopover(int index) => AddStep($"toggle addition {index} via popover", () =>
+ {
+ var popover = this.ChildrenOfType().First();
+ var ternaryButton = popover.ChildrenOfType().ToArray()[index];
+ InputManager.MoveMouseTo(ternaryButton);
+ InputManager.PressButton(MouseButton.Left);
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
private void hitObjectHasSamples(int objectIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} has samples {string.Join(',', samples)}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
@@ -411,5 +804,43 @@ namespace osu.Game.Tests.Visual.Editing
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.Samples.All(o => o.Bank == bank);
});
+
+ private void hitObjectHasSampleNormalBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has normal bank {bank}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
+ return h.Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
+ });
+
+ private void hitObjectHasSampleAdditionBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has addition bank {bank}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
+ return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
+ });
+
+ private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
+ return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples);
+ });
+
+ private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
+ return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank);
+ });
+
+ private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
+ return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
+ });
+
+ private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
+ return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
+ });
+
+ private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1));
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs
index e91596b872..3d7d0797d4 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs
@@ -6,6 +6,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.UserInterface;
@@ -62,12 +63,12 @@ namespace osu.Game.Tests.Visual.Editing
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
- AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
+ AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
- AddStep("drop focus", () => InputManager.ChangeFocus(null));
+ AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
}
@@ -77,12 +78,12 @@ namespace osu.Game.Tests.Visual.Editing
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
- AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
+ AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
- AddStep("drop focus", () => InputManager.ChangeFocus(null));
+ AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs
index a9f8e19e30..8b6f31d599 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs
@@ -3,17 +3,22 @@
#nullable disable
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
- public partial class TestSceneMetadataSection : OsuTestScene
+ public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
{
[Cached]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
@@ -26,6 +31,81 @@ namespace osu.Game.Tests.Visual.Editing
private TestMetadataSection metadataSection;
+ [Test]
+ public void TestUpdateViaTextBoxOnFocusLoss()
+ {
+ AddStep("set metadata", () =>
+ {
+ editorBeatmap.Metadata.Artist = "Example Artist";
+ editorBeatmap.Metadata.ArtistUnicode = string.Empty;
+ });
+
+ createSection();
+
+ TextBox textbox;
+
+ AddStep("focus first textbox", () =>
+ {
+ textbox = metadataSection.ChildrenOfType().First();
+ InputManager.MoveMouseTo(textbox);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("simulate changing textbox", () =>
+ {
+ // Can't simulate text input but this should work.
+ InputManager.Keys(PlatformAction.SelectAll);
+ InputManager.Keys(PlatformAction.Copy);
+ InputManager.Keys(PlatformAction.Paste);
+ InputManager.Keys(PlatformAction.Paste);
+ });
+
+ assertArtistMetadata("Example Artist");
+
+ // It's important values are committed immediately on focus loss so the editor exit sequence detects them.
+ AddAssert("value immediately changed on focus loss", () =>
+ {
+ ((IFocusManager)InputManager).TriggerFocusContention(metadataSection);
+ return editorBeatmap.Metadata.Artist;
+ }, () => Is.EqualTo("Example ArtistExample Artist"));
+ }
+
+ [Test]
+ public void TestUpdateViaTextBoxOnCommit()
+ {
+ AddStep("set metadata", () =>
+ {
+ editorBeatmap.Metadata.Artist = "Example Artist";
+ editorBeatmap.Metadata.ArtistUnicode = string.Empty;
+ });
+
+ createSection();
+
+ TextBox textbox;
+
+ AddStep("focus first textbox", () =>
+ {
+ textbox = metadataSection.ChildrenOfType().First();
+ InputManager.MoveMouseTo(textbox);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("simulate changing textbox", () =>
+ {
+ // Can't simulate text input but this should work.
+ InputManager.Keys(PlatformAction.SelectAll);
+ InputManager.Keys(PlatformAction.Copy);
+ InputManager.Keys(PlatformAction.Paste);
+ InputManager.Keys(PlatformAction.Paste);
+ });
+
+ assertArtistMetadata("Example Artist");
+
+ AddStep("commit", () => InputManager.Key(Key.Enter));
+
+ assertArtistMetadata("Example ArtistExample Artist");
+ }
+
[Test]
public void TestMinimalMetadata()
{
@@ -40,7 +120,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection();
- assertArtist("Example Artist");
+ assertArtistTextBox("Example Artist");
assertRomanisedArtist("Example Artist", false);
assertTitle("Example Title");
@@ -61,7 +141,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection();
- assertArtist("*なみりん");
+ assertArtistTextBox("*なみりん");
assertRomanisedArtist(string.Empty, true);
assertTitle("コイシテイク・プラネット");
@@ -82,7 +162,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection();
- assertArtist("*なみりん");
+ assertArtistTextBox("*なみりん");
assertRomanisedArtist("*namirin", true);
assertTitle("コイシテイク・プラネット");
@@ -104,11 +184,11 @@ namespace osu.Game.Tests.Visual.Editing
createSection();
AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin");
- assertArtist("*namirin");
+ assertArtistTextBox("*namirin");
assertRomanisedArtist("*namirin", false);
AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん");
- assertArtist("*なみりん");
+ assertArtistTextBox("*なみりん");
assertRomanisedArtist("*namirin", true);
AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori");
@@ -123,21 +203,24 @@ namespace osu.Game.Tests.Visual.Editing
private void createSection()
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
- private void assertArtist(string expected)
- => AddAssert($"artist is {expected}", () => metadataSection.ArtistTextBox.Current.Value == expected);
+ private void assertArtistMetadata(string expected)
+ => AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
+
+ private void assertArtistTextBox(string expected)
+ => AddAssert($"artist textbox is {expected}", () => metadataSection.ArtistTextBox.Current.Value, () => Is.EqualTo(expected));
private void assertRomanisedArtist(string expected, bool editable)
{
- AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value == expected);
+ AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value, () => Is.EqualTo(expected));
AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable);
}
private void assertTitle(string expected)
- => AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value == expected);
+ => AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value, () => Is.EqualTo(expected));
private void assertRomanisedTitle(string expected, bool editable)
{
- AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value == expected);
+ AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value, () => Is.EqualTo(expected));
AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable);
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs
index 1f46a08831..971eb223eb 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Editing
() => Is.EqualTo(1));
AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke());
- AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
+ AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
addStepClickLink("00:00:000 (1)", waitForSeek: false);
AddUntilStep("received 'must be in edit'",
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for song select", () =>
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
- && songSelect.IsLoaded
+ && songSelect.BeatmapSetsLoaded
);
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
AddStep("Open editor for ruleset", () =>
diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs
index a5681bea4a..fe74e1b346 100644
--- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs
+++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs
@@ -5,11 +5,14 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
@@ -26,7 +29,53 @@ namespace osu.Game.Tests.Visual.Editing
private GlobalActionContainer globalActionContainer => this.ChildrenOfType().Single();
[Test]
- public void TestCommitPlacementViaGlobalAction()
+ public void TestDeleteUsingMiddleMouse()
+ {
+ AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
+ AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("place circle", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
+ AddStep("delete with middle mouse", () => InputManager.Click(MouseButton.Middle));
+ AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty);
+ }
+
+ [Test]
+ public void TestDeleteUsingShiftRightClick()
+ {
+ AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
+ AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("place circle", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
+ AddStep("delete with right mouse", () =>
+ {
+ InputManager.PressKey(Key.ShiftLeft);
+ InputManager.Click(MouseButton.Right);
+ InputManager.ReleaseKey(Key.ShiftLeft);
+ });
+ AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty);
+ }
+
+ [Test]
+ public void TestContextMenu()
+ {
+ AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
+ AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("place circle", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
+ AddStep("delete with right mouse", () =>
+ {
+ InputManager.Click(MouseButton.Right);
+ });
+ AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items);
+ AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items);
+ }
+
+ [Test]
+ [Solo]
+ public void TestCommitPlacementViaRightClick()
{
Playfield playfield = null!;
@@ -43,11 +92,7 @@ namespace osu.Game.Tests.Visual.Editing
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
- AddStep("confirm via global action", () =>
- {
- globalActionContainer.TriggerPressed(GlobalAction.Select);
- globalActionContainer.TriggerReleased(GlobalAction.Select);
- });
+ AddStep("confirm via right click", () => InputManager.Click(MouseButton.Right));
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
}
@@ -102,5 +147,116 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("change tool to circle", () => InputManager.Key(Key.Number2));
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
}
+
+ [Test]
+ public void TestAutomaticBankAssignment()
+ {
+ AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle
+ {
+ StartTime = 0,
+ Samples =
+ {
+ new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70),
+ new HitSampleInfo(name: HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_DRUM, volume: 70),
+ }
+ }));
+
+ AddStep("seek to 500", () => EditorClock.Seek(500)); // previous object is the one at time 0
+ AddStep("enable automatic bank assignment", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.Q);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
+ AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("place circle", () => InputManager.Click(MouseButton.Left));
+ AddAssert("circle has soft bank", () => EditorBeatmap.HitObjects[1].Samples.Single().Bank, () => Is.EqualTo(HitSampleInfo.BANK_SOFT));
+ AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
+
+ AddStep("seek to 250", () => EditorClock.Seek(250)); // previous object is the one at time 0
+ AddStep("enable clap addition", () => InputManager.Key(Key.R));
+ AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
+ AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("place circle", () => InputManager.Click(MouseButton.Left));
+ AddAssert("circle has 2 samples", () => EditorBeatmap.HitObjects[1].Samples, () => Has.Count.EqualTo(2));
+ AddAssert("normal sample has soft bank", () => EditorBeatmap.HitObjects[1].Samples.Single(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank,
+ () => Is.EqualTo(HitSampleInfo.BANK_SOFT));
+ AddAssert("clap sample has drum bank", () => EditorBeatmap.HitObjects[1].Samples.Single(s => s.Name == HitSampleInfo.HIT_CLAP).Bank,
+ () => Is.EqualTo(HitSampleInfo.BANK_DRUM));
+ AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
+
+ AddStep("seek to 1000", () => EditorClock.Seek(1000)); // previous object is the one at time 500, which has no additions
+ AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
+ AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("place circle", () => InputManager.Click(MouseButton.Left));
+ AddAssert("circle has 2 samples", () => EditorBeatmap.HitObjects[3].Samples, () => Has.Count.EqualTo(2));
+ AddAssert("all samples have soft bank", () => EditorBeatmap.HitObjects[3].Samples.All(s => s.Bank == HitSampleInfo.BANK_SOFT));
+ AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[3].Samples.All(s => s.Volume == 70));
+ }
+
+ [Test]
+ public void TestVolumeIsInheritedFromLastObject()
+ {
+ AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle
+ {
+ StartTime = 0,
+ Samples =
+ {
+ new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70),
+ }
+ }));
+ AddStep("seek to 500", () => EditorClock.Seek(500));
+ AddStep("select drum bank", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.R);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
+ AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("place circle", () => InputManager.Click(MouseButton.Left));
+ AddAssert("circle has drum bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM));
+ AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
+ }
+
+ [Test]
+ public void TestNodeSamplesAndSamplesAreSame()
+ {
+ Playfield playfield = null!;
+
+ AddStep("select drum bank", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.R);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ AddStep("enable clap addition", () => InputManager.Key(Key.R));
+
+ AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
+ AddStep("move mouse to top left of playfield", () =>
+ {
+ playfield = this.ChildrenOfType