diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index b8dc201559..c4ba6e5143 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,13 +3,13 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
- "version": "2022.2.3",
+ "version": "2023.3.3",
"commands": [
"jb"
]
},
"nvika": {
- "version": "2.2.0",
+ "version": "3.0.0",
"commands": [
"nvika"
]
@@ -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/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml
index dfdcf8d320..a8a5d5e64b 100644
--- a/.github/ISSUE_TEMPLATE/bug-issue.yml
+++ b/.github/ISSUE_TEMPLATE/bug-issue.yml
@@ -42,7 +42,7 @@ body:
- type: input
attributes:
label: Version
- description: The version you encountered this bug on. This is shown at the bottom of the main menu and also at the end of the settings screen.
+ description: The version you encountered this bug on. This is shown at the end of the settings overlay.
validations:
required: true
- type: markdown
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 103e4dbc30..ba65cfa33a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,19 +13,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- # FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
- # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- - name: Install .NET 3.1.x LTS
- uses: actions/setup-dotnet@v3
+ - name: Install .NET 8.0.x
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: "3.1.x"
-
- - name: Install .NET 6.0.x
- uses: actions/setup-dotnet@v3
- with:
- dotnet-version: "6.0.x"
+ dotnet-version: "8.0.x"
- name: Restore Tools
run: dotnet tool restore
@@ -34,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') }}
@@ -71,18 +64,19 @@ 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 6.0.x
- uses: actions/setup-dotnet@v3
+ - name: Install .NET 8.0.x
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: "6.0.x"
+ dotnet-version: "8.0.x"
- name: Compile
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
@@ -106,18 +100,18 @@ 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 6.0.x
- uses: actions/setup-dotnet@v3
+ - name: Install .NET 8.0.x
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: "6.0.x"
+ dotnet-version: "8.0.x"
- name: Install .NET workloads
run: dotnet workload install maui-android
@@ -133,15 +127,18 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- - name: Install .NET 6.0.x
- uses: actions/setup-dotnet@v3
+ - name: Install .NET 8.0.x
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: "6.0.x"
+ 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 32d3d37ffe..b19f03ad7d 100644
--- a/.github/workflows/update-web-mod-definitions.yml
+++ b/.github/workflows/update-web-mod-definitions.yml
@@ -12,24 +12,24 @@ jobs:
name: Update osu-web mod definitions
runs-on: ubuntu-latest
steps:
- - name: Install .NET 6.0.x
- uses: actions/setup-dotnet@v3
+ - name: Install .NET 8.0.x
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: "6.0.x"
+ 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/.globalconfig b/.globalconfig
index a7b652c454..a4d4707f9b 100644
--- a/.globalconfig
+++ b/.globalconfig
@@ -1,5 +1,3 @@
-is_global = true
-
# .NET Code Style
# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
@@ -56,4 +54,4 @@ dotnet_diagnostic.RS0030.severity = error
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
# See: https://github.com/ppy/osu/pull/19677
-dotnet_diagnostic.OSUF001.severity = none
\ No newline at end of file
+dotnet_diagnostic.OSUF001.severity = none
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 734374c840..5ba12b845b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,8 +1,7 @@
- 10.0
- true
+ 12.0
enable
@@ -35,7 +34,7 @@
https://github.com/ppy/osu
Automated release.
ppy Pty Ltd
- Copyright (c) 2022 ppy Pty Ltd
+ Copyright (c) 2024 ppy Pty Ltd
osu game
diff --git a/LICENCE b/LICENCE
index d3e7537cef..3bb8b62d5d 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,4 +1,4 @@
-Copyright (c) 2022 ppy Pty Ltd .
+Copyright (c) 2024 ppy Pty Ltd .
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index d5dc0723af..cb722e5df3 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ A few resources are available as starting points to getting involved and underst
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
-- Track our current efforts [towards full "ranked play" support](https://github.com/orgs/ppy/projects/13?query=is%3Aopen+sort%3Aupdated-desc).
+- Track our current efforts [towards improving the game](https://github.com/orgs/ppy/projects/7/views/6).
## Running osu!
@@ -30,12 +30,12 @@ If you are just looking to give the game a whirl, you can grab the latest releas
### Latest release:
-| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
-| ------------- | ------------- | ------------- | ------------- | ------------- |
+| [Windows 10+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 12+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
+|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | ------------- | ------------- |
You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download).
-If your platform is 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/Directory.Build.props b/Templates/Rulesets/ruleset-empty/Directory.Build.props
new file mode 100644
index 0000000000..74d05ff690
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+
+ $(MSBuildThisFileDirectory)app.manifest
+
+
+ true
+ $(NoWarn);CS1591
+
+
diff --git a/Templates/Rulesets/ruleset-empty/app.manifest b/Templates/Rulesets/ruleset-empty/app.manifest
new file mode 100644
index 0000000000..1c1e5f540c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-empty/app.manifest
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
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.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs
index 03ee7c9204..63c481a623 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests
[STAThread]
public static int Main(string[] args)
{
- using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
+ using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu"))
{
host.Run(new OsuTestBrowser());
return 0;
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 2baa7ee0e0..7d43eb2b05 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -10,7 +10,7 @@
-
+
@@ -18,7 +18,7 @@
WinExe
- net6.0
+ net8.0
osu.Game.Rulesets.EmptyFreeform.Tests
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-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
index d09e7647e0..89abd5665c 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
@@ -1,6 +1,6 @@
- net6.0
+ net8.0
osu.Game.Rulesets.EmptyFreeform
Library
AnyCPU
diff --git a/Templates/Rulesets/ruleset-example/Directory.Build.props b/Templates/Rulesets/ruleset-example/Directory.Build.props
new file mode 100644
index 0000000000..74d05ff690
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+
+ $(MSBuildThisFileDirectory)app.manifest
+
+
+ true
+ $(NoWarn);CS1591
+
+
diff --git a/Templates/Rulesets/ruleset-example/app.manifest b/Templates/Rulesets/ruleset-example/app.manifest
new file mode 100644
index 0000000000..1c1e5f540c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-example/app.manifest
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
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.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
index 55c0cf6a3b..c44cbb845b 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
[STAThread]
public static int Main(string[] args)
{
- using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
+ using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu"))
{
host.Run(new OsuTestBrowser());
return 0;
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index a2308e6dfc..7dc8a1336b 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -10,7 +10,7 @@
-
+
@@ -18,7 +18,7 @@
WinExe
- net6.0
+ net8.0
osu.Game.Rulesets.Pippidon.Tests
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-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
index 9c8867f4ef..165b6b6c6b 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -1,6 +1,6 @@
- net6.0
+ net8.0
osu.Game.Rulesets.Pippidon
Library
AnyCPU
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/Directory.Build.props b/Templates/Rulesets/ruleset-scrolling-empty/Directory.Build.props
new file mode 100644
index 0000000000..74d05ff690
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+
+ $(MSBuildThisFileDirectory)app.manifest
+
+
+ true
+ $(NoWarn);CS1591
+
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/app.manifest b/Templates/Rulesets/ruleset-scrolling-empty/app.manifest
new file mode 100644
index 0000000000..1c1e5f540c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-empty/app.manifest
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
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.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs
index b45505678c..5beb6616a7 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests
[STAThread]
public static int Main(string[] args)
{
- using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
+ using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu"))
{
host.Run(new OsuTestBrowser());
return 0;
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index e839d2657c..9c4c8217f0 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -10,7 +10,7 @@
-
+
@@ -18,7 +18,7 @@
WinExe
- net6.0
+ net8.0
osu.Game.Rulesets.EmptyScrolling.Tests
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-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
index 5bf3884f53..6d9565a6f2 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
@@ -1,6 +1,6 @@
- net6.0
+ net8.0
osu.Game.Rulesets.EmptyScrolling
Library
AnyCPU
diff --git a/Templates/Rulesets/ruleset-scrolling-example/Directory.Build.props b/Templates/Rulesets/ruleset-scrolling-example/Directory.Build.props
new file mode 100644
index 0000000000..74d05ff690
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+
+ $(MSBuildThisFileDirectory)app.manifest
+
+
+ true
+ $(NoWarn);CS1591
+
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/app.manifest b/Templates/Rulesets/ruleset-scrolling-example/app.manifest
new file mode 100644
index 0000000000..1c1e5f540c
--- /dev/null
+++ b/Templates/Rulesets/ruleset-scrolling-example/app.manifest
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
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.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
index 55c0cf6a3b..c44cbb845b 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
[STAThread]
public static int Main(string[] args)
{
- using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
+ using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu"))
{
host.Run(new OsuTestBrowser());
return 0;
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index a2308e6dfc..7dc8a1336b 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -10,7 +10,7 @@
-
+
@@ -18,7 +18,7 @@
WinExe
- net6.0
+ net8.0
osu.Game.Rulesets.Pippidon.Tests
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/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
index 9c8867f4ef..165b6b6c6b 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -1,6 +1,6 @@
- net6.0
+ net8.0
osu.Game.Rulesets.Pippidon
Library
AnyCPU
diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj
index b8c3ad373a..186a6093f5 100644
--- a/Templates/osu.Game.Templates.csproj
+++ b/Templates/osu.Game.Templates.csproj
@@ -1,4 +1,4 @@
-
+
Template
ppy.osu.Game.Templates
@@ -8,7 +8,7 @@
https://github.com/ppy/osu/blob/master/Templates
https://github.com/ppy/osu
Automated release.
- Copyright (c) 2022 ppy Pty Ltd
+ Copyright (c) 2024 ppy Pty Ltd
Templates to use when creating a ruleset for consumption in osu!.
dotnet-new;templates;osu
netstandard2.1
diff --git a/assets/lazer-nuget.png b/assets/lazer-nuget.png
index c2a587fdc2..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 1e40e844cc..f564b93d6f 100644
Binary files a/assets/lazer.png and b/assets/lazer.png differ
diff --git a/global.json b/global.json
index 5dcd5f425a..789bff3bd0 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
- "version": "6.0.100",
- "rollForward": "latestFeature"
+ "version": "8.0.100",
+ "rollForward": "latestFeature",
+ "allowPrerelease": false
}
-}
-
+}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index b179b8b837..3b3385ecfe 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
diff --git a/osu.Android/AndroidMouseSettings.cs b/osu.Android/AndroidMouseSettings.cs
deleted file mode 100644
index fd01b11164..0000000000
--- a/osu.Android/AndroidMouseSettings.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using Android.OS;
-using osu.Framework.Allocation;
-using osu.Framework.Android.Input;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Localisation;
-using osu.Game.Configuration;
-using osu.Game.Localisation;
-using osu.Game.Overlays.Settings;
-using osu.Game.Overlays.Settings.Sections.Input;
-
-namespace osu.Android
-{
- public partial class AndroidMouseSettings : SettingsSubsection
- {
- private readonly AndroidMouseHandler mouseHandler;
-
- protected override LocalisableString Header => MouseSettingsStrings.Mouse;
-
- private Bindable handlerSensitivity = null!;
-
- private Bindable localSensitivity = null!;
-
- private Bindable relativeMode = null!;
-
- public AndroidMouseSettings(AndroidMouseHandler mouseHandler)
- {
- this.mouseHandler = mouseHandler;
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager osuConfig)
- {
- // use local bindable to avoid changing enabled state of game host's bindable.
- handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy();
- localSensitivity = handlerSensitivity.GetUnboundCopy();
-
- relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
-
- // High precision/pointer capture is only available on Android 8.0 and up
- if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
- {
- AddRange(new Drawable[]
- {
- new SettingsCheckbox
- {
- LabelText = MouseSettingsStrings.HighPrecisionMouse,
- TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip,
- Current = relativeMode,
- Keywords = new[] { @"raw", @"input", @"relative", @"cursor", @"captured", @"pointer" },
- },
- new MouseSettings.SensitivitySetting
- {
- LabelText = MouseSettingsStrings.CursorSensitivity,
- Current = localSensitivity,
- },
- });
- }
-
- AddRange(new Drawable[]
- {
- new SettingsCheckbox
- {
- LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
- TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
- Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel),
- },
- new SettingsCheckbox
- {
- LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
- Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons),
- },
- });
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true);
-
- handlerSensitivity.BindValueChanged(val =>
- {
- bool disabled = localSensitivity.Disabled;
-
- localSensitivity.Disabled = false;
- localSensitivity.Value = val.NewValue;
- localSensitivity.Disabled = disabled;
- }, true);
-
- localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
- }
- }
-}
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 33ffed432e..bbee491d90 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -72,9 +72,9 @@ namespace osu.Android
Debug.Assert(Resources?.DisplayMetrics != null);
Point displaySize = new Point();
-#pragma warning disable 618 // GetSize is deprecated
+#pragma warning disable CA1422 // GetSize is deprecated
WindowManager.DefaultDisplay.GetSize(displaySize);
-#pragma warning restore 618
+#pragma warning restore CA1422
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
bool isTablet = smallestWidthDp >= 600f;
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 52cfb67f42..a235913ef3 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -5,13 +5,9 @@ using System;
using Android.App;
using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
-using osu.Framework.Android.Input;
using osu.Framework.Extensions.ObjectExtensions;
-using osu.Framework.Input.Handlers;
using osu.Framework.Platform;
using osu.Game;
-using osu.Game.Overlays.Settings;
-using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Updater;
using osu.Game.Utils;
@@ -88,24 +84,6 @@ namespace osu.Android
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
- public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
- {
- switch (handler)
- {
- case AndroidMouseHandler mh:
- return new AndroidMouseSettings(mh);
-
- case AndroidJoystickHandler jh:
- return new AndroidJoystickSettings(jh);
-
- case AndroidTouchHandler th:
- return new TouchSettings(th);
-
- default:
- return base.CreateSettingsSubsectionFor(handler);
- }
- }
-
private class AndroidBatteryInfo : BatteryInfo
{
public override double? ChargeLevel => Battery.ChargeLevel;
diff --git a/osu.Android/Resources/drawable/ic_launcher_background.xml b/osu.Android/Resources/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..1af30228ec
--- /dev/null
+++ b/osu.Android/Resources/drawable/ic_launcher_background.xml
@@ -0,0 +1,618 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/osu.Android/Resources/drawable/lazer.png b/osu.Android/Resources/drawable/lazer.png
deleted file mode 100644
index fc7aa8a092..0000000000
Binary files a/osu.Android/Resources/drawable/lazer.png and /dev/null differ
diff --git a/osu.Android/Resources/drawable/monochrome.xml b/osu.Android/Resources/drawable/monochrome.xml
new file mode 100644
index 0000000000..600c070c3e
--- /dev/null
+++ b/osu.Android/Resources/drawable/monochrome.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
diff --git a/osu.Android/Resources/mipmap-anydpi-v26/ic_launcher.xml b/osu.Android/Resources/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..7133c9c861
--- /dev/null
+++ b/osu.Android/Resources/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/osu.Android/Resources/mipmap-hdpi/ic_launcher.png b/osu.Android/Resources/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..a1f717cf6c
Binary files /dev/null and b/osu.Android/Resources/mipmap-hdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..36651206e8
Binary files /dev/null and b/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-mdpi/ic_launcher.png b/osu.Android/Resources/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..c68ed7a2d5
Binary files /dev/null and b/osu.Android/Resources/mipmap-mdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..85f3084429
Binary files /dev/null and b/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..28be07512a
Binary files /dev/null and b/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..613b817f0a
Binary files /dev/null and b/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-xxhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..4330a939d9
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..ec7d602838
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..2e722ee884
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..f22db0c616
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 1507bfaa29..be2e669728 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -1,13 +1,10 @@
- net6.0-android
+ net8.0-android
Exe
osu.Android
osu.Android
- true
-
- false
0.0.0
1
$(Version)
@@ -19,4 +16,7 @@
+
+
+
diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf
index 503e5935f5..606988ccdf 100644
--- a/osu.Desktop.slnf
+++ b/osu.Desktop.slnf
@@ -16,15 +16,14 @@
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
"osu.Game.Tournament\\osu.Game.Tournament.csproj",
"osu.Game\\osu.Game.csproj",
-
- "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj",
- "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
+ "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
- "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
+ "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
- "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
- "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj"
+ "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
+ "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj"
]
}
-}
+}
\ No newline at end of file
diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
index f990fd55fc..780d367900 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -5,14 +5,21 @@ using System;
using System.Text;
using DiscordRPC;
using DiscordRPC.Message;
+using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
+using osu.Framework.Threading;
+using osu.Game;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Users;
using LogLevel = osu.Framework.Logging.LogLevel;
@@ -21,39 +28,78 @@ namespace osu.Desktop
{
internal partial class DiscordRichPresence : Component
{
- private const string client_id = "367827983903490050";
+ private const string client_id = "1216669957799018608";
private DiscordRpcClient client = null!;
[Resolved]
private IBindable ruleset { get; set; } = null!;
- private IBindable user = null!;
-
[Resolved]
private IAPIProvider api { get; set; } = null!;
+ [Resolved]
+ private OsuGame game { get; set; } = null!;
+
+ [Resolved]
+ private LoginOverlay? login { get; set; }
+
+ [Resolved]
+ private MultiplayerClient multiplayerClient { get; set; } = null!;
+
+ [Resolved]
+ private OsuConfigManager config { get; set; } = null!;
+
private readonly IBindable status = new Bindable();
private readonly IBindable activity = new Bindable();
-
private readonly Bindable privacyMode = new Bindable();
private readonly RichPresence presence = new RichPresence
{
- Assets = new Assets { LargeImageKey = "osu_logo_lazer", }
+ Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
+ Secrets = new Secrets
+ {
+ JoinSecret = null,
+ SpectateSecret = null,
+ },
};
+ private IBindable? user;
+
[BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
+ private void load()
{
client = new DiscordRpcClient(client_id)
{
- SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady.
+ // SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
+ // to check whether a difference has actually occurred before sending a command to Discord (with a minor caveat that's handled in onReady).
+ SkipIdenticalPresence = true
};
client.OnReady += onReady;
+ client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
- client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
+ try
+ {
+ client.RegisterUriScheme();
+ client.Subscribe(EventType.Join);
+ client.OnJoin += onJoin;
+ }
+ catch (Exception ex)
+ {
+ // This is known to fail in at least the following sandboxed environments:
+ // - macOS (when packaged as an app bundle)
+ // - flatpak (see: https://github.com/flathub/sh.ppy.osu/issues/170)
+ // There is currently no better way to do this offered by Discord, so the best we can do is simply ignore it for now.
+ Logger.Log($"Failed to register Discord URI scheme: {ex}");
+ }
+
+ client.Initialize();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
@@ -67,36 +113,59 @@ namespace osu.Desktop
activity.BindTo(u.NewValue.Activity);
}, true);
- ruleset.BindValueChanged(_ => updateStatus());
- status.BindValueChanged(_ => updateStatus());
- activity.BindValueChanged(_ => updateStatus());
- privacyMode.BindValueChanged(_ => updateStatus());
-
- client.Initialize();
+ ruleset.BindValueChanged(_ => schedulePresenceUpdate());
+ status.BindValueChanged(_ => schedulePresenceUpdate());
+ activity.BindValueChanged(_ => schedulePresenceUpdate());
+ privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
+ multiplayerClient.RoomUpdated += onRoomUpdated;
}
private void onReady(object _, ReadyMessage __)
{
Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug);
- updateStatus();
+
+ // when RPC is lost and reconnected, we have to clear presence state for updatePresence to work (see DiscordRpcClient.SkipIdenticalPresence).
+ if (client.CurrentPresence != null)
+ client.SetPresence(null);
+
+ schedulePresenceUpdate();
}
- private void updateStatus()
+ private void onRoomUpdated() => schedulePresenceUpdate();
+
+ private ScheduledDelegate? presenceUpdateDelegate;
+
+ private void schedulePresenceUpdate()
{
- if (!client.IsInitialized)
+ presenceUpdateDelegate?.Cancel();
+ presenceUpdateDelegate = Scheduler.AddDelayed(() =>
+ {
+ if (!client.IsInitialized)
+ return;
+
+ if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
+ {
+ client.ClearPresence();
+ return;
+ }
+
+ bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
+
+ updatePresence(hideIdentifiableInformation);
+ client.SetPresence(presence);
+ }, 200);
+ }
+
+ private void updatePresence(bool hideIdentifiableInformation)
+ {
+ if (user == null)
return;
- if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
+ // user activity
+ if (activity.Value != null)
{
- client.ClearPresence();
- return;
- }
-
- if (status.Value == UserStatus.Online && activity.Value != null)
- {
- bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited;
- presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
- presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
+ presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
+ presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0)
{
@@ -120,7 +189,42 @@ namespace osu.Desktop
presence.Details = string.Empty;
}
- // update user information
+ // user party
+ if (!hideIdentifiableInformation && multiplayerClient.Room != null)
+ {
+ MultiplayerRoom room = multiplayerClient.Room;
+
+ presence.Party = new Party
+ {
+ Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private,
+ ID = room.RoomID.ToString(),
+ // technically lobbies can have infinite users, but Discord needs this to be set to something.
+ // to make party display sensible, assign a powers of two above participants count (8 at minimum).
+ Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))),
+ Size = room.Users.Count,
+ };
+
+ RoomSecret roomSecret = new RoomSecret
+ {
+ RoomID = room.RoomID,
+ Password = room.Settings.Password,
+ };
+
+ if (client.HasRegisteredUriScheme)
+ presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret);
+
+ // discord cannot handle both secrets and buttons at the same time, so we need to choose something.
+ // the multiplayer room seems more important.
+ presence.Buttons = null;
+ }
+ else
+ {
+ presence.Party = null;
+ presence.Secrets.JoinSecret = null;
+ }
+
+ // game images:
+ // large image tooltip
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
presence.Assets.LargeImageText = string.Empty;
else
@@ -131,17 +235,55 @@ namespace osu.Desktop
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
}
- // update ruleset
+ // small image
presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
presence.Assets.SmallImageText = ruleset.Value.Name;
-
- client.SetPresence(presence);
}
+ private void onJoin(object sender, JoinMessage args) => Scheduler.AddOnce(() =>
+ {
+ game.Window?.Raise();
+
+ if (!api.IsLoggedIn)
+ {
+ login?.Show();
+ return;
+ }
+
+ Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug);
+
+ // Stable and lazer share the same Discord client ID, meaning they can accept join requests from each other.
+ // Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion.
+ if (args.Secret[0] != '{' || !tryParseRoomSecret(args.Secret, out long roomId, out string? password))
+ {
+ Logger.Log("Could not join multiplayer room, invitation is invalid or incompatible.", LoggingTarget.Network, LogLevel.Important);
+ return;
+ }
+
+ var request = new GetRoomRequest(roomId);
+ request.Success += room => Schedule(() =>
+ {
+ game.PresentMultiplayerMatch(room, password);
+ });
+ request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important);
+ api.Queue(request);
+ });
+
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
- private string truncate(string str)
+ private static string clampLength(string str)
{
+ // Empty strings are fine to discord even though single-character strings are not. Make it make sense.
+ if (string.IsNullOrEmpty(str))
+ return str;
+
+ // As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
+ // And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
+ // That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
+ // just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
+ if (str.Length < 2)
+ return str.PadRight(2, '\u200B');
+
if (Encoding.UTF8.GetByteCount(str) <= 128)
return str;
@@ -159,7 +301,31 @@ namespace osu.Desktop
});
}
- private int? getBeatmapID(UserActivity activity)
+ private static bool tryParseRoomSecret(string secretJson, out long roomId, out string? password)
+ {
+ roomId = 0;
+ password = null;
+
+ RoomSecret? roomSecret;
+
+ try
+ {
+ roomSecret = JsonConvert.DeserializeObject(secretJson);
+ }
+ catch
+ {
+ return false;
+ }
+
+ if (roomSecret == null) return false;
+
+ roomId = roomSecret.RoomID;
+ password = roomSecret.Password;
+
+ return true;
+ }
+
+ private static int? getBeatmapID(UserActivity activity)
{
switch (activity)
{
@@ -175,8 +341,20 @@ namespace osu.Desktop
protected override void Dispose(bool isDisposing)
{
+ if (multiplayerClient.IsNotNull())
+ multiplayerClient.RoomUpdated -= onRoomUpdated;
+
client.Dispose();
base.Dispose(isDisposing);
}
+
+ private class RoomSecret
+ {
+ [JsonProperty(@"roomId", Required = Required.Always)]
+ public long RoomID { get; set; }
+
+ [JsonProperty(@"password", Required = Required.AllowNull)]
+ public string? Password { get; set; }
+ }
}
}
diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs
new file mode 100644
index 0000000000..0b09613ba0
--- /dev/null
+++ b/osu.Desktop/NVAPI.cs
@@ -0,0 +1,747 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable disable
+
+#pragma warning disable IDE1006 // Naming rule violation
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using osu.Framework.Logging;
+
+namespace osu.Desktop
+{
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ [SupportedOSPlatform("windows")]
+ internal static class NVAPI
+ {
+ private const string osu_filename = "osu!.exe";
+
+ // This is a good reference:
+ // https://github.com/errollw/Warp-and-Blend-Quadros/blob/master/WarpBlend-Quadros/UnwarpAll-Quadros/include/nvapi.h
+ // Note our Stride == their VERSION (e.g. NVDRS_SETTING_VER)
+
+ public const int MAX_PHYSICAL_GPUS = 64;
+ public const int UNICODE_STRING_MAX = 2048;
+
+ public const string APPLICATION_NAME = @"osu!";
+ public const string PROFILE_NAME = @"osu!";
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus EnumPhysicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount);
+
+ public static readonly EnumPhysicalGPUsDelegate EnumPhysicalGPUs;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus EnumLogicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount);
+
+ public static readonly EnumLogicalGPUsDelegate EnumLogicalGPUs;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus GetSystemTypeDelegate(IntPtr gpuHandle, out NvSystemType systemType);
+
+ public static readonly GetSystemTypeDelegate GetSystemType;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus GetGPUTypeDelegate(IntPtr gpuHandle, out NvGpuType gpuType);
+
+ public static readonly GetGPUTypeDelegate GetGPUType;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus CreateSessionDelegate(out IntPtr sessionHandle);
+
+ public static CreateSessionDelegate CreateSession;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus LoadSettingsDelegate(IntPtr sessionHandle);
+
+ public static LoadSettingsDelegate LoadSettings;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus FindApplicationByNameDelegate(IntPtr sessionHandle, [MarshalAs(UnmanagedType.BStr)] string appName, out IntPtr profileHandle, ref NvApplication application);
+
+ public static FindApplicationByNameDelegate FindApplicationByName;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus GetCurrentGlobalProfileDelegate(IntPtr sessionHandle, out IntPtr profileHandle);
+
+ public static GetCurrentGlobalProfileDelegate GetCurrentGlobalProfile;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus GetProfileInfoDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvProfile profile);
+
+ public static GetProfileInfoDelegate GetProfileInfo;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate NvStatus GetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, NvSettingID settingID, ref NvSetting setting);
+
+ public static GetSettingDelegate GetSetting;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate NvStatus CreateProfileDelegate(IntPtr sessionHandle, ref NvProfile profile, out IntPtr profileHandle);
+
+ private static readonly CreateProfileDelegate CreateProfile;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate NvStatus SetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvSetting setting);
+
+ private static readonly SetSettingDelegate SetSetting;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate NvStatus EnumApplicationsDelegate(IntPtr sessionHandle, IntPtr profileHandle, uint startIndex, ref uint appCount, [In, Out, MarshalAs(UnmanagedType.LPArray)] NvApplication[] applications);
+
+ private static readonly EnumApplicationsDelegate EnumApplications;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate NvStatus CreateApplicationDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvApplication application);
+
+ private static readonly CreateApplicationDelegate CreateApplication;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate NvStatus SaveSettingsDelegate(IntPtr sessionHandle);
+
+ private static readonly SaveSettingsDelegate SaveSettings;
+
+ public static NvStatus Status { get; private set; } = NvStatus.OK;
+ public static bool Available { get; private set; }
+
+ private static IntPtr sessionHandle;
+
+ public static bool IsUsingOptimusDedicatedGpu
+ {
+ get
+ {
+ if (!Available)
+ return false;
+
+ if (!IsLaptop)
+ return false;
+
+ IntPtr profileHandle;
+ if (!getProfile(out profileHandle, out _, out bool _))
+ return false;
+
+ // Get the optimus setting
+ NvSetting setting;
+ if (!getSetting(NvSettingID.SHIM_RENDERING_MODE_ID, profileHandle, out setting))
+ return false;
+
+ return (setting.U32CurrentValue & (uint)NvShimSetting.SHIM_RENDERING_MODE_ENABLE) > 0;
+ }
+ }
+
+ public static bool IsLaptop
+ {
+ get
+ {
+ if (!Available)
+ return false;
+
+ // Make sure that this is a laptop.
+ IntPtr[] gpus = new IntPtr[64];
+ if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount)))
+ return false;
+
+ for (int i = 0; i < gpuCount; i++)
+ {
+ if (checkError(GetSystemType(gpus[i], out var type)))
+ return false;
+
+ if (type == NvSystemType.LAPTOP)
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ public static NvThreadControlSetting ThreadedOptimisations
+ {
+ get
+ {
+ if (!Available)
+ return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
+
+ IntPtr profileHandle;
+ if (!getProfile(out profileHandle, out _, out bool _))
+ return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
+
+ // Get the threaded optimisations setting
+ NvSetting setting;
+ if (!getSetting(NvSettingID.OGL_THREAD_CONTROL_ID, profileHandle, out setting))
+ return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
+
+ return (NvThreadControlSetting)setting.U32CurrentValue;
+ }
+ set
+ {
+ if (!Available)
+ return;
+
+ bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value);
+
+ Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!");
+ }
+ }
+
+ ///
+ /// Checks if the profile contains the current application.
+ ///
+ /// If the profile contains the current application.
+ private static bool containsApplication(IntPtr profileHandle, NvProfile profile, out NvApplication application)
+ {
+ application = new NvApplication
+ {
+ Version = NvApplication.Stride
+ };
+
+ if (profile.NumOfApps == 0)
+ return false;
+
+ NvApplication[] applications = new NvApplication[profile.NumOfApps];
+ applications[0].Version = NvApplication.Stride;
+
+ uint numApps = profile.NumOfApps;
+
+ if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications)))
+ return false;
+
+ for (uint i = 0; i < numApps; i++)
+ {
+ if (applications[i].AppName == osu_filename)
+ {
+ application = applications[i];
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Retrieves the profile of the current application.
+ ///
+ /// The profile handle.
+ /// The current application description.
+ /// If this profile is not a global (default) profile.
+ /// If the operation succeeded.
+ private static bool getProfile(out IntPtr profileHandle, out NvApplication application, out bool isApplicationSpecific)
+ {
+ application = new NvApplication
+ {
+ Version = NvApplication.Stride
+ };
+
+ isApplicationSpecific = true;
+
+ if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application)))
+ {
+ isApplicationSpecific = false;
+ if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle)))
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Creates a profile.
+ ///
+ /// The profile handle.
+ /// If the operation succeeded.
+ private static bool createProfile(out IntPtr profileHandle)
+ {
+ NvProfile newProfile = new NvProfile
+ {
+ Version = NvProfile.Stride,
+ IsPredefined = 0,
+ ProfileName = PROFILE_NAME,
+ GPUSupport = new uint[32]
+ };
+
+ newProfile.GPUSupport[0] = 1;
+
+ if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle)))
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Retrieves a setting from the profile.
+ ///
+ /// The setting to retrieve.
+ /// The profile handle to retrieve the setting from.
+ /// The setting.
+ /// If the operation succeeded.
+ private static bool getSetting(NvSettingID settingId, IntPtr profileHandle, out NvSetting setting)
+ {
+ setting = new NvSetting
+ {
+ Version = NvSetting.Stride,
+ SettingID = settingId
+ };
+
+ if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting)))
+ return false;
+
+ return true;
+ }
+
+ private static bool setSetting(NvSettingID settingId, uint settingValue)
+ {
+ NvApplication application;
+ IntPtr profileHandle;
+ bool isApplicationSpecific;
+ if (!getProfile(out profileHandle, out application, out isApplicationSpecific))
+ return false;
+
+ if (!isApplicationSpecific)
+ {
+ // We don't want to interfere with the user's other settings, so let's create a separate config for osu!
+ if (!createProfile(out profileHandle))
+ return false;
+ }
+
+ NvSetting newSetting = new NvSetting
+ {
+ Version = NvSetting.Stride,
+ SettingID = settingId,
+ U32CurrentValue = settingValue
+ };
+
+ // Set the thread state
+ if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting)))
+ return false;
+
+ // Get the profile (needed to check app count)
+ NvProfile profile = new NvProfile
+ {
+ Version = NvProfile.Stride
+ };
+ if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile)))
+ return false;
+
+ if (!containsApplication(profileHandle, profile, out application))
+ {
+ // Need to add the current application to the profile
+ application.IsPredefined = 0;
+
+ application.AppName = osu_filename;
+ application.UserFriendlyName = APPLICATION_NAME;
+
+ if (checkError(CreateApplication(sessionHandle, profileHandle, ref application)))
+ return false;
+ }
+
+ // Save!
+ return !checkError(SaveSettings(sessionHandle));
+ }
+
+ ///
+ /// Creates a session to access the driver configuration.
+ ///
+ /// If the operation succeeded.
+ private static bool createSession()
+ {
+ if (checkError(CreateSession(out sessionHandle)))
+ return false;
+
+ // Load settings into session
+ if (checkError(LoadSettings(sessionHandle)))
+ return false;
+
+ return true;
+ }
+
+ private static bool checkError(NvStatus status)
+ {
+ Status = status;
+ return status != NvStatus.OK;
+ }
+
+ static NVAPI()
+ {
+ // TODO: check whether gpu vendor contains NVIDIA before attempting load?
+
+ try
+ {
+ // Try to load NVAPI
+ if ((IntPtr.Size == 4 && loadLibrary(@"nvapi.dll") == IntPtr.Zero)
+ || (IntPtr.Size == 8 && loadLibrary(@"nvapi64.dll") == IntPtr.Zero))
+ {
+ return;
+ }
+
+ InitializeDelegate initialize;
+ getDelegate(0x0150E828, out initialize);
+
+ if (initialize?.Invoke() == NvStatus.OK)
+ {
+ // IDs can be found here: https://github.com/jNizM/AHK_NVIDIA_NvAPI/blob/master/info/NvAPI_IDs.txt
+
+ getDelegate(0xE5AC921F, out EnumPhysicalGPUs);
+ getDelegate(0x48B3EA59, out EnumLogicalGPUs);
+ getDelegate(0xBAAABFCC, out GetSystemType);
+ getDelegate(0xC33BAEB1, out GetGPUType);
+ getDelegate(0x0694D52E, out CreateSession);
+ getDelegate(0x375DBD6B, out LoadSettings);
+ getDelegate(0xEEE566B2, out FindApplicationByName);
+ getDelegate(0x617BFF9F, out GetCurrentGlobalProfile);
+ getDelegate(0x577DD202, out SetSetting);
+ getDelegate(0x61CD6FD6, out GetProfileInfo);
+ getDelegate(0x73BF8338, out GetSetting);
+ getDelegate(0xCC176068, out CreateProfile);
+ getDelegate(0x7FA2173A, out EnumApplications);
+ getDelegate(0x4347A9DE, out CreateApplication);
+ getDelegate(0xFCBC7E14, out SaveSettings);
+ }
+
+ if (createSession())
+ Available = true;
+ }
+ catch { }
+ }
+
+ private static void getDelegate(uint id, out T newDelegate) where T : class
+ {
+ IntPtr ptr = IntPtr.Size == 4 ? queryInterface32(id) : queryInterface64(id);
+ newDelegate = ptr == IntPtr.Zero ? null : Marshal.GetDelegateForFunctionPointer(ptr, typeof(T)) as T;
+ }
+
+ [DllImport("kernel32.dll", EntryPoint = "LoadLibrary")]
+ private static extern IntPtr loadLibrary(string dllToLoad);
+
+ [DllImport(@"nvapi.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)]
+ private static extern IntPtr queryInterface32(uint id);
+
+ [DllImport(@"nvapi64.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)]
+ private static extern IntPtr queryInterface64(uint id);
+
+ private delegate NvStatus InitializeDelegate();
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct NvSetting
+ {
+ public uint Version;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
+ public string SettingName;
+
+ public NvSettingID SettingID;
+ public uint SettingType;
+ public uint SettingLocation;
+ public uint IsCurrentPredefined;
+ public uint IsPredefinedValid;
+
+ public uint U32PredefinedValue;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
+ public string StringPredefinedValue;
+
+ public uint U32CurrentValue;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
+ public string StringCurrentValue;
+
+ public static uint Stride => (uint)Marshal.SizeOf(typeof(NvSetting)) | (1 << 16);
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)]
+ internal struct NvProfile
+ {
+ public uint Version;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
+ public string ProfileName;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
+ public uint[] GPUSupport;
+
+ public uint IsPredefined;
+ public uint NumOfApps;
+ public uint NumOfSettings;
+
+ public static uint Stride => (uint)Marshal.SizeOf(typeof(NvProfile)) | (1 << 16);
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)]
+ internal struct NvApplication
+ {
+ public uint Version;
+ public uint IsPredefined;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
+ public string AppName;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
+ public string UserFriendlyName;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
+ public string Launcher;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
+ public string FileInFolder;
+
+ public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16);
+ }
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ internal enum NvStatus
+ {
+ OK = 0, // Success. Request is completed.
+ ERROR = -1, // Generic error
+ LIBRARY_NOT_FOUND = -2, // NVAPI support library cannot be loaded.
+ NO_IMPLEMENTATION = -3, // not implemented in current driver installation
+ API_NOT_INITIALIZED = -4, // Initialize has not been called (successfully)
+ INVALID_ARGUMENT = -5, // The argument/parameter value is not valid or NULL.
+ NVIDIA_DEVICE_NOT_FOUND = -6, // No NVIDIA display driver, or NVIDIA GPU driving a display, was found.
+ END_ENUMERATION = -7, // No more items to enumerate
+ INVALID_HANDLE = -8, // Invalid handle
+ INCOMPATIBLE_STRUCT_VERSION = -9, // An argument's structure version is not supported
+ HANDLE_INVALIDATED = -10, // The handle is no longer valid (likely due to GPU or display re-configuration)
+ OPENGL_CONTEXT_NOT_CURRENT = -11, // No NVIDIA OpenGL context is current (but needs to be)
+ INVALID_POINTER = -14, // An invalid pointer, usually NULL, was passed as a parameter
+ NO_GL_EXPERT = -12, // OpenGL Expert is not supported by the current drivers
+ INSTRUMENTATION_DISABLED = -13, // OpenGL Expert is supported, but driver instrumentation is currently disabled
+ NO_GL_NSIGHT = -15, // OpenGL does not support Nsight
+
+ EXPECTED_LOGICAL_GPU_HANDLE = -100, // Expected a logical GPU handle for one or more parameters
+ EXPECTED_PHYSICAL_GPU_HANDLE = -101, // Expected a physical GPU handle for one or more parameters
+ EXPECTED_DISPLAY_HANDLE = -102, // Expected an NV display handle for one or more parameters
+ INVALID_COMBINATION = -103, // The combination of parameters is not valid.
+ NOT_SUPPORTED = -104, // Requested feature is not supported in the selected GPU
+ PORTID_NOT_FOUND = -105, // No port ID was found for the I2C transaction
+ EXPECTED_UNATTACHED_DISPLAY_HANDLE = -106, // Expected an unattached display handle as one of the input parameters.
+ INVALID_PERF_LEVEL = -107, // Invalid perf level
+ DEVICE_BUSY = -108, // Device is busy; request not fulfilled
+ NV_PERSIST_FILE_NOT_FOUND = -109, // NV persist file is not found
+ PERSIST_DATA_NOT_FOUND = -110, // NV persist data is not found
+ EXPECTED_TV_DISPLAY = -111, // Expected a TV output display
+ EXPECTED_TV_DISPLAY_ON_DCONNECTOR = -112, // Expected a TV output on the D Connector - HDTV_EIAJ4120.
+ NO_ACTIVE_SLI_TOPOLOGY = -113, // SLI is not active on this device.
+ SLI_RENDERING_MODE_NOTALLOWED = -114, // Setup of SLI rendering mode is not possible right now.
+ EXPECTED_DIGITAL_FLAT_PANEL = -115, // Expected a digital flat panel.
+ ARGUMENT_EXCEED_MAX_SIZE = -116, // Argument exceeds the expected size.
+ DEVICE_SWITCHING_NOT_ALLOWED = -117, // Inhibit is ON due to one of the flags in NV_GPU_DISPLAY_CHANGE_INHIBIT or SLI active.
+ TESTING_CLOCKS_NOT_SUPPORTED = -118, // Testing of clocks is not supported.
+ UNKNOWN_UNDERSCAN_CONFIG = -119, // The specified underscan config is from an unknown source (e.g. INF)
+ TIMEOUT_RECONFIGURING_GPU_TOPO = -120, // Timeout while reconfiguring GPUs
+ DATA_NOT_FOUND = -121, // Requested data was not found
+ EXPECTED_ANALOG_DISPLAY = -122, // Expected an analog display
+ NO_VIDLINK = -123, // No SLI video bridge is present
+ REQUIRES_REBOOT = -124, // NVAPI requires a reboot for the settings to take effect
+ INVALID_HYBRID_MODE = -125, // The function is not supported with the current Hybrid mode.
+ MIXED_TARGET_TYPES = -126, // The target types are not all the same
+ SYSWOW64_NOT_SUPPORTED = -127, // The function is not supported from 32-bit on a 64-bit system.
+ IMPLICIT_SET_GPU_TOPOLOGY_CHANGE_NOT_ALLOWED = -128, // There is no implicit GPU topology active. Use SetHybridMode to change topology.
+ REQUEST_USER_TO_CLOSE_NON_MIGRATABLE_APPS = -129, // Prompt the user to close all non-migratable applications.
+ OUT_OF_MEMORY = -130, // Could not allocate sufficient memory to complete the call.
+ WAS_STILL_DRAWING = -131, // The previous operation that is transferring information to or from this surface is incomplete.
+ FILE_NOT_FOUND = -132, // The file was not found.
+ TOO_MANY_UNIQUE_STATE_OBJECTS = -133, // There are too many unique instances of a particular type of state object.
+ INVALID_CALL = -134, // The method call is invalid. For example, a method's parameter may not be a valid pointer.
+ D3D10_1_LIBRARY_NOT_FOUND = -135, // d3d10_1.dll cannot be loaded.
+ FUNCTION_NOT_FOUND = -136, // Couldn't find the function in the loaded DLL.
+ INVALID_USER_PRIVILEGE = -137, // Current User is not Admin.
+ EXPECTED_NON_PRIMARY_DISPLAY_HANDLE = -138, // The handle corresponds to GDIPrimary.
+ EXPECTED_COMPUTE_GPU_HANDLE = -139, // Setting Physx GPU requires that the GPU is compute-capable.
+ STEREO_NOT_INITIALIZED = -140, // The Stereo part of NVAPI failed to initialize completely. Check if the stereo driver is installed.
+ STEREO_REGISTRY_ACCESS_FAILED = -141, // Access to stereo-related registry keys or values has failed.
+ STEREO_REGISTRY_PROFILE_TYPE_NOT_SUPPORTED = -142, // The given registry profile type is not supported.
+ STEREO_REGISTRY_VALUE_NOT_SUPPORTED = -143, // The given registry value is not supported.
+ STEREO_NOT_ENABLED = -144, // Stereo is not enabled and the function needed it to execute completely.
+ STEREO_NOT_TURNED_ON = -145, // Stereo is not turned on and the function needed it to execute completely.
+ STEREO_INVALID_DEVICE_INTERFACE = -146, // Invalid device interface.
+ STEREO_PARAMETER_OUT_OF_RANGE = -147, // Separation percentage or JPEG image capture quality is out of [0-100] range.
+ STEREO_FRUSTUM_ADJUST_MODE_NOT_SUPPORTED = -148, // The given frustum adjust mode is not supported.
+ TOPO_NOT_POSSIBLE = -149, // The mosaic topology is not possible given the current state of the hardware.
+ MODE_CHANGE_FAILED = -150, // An attempt to do a display resolution mode change has failed.
+ D3D11_LIBRARY_NOT_FOUND = -151, // d3d11.dll/d3d11_beta.dll cannot be loaded.
+ INVALID_ADDRESS = -152, // Address is outside of valid range.
+ STRING_TOO_SMALL = -153, // The pre-allocated string is too small to hold the result.
+ MATCHING_DEVICE_NOT_FOUND = -154, // The input does not match any of the available devices.
+ DRIVER_RUNNING = -155, // Driver is running.
+ DRIVER_NOTRUNNING = -156, // Driver is not running.
+ ERROR_DRIVER_RELOAD_REQUIRED = -157, // A driver reload is required to apply these settings.
+ SET_NOT_ALLOWED = -158, // Intended setting is not allowed.
+ ADVANCED_DISPLAY_TOPOLOGY_REQUIRED = -159, // Information can't be returned due to "advanced display topology".
+ SETTING_NOT_FOUND = -160, // Setting is not found.
+ SETTING_SIZE_TOO_LARGE = -161, // Setting size is too large.
+ TOO_MANY_SETTINGS_IN_PROFILE = -162, // There are too many settings for a profile.
+ PROFILE_NOT_FOUND = -163, // Profile is not found.
+ PROFILE_NAME_IN_USE = -164, // Profile name is duplicated.
+ PROFILE_NAME_EMPTY = -165, // Profile name is empty.
+ EXECUTABLE_NOT_FOUND = -166, // Application not found in the Profile.
+ EXECUTABLE_ALREADY_IN_USE = -167, // Application already exists in the other profile.
+ DATATYPE_MISMATCH = -168, // Data Type mismatch
+ PROFILE_REMOVED = -169, // The profile passed as parameter has been removed and is no longer valid.
+ UNREGISTERED_RESOURCE = -170, // An unregistered resource was passed as a parameter.
+ ID_OUT_OF_RANGE = -171, // The DisplayId corresponds to a display which is not within the normal outputId range.
+ DISPLAYCONFIG_VALIDATION_FAILED = -172, // Display topology is not valid so the driver cannot do a mode set on this configuration.
+ DPMST_CHANGED = -173, // Display Port Multi-Stream topology has been changed.
+ INSUFFICIENT_BUFFER = -174, // Input buffer is insufficient to hold the contents.
+ ACCESS_DENIED = -175, // No access to the caller.
+ MOSAIC_NOT_ACTIVE = -176, // The requested action cannot be performed without Mosaic being enabled.
+ SHARE_RESOURCE_RELOCATED = -177, // The surface is relocated away from video memory.
+ REQUEST_USER_TO_DISABLE_DWM = -178, // The user should disable DWM before calling NvAPI.
+ D3D_DEVICE_LOST = -179, // D3D device status is D3DERR_DEVICELOST or D3DERR_DEVICENOTRESET - the user has to reset the device.
+ INVALID_CONFIGURATION = -180, // The requested action cannot be performed in the current state.
+ STEREO_HANDSHAKE_NOT_DONE = -181, // Call failed as stereo handshake not completed.
+ EXECUTABLE_PATH_IS_AMBIGUOUS = -182, // The path provided was too short to determine the correct NVDRS_APPLICATION
+ DEFAULT_STEREO_PROFILE_IS_NOT_DEFINED = -183, // Default stereo profile is not currently defined
+ DEFAULT_STEREO_PROFILE_DOES_NOT_EXIST = -184, // Default stereo profile does not exist
+ CLUSTER_ALREADY_EXISTS = -185, // A cluster is already defined with the given configuration.
+ DPMST_DISPLAY_ID_EXPECTED = -186, // The input display id is not that of a multi stream enabled connector or a display device in a multi stream topology
+ INVALID_DISPLAY_ID = -187, // The input display id is not valid or the monitor associated to it does not support the current operation
+ STREAM_IS_OUT_OF_SYNC = -188, // While playing secure audio stream, stream goes out of sync
+ INCOMPATIBLE_AUDIO_DRIVER = -189, // Older audio driver version than required
+ VALUE_ALREADY_SET = -190, // Value already set, setting again not allowed.
+ TIMEOUT = -191, // Requested operation timed out
+ GPU_WORKSTATION_FEATURE_INCOMPLETE = -192, // The requested workstation feature set has incomplete driver internal allocation resources
+ STEREO_INIT_ACTIVATION_NOT_DONE = -193, // Call failed because InitActivation was not called.
+ SYNC_NOT_ACTIVE = -194, // The requested action cannot be performed without Sync being enabled.
+ SYNC_MASTER_NOT_FOUND = -195, // The requested action cannot be performed without Sync Master being enabled.
+ INVALID_SYNC_TOPOLOGY = -196, // Invalid displays passed in the NV_GSYNC_DISPLAY pointer.
+ ECID_SIGN_ALGO_UNSUPPORTED = -197, // The specified signing algorithm is not supported. Either an incorrect value was entered or the current installed driver/hardware does not support the input value.
+ ECID_KEY_VERIFICATION_FAILED = -198, // The encrypted public key verification has failed.
+ FIRMWARE_OUT_OF_DATE = -199, // The device's firmware is out of date.
+ FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported.
+ }
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ internal enum NvSystemType
+ {
+ UNKNOWN = 0,
+ LAPTOP = 1,
+ DESKTOP = 2
+ }
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ internal enum NvGpuType
+ {
+ UNKNOWN = 0,
+ IGPU = 1, // Integrated
+ DGPU = 2, // Discrete
+ }
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ internal enum NvSettingID : uint
+ {
+ OGL_AA_LINE_GAMMA_ID = 0x2089BF6C,
+ OGL_DEEP_COLOR_SCANOUT_ID = 0x2097C2F6,
+ OGL_DEFAULT_SWAP_INTERVAL_ID = 0x206A6582,
+ OGL_DEFAULT_SWAP_INTERVAL_FRACTIONAL_ID = 0x206C4581,
+ OGL_DEFAULT_SWAP_INTERVAL_SIGN_ID = 0x20655CFA,
+ OGL_EVENT_LOG_SEVERITY_THRESHOLD_ID = 0x209DF23E,
+ OGL_EXTENSION_STRING_VERSION_ID = 0x20FF7493,
+ OGL_FORCE_BLIT_ID = 0x201F619F,
+ OGL_FORCE_STEREO_ID = 0x204D9A0C,
+ OGL_IMPLICIT_GPU_AFFINITY_ID = 0x20D0F3E6,
+ OGL_MAX_FRAMES_ALLOWED_ID = 0x208E55E3,
+ OGL_MULTIMON_ID = 0x200AEBFC,
+ OGL_OVERLAY_PIXEL_TYPE_ID = 0x209AE66F,
+ OGL_OVERLAY_SUPPORT_ID = 0x206C28C4,
+ OGL_QUALITY_ENHANCEMENTS_ID = 0x20797D6C,
+ OGL_SINGLE_BACKDEPTH_BUFFER_ID = 0x20A29055,
+ OGL_THREAD_CONTROL_ID = 0x20C1221E,
+ OGL_TRIPLE_BUFFER_ID = 0x20FDD1F9,
+ OGL_VIDEO_EDITING_MODE_ID = 0x20EE02B4,
+ AA_BEHAVIOR_FLAGS_ID = 0x10ECDB82,
+ AA_MODE_ALPHATOCOVERAGE_ID = 0x10FC2D9C,
+ AA_MODE_GAMMACORRECTION_ID = 0x107D639D,
+ AA_MODE_METHOD_ID = 0x10D773D2,
+ AA_MODE_REPLAY_ID = 0x10D48A85,
+ AA_MODE_SELECTOR_ID = 0x107EFC5B,
+ AA_MODE_SELECTOR_SLIAA_ID = 0x107AFC5B,
+ ANISO_MODE_LEVEL_ID = 0x101E61A9,
+ ANISO_MODE_SELECTOR_ID = 0x10D2BB16,
+ APPLICATION_PROFILE_NOTIFICATION_TIMEOUT_ID = 0x104554B6,
+ APPLICATION_STEAM_ID_ID = 0x107CDDBC,
+ CPL_HIDDEN_PROFILE_ID = 0x106D5CFF,
+ CUDA_EXCLUDED_GPUS_ID = 0x10354FF8,
+ D3DOGL_GPU_MAX_POWER_ID = 0x10D1EF29,
+ EXPORT_PERF_COUNTERS_ID = 0x108F0841,
+ FXAA_ALLOW_ID = 0x1034CB89,
+ FXAA_ENABLE_ID = 0x1074C972,
+ FXAA_INDICATOR_ENABLE_ID = 0x1068FB9C,
+ MCSFRSHOWSPLIT_ID = 0x10287051,
+ OPTIMUS_MAXAA_ID = 0x10F9DC83,
+ PHYSXINDICATOR_ID = 0x1094F16F,
+ PREFERRED_PSTATE_ID = 0x1057EB71,
+ PREVENT_UI_AF_OVERRIDE_ID = 0x103BCCB5,
+ PS_FRAMERATE_LIMITER_ID = 0x10834FEE,
+ PS_FRAMERATE_LIMITER_GPS_CTRL_ID = 0x10834F01,
+ SHIM_MAXRES_ID = 0x10F9DC82,
+ SHIM_MCCOMPAT_ID = 0x10F9DC80,
+ SHIM_RENDERING_MODE_ID = 0x10F9DC81,
+ SHIM_RENDERING_OPTIONS_ID = 0x10F9DC84,
+ SLI_GPU_COUNT_ID = 0x1033DCD1,
+ SLI_PREDEFINED_GPU_COUNT_ID = 0x1033DCD2,
+ SLI_PREDEFINED_GPU_COUNT_DX10_ID = 0x1033DCD3,
+ SLI_PREDEFINED_MODE_ID = 0x1033CEC1,
+ SLI_PREDEFINED_MODE_DX10_ID = 0x1033CEC2,
+ SLI_RENDERING_MODE_ID = 0x1033CED1,
+ VRRFEATUREINDICATOR_ID = 0x1094F157,
+ VRROVERLAYINDICATOR_ID = 0x1095F16F,
+ VRRREQUESTSTATE_ID = 0x1094F1F7,
+ VSYNCSMOOTHAFR_ID = 0x101AE763,
+ VSYNCVRRCONTROL_ID = 0x10A879CE,
+ VSYNC_BEHAVIOR_FLAGS_ID = 0x10FDEC23,
+ WKS_API_STEREO_EYES_EXCHANGE_ID = 0x11AE435C,
+ WKS_API_STEREO_MODE_ID = 0x11E91A61,
+ WKS_MEMORY_ALLOCATION_POLICY_ID = 0x11112233,
+ WKS_STEREO_DONGLE_SUPPORT_ID = 0x112493BD,
+ WKS_STEREO_SUPPORT_ID = 0x11AA9E99,
+ WKS_STEREO_SWAP_MODE_ID = 0x11333333,
+ AO_MODE_ID = 0x00667329,
+ AO_MODE_ACTIVE_ID = 0x00664339,
+ AUTO_LODBIASADJUST_ID = 0x00638E8F,
+ ICAFE_LOGO_CONFIG_ID = 0x00DB1337,
+ LODBIASADJUST_ID = 0x00738E8F,
+ PRERENDERLIMIT_ID = 0x007BA09E,
+ PS_DYNAMIC_TILING_ID = 0x00E5C6C0,
+ PS_SHADERDISKCACHE_ID = 0x00198FFF,
+ PS_TEXFILTER_ANISO_OPTS2_ID = 0x00E73211,
+ PS_TEXFILTER_BILINEAR_IN_ANISO_ID = 0x0084CD70,
+ PS_TEXFILTER_DISABLE_TRILIN_SLOPE_ID = 0x002ECAF2,
+ PS_TEXFILTER_NO_NEG_LODBIAS_ID = 0x0019BB68,
+ QUALITY_ENHANCEMENTS_ID = 0x00CE2691,
+ REFRESH_RATE_OVERRIDE_ID = 0x0064B541,
+ SET_POWER_THROTTLE_FOR_PCIe_COMPLIANCE_ID = 0x00AE785C,
+ SET_VAB_DATA_ID = 0x00AB8687,
+ VSYNCMODE_ID = 0x00A879CF,
+ VSYNCTEARCONTROL_ID = 0x005A375C,
+ TOTAL_DWORD_SETTING_NUM = 80,
+ TOTAL_WSTRING_SETTING_NUM = 4,
+ TOTAL_SETTING_NUM = 84,
+ INVALID_SETTING_ID = 0xFFFFFFFF
+ }
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ internal enum NvShimSetting : uint
+ {
+ SHIM_RENDERING_MODE_INTEGRATED = 0x00000000,
+ SHIM_RENDERING_MODE_ENABLE = 0x00000001,
+ SHIM_RENDERING_MODE_USER_EDITABLE = 0x00000002,
+ SHIM_RENDERING_MODE_MASK = 0x00000003,
+ SHIM_RENDERING_MODE_VIDEO_MASK = 0x00000004,
+ SHIM_RENDERING_MODE_VARYING_BIT = 0x00000008,
+ SHIM_RENDERING_MODE_AUTO_SELECT = 0x00000010,
+ SHIM_RENDERING_MODE_OVERRIDE_BIT = 0x80000000,
+ SHIM_RENDERING_MODE_NUM_VALUES = 8,
+ SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT
+ }
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ internal enum NvThreadControlSetting : uint
+ {
+ OGL_THREAD_CONTROL_ENABLE = 0x00000001,
+ OGL_THREAD_CONTROL_DISABLE = 0x00000002,
+ OGL_THREAD_CONTROL_NUM_VALUES = 2,
+ OGL_THREAD_CONTROL_DEFAULT = 0
+ }
+}
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index a0db896f46..3e06dad4c5 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
using Microsoft.Win32;
+using osu.Desktop.Performance;
using osu.Desktop.Security;
using osu.Framework.Platform;
using osu.Game;
@@ -15,11 +16,12 @@ using osu.Framework;
using osu.Framework.Logging;
using osu.Game.Updater;
using osu.Desktop.Windows;
+using osu.Framework.Allocation;
using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Online.Multiplayer;
+using osu.Game.Performance;
using osu.Game.Utils;
-using SDL2;
namespace osu.Desktop
{
@@ -28,6 +30,9 @@ namespace osu.Desktop
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
private ArchiveImportIPCChannel? archiveImportIPCChannel;
+ [Cached(typeof(IHighPerformanceSessionManager))]
+ private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
+
public OsuGameDesktop(string[]? args = null)
: base(args)
{
@@ -86,8 +91,8 @@ namespace osu.Desktop
[SupportedOSPlatform("windows")]
private string? getStableInstallPathFromRegistry()
{
- using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
- return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
+ using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
+ return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
}
protected override UpdateManager CreateUpdateManager()
@@ -155,7 +160,7 @@ namespace osu.Desktop
host.Window.Title = Name;
}
- protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
+ protected override BatteryInfo CreateBatteryInfo() => FrameworkEnvironment.UseSDL3 ? new SDL3BatteryInfo() : new SDL2BatteryInfo();
protected override void Dispose(bool isDisposing)
{
@@ -163,23 +168,5 @@ namespace osu.Desktop
osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.Dispose();
}
-
- private class SDL2BatteryInfo : BatteryInfo
- {
- public override double? ChargeLevel
- {
- get
- {
- SDL.SDL_GetPowerInfo(out _, out int percentage);
-
- if (percentage == -1)
- return null;
-
- return percentage / 100.0;
- }
- }
-
- public override bool OnBattery => SDL.SDL_GetPowerInfo(out _, out _) == SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
- }
}
}
diff --git a/osu.Desktop/Performance/HighPerformanceSessionManager.cs b/osu.Desktop/Performance/HighPerformanceSessionManager.cs
new file mode 100644
index 0000000000..0df87ab007
--- /dev/null
+++ b/osu.Desktop/Performance/HighPerformanceSessionManager.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 System;
+using System.Runtime;
+using System.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Logging;
+using osu.Game.Performance;
+
+namespace osu.Desktop.Performance
+{
+ public class HighPerformanceSessionManager : IHighPerformanceSessionManager
+ {
+ public bool IsSessionActive => activeSessions > 0;
+
+ private int activeSessions;
+
+ private GCLatencyMode originalGCMode;
+
+ public IDisposable BeginSession()
+ {
+ enterSession();
+ return new InvokeOnDisposal(this, static m => m.exitSession());
+ }
+
+ private void enterSession()
+ {
+ if (Interlocked.Increment(ref activeSessions) > 1)
+ {
+ Logger.Log($"High performance session requested ({activeSessions} running in total)");
+ return;
+ }
+
+ Logger.Log("Starting high performance session");
+
+ originalGCMode = GCSettings.LatencyMode;
+ GCSettings.LatencyMode = GCLatencyMode.LowLatency;
+
+ // Without doing this, the new GC mode won't kick in until the next GC, which could be at a more noticeable point in time.
+ GC.Collect(0);
+ }
+
+ private void exitSession()
+ {
+ if (Interlocked.Decrement(ref activeSessions) > 0)
+ {
+ Logger.Log($"High performance session finished ({activeSessions} others remain)");
+ return;
+ }
+
+ Logger.Log("Ending high performance session");
+
+ if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
+ GCSettings.LatencyMode = originalGCMode;
+
+ // No GC.Collect() as we were already collecting at a higher frequency in the old mode.
+ }
+ }
+}
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index a33e845f5b..23e56cdce9 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Runtime.Versioning;
using osu.Desktop.LegacyIpc;
+using osu.Desktop.Windows;
using osu.Framework;
using osu.Framework.Development;
using osu.Framework.Logging;
@@ -12,7 +13,7 @@ using osu.Framework.Platform;
using osu.Game;
using osu.Game.IPC;
using osu.Game.Tournament;
-using SDL2;
+using SDL;
using Squirrel;
namespace osu.Desktop
@@ -30,30 +31,51 @@ namespace osu.Desktop
[STAThread]
public static void Main(string[] args)
{
- // run Squirrel first, as the app may exit after these run
+ /*
+ * WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
+ *
+ * Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
+ * To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
+ * namely by checking loaded assemblies:
+ * https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
+ *
+ * If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
+ * the app will then do completely broken things like:
+ * - not creating system shortcuts (as the logic is if'd out if "running tests")
+ * - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
+ */
if (OperatingSystem.IsWindows())
{
var windowsVersion = Environment.OSVersion.Version;
- // While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
- // See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
+ // While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher.
+ // See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
{
- // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
- // disabling it ourselves.
- // We could also better detect compatibility mode if required:
- // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
- SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
- "Your operating system is too old to run osu!",
- "This version of osu! requires at least Windows 8.1 to run.\n"
- + "Please upgrade your operating system or consider using an older version of osu!.\n\n"
- + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero);
- return;
+ unsafe
+ {
+ // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
+ // disabling it ourselves.
+ // We could also better detect compatibility mode if required:
+ // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
+ SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
+ "Your operating system is too old to run osu!"u8,
+ "This version of osu! requires at least Windows 8.1 to run.\n"u8
+ + "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8
+ + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null);
+ return;
+ }
}
setupSquirrel();
}
+ // NVIDIA profiles are based on the executable name of a process.
+ // Lazer and stable share the same executable name.
+ // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
+ if (OperatingSystem.IsWindows())
+ NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
+
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
@@ -85,7 +107,13 @@ namespace osu.Desktop
}
}
- using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = !tournamentClient }))
+ var hostOptions = new HostOptions
+ {
+ IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null,
+ FriendlyGameName = OsuGameBase.GAME_NAME,
+ };
+
+ using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, hostOptions))
{
if (!host.IsPrimaryInstance)
{
@@ -156,13 +184,16 @@ namespace osu.Desktop
{
tools.CreateShortcutForThisExe();
tools.CreateUninstallerRegistryEntry();
+ WindowsAssociationManager.InstallAssociations();
}, onAppUpdate: (_, tools) =>
{
tools.CreateUninstallerRegistryEntry();
+ WindowsAssociationManager.UpdateAssociations();
}, onAppUninstall: (_, tools) =>
{
tools.RemoveShortcutForThisExe();
tools.RemoveUninstallerRegistryEntry();
+ WindowsAssociationManager.UninstallAssociations();
}, onEveryRun: (_, _, _) =>
{
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
diff --git a/osu.Desktop/SDL2BatteryInfo.cs b/osu.Desktop/SDL2BatteryInfo.cs
new file mode 100644
index 0000000000..9ca2dc3a5c
--- /dev/null
+++ b/osu.Desktop/SDL2BatteryInfo.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Utils;
+
+namespace osu.Desktop
+{
+ internal class SDL2BatteryInfo : BatteryInfo
+ {
+ public override double? ChargeLevel
+ {
+ get
+ {
+ SDL2.SDL.SDL_GetPowerInfo(out _, out int percentage);
+
+ if (percentage == -1)
+ return null;
+
+ return percentage / 100.0;
+ }
+ }
+
+ public override bool OnBattery => SDL2.SDL.SDL_GetPowerInfo(out _, out _) == SDL2.SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
+ }
+}
diff --git a/osu.Desktop/SDL3BatteryInfo.cs b/osu.Desktop/SDL3BatteryInfo.cs
new file mode 100644
index 0000000000..89084b5a15
--- /dev/null
+++ b/osu.Desktop/SDL3BatteryInfo.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Utils;
+using SDL;
+
+namespace osu.Desktop
+{
+ internal unsafe class SDL3BatteryInfo : BatteryInfo
+ {
+ public override double? ChargeLevel
+ {
+ get
+ {
+ int percentage;
+ SDL3.SDL_GetPowerInfo(null, &percentage);
+
+ if (percentage == -1)
+ return null;
+
+ return percentage / 100.0;
+ }
+ }
+
+ public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
+ }
+}
diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
index 6665733656..0bed9830df 100644
--- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
+++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Security.Principal;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -21,48 +20,14 @@ namespace osu.Desktop.Security
[Resolved]
private INotificationOverlay notifications { get; set; } = null!;
- private bool elevated;
-
- [BackgroundDependencyLoader]
- private void load()
- {
- elevated = checkElevated();
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
- if (elevated)
+ if (Environment.IsPrivilegedProcess)
notifications.Post(new ElevatedPrivilegesNotification());
}
- private bool checkElevated()
- {
- try
- {
- switch (RuntimeInfo.OS)
- {
- case RuntimeInfo.Platform.Windows:
- if (!OperatingSystem.IsWindows()) return false;
-
- var windowsIdentity = WindowsIdentity.GetCurrent();
- var windowsPrincipal = new WindowsPrincipal(windowsIdentity);
-
- return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
-
- case RuntimeInfo.Platform.macOS:
- case RuntimeInfo.Platform.Linux:
- return Mono.Unix.Native.Syscall.geteuid() == 0;
- }
- }
- catch
- {
- }
-
- return false;
- }
-
private partial class ElevatedPrivilegesNotification : SimpleNotification
{
public override bool IsImportant => true;
diff --git a/osu.Desktop/Windows/Icons.cs b/osu.Desktop/Windows/Icons.cs
new file mode 100644
index 0000000000..67915c101a
--- /dev/null
+++ b/osu.Desktop/Windows/Icons.cs
@@ -0,0 +1,17 @@
+// 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;
+
+namespace osu.Desktop.Windows
+{
+ public static class Icons
+ {
+ ///
+ /// Fully qualified path to the directory that contains icons (in the installation folder).
+ ///
+ private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
+
+ public static string Lazer => Path.Join(icon_directory, "lazer.ico");
+ }
+}
diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs
new file mode 100644
index 0000000000..b32c01433d
--- /dev/null
+++ b/osu.Desktop/Windows/WindowsAssociationManager.cs
@@ -0,0 +1,295 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using Microsoft.Win32;
+using osu.Framework.Localisation;
+using osu.Framework.Logging;
+using osu.Game.Localisation;
+
+namespace osu.Desktop.Windows
+{
+ [SupportedOSPlatform("windows")]
+ public static class WindowsAssociationManager
+ {
+ private const string software_classes = @"Software\Classes";
+
+ ///
+ /// Sub key for setting the icon.
+ /// https://learn.microsoft.com/en-us/windows/win32/com/defaulticon
+ ///
+ private const string default_icon = @"DefaultIcon";
+
+ ///
+ /// Sub key for setting the command line that the shell invokes.
+ /// https://learn.microsoft.com/en-us/windows/win32/com/shell
+ ///
+ internal const string SHELL_OPEN_COMMAND = @"Shell\Open\Command";
+
+ private static readonly string exe_path = Path.ChangeExtension(typeof(WindowsAssociationManager).Assembly.Location, ".exe").Replace('/', '\\');
+
+ ///
+ /// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
+ /// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
+ ///
+ private const string program_id_prefix = "osu.File";
+
+ private static readonly FileAssociation[] file_associations =
+ {
+ new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
+ new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
+ new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
+ new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
+ };
+
+ private static readonly UriAssociation[] uri_associations =
+ {
+ new UriAssociation(@"osu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
+ new UriAssociation(@"osump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
+ };
+
+ ///
+ /// Installs file and URI associations.
+ ///
+ ///
+ /// Call in a timely fashion to keep descriptions up-to-date and localised.
+ ///
+ public static void InstallAssociations()
+ {
+ try
+ {
+ updateAssociations();
+ updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
+ NotifyShellUpdate();
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, @$"Failed to install file and URI associations: {e.Message}");
+ }
+ }
+
+ ///
+ /// Updates associations with latest definitions.
+ ///
+ ///
+ /// Call in a timely fashion to keep descriptions up-to-date and localised.
+ ///
+ public static void UpdateAssociations()
+ {
+ try
+ {
+ updateAssociations();
+
+ // TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
+ updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
+
+ NotifyShellUpdate();
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, @"Failed to update file and URI associations.");
+ }
+ }
+
+ public static void UpdateDescriptions(LocalisationManager localisationManager)
+ {
+ try
+ {
+ updateDescriptions(localisationManager);
+ NotifyShellUpdate();
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, @"Failed to update file and URI association descriptions.");
+ }
+ }
+
+ public static void UninstallAssociations()
+ {
+ try
+ {
+ foreach (var association in file_associations)
+ association.Uninstall();
+
+ foreach (var association in uri_associations)
+ association.Uninstall();
+
+ NotifyShellUpdate();
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, @"Failed to uninstall file and URI associations.");
+ }
+ }
+
+ public static void NotifyShellUpdate() => SHChangeNotify(EventId.SHCNE_ASSOCCHANGED, Flags.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
+
+ ///
+ /// Installs or updates associations.
+ ///
+ private static void updateAssociations()
+ {
+ foreach (var association in file_associations)
+ association.Install();
+
+ foreach (var association in uri_associations)
+ association.Install();
+ }
+
+ private static void updateDescriptions(LocalisationManager? localisation)
+ {
+ foreach (var association in file_associations)
+ association.UpdateDescription(getLocalisedString(association.Description));
+
+ foreach (var association in uri_associations)
+ association.UpdateDescription(getLocalisedString(association.Description));
+
+ string getLocalisedString(LocalisableString s)
+ {
+ if (localisation == null)
+ return s.ToString();
+
+ var b = localisation.GetLocalisedBindableString(s);
+ b.UnbindAll();
+ return b.Value;
+ }
+ }
+
+ #region Native interop
+
+ [DllImport("Shell32.dll")]
+ private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private enum EventId
+ {
+ ///
+ /// A file type association has changed. must be specified in the uFlags parameter.
+ /// dwItem1 and dwItem2 are not used and must be . This event should also be sent for registered protocols.
+ ///
+ SHCNE_ASSOCCHANGED = 0x08000000
+ }
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private enum Flags : uint
+ {
+ SHCNF_IDLIST = 0x0000
+ }
+
+ #endregion
+
+ private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
+ {
+ private string programId => $@"{program_id_prefix}{Extension}";
+
+ ///
+ /// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
+ ///
+ public void Install()
+ {
+ using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
+ if (classes == null) return;
+
+ // register a program id for the given extension
+ using (var programKey = classes.CreateSubKey(programId))
+ {
+ using (var defaultIconKey = programKey.CreateSubKey(default_icon))
+ defaultIconKey.SetValue(null, IconPath);
+
+ using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
+ openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
+ }
+
+ using (var extensionKey = classes.CreateSubKey(Extension))
+ {
+ // set ourselves as the default program
+ extensionKey.SetValue(null, programId);
+
+ // add to the open with dialog
+ // https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
+ using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
+ openWithKey.SetValue(programId, string.Empty);
+ }
+ }
+
+ public void UpdateDescription(string description)
+ {
+ using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
+ if (classes == null) return;
+
+ using (var programKey = classes.OpenSubKey(programId, true))
+ programKey?.SetValue(null, description);
+ }
+
+ ///
+ /// Uninstalls the file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation
+ ///
+ public void Uninstall()
+ {
+ using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
+ if (classes == null) return;
+
+ using (var extensionKey = classes.OpenSubKey(Extension, true))
+ {
+ // clear our default association so that Explorer doesn't show the raw programId to users
+ // the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
+ if (extensionKey?.GetValue(null) is string s && s == programId)
+ extensionKey.SetValue(null, string.Empty);
+
+ using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
+ openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
+ }
+
+ classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
+ }
+ }
+
+ private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
+ {
+ ///
+ /// "The URL Protocol string value indicates that this key declares a custom pluggable protocol handler."
+ /// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
+ ///
+ public const string URL_PROTOCOL = @"URL Protocol";
+
+ ///
+ /// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
+ ///
+ public void Install()
+ {
+ using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
+ if (classes == null) return;
+
+ using (var protocolKey = classes.CreateSubKey(Protocol))
+ {
+ protocolKey.SetValue(URL_PROTOCOL, string.Empty);
+
+ using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
+ defaultIconKey.SetValue(null, IconPath);
+
+ using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
+ openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
+ }
+ }
+
+ public void UpdateDescription(string description)
+ {
+ using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
+ if (classes == null) return;
+
+ using (var protocolKey = classes.OpenSubKey(Protocol, true))
+ protocolKey?.SetValue(null, $@"URL:{description}");
+ }
+
+ public void Uninstall()
+ {
+ using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
+ classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
+ }
+ }
+ }
+}
diff --git a/osu.Desktop/lazer.ico b/osu.Desktop/lazer.ico
old mode 100755
new mode 100644
index a6aa8abb9f..d5dbf933c1
Binary files a/osu.Desktop/lazer.ico and b/osu.Desktop/lazer.ico differ
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index f37cfdc5f1..acb53835a3 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -1,6 +1,6 @@
- net6.0
+ net8.0
WinExe
true
A free-to-win rhythm game. Rhythm is just a *click* away!
@@ -23,12 +23,14 @@
-
-
-
+
+
+
+
+
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index db58c325bd..66b3970351 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -7,11 +7,12 @@
ppy Pty Ltd
Dean Herbert
https://osu.ppy.sh/
- https://puu.sh/tYyXZ/9a01a5d1b0.ico
+ https://github.com/ppy/osu/blob/master/assets/lazer-nuget.png?raw=true
+ icon.png
false
A free-to-win rhythm game. Rhythm is just a *click* away!
testing
- Copyright (c) 2022 ppy Pty Ltd
+ Copyright (c) 2024 ppy Pty Ltd
en-AU
@@ -19,5 +20,7 @@
+
+
diff --git a/osu.Game.Benchmarks/BenchmarkStringComparison.cs b/osu.Game.Benchmarks/BenchmarkStringComparison.cs
new file mode 100644
index 0000000000..d40b92db5f
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkStringComparison.cs
@@ -0,0 +1,48 @@
+// 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 BenchmarkDotNet.Attributes;
+using osu.Game.Utils;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkStringComparison
+ {
+ private string[] strings = null!;
+
+ [GlobalSetup]
+ public void GlobalSetUp()
+ {
+ strings = new string[10000];
+
+ for (int i = 0; i < strings.Length; ++i)
+ strings[i] = Guid.NewGuid().ToString();
+
+ for (int i = 0; i < strings.Length; ++i)
+ {
+ if (i % 2 == 0)
+ strings[i] = strings[i].ToUpperInvariant();
+ }
+ }
+
+ [Benchmark]
+ public void OrdinalIgnoreCase() => compare(StringComparer.OrdinalIgnoreCase);
+
+ [Benchmark]
+ public void OrdinalSortByCase() => compare(OrdinalSortByCaseStringComparer.DEFAULT);
+
+ [Benchmark]
+ public void InvariantCulture() => compare(StringComparer.InvariantCulture);
+
+ private void compare(IComparer comparer)
+ {
+ for (int i = 0; i < strings.Length; ++i)
+ {
+ for (int j = i + 1; j < strings.Length; ++j)
+ _ = comparer.Compare(strings[i], strings[j]);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs
new file mode 100644
index 0000000000..aa229c7d06
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs
@@ -0,0 +1,31 @@
+// 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 BenchmarkDotNet.Attributes;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkUnstableRate : BenchmarkTest
+ {
+ private List events = null!;
+
+ public override void SetUp()
+ {
+ base.SetUp();
+ events = new List();
+
+ for (int i = 0; i < 1000; i++)
+ events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null));
+ }
+
+ [Benchmark]
+ public void CalculateUnstableRate()
+ {
+ _ = events.CalculateUnstableRate();
+ }
+ }
+}
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 5de21a68d0..af84ee47f1 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -1,14 +1,14 @@
- net6.0
+ net8.0
Exe
false
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
index 52b34959b9..b6ab91ed5c 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
index 4ee3219442..4b2e54be67 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
@@ -1,7 +1,7 @@
- net6.0-android
+ net8.0-android
Exe
osu.Game.Rulesets.Catch.Tests
osu.Game.Rulesets.Catch.Tests.Android
@@ -21,4 +21,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
index acf12bb0ac..9c262a752a 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
@@ -1,7 +1,7 @@
Exe
- net6.0-ios
+ net8.0-ios
13.4
osu.Game.Rulesets.Catch.Tests
osu.Game.Rulesets.Catch.Tests.iOS
diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
index 201343a036..7b9291c870 100644
--- a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
+++ b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Catch.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Catch.Tests.dll"
+ "${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index d0ecb828df..f4c36d5188 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -53,6 +53,8 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
[TestCase("112643")]
+ [TestCase("1041052", new[] { typeof(CatchModHardRock) })]
+ [TestCase("high-speed-multiplier-precision")]
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
new file mode 100644
index 0000000000..1b46be01fb
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
@@ -0,0 +1,59 @@
+// 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.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.Judgements;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Scoring;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ [TestFixture]
+ public class CatchHealthProcessorTest
+ {
+ private static readonly object[][] test_cases =
+ [
+ // hitobject, starting HP, fail expected after miss
+ [new Fruit(), 0.01, true],
+ [new Droplet(), 0.01, true],
+ [new TinyDroplet(), 0, false],
+ [new Banana(), 0, false],
+ [new BananaShower(), 0, false]
+ ];
+
+ [TestCaseSource(nameof(test_cases))]
+ public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected)
+ {
+ var healthProcessor = new CatchHealthProcessor(0);
+ healthProcessor.ApplyBeatmap(new CatchBeatmap
+ {
+ HitObjects = { hitObject }
+ });
+ healthProcessor.Health.Value = startingHealth;
+
+ var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
+ result.Type = result.Judgement.MinResult;
+ healthProcessor.ApplyResult(result);
+
+ Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
+ }
+
+ [TestCaseSource(nameof(test_cases))]
+ public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _)
+ {
+ var healthProcessor = new CatchHealthProcessor(0);
+ healthProcessor.ApplyBeatmap(new CatchBeatmap
+ {
+ HitObjects = { hitObject }
+ });
+ healthProcessor.Health.Value = startingHealth;
+
+ var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
+ result.Type = result.Judgement.MaxResult;
+ healthProcessor.ApplyResult(result);
+
+ Assert.That(healthProcessor.HasFailed, Is.False);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs
new file mode 100644
index 0000000000..33aa4cba5d
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs
@@ -0,0 +1,158 @@
+// 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.Catch.Edit.Checks;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks
+{
+ [TestFixture]
+ public class CheckCatchAbnormalDifficultySettingsTest
+ {
+ private CheckCatchAbnormalDifficultySettings check = null!;
+
+ private readonly IBeatmap beatmap = new Beatmap();
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckCatchAbnormalDifficultySettings();
+
+ beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo;
+ beatmap.Difficulty = new BeatmapDifficulty
+ {
+ ApproachRate = 5,
+ CircleSize = 5,
+ DrainRate = 5,
+ };
+ }
+
+ [Test]
+ public void TestNormalSettings()
+ {
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestApproachRateTwoDecimals()
+ {
+ beatmap.Difficulty.ApproachRate = 5.55f;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
+ }
+
+ [Test]
+ public void TestCircleSizeTwoDecimals()
+ {
+ beatmap.Difficulty.CircleSize = 5.55f;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
+ }
+
+ [Test]
+ public void TestDrainRateTwoDecimals()
+ {
+ beatmap.Difficulty.DrainRate = 5.55f;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
+ }
+
+ [Test]
+ public void TestApproachRateUnder()
+ {
+ beatmap.Difficulty.ApproachRate = -10;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestCircleSizeUnder()
+ {
+ beatmap.Difficulty.CircleSize = -10;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestDrainRateUnder()
+ {
+ beatmap.Difficulty.DrainRate = -10;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestApproachRateOver()
+ {
+ beatmap.Difficulty.ApproachRate = 20;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestCircleSizeOver()
+ {
+ beatmap.Difficulty.CircleSize = 20;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestDrainRateOver()
+ {
+ beatmap.Difficulty.DrainRate = 20;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ private BeatmapVerifierContext getContext()
+ {
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs
new file mode 100644
index 0000000000..53ef24e02c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs
@@ -0,0 +1,66 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public partial class TestSceneCatchEditorSaving : EditorSavingTestScene
+ {
+ protected override Ruleset CreateRuleset() => new CatchRuleset();
+
+ [Test]
+ public void TestCatchJuiceStreamTickCorrect()
+ {
+ AddStep("enter timing mode", () => InputManager.Key(Key.F3));
+ AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
+ AddStep("enter compose mode", () => InputManager.Key(Key.F1));
+
+ Vector2 startPoint = Vector2.Zero;
+ float increment = 0;
+
+ AddUntilStep("wait for playfield", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
+ AddStep("move to centre", () =>
+ {
+ var playfield = this.ChildrenOfType().Single();
+ startPoint = playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Height / 3);
+ increment = playfield.ScreenSpaceDrawQuad.Height / 10;
+ InputManager.MoveMouseTo(startPoint);
+ });
+ AddStep("choose juice stream placing tool", () => InputManager.Key(Key.Number3));
+ AddStep("start placement", () => InputManager.Click(MouseButton.Left));
+
+ AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(2 * increment, -increment)));
+ AddStep("add node", () => InputManager.Click(MouseButton.Left));
+
+ AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(-2 * increment, -2 * increment)));
+ AddStep("add node", () => InputManager.Click(MouseButton.Left));
+
+ AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(0, -3 * increment)));
+ AddStep("end placement", () => InputManager.Click(MouseButton.Right));
+
+ AddUntilStep("juice stream placed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(1));
+
+ int largeDropletCount = 0, tinyDropletCount = 0;
+ AddStep("store droplet count", () =>
+ {
+ largeDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet));
+ tinyDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet));
+ });
+
+ SaveEditor();
+ ReloadEditorToSameBeatmap();
+
+ AddAssert("large droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet)), () => Is.EqualTo(largeDropletCount));
+ AddAssert("tiny droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet)), () => Is.EqualTo(tinyDropletCount));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs
new file mode 100644
index 0000000000..36a0e3388e
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs
@@ -0,0 +1,253 @@
+// 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;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ [TestFixture]
+ public partial class TestSceneCatchReverseSelection : TestSceneEditor
+ {
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ [Test]
+ public void TestReverseSelectionTwoFruits()
+ {
+ CatchHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ addObjects([
+ new Fruit
+ {
+ StartTime = 200,
+ X = 0,
+ },
+ new Fruit
+ {
+ StartTime = 400,
+ X = 20,
+ }
+ ]);
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ selectEverything();
+ reverseSelection();
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+ }
+
+ [Test]
+ public void TestReverseSelectionThreeFruits()
+ {
+ CatchHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ addObjects([
+ new Fruit
+ {
+ StartTime = 200,
+ X = 0,
+ },
+ new Fruit
+ {
+ StartTime = 400,
+ X = 20,
+ },
+ new Fruit
+ {
+ StartTime = 600,
+ X = 40,
+ }
+ ]);
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ selectEverything();
+ reverseSelection();
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+ }
+
+ [Test]
+ public void TestReverseSelectionFruitAndJuiceStream()
+ {
+ CatchHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ addObjects([
+ new Fruit
+ {
+ StartTime = 200,
+ X = 0,
+ },
+ new JuiceStream
+ {
+ StartTime = 400,
+ X = 20,
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(50))
+ }
+ }
+ }
+ ]);
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ selectEverything();
+ reverseSelection();
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+ }
+
+ [Test]
+ public void TestReverseSelectionTwoFruitsAndJuiceStream()
+ {
+ CatchHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ addObjects([
+ new Fruit
+ {
+ StartTime = 200,
+ X = 0,
+ },
+ new Fruit
+ {
+ StartTime = 400,
+ X = 20,
+ },
+ new JuiceStream
+ {
+ StartTime = 600,
+ X = 40,
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(50))
+ }
+ }
+ }
+ ]);
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ selectEverything();
+ reverseSelection();
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+ }
+
+ [Test]
+ public void TestReverseSelectionTwoCombos()
+ {
+ CatchHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ addObjects([
+ new Fruit
+ {
+ StartTime = 200,
+ X = 0,
+ },
+ new Fruit
+ {
+ StartTime = 400,
+ X = 20,
+ },
+ new Fruit
+ {
+ StartTime = 600,
+ X = 40,
+ },
+
+ new Fruit
+ {
+ StartTime = 800,
+ NewCombo = true,
+ X = 60,
+ },
+ new Fruit
+ {
+ StartTime = 1000,
+ X = 80,
+ },
+ new Fruit
+ {
+ StartTime = 1200,
+ X = 100,
+ }
+ ]);
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ selectEverything();
+ reverseSelection();
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+ }
+
+ private void addObjects(CatchHitObject[] hitObjects) => AddStep("Add objects", () => EditorBeatmap.AddRange(hitObjects));
+
+ private IEnumerable getObjects() => EditorBeatmap.HitObjects.OfType();
+
+ private IEnumerable getObjectNewCombos() => getObjects().Select(ho => ho.NewCombo);
+
+ private void selectEverything()
+ {
+ AddStep("Select everything", () =>
+ {
+ EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
+ });
+ }
+
+ private void reverseSelection()
+ {
+ AddStep("Reverse selection", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.Key(Key.G);
+ InputManager.ReleaseKey(Key.LControl);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
index c96f32d87c..7b665b1ff9 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
@@ -82,6 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMouseMoveStep(-100, 100);
addVertexCheckStep(3, 1, times[0], positions[0]);
+ addDragEndStep();
}
[Test]
@@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
+
+ AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
+ addDragEndStep();
}
[Test]
@@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400);
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
+ addDragEndStep();
}
[Test]
@@ -129,6 +134,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
+ addDragEndStep();
}
[Test]
@@ -161,18 +167,18 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150);
- addAddVertexSteps(90, 200);
- addVertexCheckStep(4, 1, times[0], positions[0]);
+ addAddVertexSteps(160, 200);
+ addVertexCheckStep(4, 1, 160, 200);
addAddVertexSteps(750, 180);
- addVertexCheckStep(5, 4, 750, 180);
+ addVertexCheckStep(5, 4, 800, 160);
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
}
[Test]
public void TestDeleteVertex()
{
- double[] times = { 100, 300, 500 };
+ double[] times = { 100, 300, 400 };
float[] positions = { 100, 200, 150 };
addBlueprintStep(times, positions);
@@ -265,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("delete vertex", () =>
{
InputManager.PressKey(Key.ShiftLeft);
- InputManager.Click(MouseButton.Left);
+ InputManager.Click(MouseButton.Right);
InputManager.ReleaseKey(Key.ShiftLeft);
});
}
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
index c48bf7adc9..c8f7da1aae 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
@@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
StartTime = 5000,
}
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(2000, 4000),
}
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052-expected-conversion.json
new file mode 100644
index 0000000000..01150e701d
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052-expected-conversion.json
@@ -0,0 +1 @@
+{"Mappings":[{"StartTime":1155.0,"Objects":[{"StartTime":1155.0,"Position":208.0,"HyperDash":false},{"StartTime":1251.0,"Position":167.675858,"HyperDash":false},{"StartTime":1348.0,"Position":157.087921,"HyperDash":false},{"StartTime":1445.0,"Position":128.5,"HyperDash":false},{"StartTime":1542.0,"Position":105.912064,"HyperDash":false},{"StartTime":1620.0,"Position":102.336205,"HyperDash":false},{"StartTime":1735.0,"Position":55.0,"HyperDash":false}]},{"StartTime":2122.0,"Objects":[{"StartTime":2122.0,"Position":275.0,"HyperDash":false}]},{"StartTime":2509.0,"Objects":[{"StartTime":2509.0,"Position":269.0,"HyperDash":false}]},{"StartTime":3284.0,"Objects":[{"StartTime":3284.0,"Position":448.0,"HyperDash":false},{"StartTime":3380.0,"Position":466.562317,"HyperDash":false},{"StartTime":3477.0,"Position":477.575867,"HyperDash":false},{"StartTime":3556.0,"Position":467.440033,"HyperDash":false},{"StartTime":3671.0,"Position":481.146881,"HyperDash":false}]},{"StartTime":4058.0,"Objects":[{"StartTime":4058.0,"Position":288.0,"HyperDash":false}]},{"StartTime":5025.0,"Objects":[{"StartTime":5025.0,"Position":128.0,"HyperDash":false},{"StartTime":5121.0,"Position":94.67586,"HyperDash":false},{"StartTime":5218.0,"Position":77.08793,"HyperDash":false},{"StartTime":5315.0,"Position":51.5,"HyperDash":false},{"StartTime":5412.0,"Position":77.08794,"HyperDash":false},{"StartTime":5490.0,"Position":111.663795,"HyperDash":false},{"StartTime":5605.0,"Position":128.0,"HyperDash":false}]},{"StartTime":5800.0,"Objects":[{"StartTime":5800.0,"Position":352.0,"HyperDash":false}]},{"StartTime":5993.0,"Objects":[{"StartTime":5993.0,"Position":240.0,"HyperDash":false}]},{"StartTime":6187.0,"Objects":[{"StartTime":6187.0,"Position":336.0,"HyperDash":false}]},{"StartTime":6574.0,"Objects":[{"StartTime":6574.0,"Position":416.0,"HyperDash":false},{"StartTime":6670.0,"Position":394.697662,"HyperDash":false},{"StartTime":6767.0,"Position":365.131775,"HyperDash":false},{"StartTime":6864.0,"Position":344.5659,"HyperDash":false},{"StartTime":6961.0,"Position":314.0,"HyperDash":false},{"StartTime":7057.0,"Position":279.6977,"HyperDash":false},{"StartTime":7154.0,"Position":263.131775,"HyperDash":false},{"StartTime":7233.0,"Position":259.310059,"HyperDash":false},{"StartTime":7348.0,"Position":212.0,"HyperDash":false}]},{"StartTime":8122.0,"Objects":[{"StartTime":8122.0,"Position":488.0,"HyperDash":false},{"StartTime":8218.0,"Position":475.697662,"HyperDash":false},{"StartTime":8315.0,"Position":437.131775,"HyperDash":false},{"StartTime":8394.0,"Position":399.3101,"HyperDash":false},{"StartTime":8509.0,"Position":386.0,"HyperDash":false}]},{"StartTime":8896.0,"Objects":[{"StartTime":8896.0,"Position":457.0,"HyperDash":false},{"StartTime":8992.0,"Position":444.675873,"HyperDash":false},{"StartTime":9089.0,"Position":406.087921,"HyperDash":false},{"StartTime":9186.0,"Position":384.5,"HyperDash":false},{"StartTime":9283.0,"Position":354.912048,"HyperDash":false},{"StartTime":9361.0,"Position":330.3362,"HyperDash":false},{"StartTime":9476.0,"Position":304.0,"HyperDash":false}]},{"StartTime":10058.0,"Objects":[{"StartTime":10058.0,"Position":400.0,"HyperDash":false}]},{"StartTime":10445.0,"Objects":[{"StartTime":10445.0,"Position":304.0,"HyperDash":false},{"StartTime":10541.0,"Position":277.697662,"HyperDash":false},{"StartTime":10638.0,"Position":253.131775,"HyperDash":false},{"StartTime":10735.0,"Position":212.565887,"HyperDash":false},{"StartTime":10832.0,"Position":202.0,"HyperDash":false},{"StartTime":10928.0,"Position":208.302322,"HyperDash":false},{"StartTime":11025.0,"Position":252.868225,"HyperDash":false},{"StartTime":11104.0,"Position":286.6899,"HyperDash":false},{"StartTime":11219.0,"Position":304.0,"HyperDash":false}]},{"StartTime":11606.0,"Objects":[{"StartTime":11606.0,"Position":400.0,"HyperDash":false}]},{"StartTime":11993.0,"Objects":[{"StartTime":11993.0,"Position":240.0,"HyperDash":false},{"StartTime":12089.0,"Position":231.675858,"HyperDash":false},{"StartTime":12186.0,"Position":189.087921,"HyperDash":false},{"StartTime":12283.0,"Position":156.5,"HyperDash":false},{"StartTime":12380.0,"Position":137.912064,"HyperDash":false},{"StartTime":12458.0,"Position":136.336212,"HyperDash":false},{"StartTime":12573.0,"Position":87.0,"HyperDash":false}]},{"StartTime":13154.0,"Objects":[{"StartTime":13154.0,"Position":0.0,"HyperDash":false}]},{"StartTime":13542.0,"Objects":[{"StartTime":13542.0,"Position":112.0,"HyperDash":false},{"StartTime":13638.0,"Position":119.913734,"HyperDash":false},{"StartTime":13735.0,"Position":144.5726,"HyperDash":false},{"StartTime":13832.0,"Position":179.8026,"HyperDash":false},{"StartTime":13929.0,"Position":191.357681,"HyperDash":false},{"StartTime":14007.0,"Position":228.85704,"HyperDash":false},{"StartTime":14122.0,"Position":241.622177,"HyperDash":false}]},{"StartTime":14316.0,"Objects":[{"StartTime":14316.0,"Position":288.0,"HyperDash":false},{"StartTime":14412.0,"Position":328.324127,"HyperDash":false},{"StartTime":14509.0,"Position":338.912079,"HyperDash":false},{"StartTime":14606.0,"Position":364.5,"HyperDash":false},{"StartTime":14703.0,"Position":338.912079,"HyperDash":false},{"StartTime":14781.0,"Position":301.3362,"HyperDash":false},{"StartTime":14896.0,"Position":288.0,"HyperDash":false}]},{"StartTime":15284.0,"Objects":[{"StartTime":15284.0,"Position":192.0,"HyperDash":false},{"StartTime":15362.0,"Position":187.7823,"HyperDash":false},{"StartTime":15477.0,"Position":169.192108,"HyperDash":false}]},{"StartTime":15864.0,"Objects":[{"StartTime":15864.0,"Position":464.0,"HyperDash":false}]},{"StartTime":16638.0,"Objects":[{"StartTime":16638.0,"Position":128.0,"HyperDash":false},{"StartTime":16734.0,"Position":90.86488,"HyperDash":false},{"StartTime":16831.0,"Position":78.3134155,"HyperDash":false},{"StartTime":16928.0,"Position":73.3517761,"HyperDash":false},{"StartTime":17025.0,"Position":34.6746674,"HyperDash":false},{"StartTime":17121.0,"Position":33.0714569,"HyperDash":false},{"StartTime":17218.0,"Position":2.65127468,"HyperDash":false},{"StartTime":17315.0,"Position":19.907608,"HyperDash":false},{"StartTime":17412.0,"Position":34.674675,"HyperDash":false},{"StartTime":17508.0,"Position":56.12827,"HyperDash":false},{"StartTime":17605.0,"Position":78.0694351,"HyperDash":false},{"StartTime":17684.0,"Position":97.97488,"HyperDash":false},{"StartTime":17799.0,"Position":128.0,"HyperDash":false}]},{"StartTime":18187.0,"Objects":[{"StartTime":18187.0,"Position":224.0,"HyperDash":false},{"StartTime":18283.0,"Position":231.165024,"HyperDash":false},{"StartTime":18380.0,"Position":273.681732,"HyperDash":false},{"StartTime":18477.0,"Position":304.374176,"HyperDash":false},{"StartTime":18574.0,"Position":316.397827,"HyperDash":false},{"StartTime":18670.0,"Position":312.8548,"HyperDash":false},{"StartTime":18767.0,"Position":345.5291,"HyperDash":false},{"StartTime":18864.0,"Position":328.007355,"HyperDash":false},{"StartTime":18961.0,"Position":316.397827,"HyperDash":false},{"StartTime":19057.0,"Position":304.594452,"HyperDash":false},{"StartTime":19154.0,"Position":273.924957,"HyperDash":false},{"StartTime":19233.0,"Position":271.063354,"HyperDash":false},{"StartTime":19348.0,"Position":224.0,"HyperDash":false}]},{"StartTime":19735.0,"Objects":[{"StartTime":19735.0,"Position":128.0,"HyperDash":false},{"StartTime":19831.0,"Position":136.324142,"HyperDash":false},{"StartTime":19928.0,"Position":178.912079,"HyperDash":false},{"StartTime":20025.0,"Position":223.5,"HyperDash":false},{"StartTime":20122.0,"Position":230.087936,"HyperDash":false},{"StartTime":20200.0,"Position":260.6638,"HyperDash":false},{"StartTime":20315.0,"Position":281.0,"HyperDash":false}]},{"StartTime":20896.0,"Objects":[{"StartTime":20896.0,"Position":432.0,"HyperDash":false}]},{"StartTime":21284.0,"Objects":[{"StartTime":21284.0,"Position":328.0,"HyperDash":false},{"StartTime":21380.0,"Position":357.324127,"HyperDash":false},{"StartTime":21477.0,"Position":378.912079,"HyperDash":false},{"StartTime":21574.0,"Position":420.5,"HyperDash":false},{"StartTime":21671.0,"Position":430.087952,"HyperDash":false},{"StartTime":21749.0,"Position":447.6638,"HyperDash":false},{"StartTime":21864.0,"Position":481.0,"HyperDash":false}]},{"StartTime":22445.0,"Objects":[{"StartTime":22445.0,"Position":328.0,"HyperDash":false}]},{"StartTime":22832.0,"Objects":[{"StartTime":22832.0,"Position":224.0,"HyperDash":false},{"StartTime":22928.0,"Position":188.675858,"HyperDash":false},{"StartTime":23025.0,"Position":173.087921,"HyperDash":false},{"StartTime":23122.0,"Position":156.5,"HyperDash":false},{"StartTime":23219.0,"Position":121.912064,"HyperDash":false},{"StartTime":23297.0,"Position":91.3362045,"HyperDash":false},{"StartTime":23412.0,"Position":71.0,"HyperDash":false}]},{"StartTime":23993.0,"Objects":[{"StartTime":23993.0,"Position":224.0,"HyperDash":false}]},{"StartTime":24380.0,"Objects":[{"StartTime":24380.0,"Position":112.0,"HyperDash":false},{"StartTime":24476.0,"Position":127.324142,"HyperDash":false},{"StartTime":24573.0,"Position":162.912079,"HyperDash":false},{"StartTime":24670.0,"Position":202.5,"HyperDash":false},{"StartTime":24767.0,"Position":214.087936,"HyperDash":false},{"StartTime":24845.0,"Position":246.663788,"HyperDash":false},{"StartTime":24960.0,"Position":265.0,"HyperDash":false}]},{"StartTime":25541.0,"Objects":[{"StartTime":25541.0,"Position":416.0,"HyperDash":false}]},{"StartTime":25929.0,"Objects":[{"StartTime":25929.0,"Position":304.0,"HyperDash":false},{"StartTime":26025.0,"Position":287.9714,"HyperDash":false},{"StartTime":26122.0,"Position":274.232758,"HyperDash":false},{"StartTime":26219.0,"Position":253.164063,"HyperDash":false},{"StartTime":26316.0,"Position":274.1704,"HyperDash":false},{"StartTime":26394.0,"Position":290.949921,"HyperDash":false},{"StartTime":26509.0,"Position":303.819763,"HyperDash":false}]},{"StartTime":27090.0,"Objects":[{"StartTime":27090.0,"Position":480.0,"HyperDash":false}]},{"StartTime":27477.0,"Objects":[{"StartTime":27477.0,"Position":384.0,"HyperDash":false},{"StartTime":27573.0,"Position":351.697662,"HyperDash":false},{"StartTime":27670.0,"Position":333.0,"HyperDash":false},{"StartTime":27749.0,"Position":356.6899,"HyperDash":false},{"StartTime":27864.0,"Position":384.0,"HyperDash":false}]},{"StartTime":28058.0,"Objects":[{"StartTime":28058.0,"Position":432.0,"HyperDash":false}]},{"StartTime":28445.0,"Objects":[{"StartTime":28445.0,"Position":333.0,"HyperDash":false},{"StartTime":28541.0,"Position":305.697662,"HyperDash":false},{"StartTime":28638.0,"Position":282.0,"HyperDash":false},{"StartTime":28717.0,"Position":319.6899,"HyperDash":false},{"StartTime":28832.0,"Position":333.0,"HyperDash":false}]},{"StartTime":29025.0,"Objects":[{"StartTime":29025.0,"Position":384.0,"HyperDash":false},{"StartTime":29121.0,"Position":341.697662,"HyperDash":false},{"StartTime":29218.0,"Position":333.131775,"HyperDash":false},{"StartTime":29297.0,"Position":293.3101,"HyperDash":false},{"StartTime":29412.0,"Position":282.0,"HyperDash":false}]},{"StartTime":29606.0,"Objects":[{"StartTime":29606.0,"Position":224.0,"HyperDash":false},{"StartTime":29702.0,"Position":206.103424,"HyperDash":false},{"StartTime":29799.0,"Position":176.49205,"HyperDash":false},{"StartTime":29896.0,"Position":149.701324,"HyperDash":false},{"StartTime":29993.0,"Position":147.519775,"HyperDash":false},{"StartTime":30071.0,"Position":144.128265,"HyperDash":false},{"StartTime":30186.0,"Position":148.650436,"HyperDash":false}]},{"StartTime":30574.0,"Objects":[{"StartTime":30574.0,"Position":272.0,"HyperDash":false},{"StartTime":30670.0,"Position":303.302338,"HyperDash":false},{"StartTime":30767.0,"Position":322.868225,"HyperDash":false},{"StartTime":30846.0,"Position":351.6899,"HyperDash":false},{"StartTime":30961.0,"Position":374.0,"HyperDash":false}]},{"StartTime":31154.0,"Objects":[{"StartTime":31154.0,"Position":424.0,"HyperDash":false},{"StartTime":31250.0,"Position":439.371674,"HyperDash":false},{"StartTime":31347.0,"Position":424.705872,"HyperDash":false},{"StartTime":31444.0,"Position":417.305573,"HyperDash":false},{"StartTime":31541.0,"Position":395.3253,"HyperDash":false},{"StartTime":31619.0,"Position":372.2993,"HyperDash":false},{"StartTime":31734.0,"Position":347.633759,"HyperDash":false}]},{"StartTime":32122.0,"Objects":[{"StartTime":32122.0,"Position":224.0,"HyperDash":false},{"StartTime":32218.0,"Position":200.129822,"HyperDash":false},{"StartTime":32315.0,"Position":176.418152,"HyperDash":false},{"StartTime":32394.0,"Position":164.275757,"HyperDash":false},{"StartTime":32509.0,"Position":146.329926,"HyperDash":false}]},{"StartTime":32703.0,"Objects":[{"StartTime":32703.0,"Position":256.0,"HyperDash":false}]},{"StartTime":33284.0,"Objects":[{"StartTime":33284.0,"Position":496.0,"HyperDash":false}]},{"StartTime":33671.0,"Objects":[{"StartTime":33671.0,"Position":304.0,"HyperDash":false},{"StartTime":33767.0,"Position":297.697662,"HyperDash":false},{"StartTime":33864.0,"Position":253.0,"HyperDash":false},{"StartTime":33943.0,"Position":270.6899,"HyperDash":false},{"StartTime":34058.0,"Position":304.0,"HyperDash":false}]},{"StartTime":34251.0,"Objects":[{"StartTime":34251.0,"Position":352.0,"HyperDash":false},{"StartTime":34347.0,"Position":374.083679,"HyperDash":false},{"StartTime":34444.0,"Position":391.360016,"HyperDash":false},{"StartTime":34541.0,"Position":387.812561,"HyperDash":false},{"StartTime":34638.0,"Position":404.369629,"HyperDash":false},{"StartTime":34716.0,"Position":417.578369,"HyperDash":false},{"StartTime":34831.0,"Position":385.75708,"HyperDash":false}]},{"StartTime":35219.0,"Objects":[{"StartTime":35219.0,"Position":280.0,"HyperDash":false},{"StartTime":35315.0,"Position":277.964783,"HyperDash":false},{"StartTime":35412.0,"Position":251.783386,"HyperDash":false},{"StartTime":35491.0,"Position":238.233582,"HyperDash":false},{"StartTime":35606.0,"Position":223.420578,"HyperDash":false}]},{"StartTime":35800.0,"Objects":[{"StartTime":35800.0,"Position":272.0,"HyperDash":false},{"StartTime":35896.0,"Position":303.035217,"HyperDash":false},{"StartTime":35993.0,"Position":300.2166,"HyperDash":false},{"StartTime":36072.0,"Position":326.766418,"HyperDash":false},{"StartTime":36187.0,"Position":328.5794,"HyperDash":false}]},{"StartTime":36380.0,"Objects":[{"StartTime":36380.0,"Position":224.0,"HyperDash":false}]},{"StartTime":36767.0,"Objects":[{"StartTime":36767.0,"Position":176.0,"HyperDash":false},{"StartTime":36863.0,"Position":148.9648,"HyperDash":false},{"StartTime":36960.0,"Position":147.783386,"HyperDash":false},{"StartTime":37039.0,"Position":140.233582,"HyperDash":false},{"StartTime":37154.0,"Position":119.420578,"HyperDash":false}]},{"StartTime":37348.0,"Objects":[{"StartTime":37348.0,"Position":168.0,"HyperDash":false},{"StartTime":37444.0,"Position":170.0352,"HyperDash":false},{"StartTime":37541.0,"Position":196.216614,"HyperDash":false},{"StartTime":37620.0,"Position":195.766418,"HyperDash":false},{"StartTime":37735.0,"Position":224.579422,"HyperDash":false}]},{"StartTime":37928.0,"Objects":[{"StartTime":37928.0,"Position":120.0,"HyperDash":false}]},{"StartTime":38316.0,"Objects":[{"StartTime":38316.0,"Position":304.0,"HyperDash":false},{"StartTime":38412.0,"Position":277.697662,"HyperDash":false},{"StartTime":38509.0,"Position":253.131775,"HyperDash":false},{"StartTime":38588.0,"Position":226.310089,"HyperDash":false},{"StartTime":38703.0,"Position":202.0,"HyperDash":false}]},{"StartTime":38896.0,"Objects":[{"StartTime":38896.0,"Position":88.0,"HyperDash":false}]},{"StartTime":39477.0,"Objects":[{"StartTime":39477.0,"Position":280.0,"HyperDash":false},{"StartTime":39555.0,"Position":292.6114,"HyperDash":false},{"StartTime":39670.0,"Position":331.0,"HyperDash":false}]},{"StartTime":39864.0,"Objects":[{"StartTime":39864.0,"Position":424.0,"HyperDash":false},{"StartTime":39960.0,"Position":428.059753,"HyperDash":false},{"StartTime":40057.0,"Position":441.062134,"HyperDash":false},{"StartTime":40136.0,"Position":432.009247,"HyperDash":false},{"StartTime":40251.0,"Position":420.508881,"HyperDash":false}]},{"StartTime":40445.0,"Objects":[{"StartTime":40445.0,"Position":288.0,"HyperDash":false}]},{"StartTime":41025.0,"Objects":[{"StartTime":41025.0,"Position":32.0,"HyperDash":false}]},{"StartTime":41413.0,"Objects":[{"StartTime":41413.0,"Position":256.0,"HyperDash":false},{"StartTime":41509.0,"Position":291.8391,"HyperDash":false},{"StartTime":41606.0,"Position":302.265045,"HyperDash":false},{"StartTime":41685.0,"Position":310.9909,"HyperDash":false},{"StartTime":41800.0,"Position":352.8495,"HyperDash":false}]},{"StartTime":41993.0,"Objects":[{"StartTime":41993.0,"Position":447.0,"HyperDash":false}]},{"StartTime":42187.0,"Objects":[{"StartTime":42187.0,"Position":440.0,"HyperDash":false},{"StartTime":42283.0,"Position":403.2554,"HyperDash":false},{"StartTime":42380.0,"Position":389.7651,"HyperDash":false},{"StartTime":42459.0,"Position":365.6506,"HyperDash":false},{"StartTime":42574.0,"Position":342.9871,"HyperDash":false}]},{"StartTime":42961.0,"Objects":[{"StartTime":42961.0,"Position":248.0,"HyperDash":false},{"StartTime":43057.0,"Position":264.94986,"HyperDash":false},{"StartTime":43154.0,"Position":281.6838,"HyperDash":false},{"StartTime":43251.0,"Position":305.2995,"HyperDash":false},{"StartTime":43348.0,"Position":330.6694,"HyperDash":false},{"StartTime":43444.0,"Position":352.264221,"HyperDash":false},{"StartTime":43541.0,"Position":377.479736,"HyperDash":false},{"StartTime":43638.0,"Position":371.509277,"HyperDash":false},{"StartTime":43735.0,"Position":330.6694,"HyperDash":false},{"StartTime":43831.0,"Position":294.557373,"HyperDash":false},{"StartTime":43928.0,"Position":281.915039,"HyperDash":false},{"StartTime":44007.0,"Position":283.462677,"HyperDash":false},{"StartTime":44122.0,"Position":248.0,"HyperDash":false}]},{"StartTime":44509.0,"Objects":[{"StartTime":44509.0,"Position":144.0,"HyperDash":false},{"StartTime":44605.0,"Position":140.034851,"HyperDash":false},{"StartTime":44702.0,"Position":110.27771,"HyperDash":false},{"StartTime":44799.0,"Position":69.63604,"HyperDash":false},{"StartTime":44896.0,"Position":61.2429733,"HyperDash":false},{"StartTime":44974.0,"Position":47.09105,"HyperDash":false},{"StartTime":45089.0,"Position":14.520277,"HyperDash":false}]},{"StartTime":45284.0,"Objects":[{"StartTime":45284.0,"Position":56.0,"HyperDash":false}]},{"StartTime":45671.0,"Objects":[{"StartTime":45671.0,"Position":264.0,"HyperDash":false}]},{"StartTime":46058.0,"Objects":[{"StartTime":46058.0,"Position":264.0,"HyperDash":false},{"StartTime":46154.0,"Position":301.302338,"HyperDash":false},{"StartTime":46251.0,"Position":314.868225,"HyperDash":false},{"StartTime":46330.0,"Position":321.6899,"HyperDash":false},{"StartTime":46445.0,"Position":366.0,"HyperDash":false}]},{"StartTime":46638.0,"Objects":[{"StartTime":46638.0,"Position":416.0,"HyperDash":false},{"StartTime":46734.0,"Position":371.675873,"HyperDash":false},{"StartTime":46831.0,"Position":365.087921,"HyperDash":false},{"StartTime":46928.0,"Position":325.5,"HyperDash":false},{"StartTime":47025.0,"Position":313.912048,"HyperDash":false},{"StartTime":47103.0,"Position":306.3362,"HyperDash":false},{"StartTime":47218.0,"Position":263.0,"HyperDash":false}]},{"StartTime":47606.0,"Objects":[{"StartTime":47606.0,"Position":360.0,"HyperDash":false},{"StartTime":47702.0,"Position":324.675873,"HyperDash":false},{"StartTime":47799.0,"Position":309.087921,"HyperDash":false},{"StartTime":47896.0,"Position":293.5,"HyperDash":false},{"StartTime":47993.0,"Position":257.912048,"HyperDash":false},{"StartTime":48071.0,"Position":244.336212,"HyperDash":false},{"StartTime":48186.0,"Position":207.0,"HyperDash":false}]},{"StartTime":48380.0,"Objects":[{"StartTime":48380.0,"Position":160.0,"HyperDash":false},{"StartTime":48476.0,"Position":166.997681,"HyperDash":false},{"StartTime":48573.0,"Position":187.484192,"HyperDash":false},{"StartTime":48652.0,"Position":200.988281,"HyperDash":false},{"StartTime":48767.0,"Position":236.72261,"HyperDash":false}]},{"StartTime":49154.0,"Objects":[{"StartTime":49154.0,"Position":32.0,"HyperDash":false}]},{"StartTime":49542.0,"Objects":[{"StartTime":49542.0,"Position":248.0,"HyperDash":false},{"StartTime":49638.0,"Position":266.302338,"HyperDash":false},{"StartTime":49735.0,"Position":298.868225,"HyperDash":false},{"StartTime":49814.0,"Position":314.6899,"HyperDash":false},{"StartTime":49929.0,"Position":350.0,"HyperDash":false}]},{"StartTime":50316.0,"Objects":[{"StartTime":50316.0,"Position":256.0,"HyperDash":false},{"StartTime":50394.0,"Position":235.3886,"HyperDash":false},{"StartTime":50509.0,"Position":205.0,"HyperDash":false}]},{"StartTime":50703.0,"Objects":[{"StartTime":50703.0,"Position":256.0,"HyperDash":false},{"StartTime":50799.0,"Position":296.302338,"HyperDash":false},{"StartTime":50896.0,"Position":306.868225,"HyperDash":false},{"StartTime":50975.0,"Position":344.6899,"HyperDash":false},{"StartTime":51090.0,"Position":358.0,"HyperDash":false}]},{"StartTime":51284.0,"Objects":[{"StartTime":51284.0,"Position":440.0,"HyperDash":false}]},{"StartTime":51477.0,"Objects":[{"StartTime":51477.0,"Position":352.0,"HyperDash":false},{"StartTime":51573.0,"Position":329.697662,"HyperDash":false},{"StartTime":51670.0,"Position":301.131775,"HyperDash":false},{"StartTime":51749.0,"Position":288.3101,"HyperDash":false},{"StartTime":51864.0,"Position":250.0,"HyperDash":false}]},{"StartTime":52251.0,"Objects":[{"StartTime":52251.0,"Position":128.0,"HyperDash":false},{"StartTime":52347.0,"Position":102.275604,"HyperDash":false},{"StartTime":52444.0,"Position":77.8078156,"HyperDash":false},{"StartTime":52541.0,"Position":41.6188354,"HyperDash":false},{"StartTime":52638.0,"Position":32.3890381,"HyperDash":false},{"StartTime":52716.0,"Position":11.4332657,"HyperDash":false},{"StartTime":52831.0,"Position":4.4833107,"HyperDash":false}]},{"StartTime":53025.0,"Objects":[{"StartTime":53025.0,"Position":88.0,"HyperDash":false}]},{"StartTime":53413.0,"Objects":[{"StartTime":53413.0,"Position":168.0,"HyperDash":false},{"StartTime":53491.0,"Position":145.000992,"HyperDash":false},{"StartTime":53606.0,"Position":155.630676,"HyperDash":false}]},{"StartTime":53800.0,"Objects":[{"StartTime":53800.0,"Position":248.0,"HyperDash":false},{"StartTime":53896.0,"Position":263.12973,"HyperDash":false},{"StartTime":53993.0,"Position":290.128967,"HyperDash":false},{"StartTime":54090.0,"Position":316.20874,"HyperDash":false},{"StartTime":54187.0,"Position":340.6195,"HyperDash":false},{"StartTime":54265.0,"Position":376.1075,"HyperDash":false},{"StartTime":54380.0,"Position":385.246277,"HyperDash":false}]},{"StartTime":54574.0,"Objects":[{"StartTime":54574.0,"Position":472.0,"HyperDash":false}]},{"StartTime":54961.0,"Objects":[{"StartTime":54961.0,"Position":328.0,"HyperDash":false}]},{"StartTime":55348.0,"Objects":[{"StartTime":55348.0,"Position":224.0,"HyperDash":false},{"StartTime":55444.0,"Position":195.951981,"HyperDash":false},{"StartTime":55541.0,"Position":173.643036,"HyperDash":false},{"StartTime":55620.0,"Position":140.030609,"HyperDash":false},{"StartTime":55735.0,"Position":123.025146,"HyperDash":false}]},{"StartTime":55929.0,"Objects":[{"StartTime":55929.0,"Position":72.0,"HyperDash":false},{"StartTime":56025.0,"Position":95.8109741,"HyperDash":false},{"StartTime":56122.0,"Position":121.880394,"HyperDash":false},{"StartTime":56201.0,"Position":145.29776,"HyperDash":false},{"StartTime":56316.0,"Position":172.019226,"HyperDash":false}]},{"StartTime":56509.0,"Objects":[{"StartTime":56509.0,"Position":256.0,"HyperDash":false}]},{"StartTime":56896.0,"Objects":[{"StartTime":56896.0,"Position":328.0,"HyperDash":false},{"StartTime":56992.0,"Position":356.5922,"HyperDash":false},{"StartTime":57089.0,"Position":368.955872,"HyperDash":false},{"StartTime":57186.0,"Position":373.612579,"HyperDash":false},{"StartTime":57283.0,"Position":419.1303,"HyperDash":false},{"StartTime":57361.0,"Position":444.262817,"HyperDash":false},{"StartTime":57476.0,"Position":466.641,"HyperDash":false}]},{"StartTime":57671.0,"Objects":[{"StartTime":57671.0,"Position":416.0,"HyperDash":false},{"StartTime":57767.0,"Position":391.6712,"HyperDash":false},{"StartTime":57864.0,"Position":367.089,"HyperDash":false},{"StartTime":57943.0,"Position":328.06842,"HyperDash":false},{"StartTime":58058.0,"Position":317.924561,"HyperDash":false}]},{"StartTime":58445.0,"Objects":[{"StartTime":58445.0,"Position":144.0,"HyperDash":false}]},{"StartTime":58832.0,"Objects":[{"StartTime":58832.0,"Position":320.0,"HyperDash":false}]},{"StartTime":59219.0,"Objects":[{"StartTime":59219.0,"Position":128.0,"HyperDash":false}]},{"StartTime":59606.0,"Objects":[{"StartTime":59606.0,"Position":112.0,"HyperDash":false}]},{"StartTime":59993.0,"Objects":[{"StartTime":59993.0,"Position":224.0,"HyperDash":false},{"StartTime":60089.0,"Position":217.725632,"HyperDash":false},{"StartTime":60186.0,"Position":227.34639,"HyperDash":false},{"StartTime":60265.0,"Position":214.703522,"HyperDash":false},{"StartTime":60380.0,"Position":206.754791,"HyperDash":false}]},{"StartTime":60767.0,"Objects":[{"StartTime":60767.0,"Position":80.0,"HyperDash":false},{"StartTime":60863.0,"Position":90.79409,"HyperDash":false},{"StartTime":60960.0,"Position":75.87808,"HyperDash":false},{"StartTime":61039.0,"Position":74.4997559,"HyperDash":false},{"StartTime":61154.0,"Position":96.53038,"HyperDash":false}]},{"StartTime":61542.0,"Objects":[{"StartTime":61542.0,"Position":200.0,"HyperDash":false},{"StartTime":61638.0,"Position":196.089508,"HyperDash":false},{"StartTime":61735.0,"Position":236.445679,"HyperDash":false},{"StartTime":61814.0,"Position":270.5239,"HyperDash":false},{"StartTime":61929.0,"Position":286.4193,"HyperDash":false}]},{"StartTime":62316.0,"Objects":[{"StartTime":62316.0,"Position":376.0,"HyperDash":false},{"StartTime":62412.0,"Position":361.9105,"HyperDash":false},{"StartTime":62509.0,"Position":339.554321,"HyperDash":false},{"StartTime":62588.0,"Position":316.4761,"HyperDash":false},{"StartTime":62703.0,"Position":289.5807,"HyperDash":false}]},{"StartTime":63090.0,"Objects":[{"StartTime":63090.0,"Position":184.0,"HyperDash":false},{"StartTime":63186.0,"Position":174.5783,"HyperDash":false},{"StartTime":63283.0,"Position":191.193848,"HyperDash":false},{"StartTime":63362.0,"Position":204.138489,"HyperDash":false},{"StartTime":63477.0,"Position":198.424973,"HyperDash":false}]},{"StartTime":63864.0,"Objects":[{"StartTime":63864.0,"Position":88.0,"HyperDash":false},{"StartTime":63960.0,"Position":45.16764,"HyperDash":false},{"StartTime":64057.0,"Position":38.0766068,"HyperDash":false},{"StartTime":64154.0,"Position":12.98558,"HyperDash":false},{"StartTime":64251.0,"Position":38.0766144,"HyperDash":false},{"StartTime":64329.0,"Position":69.2529,"HyperDash":false},{"StartTime":64444.0,"Position":88.0,"HyperDash":false}]},{"StartTime":64638.0,"Objects":[{"StartTime":64638.0,"Position":312.0,"HyperDash":false}]},{"StartTime":64832.0,"Objects":[{"StartTime":64832.0,"Position":208.0,"HyperDash":false}]},{"StartTime":65025.0,"Objects":[{"StartTime":65025.0,"Position":304.0,"HyperDash":false}]},{"StartTime":65413.0,"Objects":[{"StartTime":65413.0,"Position":360.0,"HyperDash":false},{"StartTime":65491.0,"Position":361.6114,"HyperDash":false},{"StartTime":65606.0,"Position":411.0,"HyperDash":false}]},{"StartTime":65800.0,"Objects":[{"StartTime":65800.0,"Position":462.0,"HyperDash":false},{"StartTime":65878.0,"Position":458.3886,"HyperDash":false},{"StartTime":65993.0,"Position":411.0,"HyperDash":false}]},{"StartTime":66187.0,"Objects":[{"StartTime":66187.0,"Position":344.0,"HyperDash":false},{"StartTime":66283.0,"Position":327.697662,"HyperDash":false},{"StartTime":66380.0,"Position":293.131775,"HyperDash":false},{"StartTime":66459.0,"Position":271.3101,"HyperDash":false},{"StartTime":66574.0,"Position":242.0,"HyperDash":false}]},{"StartTime":66961.0,"Objects":[{"StartTime":66961.0,"Position":152.0,"HyperDash":false},{"StartTime":67057.0,"Position":167.659241,"HyperDash":false},{"StartTime":67154.0,"Position":147.9835,"HyperDash":false},{"StartTime":67233.0,"Position":135.201309,"HyperDash":false},{"StartTime":67348.0,"Position":106.616547,"HyperDash":false}]},{"StartTime":67735.0,"Objects":[{"StartTime":67735.0,"Position":32.0,"HyperDash":false},{"StartTime":67831.0,"Position":42.2565079,"HyperDash":false},{"StartTime":67928.0,"Position":78.75527,"HyperDash":false},{"StartTime":68007.0,"Position":85.89344,"HyperDash":false},{"StartTime":68122.0,"Position":125.752792,"HyperDash":false}]},{"StartTime":68316.0,"Objects":[{"StartTime":68316.0,"Position":208.0,"HyperDash":false}]},{"StartTime":68509.0,"Objects":[{"StartTime":68509.0,"Position":224.0,"HyperDash":false},{"StartTime":68605.0,"Position":240.243561,"HyperDash":false},{"StartTime":68702.0,"Position":270.729248,"HyperDash":false},{"StartTime":68781.0,"Position":291.85675,"HyperDash":false},{"StartTime":68896.0,"Position":317.7006,"HyperDash":false}]},{"StartTime":69284.0,"Objects":[{"StartTime":69284.0,"Position":216.0,"HyperDash":false},{"StartTime":69380.0,"Position":191.846649,"HyperDash":false},{"StartTime":69477.0,"Position":185.704849,"HyperDash":false},{"StartTime":69556.0,"Position":168.950912,"HyperDash":false},{"StartTime":69671.0,"Position":193.879364,"HyperDash":false}]},{"StartTime":70058.0,"Objects":[{"StartTime":70058.0,"Position":360.0,"HyperDash":false},{"StartTime":70154.0,"Position":374.986725,"HyperDash":false},{"StartTime":70251.0,"Position":367.9918,"HyperDash":false},{"StartTime":70330.0,"Position":353.6845,"HyperDash":false},{"StartTime":70445.0,"Position":337.885529,"HyperDash":false}]},{"StartTime":70832.0,"Objects":[{"StartTime":70832.0,"Position":264.0,"HyperDash":false},{"StartTime":70928.0,"Position":225.951981,"HyperDash":false},{"StartTime":71025.0,"Position":213.643036,"HyperDash":false},{"StartTime":71104.0,"Position":187.030609,"HyperDash":false},{"StartTime":71219.0,"Position":163.025146,"HyperDash":false}]},{"StartTime":71413.0,"Objects":[{"StartTime":71413.0,"Position":112.0,"HyperDash":false},{"StartTime":71509.0,"Position":75.97218,"HyperDash":false},{"StartTime":71606.0,"Position":61.6836624,"HyperDash":false},{"StartTime":71685.0,"Position":27.0878525,"HyperDash":false},{"StartTime":71800.0,"Position":11.1066208,"HyperDash":false}]},{"StartTime":71993.0,"Objects":[{"StartTime":71993.0,"Position":40.0,"HyperDash":false},{"StartTime":72071.0,"Position":52.4331474,"HyperDash":false},{"StartTime":72186.0,"Position":68.28971,"HyperDash":false}]},{"StartTime":72380.0,"Objects":[{"StartTime":72380.0,"Position":176.0,"HyperDash":false},{"StartTime":72476.0,"Position":187.970581,"HyperDash":false},{"StartTime":72573.0,"Position":161.344147,"HyperDash":false},{"StartTime":72670.0,"Position":136.575012,"HyperDash":false},{"StartTime":72767.0,"Position":119.0057,"HyperDash":false},{"StartTime":72845.0,"Position":79.55367,"HyperDash":false},{"StartTime":72960.0,"Position":69.6823959,"HyperDash":false}]},{"StartTime":73154.0,"Objects":[{"StartTime":73154.0,"Position":120.0,"HyperDash":false},{"StartTime":73250.0,"Position":160.2306,"HyperDash":false},{"StartTime":73347.0,"Position":169.814178,"HyperDash":false},{"StartTime":73426.0,"Position":170.964127,"HyperDash":false},{"StartTime":73541.0,"Position":209.453659,"HyperDash":false}]},{"StartTime":73929.0,"Objects":[{"StartTime":73929.0,"Position":312.0,"HyperDash":false},{"StartTime":74025.0,"Position":321.048035,"HyperDash":false},{"StartTime":74122.0,"Position":362.356964,"HyperDash":false},{"StartTime":74201.0,"Position":394.9694,"HyperDash":false},{"StartTime":74316.0,"Position":412.974854,"HyperDash":false}]},{"StartTime":74703.0,"Objects":[{"StartTime":74703.0,"Position":336.0,"HyperDash":false},{"StartTime":74781.0,"Position":342.880768,"HyperDash":false},{"StartTime":74896.0,"Position":315.910126,"HyperDash":false}]},{"StartTime":75090.0,"Objects":[{"StartTime":75090.0,"Position":400.0,"HyperDash":false},{"StartTime":75168.0,"Position":399.237152,"HyperDash":false},{"StartTime":75283.0,"Position":417.9073,"HyperDash":false}]},{"StartTime":75477.0,"Objects":[{"StartTime":75477.0,"Position":328.0,"HyperDash":false},{"StartTime":75573.0,"Position":296.892883,"HyperDash":false},{"StartTime":75670.0,"Position":288.19165,"HyperDash":false},{"StartTime":75767.0,"Position":275.9188,"HyperDash":false},{"StartTime":75864.0,"Position":238.263733,"HyperDash":false},{"StartTime":75942.0,"Position":221.022842,"HyperDash":false},{"StartTime":76057.0,"Position":202.919159,"HyperDash":false}]},{"StartTime":76251.0,"Objects":[{"StartTime":76251.0,"Position":296.0,"HyperDash":false},{"StartTime":76347.0,"Position":313.798157,"HyperDash":false},{"StartTime":76444.0,"Position":346.261322,"HyperDash":false},{"StartTime":76523.0,"Position":375.263733,"HyperDash":false},{"StartTime":76638.0,"Position":392.493958,"HyperDash":false}]},{"StartTime":77219.0,"Objects":[{"StartTime":77219.0,"Position":152.0,"HyperDash":false},{"StartTime":77315.0,"Position":119.697678,"HyperDash":false},{"StartTime":77412.0,"Position":101.0,"HyperDash":false},{"StartTime":77491.0,"Position":137.689911,"HyperDash":false},{"StartTime":77606.0,"Position":152.0,"HyperDash":false}]},{"StartTime":77800.0,"Objects":[{"StartTime":77800.0,"Position":320.0,"HyperDash":false}]},{"StartTime":78187.0,"Objects":[{"StartTime":78187.0,"Position":320.0,"HyperDash":false},{"StartTime":78265.0,"Position":323.92218,"HyperDash":false},{"StartTime":78380.0,"Position":369.294647,"HyperDash":false}]},{"StartTime":78574.0,"Objects":[{"StartTime":78574.0,"Position":456.0,"HyperDash":false},{"StartTime":78670.0,"Position":443.684448,"HyperDash":false},{"StartTime":78767.0,"Position":433.251038,"HyperDash":false},{"StartTime":78846.0,"Position":411.9393,"HyperDash":false},{"StartTime":78961.0,"Position":410.384216,"HyperDash":false}]},{"StartTime":79348.0,"Objects":[{"StartTime":79348.0,"Position":288.0,"HyperDash":false},{"StartTime":79444.0,"Position":287.315552,"HyperDash":false},{"StartTime":79541.0,"Position":310.748962,"HyperDash":false},{"StartTime":79620.0,"Position":322.0607,"HyperDash":false},{"StartTime":79735.0,"Position":333.615784,"HyperDash":false}]},{"StartTime":80122.0,"Objects":[{"StartTime":80122.0,"Position":240.0,"HyperDash":false},{"StartTime":80218.0,"Position":206.699463,"HyperDash":false},{"StartTime":80315.0,"Position":192.5893,"HyperDash":false},{"StartTime":80394.0,"Position":180.9993,"HyperDash":false},{"StartTime":80509.0,"Position":144.773514,"HyperDash":false}]},{"StartTime":80703.0,"Objects":[{"StartTime":80703.0,"Position":64.0,"HyperDash":false}]},{"StartTime":80896.0,"Objects":[{"StartTime":80896.0,"Position":40.0,"HyperDash":false},{"StartTime":80992.0,"Position":52.30054,"HyperDash":false},{"StartTime":81089.0,"Position":87.4107056,"HyperDash":false},{"StartTime":81168.0,"Position":99.0006943,"HyperDash":false},{"StartTime":81283.0,"Position":135.226486,"HyperDash":false}]},{"StartTime":81671.0,"Objects":[{"StartTime":81671.0,"Position":248.0,"HyperDash":false},{"StartTime":81767.0,"Position":248.315552,"HyperDash":false},{"StartTime":81864.0,"Position":270.748962,"HyperDash":false},{"StartTime":81943.0,"Position":270.0607,"HyperDash":false},{"StartTime":82058.0,"Position":293.615784,"HyperDash":false}]},{"StartTime":82445.0,"Objects":[{"StartTime":82445.0,"Position":120.0,"HyperDash":false}]},{"StartTime":82832.0,"Objects":[{"StartTime":82832.0,"Position":312.0,"HyperDash":false}]},{"StartTime":83219.0,"Objects":[{"StartTime":83219.0,"Position":400.0,"HyperDash":false},{"StartTime":83297.0,"Position":406.999,"HyperDash":false},{"StartTime":83412.0,"Position":412.369324,"HyperDash":false}]},{"StartTime":83606.0,"Objects":[{"StartTime":83606.0,"Position":360.0,"HyperDash":false},{"StartTime":83684.0,"Position":344.762848,"HyperDash":false},{"StartTime":83799.0,"Position":342.0927,"HyperDash":false}]},{"StartTime":83993.0,"Objects":[{"StartTime":83993.0,"Position":272.0,"HyperDash":false},{"StartTime":84089.0,"Position":267.17865,"HyperDash":false},{"StartTime":84186.0,"Position":223.813,"HyperDash":false},{"StartTime":84265.0,"Position":218.85318,"HyperDash":false},{"StartTime":84380.0,"Position":179.797424,"HyperDash":false}]},{"StartTime":84767.0,"Objects":[{"StartTime":84767.0,"Position":80.0,"HyperDash":false},{"StartTime":84845.0,"Position":91.5179,"HyperDash":false},{"StartTime":84960.0,"Position":96.12762,"HyperDash":false}]},{"StartTime":85154.0,"Objects":[{"StartTime":85154.0,"Position":16.0,"HyperDash":false},{"StartTime":85232.0,"Position":0.0,"HyperDash":false},{"StartTime":85347.0,"Position":16.0,"HyperDash":false}]},{"StartTime":85542.0,"Objects":[{"StartTime":85542.0,"Position":104.0,"HyperDash":false},{"StartTime":85638.0,"Position":112.302322,"HyperDash":false},{"StartTime":85735.0,"Position":154.868225,"HyperDash":false},{"StartTime":85814.0,"Position":182.689911,"HyperDash":false},{"StartTime":85929.0,"Position":206.0,"HyperDash":false}]},{"StartTime":86316.0,"Objects":[{"StartTime":86316.0,"Position":376.0,"HyperDash":false},{"StartTime":86412.0,"Position":382.0039,"HyperDash":false},{"StartTime":86509.0,"Position":424.2578,"HyperDash":false},{"StartTime":86588.0,"Position":445.011047,"HyperDash":false},{"StartTime":86703.0,"Position":472.7657,"HyperDash":false}]},{"StartTime":87090.0,"Objects":[{"StartTime":87090.0,"Position":296.0,"HyperDash":false},{"StartTime":87186.0,"Position":311.353729,"HyperDash":false},{"StartTime":87283.0,"Position":308.602417,"HyperDash":false},{"StartTime":87362.0,"Position":307.240021,"HyperDash":false},{"StartTime":87477.0,"Position":312.385956,"HyperDash":false}]},{"StartTime":87864.0,"Objects":[{"StartTime":87864.0,"Position":24.0,"HyperDash":false}]},{"StartTime":88251.0,"Objects":[{"StartTime":88251.0,"Position":249.0,"HyperDash":false},{"StartTime":88347.0,"Position":233.0,"HyperDash":false},{"StartTime":88444.0,"Position":449.0,"HyperDash":false},{"StartTime":88541.0,"Position":411.0,"HyperDash":false},{"StartTime":88638.0,"Position":75.0,"HyperDash":false},{"StartTime":88735.0,"Position":474.0,"HyperDash":false},{"StartTime":88831.0,"Position":176.0,"HyperDash":false},{"StartTime":88928.0,"Position":1.0,"HyperDash":false},{"StartTime":89025.0,"Position":37.0,"HyperDash":false},{"StartTime":89122.0,"Position":481.0,"HyperDash":false},{"StartTime":89219.0,"Position":375.0,"HyperDash":false},{"StartTime":89315.0,"Position":407.0,"HyperDash":false},{"StartTime":89412.0,"Position":231.0,"HyperDash":false},{"StartTime":89509.0,"Position":338.0,"HyperDash":false},{"StartTime":89606.0,"Position":322.0,"HyperDash":false},{"StartTime":89703.0,"Position":347.0,"HyperDash":false},{"StartTime":89800.0,"Position":365.0,"HyperDash":false}]}]}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052.osu
new file mode 100644
index 0000000000..a0cecc1b18
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052.osu
@@ -0,0 +1,210 @@
+osu file format v14
+
+[General]
+AudioFilename: audio.mp3
+AudioLeadIn: 0
+PreviewTime: 65316
+Countdown: 0
+SampleSet: Soft
+StackLeniency: 0.7
+Mode: 2
+LetterboxInBreaks: 0
+WidescreenStoryboard: 0
+
+[Editor]
+DistanceSpacing: 1.4
+BeatDivisor: 4
+GridSize: 8
+TimelineZoom: 1.4
+
+[Metadata]
+Title:Nanairo Symphony -TV Size-
+TitleUnicode:七色シンフォニー -TV Size-
+Artist:Coalamode.
+ArtistUnicode:コアラモード.
+Creator:Ascendance
+Version:Aru's Cup
+Source:四月は君の嘘
+Tags:shigatsu wa kimi no uso your lie in april opening arusamour tenshichan [superstar]
+BeatmapID:1041052
+BeatmapSetID:488149
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:2.5
+OverallDifficulty:6
+ApproachRate:6
+SliderMultiplier:1.02
+SliderTickRate:2
+
+[Events]
+//Background and Video events
+Video,500,"forty.avi"
+0,0,"cropped-1366-768-647733.jpg",0,0
+//Break Periods
+//Storyboard Layer 0 (Background)
+//Storyboard Layer 1 (Fail)
+//Storyboard Layer 2 (Pass)
+//Storyboard Layer 3 (Foreground)
+//Storyboard Sound Samples
+
+[TimingPoints]
+1155,387.096774193548,4,2,1,50,1,0
+15284,-100,4,2,1,60,0,0
+16638,-100,4,2,1,50,0,0
+41413,-100,4,2,1,60,0,0
+59993,-100,4,2,1,65,0,0
+66187,-100,4,2,1,70,0,1
+87284,-100,4,2,1,60,0,1
+87864,-100,4,2,1,70,0,0
+87961,-100,4,2,1,50,0,0
+88638,-100,4,2,1,30,0,0
+89413,-100,4,2,1,10,0,0
+89800,-100,4,2,1,5,0,0
+
+
+[Colours]
+Combo1 : 255,128,64
+Combo2 : 0,128,255
+Combo3 : 255,128,192
+Combo4 : 0,128,192
+
+[HitObjects]
+208,160,1155,6,0,L|45:160,1,153,2|2,0:0|0:0,0:0:0:0:
+160,160,2122,1,0,0:0:0:0:
+272,160,2509,1,2,0:0:0:0:
+448,288,3284,6,0,P|480:240|480:192,1,102,2|0,0:0|0:0,0:0:0:0:
+384,96,4058,1,2,0:0:0:0:
+128,64,5025,6,0,L|32:64,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
+192,64,5800,1,2,0:0:0:0:
+240,64,5993,1,2,0:0:0:0:
+288,64,6187,1,2,0:0:0:0:
+416,80,6574,6,0,L|192:80,1,204,0|2,0:0|0:0,0:0:0:0:
+488,160,8122,2,0,L|376:160,1,102
+457,288,8896,2,0,L|297:288,1,153,2|2,0:0|0:0,0:0:0:0:
+400,288,10058,1,0,0:0:0:0:
+304,288,10445,6,0,L|192:288,2,102,2|0|2,0:0|0:0|0:0,0:0:0:0:
+400,288,11606,1,0,0:0:0:0:
+240,288,11993,2,0,L|80:288,1,153,2|0,0:0|0:0,0:0:0:0:
+0,288,13154,1,0,0:0:0:0:
+112,240,13542,6,0,P|160:288|256:288,1,153,6|2,0:0|0:0,0:0:0:0:
+288,288,14316,2,0,L|368:288,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
+192,288,15284,2,0,L|160:224,1,51,0|12,0:0|0:0,0:0:0:0:
+312,208,15864,1,6,0:0:0:0:
+128,176,16638,6,0,P|64:160|0:96,2,153,6|2|0,0:0|0:0|0:0,0:0:0:0:
+224,176,18187,2,0,P|288:192|352:272,2,153,2|2|0,0:0|0:0|0:0,0:0:0:0:
+128,176,19735,6,0,L|288:176,1,153,2|2,0:0|0:0,0:0:0:0:
+432,176,20896,1,0,0:0:0:0:
+328,176,21284,2,0,L|488:176,1,153,2|2,0:0|0:0,0:0:0:0:
+328,176,22445,1,0,0:0:0:0:
+224,176,22832,6,0,L|64:176,1,153,2|2,0:0|0:0,0:0:0:0:
+224,176,23993,1,0,0:0:0:0:
+112,176,24380,2,0,L|272:176,1,153,2|2,0:0|0:0,0:0:0:0:
+416,176,25541,1,0,0:0:0:0:
+304,256,25929,6,0,P|272:208|312:120,1,153,2|2,0:0|0:0,0:0:0:0:
+480,112,27090,1,0,0:0:0:0:
+384,112,27477,6,0,L|320:112,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
+432,112,28058,1,2,0:0:0:0:
+333,112,28445,2,0,L|282:112,2,51,0|0|0,0:0|0:0|0:0,0:0:0:0:
+384,112,29025,6,0,L|272:112,1,102,6|0,0:0|0:0,0:0:0:0:
+224,112,29606,2,0,P|160:144|160:240,1,153,2|2,0:0|0:0,0:0:0:0:
+272,272,30574,2,0,L|374:272,1,102
+424,272,31154,2,0,P|414:344|348:378,1,153,0|0,0:0|0:0,0:0:0:0:
+224,304,32122,6,0,P|176:320|144:368,1,102,2|0,0:0|0:0,0:0:0:0:
+200,368,32703,1,2,0:0:0:0:
+376,368,33284,1,0,0:0:0:0:
+304,296,33671,2,0,L|240:296,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
+352,296,34251,2,0,P|400:248|384:168,1,153,2|0,0:0|0:0,0:0:0:0:
+280,176,35219,6,0,L|216:80,1,102,2|0,0:0|0:0,0:0:0:0:
+272,104,35800,2,0,L|336:8,1,102,2|0,0:0|0:0,0:0:0:0:
+280,16,36380,1,2,0:0:0:0:
+176,32,36767,6,0,L|112:128,1,102,2|0,0:0|0:0,0:0:0:0:
+168,128,37348,2,0,L|232:224,1,102,2|0,0:0|0:0,0:0:0:0:
+176,224,37928,1,2,0:0:0:0:
+304,264,38316,6,0,L|200:264,1,102,2|0,0:0|0:0,0:0:0:0:
+144,264,38896,1,2,0:0:0:0:
+280,336,39477,2,0,L|336:336,1,51
+424,336,39864,2,0,P|440:304|416:240,1,102,8|0,0:3|0:3,0:3:0:0:
+352,232,40445,1,4,0:1:0:0:
+160,224,41025,1,8,0:3:0:0:
+256,48,41413,6,0,P|302:28|353:31,1,102,6|0,0:0|0:0,0:0:0:0:
+400,40,41993,1,0,0:0:0:0:
+440,80,42187,2,0,P|389:76|342:96,1,102,2|8,0:0|0:0,0:0:0:0:
+248,128,42961,2,0,P|312:176|392:144,2,153,2|2|8,0:0|0:0|0:3,0:0:0:0:
+144,136,44509,6,0,P|80:88|0:120,1,153,2|0,0:0|0:0,0:0:0:0:
+56,136,45284,1,2,0:0:0:0:
+160,144,45671,1,8,0:0:0:0:
+264,144,46058,2,0,L|384:144,1,102,2|0,0:0|0:0,0:0:0:0:
+416,152,46638,2,0,L|264:152,1,153,2|8,0:0|0:3,0:0:0:0:
+360,120,47606,6,0,L|192:120,1,153,2|0,0:0|0:0,0:0:0:0:
+160,128,48380,2,0,P|208:80|256:96,1,102,2|8,0:0|0:0,0:0:0:0:
+144,136,49154,1,2,0:0:0:0:
+248,144,49542,2,0,L|368:144,1,102,0|2,0:0|0:0,0:0:0:0:
+256,192,50316,2,0,L|200:192,1,51,10|0,0:0|0:0,0:0:0:0:
+256,184,50703,6,0,L|360:184,1,102,2|0,0:0|0:0,0:0:0:0:
+400,208,51284,1,0,0:0:0:0:
+352,240,51477,2,0,L|240:240,1,102
+128,336,52251,6,0,P|64:336|0:256,1,153,2|2,0:0|0:0,0:0:0:0:
+88,264,53025,1,2,0:0:0:0:
+168,208,53413,2,0,L|152:144,1,51,8|8,0:0|0:3,0:0:0:0:
+248,120,53800,6,0,P|328:152|392:120,1,153,6|0,0:0|0:0,0:0:0:0:
+432,120,54574,1,2,0:0:0:0:
+328,128,54961,1,8,0:0:0:0:
+224,128,55348,6,0,L|112:144,1,102,2|0,0:0|0:0,0:0:0:0:
+72,152,55929,2,0,L|192:176,1,102,2|0,0:0|0:0,0:0:0:0:
+224,184,56509,1,8,0:3:0:0:
+328,176,56896,6,0,P|376:208|472:192,1,153,2|0,0:0|0:0,0:0:0:0:
+416,208,57671,2,0,L|304:240,1,102,2|8,0:0|0:0,0:0:0:0:
+224,272,58445,5,2,0:0:0:0:
+320,296,58832,1,0,0:0:0:0:
+224,328,59219,1,2,0:0:0:0:
+120,328,59606,1,8,0:3:0:0:
+224,264,59993,6,0,P|224:200|192:152,1,102,6|0,0:0|0:0,0:0:0:0:
+80,184,60767,2,0,P|76:133|97:87,1,102,2|8,0:0|0:0,0:0:0:0:
+200,80,61542,2,0,P|232:112|296:112,1,102,2|0,0:0|0:0,0:0:0:0:
+376,160,62316,2,0,P|344:192|280:192,1,102,2|8,0:0|0:0,0:0:0:0:
+184,240,63090,6,0,L|200:128,1,102,2|8,0:0|0:0,0:0:0:0:
+88,136,63864,2,0,L|8:152,2,76.5,6|2|2,0:0|0:0|0:0,0:0:0:0:
+160,112,64638,1,8,0:0:0:0:
+208,128,64832,1,8,0:0:0:0:
+256,144,65025,1,8,0:0:0:0:
+360,152,65413,6,0,L|424:152,1,51,8|0,0:0|0:0,0:0:0:0:
+462,152,65800,2,0,L|398:152,1,51,8|8,0:0|0:3,0:0:0:0:
+344,144,66187,6,0,L|232:144,1,102,12|8,0:0|0:0,0:0:0:0:
+152,120,66961,2,0,P|148:169|107:196,1,102,8|8,0:0|0:0,0:0:0:0:
+32,264,67735,6,0,L|144:216,1,102,8|8,0:0|0:0,0:0:0:0:
+176,208,68316,1,0,0:0:0:0:
+224,200,68509,2,0,L|317:240,1,102,8|8,0:0|0:0,0:0:0:0:
+216,256,69284,6,0,P|184:304|200:352,1,102,8|8,0:0|0:0,0:0:0:0:
+360,256,70058,2,0,P|368:207|337:167,1,102,8|8,0:0|0:0,0:0:0:0:
+264,80,70832,6,0,L|152:96,1,102,8|8,0:0|0:0,0:0:0:0:
+112,104,71413,2,0,L|11:89,1,102,8|0,0:0|0:0,0:0:0:0:
+40,128,71993,2,0,L|72:176,1,51,8|8,0:0|0:3,0:0:0:0:
+176,216,72380,6,0,P|144:280|64:280,1,153,12|0,0:0|0:0,0:0:0:0:
+120,280,73154,2,0,P|191:299|216:328,1,102,8|8,0:0|0:0,0:0:0:0:
+312,320,73929,6,0,L|424:304,1,102,8|8,0:0|0:0,0:0:0:0:
+336,272,74703,2,0,L|312:216,1,51,8|0,0:0|0:0,0:0:0:0:
+400,200,75090,2,0,L|424:136,1,51,8|0,0:0|0:0,0:0:0:0:
+328,152,75477,6,0,P|280:184|200:136,1,153,12|0,0:0|0:0,0:0:0:0:
+296,136,76251,2,0,P|360:136|408:168,1,102,8|8,0:0|0:0,0:0:0:0:
+152,248,77219,6,0,L|96:248,2,51,0|12|0,0:0|0:0|0:0,0:0:0:0:
+208,248,77800,1,8,0:0:0:0:
+320,256,78187,2,0,L|369:243,1,51,8|8,0:0|0:3,0:0:0:0:
+456,232,78574,6,0,L|408:136,1,102,12|8,0:0|0:0,0:0:0:0:
+288,136,79348,2,0,L|336:40,1,102,8|8,0:0|0:0,0:0:0:0:
+240,80,80122,6,0,P|144:80|128:64,1,102,8|8,0:0|0:0,0:0:0:0:
+96,72,80703,1,0,0:0:0:0:
+40,104,80896,2,0,P|136:104|152:88,1,102,8|8,0:0|0:0,0:0:0:0:
+248,128,81671,6,0,L|296:224,1,102,12|8,0:0|0:0,0:0:0:0:
+208,272,82445,1,10,0:0:0:0:
+312,272,82832,1,8,0:0:0:0:
+400,224,83219,6,0,L|416:160,1,51,8|2,0:0|0:0,0:0:0:0:
+360,56,83606,2,0,L|336:120,1,51,8|0,0:0|0:0,0:0:0:0:
+272,152,83993,2,0,P|192:152|176:136,1,102,0|8,0:0|0:0,0:0:0:0:
+80,160,84767,6,0,L|96:208,1,51,8|0,0:0|0:0,0:0:0:0:
+16,272,85154,2,0,L|16:328,1,51,8|0,0:0|0:0,0:0:0:0:
+104,304,85542,2,0,L|208:304,1,102,2|8,0:0|0:0,0:0:0:0:
+376,336,86316,6,0,L|472:304,1,102,4|0,0:0|0:0,0:0:0:0:
+296,248,87090,2,0,P|312:168|312:136,1,102,2|8,0:0|0:3,0:0:0:0:
+168,96,87864,1,4,0:0:0:0:
+256,192,88251,12,0,89800,0:0:0:0:
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json
new file mode 100644
index 0000000000..a562074fe9
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json
@@ -0,0 +1 @@
+{"Mappings":[{"StartTime":265568.0,"Objects":[{"StartTime":265568.0,"Position":486.0,"HyperDash":false},{"StartTime":265658.0,"Position":465.1873,"HyperDash":false},{"StartTime":265749.0,"Position":463.208435,"HyperDash":false},{"StartTime":265840.0,"Position":465.146484,"HyperDash":false},{"StartTime":265967.0,"Position":459.5862,"HyperDash":false}]}]}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu
new file mode 100644
index 0000000000..ff641d9b0a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu
@@ -0,0 +1,238 @@
+osu file format v14
+
+[General]
+AudioFilename: audio.mp3
+AudioLeadIn: 0
+PreviewTime: 226943
+Countdown: 0
+SampleSet: Soft
+StackLeniency: 0.7
+Mode: 2
+LetterboxInBreaks: 0
+WidescreenStoryboard: 1
+
+[Editor]
+Bookmarks: 85568,86768,90968,265568
+DistanceSpacing: 0.9
+BeatDivisor: 12
+GridSize: 16
+TimelineZoom: 1
+
+[Metadata]
+Title:Snow
+TitleUnicode:Snow
+Artist:Ricky Montgomery
+ArtistUnicode:Ricky Montgomery
+Creator:Crowley
+Version:Bury Me Six Feet in Snow
+Source:
+Tags:indie the honeysticks alternative english
+BeatmapID:2062131
+BeatmapSetID:971028
+
+[Difficulty]
+HPDrainRate:6
+CircleSize:4.2
+OverallDifficulty:8.3
+ApproachRate:8.3
+SliderMultiplier:3.59999990463257
+SliderTickRate:1
+
+[Events]
+//Background and Video events
+0,0,"me.jpg",0,0
+//Break Periods
+//Storyboard Layer 0 (Background)
+//Storyboard Layer 1 (Fail)
+//Storyboard Layer 2 (Pass)
+//Storyboard Layer 3 (Foreground)
+//Storyboard Layer 4 (Overlay)
+//Storyboard Sound Samples
+
+[TimingPoints]
+368,1200,2,2,1,30,1,0
+368,-66.6666666666667,2,2,1,30,0,0
+29168,-58.8235294117647,2,2,1,40,0,0
+30368,-58.8235294117647,2,2,2,40,0,0
+30568,-58.8235294117647,2,2,1,40,0,0
+31368,-58.8235294117647,2,2,2,40,0,0
+31568,-58.8235294117647,2,2,1,40,0,0
+32768,-58.8235294117647,2,2,2,40,0,0
+33568,-58.8235294117647,2,2,2,40,0,0
+33968,-58.8235294117647,2,2,1,40,0,0
+35168,-58.8235294117647,2,2,2,40,0,0
+35968,-58.8235294117647,2,2,1,40,0,0
+36168,-58.8235294117647,2,2,2,40,0,0
+36368,-58.8235294117647,2,2,1,40,0,0
+37568,-58.8235294117647,2,2,2,40,0,0
+37968,-58.8235294117647,2,2,1,40,0,0
+38368,-58.8235294117647,2,2,2,40,0,0
+38768,-58.8235294117647,2,2,1,40,0,0
+39968,-58.8235294117647,2,2,2,40,0,0
+40168,-58.8235294117647,2,2,1,40,0,0
+40968,-58.8235294117647,2,2,2,40,0,0
+41168,-58.8235294117647,2,2,1,40,0,0
+42368,-58.8235294117647,2,2,2,40,0,0
+43168,-58.8235294117647,2,2,2,40,0,0
+43568,-58.8235294117647,2,2,1,40,0,0
+44768,-58.8235294117647,2,2,2,40,0,0
+45768,-58.8235294117647,2,2,2,40,0,0
+45968,-58.8235294117647,2,2,1,50,0,0
+47168,-58.8235294117647,2,2,2,50,0,0
+48368,-62.5,2,2,1,50,0,0
+67568,-58.8235294117647,2,2,1,70,0,1
+84668,-58.8235294117647,2,2,1,5,0,1
+84768,-58.8235294117647,2,2,1,70,0,1
+85068,-58.8235294117647,2,2,1,5,0,1
+85168,-58.8235294117647,2,2,1,70,0,1
+85468,-58.8235294117647,2,2,1,5,0,1
+85568,-58.8235294117647,2,2,1,70,0,1
+86768,-58.8235294117647,2,2,1,30,0,0
+91168,-58.8235294117647,2,2,1,50,0,0
+91568,1200,2,2,1,50,1,0
+91568,-58.8235294117647,2,2,1,50,0,1
+91643,-58.8235294117647,2,2,1,50,0,0
+92768,-58.8235294117647,2,2,2,50,0,0
+92968,-58.8235294117647,2,2,1,50,0,0
+95168,-58.8235294117647,2,2,2,50,0,0
+95368,-58.8235294117647,2,2,1,50,0,0
+97568,-58.8235294117647,2,2,2,50,0,0
+97768,-58.8235294117647,2,2,1,50,0,0
+99968,-58.8235294117647,2,2,2,50,0,0
+100168,-58.8235294117647,2,2,1,50,0,0
+100768,-58.8235294117647,2,2,2,50,0,0
+101168,-58.8235294117647,2,2,1,50,0,0
+102368,-58.8235294117647,2,2,2,50,0,0
+102568,-58.8235294117647,2,2,1,50,0,0
+104768,-58.8235294117647,2,2,2,50,0,0
+104968,-58.8235294117647,2,2,1,50,0,0
+107168,-58.8235294117647,2,2,2,50,0,0
+107368,-58.8235294117647,2,2,1,50,0,0
+108968,-58.8235294117647,2,2,2,50,0,0
+109168,-58.8235294117647,2,2,1,50,0,0
+109568,-58.8235294117647,2,2,2,50,0,0
+109968,-58.8235294117647,2,2,1,50,0,0
+110368,-58.8235294117647,2,2,2,50,0,0
+110768,-100,2,2,1,40,0,0
+127568,-62.5,2,2,2,50,0,0
+127968,-62.5,2,2,1,50,0,0
+128168,-62.5,2,2,2,50,0,0
+129968,-58.8235294117647,2,2,1,50,0,0
+131168,-58.8235294117647,2,2,2,50,0,0
+131368,-58.8235294117647,2,2,1,50,0,0
+133568,-58.8235294117647,2,2,2,50,0,0
+133768,-58.8235294117647,2,2,1,50,0,0
+135968,-58.8235294117647,2,2,2,50,0,0
+136168,-58.8235294117647,2,2,1,50,0,0
+138368,-58.8235294117647,2,2,2,50,0,0
+138568,-58.8235294117647,2,2,1,50,0,0
+139168,-58.8235294117647,2,2,2,50,0,0
+139368,-58.8235294117647,2,2,1,50,0,0
+139568,-58.8235294117647,2,2,1,50,0,0
+140768,-58.8235294117647,2,2,2,50,0,0
+140968,-58.8235294117647,2,2,1,50,0,0
+143168,-58.8235294117647,2,2,2,50,0,0
+143368,-58.8235294117647,2,2,1,50,0,0
+145568,-58.8235294117647,2,2,2,50,0,0
+145768,-58.8235294117647,2,2,1,50,0,0
+147368,-58.8235294117647,2,2,2,50,0,0
+147768,-58.8235294117647,2,2,1,50,0,0
+147968,-58.8235294117647,2,2,1,60,0,0
+148768,-58.8235294117647,2,2,2,60,0,0
+149168,-58.8235294117647,2,2,1,70,0,1
+158268,-58.8235294117647,2,2,2,70,0,1
+158568,-58.8235294117647,2,2,1,70,0,1
+166268,-58.8235294117647,2,2,1,5,0,1
+166368,-58.8235294117647,2,2,1,70,0,1
+166668,-58.8235294117647,2,2,1,5,0,1
+166768,-58.8235294117647,2,2,1,70,0,1
+167068,-58.8235294117647,2,2,1,5,0,1
+167168,-58.8235294117647,2,2,1,70,0,1
+168368,-62.5,2,2,1,50,0,0
+172368,-62.5,2,2,1,50,0,1
+173168,-62.5,2,2,1,50,0,0
+185168,-62.5,2,2,1,60,0,0
+185468,-62.5,2,2,1,5,0,0
+185568,-62.5,2,2,1,60,0,0
+185868,-62.5,2,2,1,5,0,0
+185968,-62.5,2,2,1,60,0,0
+186268,-62.5,2,2,1,5,0,0
+186368,-62.5,2,2,1,60,0,0
+186668,-62.5,2,2,1,5,0,0
+186768,-52.6315789473684,2,2,1,60,0,0
+187068,-62.5,2,2,1,5,0,0
+187168,-62.5,2,2,1,60,0,0
+187468,-62.5,2,2,1,5,0,0
+187568,-62.5,2,2,1,20,0,0
+187768,-62.5,2,2,1,24,0,0
+187968,-62.5,2,2,1,28,0,0
+188168,-62.5,2,2,1,32,0,0
+188368,-62.5,2,2,1,36,0,0
+188568,-62.5,2,2,1,40,0,0
+188768,1200,2,2,1,50,1,1
+188768,-58.8235294117647,2,2,1,50,0,1
+188843,-58.8235294117647,2,2,1,50,0,0
+189968,-58.8235294117647,2,2,2,50,0,0
+190168,-58.8235294117647,2,2,1,50,0,0
+192368,-58.8235294117647,2,2,2,50,0,0
+192568,-58.8235294117647,2,2,1,50,0,0
+194768,-58.8235294117647,2,2,2,50,0,0
+194968,-58.8235294117647,2,2,1,50,0,0
+196568,-58.8235294117647,2,2,2,50,0,0
+196768,-58.8235294117647,2,2,1,50,0,0
+197168,-58.8235294117647,2,2,2,50,0,0
+197368,-58.8235294117647,2,2,1,50,0,0
+197568,-58.8235294117647,2,2,2,50,0,0
+197968,-58.8235294117647,2,2,1,50,0,0
+198368,-58.8235294117647,2,2,1,50,0,0
+199568,-58.8235294117647,2,2,2,50,0,0
+199768,-58.8235294117647,2,2,1,50,0,0
+201968,-58.8235294117647,2,2,2,50,0,0
+202168,-58.8235294117647,2,2,1,50,0,0
+204368,-58.8235294117647,2,2,2,50,0,0
+204568,-58.8235294117647,2,2,1,50,0,0
+206768,-58.8235294117647,2,2,1,60,0,0
+207168,-58.8235294117647,2,2,2,60,0,0
+207968,-58.8235294117647,2,2,1,70,0,1
+216968,-58.8235294117647,2,2,2,70,0,1
+217168,-58.8235294117647,2,2,1,70,0,1
+217368,-58.8235294117647,2,2,2,70,0,1
+217568,-58.8235294117647,2,2,1,70,0,1
+225068,-58.8235294117647,2,2,1,5,0,1
+225168,-58.8235294117647,2,2,1,70,0,1
+225468,-58.8235294117647,2,2,1,5,0,1
+225568,-58.8235294117647,2,2,1,70,0,1
+225868,-58.8235294117647,2,2,1,5,0,1
+225968,-58.8235294117647,2,2,1,70,0,1
+227168,-58.8235294117647,2,2,1,30,0,0
+234368,-58.8235294117647,2,2,1,40,0,0
+236768,-58.8235294117647,2,2,1,70,0,1
+255968,-58.8235294117647,2,2,1,70,0,1
+261168,-58.8235294117647,2,2,1,70,0,1
+263068,-58.8235294117647,2,2,1,70,0,0
+263168,-58.8235294117647,2,2,1,60,0,1
+263243,-58.8235294117647,2,2,1,60,0,0
+264368,-58.8235294117647,2,2,1,60,0,1
+264443,-58.8235294117647,2,2,1,60,0,0
+265568,-444.444444444444,2,2,1,50,0,1
+265643,-444.444444444444,2,2,1,50,0,0
+266768,-444.444444444444,2,2,1,40,0,0
+267968,-444.444444444444,2,2,1,30,0,0
+269168,-444.444444444444,2,2,1,20,0,0
+270368,-444.444444444444,2,2,1,10,0,0
+271168,-444.444444444444,2,2,1,9,0,0
+271568,-444.444444444444,2,2,1,8,0,0
+271968,-444.444444444444,2,2,1,7,0,0
+272368,-444.444444444444,2,2,1,6,0,0
+272768,-444.444444444444,2,2,1,5,0,0
+275168,-444.444444444444,2,2,1,5,0,0
+
+
+[Colours]
+Combo1 : 255,128,128
+Combo2 : 72,72,255
+Combo3 : 192,192,192
+Combo4 : 255,136,79
+
+[HitObjects]
+486,179,265568,6,0,P|461:174|454:174,1,26.999997997284,6|0,1:2|0:0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
index 419a846ec3..825e8c697c 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Mod = new CatchModHidden(),
PassCondition = () => Player.Results.Count > 0
&& Player.ChildrenOfType().Single().Alpha > 0
- && Player.ChildrenOfType().Last().Alpha > 0
+ && Player.ChildrenOfType().First().Alpha > 0
});
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs
new file mode 100644
index 0000000000..1721703e48
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Scoring;
+using osu.Game.Tests.Visual;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public partial class TestSceneCatchReplayHandling : OsuManualInputManagerTestScene
+ {
+ [Test]
+ public void TestReplayDetach()
+ {
+ DrawableCatchRuleset drawableRuleset = null!;
+ float catcherPosition = 0;
+
+ AddStep("create drawable ruleset", () => Child = drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), []));
+ AddStep("attach replay", () => drawableRuleset.SetReplayScore(new Score()));
+ AddStep("store catcher position", () => catcherPosition = drawableRuleset.ChildrenOfType().Single().X);
+ AddStep("hold down left", () => InputManager.PressKey(Key.Left));
+ AddAssert("catcher didn't move", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.EqualTo(catcherPosition));
+ AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
+
+ AddStep("detach replay", () => drawableRuleset.SetReplayScore(null));
+ AddStep("hold down left", () => InputManager.PressKey(Key.Left));
+ AddUntilStep("catcher moved", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.Not.EqualTo(catcherPosition));
+ AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
deleted file mode 100644
index e2fc31d869..0000000000
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System.Linq;
-using NUnit.Framework;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Testing;
-using osu.Framework.Utils;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Catch.Judgements;
-using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawables;
-using osu.Game.Rulesets.Catch.Skinning;
-using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Skinning;
-using osu.Game.Tests.Visual;
-using Direction = osu.Game.Rulesets.Catch.UI.Direction;
-
-namespace osu.Game.Rulesets.Catch.Tests
-{
- public partial class TestSceneCatchSkinConfiguration : OsuTestScene
- {
- private Catcher catcher;
-
- private readonly Container container;
-
- public TestSceneCatchSkinConfiguration()
- {
- Add(container = new Container { RelativeSizeAxes = Axes.Both });
- }
-
- [TestCase(false)]
- [TestCase(true)]
- public void TestCatcherPlateFlipping(bool flip)
- {
- AddStep("setup catcher", () =>
- {
- var skin = new TestSkin { FlipCatcherPlate = flip };
- container.Child = new SkinProvidingContainer(skin)
- {
- Child = catcher = new Catcher(new DroppedObjectContainer())
- {
- Anchor = Anchor.Centre
- }
- };
- });
-
- Fruit fruit = new Fruit();
-
- AddStep("catch fruit", () => catchFruit(fruit, 20));
-
- float position = 0;
-
- AddStep("record fruit position", () => position = getCaughtObjectPosition(fruit));
-
- AddStep("face left", () => catcher.VisualDirection = Direction.Left);
-
- if (flip)
- AddAssert("fruit position changed", () => !Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
- else
- AddAssert("fruit position unchanged", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
-
- AddStep("face right", () => catcher.VisualDirection = Direction.Right);
-
- AddAssert("fruit position restored", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
- }
-
- private float getCaughtObjectPosition(Fruit fruit)
- {
- var caughtObject = catcher.ChildrenOfType().Single(c => c.HitObject == fruit);
- return caughtObject.Parent!.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
- }
-
- private void catchFruit(Fruit fruit, float x)
- {
- fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- var drawableFruit = new DrawableFruit(fruit) { X = x };
- var judgement = fruit.CreateJudgement();
- catcher.OnNewResult(drawableFruit, new CatchJudgementResult(fruit, judgement)
- {
- Type = judgement.MaxResult
- });
- }
-
- private class TestSkin : TrianglesSkin
- {
- public bool FlipCatcherPlate { get; set; }
-
- public TestSkin()
- : base(null!)
- {
- }
-
- public override IBindable GetConfig(TLookup lookup)
- {
- if (lookup is CatchSkinConfiguration config)
- {
- if (config == CatchSkinConfiguration.FlipCatcherPlate)
- return SkinUtils.As(new Bindable(FlipCatcherPlate));
- }
-
- return base.GetConfig(lookup);
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index f60ae29f77..b03fa00f76 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private JudgementResult createResult(CatchHitObject hitObject)
{
- return new CatchJudgementResult(hitObject, hitObject.CreateJudgement())
+ return new CatchJudgementResult(hitObject, hitObject.Judgement)
{
Type = catcher.CanCatch(hitObject) ? HitResult.Great : HitResult.Miss
};
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index c45c85833c..619081c754 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,12 +2,12 @@
-
+
WinExe
- net6.0
+ net8.0
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 8c460586b0..f5c5ffb529 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in ConvertFromLegacyMods(LegacyMods mods)
{
- if (mods.HasFlagFast(LegacyMods.Nightcore))
+ if (mods.HasFlag(LegacyMods.Nightcore))
yield return new CatchModNightcore();
- else if (mods.HasFlagFast(LegacyMods.DoubleTime))
+ else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new CatchModDoubleTime();
- if (mods.HasFlagFast(LegacyMods.Perfect))
+ if (mods.HasFlag(LegacyMods.Perfect))
yield return new CatchModPerfect();
- else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
+ else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new CatchModSuddenDeath();
- if (mods.HasFlagFast(LegacyMods.Cinema))
+ if (mods.HasFlag(LegacyMods.Cinema))
yield return new CatchModCinema();
- else if (mods.HasFlagFast(LegacyMods.Autoplay))
+ else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new CatchModAutoplay();
- if (mods.HasFlagFast(LegacyMods.Easy))
+ if (mods.HasFlag(LegacyMods.Easy))
yield return new CatchModEasy();
- if (mods.HasFlagFast(LegacyMods.Flashlight))
+ if (mods.HasFlag(LegacyMods.Flashlight))
yield return new CatchModFlashlight();
- if (mods.HasFlagFast(LegacyMods.HalfTime))
+ if (mods.HasFlag(LegacyMods.HalfTime))
yield return new CatchModHalfTime();
- if (mods.HasFlagFast(LegacyMods.HardRock))
+ if (mods.HasFlag(LegacyMods.HardRock))
yield return new CatchModHardRock();
- if (mods.HasFlagFast(LegacyMods.Hidden))
+ if (mods.HasFlag(LegacyMods.Hidden))
yield return new CatchModHidden();
- if (mods.HasFlagFast(LegacyMods.NoFail))
+ if (mods.HasFlag(LegacyMods.NoFail))
yield return new CatchModNoFail();
- if (mods.HasFlagFast(LegacyMods.Relax))
+ if (mods.HasFlag(LegacyMods.Relax))
yield return new CatchModRelax();
- if (mods.HasFlagFast(LegacyMods.ScoreV2))
+ if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
@@ -223,6 +223,12 @@ namespace osu.Game.Rulesets.Catch
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
+ public override IEnumerable CreateEditorSetupSections() =>
+ [
+ new DifficultySection(),
+ new ColoursSection(),
+ ];
+
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs b/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs
index 149aae1cb4..596b102ac5 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs
@@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Catch
: base(component)
{
}
-
- protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME;
-
- protected override string ComponentName => Component.ToString().ToLowerInvariant();
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
index f65b6ef381..f931795ff2 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
@@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
}
- int difficultyPeppyStars = (int)Math.Round(
- (baseBeatmap.Difficulty.DrainRate
- + baseBeatmap.Difficulty.OverallDifficulty
- + baseBeatmap.Difficulty.CircleSize
- + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
-
- scoreMultiplier = difficultyPeppyStars;
+ scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index b30b85be2d..55232a9598 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -2,22 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Scoring.Legacy;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchPerformanceCalculator : PerformanceCalculator
{
- private int fruitsHit;
- private int ticksHit;
- private int tinyTicksHit;
- private int tinyTicksMissed;
- private int misses;
+ private int num300;
+ private int num100;
+ private int num50;
+ private int numKatu;
+ private int numMiss;
public CatchPerformanceCalculator()
: base(new CatchRuleset())
@@ -28,11 +27,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
var catchAttributes = (CatchDifficultyAttributes)attributes;
- fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
- ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
- tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
- tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
- misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
+ num300 = score.GetCount300() ?? 0; // HitResult.Great
+ num100 = score.GetCount100() ?? 0; // HitResult.LargeTickHit
+ num50 = score.GetCount50() ?? 0; // HitResult.SmallTickHit
+ numKatu = score.GetCountKatu() ?? 0; // HitResult.SmallTickMiss
+ numMiss = score.GetCountMiss() ?? 0; // HitResult.Miss PLUS HitResult.LargeTickMiss
// We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
@@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
value *= lengthBonus;
- value *= Math.Pow(0.97, misses);
+ value *= Math.Pow(0.97, numMiss);
// Combo scaling
if (catchAttributes.MaxCombo > 0)
@@ -77,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= Math.Pow(accuracy(), 5.5);
if (score.Mods.Any(m => m is ModNoFail))
- value *= 0.90;
+ value *= Math.Max(0.90, 1.0 - 0.02 * numMiss);
return new CatchPerformanceAttributes
{
@@ -86,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
}
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
- private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
- private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
- private int totalComboHits() => misses + ticksHit + fruitsHit;
+ private int totalHits() => num50 + num100 + num300 + numMiss + numKatu;
+ private int totalSuccessfulHits() => num50 + num100 + num300;
+ private int totalComboHits() => numMiss + num100 + num300;
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
index 86f92d16ca..e626392234 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
@@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[Resolved]
private IBeatSnapProvider? beatSnapProvider { get; set; }
+ [Resolved]
+ protected EditorBeatmap? EditorBeatmap { get; private set; }
+
protected EditablePath(Func positionToTime)
{
PositionToTime = positionToTime;
@@ -103,15 +107,23 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
//
// The value is clamped here by the bindable min and max values.
// In case the required velocity is too large, the path is not preserved.
+ double previousVelocity = svBindable.Value;
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
- path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
+ // adjust velocity locally, so that once the SV change is applied by applying defaults
+ // (triggered by `EditorBeatmap.Update()` call at end of method),
+ // it results in the outcome desired by the user.
+ double relativeChange = svBindable.Value / previousVelocity;
+ double localVelocity = hitObject.Velocity * relativeChange;
+ path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, localVelocity);
if (beatSnapProvider == null) return;
double endTime = hitObject.StartTime + path.Duration;
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
- hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
+ hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * localVelocity;
+
+ EditorBeatmap?.Update(hitObject);
}
public Vector2 ToRelativePosition(Vector2 screenSpacePosition)
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
index c7a26ca15a..b2ee43ba16 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
@@ -4,12 +4,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Screens.Edit;
+using osu.Game.Rulesets.Catch.Objects;
using osuTK;
using osuTK.Input;
@@ -19,22 +18,27 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
+ private readonly JuiceStream juiceStream;
+
// To handle when the editor is scrolled while dragging.
private Vector2 dragStartPosition;
- [Resolved]
- private IEditorChangeHandler? changeHandler { get; set; }
-
- public SelectionEditablePath(Func positionToTime)
+ public SelectionEditablePath(JuiceStream juiceStream, Func positionToTime)
: base(positionToTime)
{
+ this.juiceStream = juiceStream;
}
public void AddVertex(Vector2 relativePosition)
{
+ EditorBeatmap?.BeginChange();
+
double time = Math.Max(0, PositionToTime(relativePosition.Y));
int index = AddVertex(time, relativePosition.X);
+ UpdateHitObjectFromPath(juiceStream);
selectOnly(index);
+
+ EditorBeatmap?.EndChange();
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
@@ -45,9 +49,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
if (index == -1 || VertexStates[index].IsFixed)
return false;
- if (e.Button == MouseButton.Left && e.ShiftPressed)
+ if (e.Button == MouseButton.Right && e.ShiftPressed)
{
+ EditorBeatmap?.BeginChange();
RemoveVertex(index);
+ UpdateHitObjectFromPath(juiceStream);
+ EditorBeatmap?.EndChange();
+
return true;
}
@@ -74,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
for (int i = 0; i < VertexCount; i++)
VertexStates[i].VertexBeforeChange = Vertices[i];
- changeHandler?.BeginChange();
+ EditorBeatmap?.BeginChange();
return true;
}
@@ -88,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override void OnDragEnd(DragEndEvent e)
{
- changeHandler?.EndChange();
+ EditorBeatmap?.EndChange();
}
private int getMouseTargetVertex(Vector2 screenSpacePosition)
@@ -118,11 +126,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
private void deleteSelectedVertices()
{
+ EditorBeatmap?.BeginChange();
+
for (int i = VertexCount - 1; i >= 0; i--)
{
if (VertexStates[i].IsSelected)
RemoveVertex(i);
}
+
+ UpdateHitObjectFromPath(juiceStream);
+
+ EditorBeatmap?.EndChange();
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs
index 07d7c72698..a3f8e85278 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
@@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public partial class VertexPiece : Circle
{
+ private VertexState state = new VertexState();
+
[Resolved]
private OsuColour osuColour { get; set; } = null!;
@@ -24,7 +27,32 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateFrom(VertexState state)
{
- Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow;
+ this.state = state;
+ updateMarkerDisplay();
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateMarkerDisplay();
+ return false;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateMarkerDisplay();
+ }
+
+ ///
+ /// Updates the state of the circular control point marker.
+ ///
+ private void updateMarkerDisplay()
+ {
+ var colour = osuColour.Yellow;
+
+ if (IsHovered || state.IsSelected)
+ colour = colour.Lighten(1);
+
+ Colour = colour;
Alpha = state.IsFixed ? 0.5f : 1;
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
index c8c8db1ebd..7b57dac36e 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
base.LoadComplete();
- inputManager = GetContainingInputManager();
+ inputManager = GetContainingInputManager()!;
BeginPlacement();
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
index 49d778ad08..a492920d3a 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(),
- editablePath = new SelectionEditablePath(positionToTime)
+ editablePath = new SelectionEditablePath(hitObject, positionToTime)
};
}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs
index c7a41a4e22..71da6d5014 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs
@@ -13,7 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit
{
private readonly List checks = new List
{
- new CheckBananaShowerGap()
+ new CheckBananaShowerGap(),
+ new CheckCatchAbnormalDifficultySettings(),
};
public IEnumerable Run(BeatmapVerifierContext context)
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index 4172720ada..83f48816f9 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@@ -121,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Edit
result.ScreenSpacePosition.X = screenSpacePosition.X;
- if (snapType.HasFlagFast(SnapType.RelativeGrids))
+ if (snapType.HasFlag(SnapType.RelativeGrids))
{
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
index 418351e2f3..a2784126eb 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
@@ -76,21 +76,38 @@ namespace osu.Game.Rulesets.Catch.Edit
public override bool HandleReverse()
{
+ var hitObjects = EditorBeatmap.SelectedHitObjects
+ .OfType()
+ .OrderBy(obj => obj.StartTime)
+ .ToList();
+
double selectionStartTime = SelectedItems.Min(h => h.StartTime);
double selectionEndTime = SelectedItems.Max(h => h.GetEndTime());
- EditorBeatmap.PerformOnSelection(hitObject =>
- {
- hitObject.StartTime = selectionEndTime - (hitObject.GetEndTime() - selectionStartTime);
+ // the expectation is that even if the objects themselves are reversed temporally,
+ // the position of new combos in the selection should remain the same.
+ // preserve it for later before doing the reversal.
+ var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList();
- if (hitObject is JuiceStream juiceStream)
+ foreach (var h in hitObjects)
+ {
+ h.StartTime = selectionEndTime - (h.GetEndTime() - selectionStartTime);
+
+ if (h is JuiceStream juiceStream)
{
juiceStream.Path.Reverse(out Vector2 positionalOffset);
juiceStream.OriginalX += positionalOffset.X;
juiceStream.LegacyConvertedY += positionalOffset.Y;
EditorBeatmap.Update(juiceStream);
}
- });
+ }
+
+ // re-order objects by start time again after reversing, and restore new combo flag positioning
+ hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
+
+ for (int i = 0; i < hitObjects.Count; ++i)
+ hitObjects[i].NewCombo = newComboOrder[i];
+
return true;
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs
new file mode 100644
index 0000000000..d2c3df0872
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs
@@ -0,0 +1,39 @@
+// 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 osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Rulesets.Catch.Edit.Checks
+{
+ public class CheckCatchAbnormalDifficultySettings : CheckAbnormalDifficultySettings
+ {
+ public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks catch relevant settings");
+
+ public override IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var diff = context.Beatmap.Difficulty;
+ Issue? issue;
+
+ if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
+ yield return issue;
+
+ if (OutOfRange("Approach rate", diff.ApproachRate, out issue))
+ yield return issue;
+
+ if (HasMoreThanOneDecimalPlace("Circle size", diff.CircleSize, out issue))
+ yield return issue;
+
+ if (OutOfRange("Circle size", diff.CircleSize, out issue))
+ yield return issue;
+
+ if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue))
+ yield return issue;
+
+ if (OutOfRange("Drain rate", diff.DrainRate, out issue))
+ yield return issue;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
index 9d88c90576..f933b9a28f 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
@@ -21,7 +21,6 @@ namespace osu.Game.Rulesets.Catch.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
drawableRuleset.PlayfieldAdjustmentContainer.Scale = new Vector2(1, -1);
- drawableRuleset.PlayfieldAdjustmentContainer.Y = 1 - drawableRuleset.PlayfieldAdjustmentContainer.Y;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index 52c42dfddb..329055b3dd 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -15,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Objects
{
- public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation
+ public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation, IHasTimePreempt
{
public const float OBJECT_RADIUS = 64;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
index 03adbce885..9ee4a15182 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
RelativeSizeAxes = Axes.X;
Origin = Anchor.BottomLeft;
- AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both });
+ AddInternal(bananaContainer = new NestedFruitContainer { RelativeSizeAxes = Axes.Both });
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index 7f8c17861d..64705f9909 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -63,7 +63,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
if (CheckPosition == null) return;
if (timeOffset >= 0 && Result != null)
- ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult);
+ {
+ if (CheckPosition.Invoke(HitObject))
+ ApplyMaxResult();
+ else
+ ApplyMinResult();
+ }
}
protected override void UpdateHitStateTransforms(ArmedState state)
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
index 41ecf59276..677b61df47 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
RelativeSizeAxes = Axes.X;
Origin = Anchor.BottomLeft;
- AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, });
+ AddInternal(dropletContainer = new NestedFruitContainer { RelativeSizeAxes = Axes.Both, });
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.cs
new file mode 100644
index 0000000000..90bdb0237e
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public partial class NestedFruitContainer : Container
+ {
+ ///
+ /// This comparison logic is a copy of comparison logic,
+ /// which can't be easily extracted to a more common place.
+ ///
+ ///
+ protected override int Compare(Drawable x, Drawable y)
+ {
+ if (x is not DrawableCatchHitObject xObj || y is not DrawableCatchHitObject yObj)
+ return base.Compare(x, y);
+
+ int result = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
+ return result == 0 ? CompareReverseChildID(x, y) : result;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 671291ef0e..9e7e0b6042 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
@@ -29,7 +30,6 @@ namespace osu.Game.Rulesets.Catch.Objects
public BindableNumber SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
{
- Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
@@ -47,16 +47,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TickDistanceMultiplier = 1;
[JsonIgnore]
- private double velocityFactor;
+ public double Velocity { get; private set; }
[JsonIgnore]
- private double tickDistanceFactor;
-
- [JsonIgnore]
- public double Velocity => velocityFactor * SliderVelocityMultiplier;
-
- [JsonIgnore]
- public double TickDistance => tickDistanceFactor * TickDistanceMultiplier;
+ public double TickDistance { get; private set; }
///
/// The length of one span of this .
@@ -69,14 +63,21 @@ namespace osu.Game.Rulesets.Catch.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
- tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
+ Velocity = base_scoring_distance * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, CatchRuleset.SHORT_NAME);
+
+ // WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
+ // for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
+ double scoringDistance = Velocity * timingPoint.BeatLength;
+
+ TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
+ this.PopulateNodeSamples();
+
var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();
int nodeIndex = 0;
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
index c3cc488941..b2509091fe 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@@ -21,6 +22,23 @@ namespace osu.Game.Rulesets.Catch.Scoring
protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty();
+ protected override bool CheckDefaultFailCondition(JudgementResult result)
+ {
+ // matches stable.
+ // see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
+ // the above early-return skips the failure check at the end of the same method:
+ // https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
+ // making it impossible to fail on a tiny droplet regardless of result.
+ if (result.Type == HitResult.SmallTickMiss)
+ return false;
+
+ // on stable, banana showers don't exist as concrete objects themselves, so they can't cause a fail.
+ if (result.HitObject is BananaShower)
+ return false;
+
+ return base.CheckDefaultFailCondition(result);
+ }
+
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
{
double increase = 0;
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index 4b3d378889..12a4182bf1 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -20,22 +21,75 @@ namespace osu.Game.Rulesets.Catch.Scoring
private const int combo_cap = 200;
private const double combo_base = 4;
+ private double fruitTinyScale;
+
public CatchScoreProcessor()
: base(new CatchRuleset())
{
}
+ protected override void Reset(bool storeResults)
+ {
+ base.Reset(storeResults);
+
+ // large ticks are *purposefully* not counted to match stable
+ int fruitTinyScaleDivisor = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) + MaximumResultCounts.GetValueOrDefault(HitResult.Great);
+ fruitTinyScale = fruitTinyScaleDivisor == 0
+ ? 0
+ : (double)MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor;
+ }
+
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
- return 600000 * comboProgress
- + 400000 * Accuracy.Value * accuracyProgress
+ const int max_tiny_droplets_portion = 400000;
+
+ double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale);
+ double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale;
+ double dropletsHit = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) == 0
+ ? 0
+ : (double)ScoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit);
+
+ return comboPortion * comboProgress
+ + dropletsPortion * dropletsHit
+ bonusPortion;
}
- protected override double GetComboScoreChange(JudgementResult result)
- => GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
+ public override int GetBaseScoreForResult(HitResult result)
+ {
+ switch (result)
+ {
+ // dirty hack to emulate accuracy on stable weighting every object equally in accuracy portion
+ case HitResult.Great:
+ case HitResult.LargeTickHit:
+ case HitResult.SmallTickHit:
+ return 300;
- public override ScoreRank RankFromAccuracy(double accuracy)
+ case HitResult.LargeBonus:
+ return 200;
+ }
+
+ return base.GetBaseScoreForResult(result);
+ }
+
+ protected override double GetComboScoreChange(JudgementResult result)
+ {
+ double baseIncrease = 0;
+
+ switch (result.Type)
+ {
+ case HitResult.Great:
+ baseIncrease = 300;
+ break;
+
+ case HitResult.LargeTickHit:
+ baseIncrease = 100;
+ break;
+ }
+
+ return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
+ }
+
+ public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results)
{
if (accuracy == accuracy_cutoff_x)
return ScoreRank.X;
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs
index a67945df98..520c2de248 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs
@@ -6,7 +6,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Skinning.Argon
{
- public class CatchArgonSkinTransformer : ArgonSkinTransformer
+ public class CatchArgonSkinTransformer : SkinTransformer
{
public CatchArgonSkinTransformer(ISkin skin)
: base(skin)
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs
deleted file mode 100644
index ea8d742b1a..0000000000
--- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Rulesets.Catch.Skinning
-{
- public enum CatchSkinConfiguration
- {
- ///
- /// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
- ///
- FlipCatcherPlate
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index 675c61a2c5..44fc3ecc07 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Diagnostics;
+using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
+using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
@@ -30,15 +32,39 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (lookup)
{
case SkinComponentsContainerLookup containerLookup:
- switch (containerLookup.Target)
- {
- case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null:
- Debug.Assert(containerLookup.Ruleset.ShortName == CatchRuleset.SHORT_NAME);
- // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
- return Skin.GetDrawableComponent(lookup);
- }
+ if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents)
+ return base.GetDrawableComponent(lookup);
- break;
+ // Modifications for global components.
+ if (containerLookup.Ruleset == null)
+ return base.GetDrawableComponent(lookup) as Container;
+
+ // Skin has configuration.
+ if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
+ return d;
+
+ // Our own ruleset components default.
+ // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
+ return new DefaultSkinComponentsContainer(container =>
+ {
+ var keyCounter = container.OfType().FirstOrDefault();
+
+ if (keyCounter != null)
+ {
+ // set the anchor to top right so that it won't squash to the return button to the top
+ keyCounter.Anchor = Anchor.CentreRight;
+ keyCounter.Origin = Anchor.CentreRight;
+ keyCounter.X = 0;
+ // 340px is the default height inherit from stable
+ keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
+ }
+ })
+ {
+ Children = new Drawable[]
+ {
+ new LegacyKeyCounterDisplay(),
+ }
+ };
case CatchSkinComponentLookup catchSkinComponent:
switch (catchSkinComponent.Component)
@@ -114,19 +140,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
return (IBindable)result;
-
- case CatchSkinConfiguration config:
- switch (config)
- {
- case CatchSkinConfiguration.FlipCatcherPlate:
- // Don't flip catcher plate contents if the catcher is provided by this legacy skin.
- if (GetDrawableComponent(new CatchSkinComponentLookup(CatchSkinComponents.Catcher)) != null)
- return (IBindable)new Bindable();
-
- break;
- }
-
- break;
}
return base.GetConfig(lookup);
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
index 11531011ee..74dfa6c1fd 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
@@ -17,24 +17,36 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfieldAdjustmentContainer()
{
- Anchor = Anchor.TopCentre;
- Origin = Anchor.TopCentre;
+ const float base_game_width = 1024f;
+ const float base_game_height = 768f;
- // playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
- // we can match that in lazer by using relative coordinates for Y and considering window height to be 1, and playfield height to be 0.8.
- RelativePositionAxes = Axes.Y;
- Y = (1 - playfield_size_adjust) / 4 * 3;
+ // extra bottom space for the catcher to not get cut off at tall resolutions lower than 4:3 (e.g. 5:4). number chosen based on testing with maximum catcher scale (i.e. CS 0).
+ const float extra_bottom_space = 200f;
- Size = new Vector2(playfield_size_adjust);
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
InternalChild = new Container
{
+ // This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits).
+ // Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off.
+ Name = "Visible area",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
- FillAspectRatio = 4f / 3,
- Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both, }
+ RelativeSizeAxes = Axes.X,
+ Height = base_game_height + extra_bottom_space,
+ Y = extra_bottom_space / 2,
+ Masking = true,
+ Child = new Container
+ {
+ Name = "Playable area",
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ // playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
+ Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3),
+ Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust,
+ Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
+ },
};
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 147850a9b7..dca01fc61a 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -112,11 +112,6 @@ namespace osu.Game.Rulesets.Catch.UI
public Vector2 BodyScale => Scale * body.Scale;
- ///
- /// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
- ///
- private bool flipCatcherPlate;
-
///
/// Width of the area that can be used to attempt catches during gameplay.
///
@@ -339,8 +334,6 @@ namespace osu.Game.Rulesets.Catch.UI
skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
DEFAULT_HYPER_DASH_COLOUR;
- flipCatcherPlate = skin.GetConfig(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
-
runHyperDashStateTransition(HyperDashing);
}
@@ -352,8 +345,7 @@ namespace osu.Game.Rulesets.Catch.UI
body.Scale = scaleFromDirection;
// Inverse of catcher scale is applied here, as catcher gets scaled by circle size and so do the incoming fruit.
- caughtObjectContainer.Scale = (1 / Scale.X) * (flipCatcherPlate ? scaleFromDirection : Vector2.One);
- hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
+ caughtObjectContainer.Scale = new Vector2(1 / Scale.X);
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 567c288b47..21faec56de 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
- var replayState = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
+ var replayState = (GetContainingInputManager()!.CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
SetCatcherPosition(
replayState?.CatcherX ??
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index f0a327d7ac..32ebdc1159 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -16,6 +16,8 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
@@ -52,5 +54,7 @@ namespace osu.Game.Rulesets.Catch.UI
protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
public override DrawableHitObject? CreateDrawableRepresentation(CatchHitObject h) => null;
+
+ protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
}
}
diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
index ecce7c1b3f..a5138ffb39 100644
--- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
+++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
@@ -1,6 +1,6 @@
- net6.0
+ net8.0
Library
true
catch the fruit. to the beat.
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
index f5a49210ea..df4930419c 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
index 25335754d2..2866508a02 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
@@ -1,7 +1,7 @@
- net6.0-android
+ net8.0-android
Exe
osu.Game.Rulesets.Mania.Tests
osu.Game.Rulesets.Mania.Tests.Android
@@ -21,4 +21,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
index 51e07dd6c1..d51e541e95 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
@@ -1,7 +1,7 @@
Exe
- net6.0-ios
+ net8.0-ios
13.4
osu.Game.Rulesets.Mania.Tests
osu.Game.Rulesets.Mania.Tests.iOS
diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json
index f6a067a831..b8dafda8b5 100644
--- a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json
+++ b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Mania.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.Mania.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Mania.Tests.dll"
+ "${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.Mania.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs
new file mode 100644
index 0000000000..b40a62176c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Rulesets.Mania.Edit.Checks;
+using System.Linq;
+
+namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
+{
+ [TestFixture]
+ public class CheckKeyCountTest
+ {
+ private CheckKeyCount check = null!;
+
+ private IBeatmap beatmap = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckKeyCount();
+
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ Ruleset = new ManiaRuleset().RulesetInfo
+ }
+ };
+ }
+
+ [Test]
+ public void TestKeycountFour()
+ {
+ beatmap.Difficulty.CircleSize = 4;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestKeycountSmallerThanFour()
+ {
+ beatmap.Difficulty.CircleSize = 1;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountTooLow);
+ }
+
+ private BeatmapVerifierContext getContext()
+ {
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs
new file mode 100644
index 0000000000..da5ab037e5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs
@@ -0,0 +1,121 @@
+// 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.Mania.Edit.Checks;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
+{
+ [TestFixture]
+ public class CheckManiaAbnormalDifficultySettingsTest
+ {
+ private CheckManiaAbnormalDifficultySettings check = null!;
+
+ private readonly IBeatmap beatmap = new Beatmap();
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckManiaAbnormalDifficultySettings();
+
+ beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo;
+ beatmap.Difficulty = new BeatmapDifficulty
+ {
+ OverallDifficulty = 5,
+ DrainRate = 5,
+ };
+ }
+
+ [Test]
+ public void TestNormalSettings()
+ {
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestOverallDifficultyTwoDecimals()
+ {
+ beatmap.Difficulty.OverallDifficulty = 5.55f;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
+ }
+
+ [Test]
+ public void TestDrainRateTwoDecimals()
+ {
+ beatmap.Difficulty.DrainRate = 5.55f;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
+ }
+
+ [Test]
+ public void TestOverallDifficultyUnder()
+ {
+ beatmap.Difficulty.OverallDifficulty = -10;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestDrainRateUnder()
+ {
+ beatmap.Difficulty.DrainRate = -10;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestOverallDifficultyOver()
+ {
+ beatmap.Difficulty.OverallDifficulty = 20;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestDrainRateOver()
+ {
+ beatmap.Difficulty.DrainRate = 20;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ private BeatmapVerifierContext getContext()
+ {
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs
index 762238be47..ef4e09c26b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs
@@ -1,13 +1,18 @@
// 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.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Visual;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
@@ -30,5 +35,43 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(ManiaRulesetSetting.ScrollDirection, direction);
}
+
+ [Test]
+ public void TestReloadOnBPMChange()
+ {
+ HitObjectComposer oldComposer = null!;
+
+ AddStep("store composer", () => oldComposer = this.ChildrenOfType().Single());
+ AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
+ AddStep("switch to timing tab", () => InputManager.Key(Key.F3));
+ AddUntilStep("wait for loaded", () => this.ChildrenOfType().ElementAtOrDefault(1), () => Is.Not.Null);
+ AddStep("change timing point BPM", () =>
+ {
+ var bpmControl = this.ChildrenOfType().ElementAt(1);
+ InputManager.MoveMouseTo(bpmControl);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("switch back to composer", () => InputManager.Key(Key.F1));
+ AddUntilStep("composer reloaded", () =>
+ {
+ var composer = this.ChildrenOfType().SingleOrDefault();
+ return composer != null && composer != oldComposer;
+ });
+
+ AddStep("store composer", () => oldComposer = this.ChildrenOfType().Single());
+ AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
+ AddStep("undo", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.Z);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddUntilStep("composer reloaded", () =>
+ {
+ var composer = this.ChildrenOfType().SingleOrDefault();
+ return composer != null && composer != oldComposer;
+ });
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index fbc0ed1785..127beed83e 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@@ -17,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual;
using osuTK;
@@ -84,6 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public partial class TestHitObjectComposer : HitObjectComposer
{
public override Playfield Playfield { get; }
+ public override ComposeBlueprintContainer BlueprintContainer => throw new NotImplementedException();
public override IEnumerable HitObjects => Enumerable.Empty();
public override bool CursorInPlacementArea => false;
@@ -100,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
- throw new System.NotImplementedException();
+ throw new NotImplementedException();
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs
new file mode 100644
index 0000000000..9765648f44
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Setup;
+using osu.Game.Tests.Visual;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Mania.Tests.Editor
+{
+ public partial class TestSceneManiaEditorSaving : EditorSavingTestScene
+ {
+ protected override Ruleset CreateRuleset() => new ManiaRuleset();
+
+ [Test]
+ public void TestKeyCountChange()
+ {
+ LabelledSliderBar keyCount = null!;
+
+ AddStep("go to setup screen", () => InputManager.Key(Key.F4));
+ AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType().Single().ChildrenOfType>().First(), () => Is.Not.Null);
+ AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
+ AddStep("change key count to 8", () =>
+ {
+ keyCount.Current.Value = 8;
+ });
+ AddUntilStep("dialog visible", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog, Is.InstanceOf);
+ AddStep("refuse", () => InputManager.Key(Key.Number2));
+ AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
+
+ AddStep("change key count to 8 again", () =>
+ {
+ keyCount.Current.Value = 8;
+ });
+ AddUntilStep("dialog visible", () => Game.ChildrenOfType().Single().CurrentDialog, Is.InstanceOf);
+ AddStep("acquiesce", () => InputManager.Key(Key.Number1));
+ AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
index 8e0b51dcf8..d88f488582 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
@@ -186,8 +186,106 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
- AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
- AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
+ AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
+ AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
+ }
+
+ [Test]
+ public void TestDragHoldNoteHead()
+ {
+ setScrollStep(ScrollingDirection.Down);
+
+ HoldNote holdNote = null;
+ AddStep("setup beatmap", () =>
+ {
+ composer.EditorBeatmap.Clear();
+ composer.EditorBeatmap.Add(holdNote = new HoldNote
+ {
+ Column = 1,
+ StartTime = 250,
+ EndTime = 750,
+ });
+ });
+
+ DrawableHoldNote drawableHoldNote = null;
+ EditHoldNoteEndPiece headPiece = null;
+
+ AddStep("select blueprint", () =>
+ {
+ drawableHoldNote = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(drawableHoldNote);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddStep("grab hold note head", () =>
+ {
+ headPiece = this.ChildrenOfType().First();
+ InputManager.MoveMouseTo(headPiece);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("drag head downwards", () =>
+ {
+ InputManager.MoveMouseTo(headPiece, new Vector2(0, 100));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("start time moved back", () => holdNote!.StartTime, () => Is.LessThan(250));
+ AddAssert("end time unchanged", () => holdNote.EndTime, () => Is.EqualTo(750));
+
+ AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft));
+ AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
+
+ AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition);
+ AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition);
+ }
+
+ [Test]
+ public void TestDragHoldNoteTail()
+ {
+ setScrollStep(ScrollingDirection.Down);
+
+ HoldNote holdNote = null;
+ AddStep("setup beatmap", () =>
+ {
+ composer.EditorBeatmap.Clear();
+ composer.EditorBeatmap.Add(holdNote = new HoldNote
+ {
+ Column = 1,
+ StartTime = 250,
+ EndTime = 750,
+ });
+ });
+
+ DrawableHoldNote drawableHoldNote = null;
+ EditHoldNoteEndPiece tailPiece = null;
+
+ AddStep("select blueprint", () =>
+ {
+ drawableHoldNote = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(drawableHoldNote);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddStep("grab hold note tail", () =>
+ {
+ tailPiece = this.ChildrenOfType().Last();
+ InputManager.MoveMouseTo(tailPiece);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("drag tail upwards", () =>
+ {
+ InputManager.MoveMouseTo(tailPiece, new Vector2(0, -100));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("start time unchanged", () => holdNote!.StartTime, () => Is.EqualTo(250));
+ AddAssert("end time moved forward", () => holdNote.EndTime, () => Is.GreaterThan(750));
+
+ AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft));
+ AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
+
+ AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition);
+ AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition);
}
private void setScrollStep(ScrollingDirection direction)
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs
new file mode 100644
index 0000000000..b48f579ec0
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.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 System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Visual;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Mania.Tests.Editor
+{
+ public partial class TestSceneManiaSelectionHandler : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ [Test]
+ public void TestHorizontalFlipOverSelection()
+ {
+ ManiaHitObject first = null!, second = null!, third = null!;
+
+ AddStep("create objects", () =>
+ {
+ EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
+ EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
+ EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
+ });
+
+ AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ AddStep("flip horizontally over selection", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("first object stayed in place", () => first.Column, () => Is.EqualTo(2));
+ AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(3));
+ AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestHorizontalFlipOverPlayfield()
+ {
+ ManiaHitObject first = null!, second = null!, third = null!;
+
+ AddStep("create objects", () =>
+ {
+ EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
+ EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
+ EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
+ });
+
+ AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ AddStep("flip horizontally", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.H);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ AddAssert("first object flipped", () => first.Column, () => Is.EqualTo(1));
+ AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(2));
+ AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(0));
+ }
+
+ [Test]
+ public void TestVerticalFlip()
+ {
+ ManiaHitObject first = null!, second = null!, third = null!;
+
+ AddStep("create objects", () =>
+ {
+ EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
+ EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
+ EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
+ });
+
+ AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ AddStep("flip vertically", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.J);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ AddAssert("first object flipped", () => first.StartTime, () => Is.EqualTo(2250));
+ AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
+ AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs
index 315849f7de..a9771a46f3 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
@@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
// No matter what, mania doesn't have passive HP drain.
Assert.That(processor.DrainRate, Is.Zero);
}
+
+ private static readonly object[][] test_cases =
+ [
+ // hitobject, starting HP, fail expected after miss
+ [new Note(), 0.01, true],
+ [new HeadNote(), 0.01, true],
+ [new TailNote(), 0.01, true],
+ [new HoldNoteBody(), 0, true], // hold note break
+ [new HoldNote(), 0, true],
+ ];
+
+ [TestCaseSource(nameof(test_cases))]
+ public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
+ {
+ var healthProcessor = new ManiaHealthProcessor(0);
+ healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
+ {
+ HitObjects = { hitObject }
+ });
+ healthProcessor.Health.Value = startingHealth;
+
+ var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
+ result.Type = result.Judgement.MinResult;
+ healthProcessor.ApplyResult(result);
+
+ Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
+ }
+
+ [TestCaseSource(nameof(test_cases))]
+ public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
+ {
+ var healthProcessor = new ManiaHealthProcessor(0);
+ healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
+ {
+ HitObjects = { hitObject }
+ });
+ healthProcessor.Health.Value = startingHealth;
+
+ var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
+ result.Type = result.Judgement.MaxResult;
+ healthProcessor.ApplyResult(result);
+
+ Assert.That(healthProcessor.HasFailed, Is.False);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs
index 2c8c151e7f..9620897983 100644
--- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs
@@ -1,8 +1,18 @@
// 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.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
@@ -11,9 +21,80 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
- [TestCase(0.5f)]
- [TestCase(0.1f)]
- [TestCase(0.7f)]
- public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModFadeIn { Coverage = { Value = coverage } }, PassCondition = () => true });
+ [Test]
+ public void TestMinCoverageFullWidth()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
+ });
+ }
+
+ [Test]
+ public void TestMinCoverageHalfWidth()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
+ });
+
+ AddStep("set playfield width to 0.5", () => Player.Width = 0.5f);
+ }
+
+ [Test]
+ public void TestMaxCoverageFullWidth()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
+ });
+
+ AddStep("set combo to 480", () => Player.ScoreProcessor.Combo.Value = 480);
+ }
+
+ [Test]
+ public void TestMaxCoverageHalfWidth()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
+ });
+
+ AddStep("set combo to 480", () => Player.ScoreProcessor.Combo.Value = 480);
+ AddStep("set playfield width to 0.5", () => Player.Width = 0.5f);
+ }
+
+ [Test]
+ public void TestNoCoverageDuringBreak()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ Beatmap = new Beatmap
+ {
+ HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
+ Breaks = { new BreakPeriod(2000, 28000) }
+ },
+ PassCondition = () => Player.IsBreakTime.Value && checkCoverage(0)
+ });
+ }
+
+ private bool checkCoverage(float expected)
+ {
+ Drawable? cover = this.ChildrenOfType().FirstOrDefault();
+ Drawable? filledArea = cover?.ChildrenOfType().LastOrDefault();
+
+ if (filledArea == null)
+ return false;
+
+ float scale = cover!.DrawHeight / (768 - Stage.HIT_TARGET_POSITION);
+
+ // A bit of lenience because the test may end up hitting hitobjects before any assertions.
+ return Precision.AlmostEquals(filledArea.DrawHeight / scale, expected, 0.1);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs
index 204f26f151..ae23c4573c 100644
--- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs
@@ -1,8 +1,18 @@
// 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.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
@@ -11,9 +21,80 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
- [TestCase(0.5f)]
- [TestCase(0.2f)]
- [TestCase(0.8f)]
- public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModHidden { Coverage = { Value = coverage } }, PassCondition = () => true });
+ [Test]
+ public void TestMinCoverageFullWidth()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
+ });
+ }
+
+ [Test]
+ public void TestMinCoverageHalfWidth()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
+ });
+
+ AddStep("set playfield width to 0.5", () => Player.Width = 0.5f);
+ }
+
+ [Test]
+ public void TestMaxCoverageFullWidth()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
+ });
+
+ AddStep("set combo to 480", () => Player.ScoreProcessor.Combo.Value = 480);
+ }
+
+ [Test]
+ public void TestMaxCoverageHalfWidth()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
+ });
+
+ AddStep("set combo to 480", () => Player.ScoreProcessor.Combo.Value = 480);
+ AddStep("set playfield width to 0.5", () => Player.Width = 0.5f);
+ }
+
+ [Test]
+ public void TestNoCoverageDuringBreak()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModHidden(),
+ Beatmap = new Beatmap
+ {
+ HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
+ Breaks = { new BreakPeriod(2000, 28000) }
+ },
+ PassCondition = () => Player.IsBreakTime.Value && checkCoverage(0)
+ });
+ }
+
+ private bool checkCoverage(float expected)
+ {
+ Drawable? cover = this.ChildrenOfType().FirstOrDefault();
+ Drawable? filledArea = cover?.ChildrenOfType().LastOrDefault();
+
+ if (filledArea == null)
+ return false;
+
+ float scale = cover!.DrawHeight / (768 - Stage.HIT_TARGET_POSITION);
+
+ // A bit of lenience because the test may end up hitting hitobjects before any assertions.
+ return Precision.AlmostEquals(filledArea.DrawHeight / scale, expected, 0.1);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs
index 2977241dc6..95fe73db50 100644
--- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
@@ -17,5 +19,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Mod = new ManiaModInvert(),
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
});
+
+ [Test]
+ public void TestBreaksPreservedOnOriginalBeatmap()
+ {
+ var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo);
+ beatmap.Breaks.Clear();
+ beatmap.Breaks.Add(new BreakPeriod(0, 1000));
+
+ var workingBeatmap = new FlatWorkingBeatmap(beatmap);
+
+ var playableWithInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo, new[] { new ManiaModInvert() });
+ Assert.That(playableWithInvert.Breaks.Count, Is.Zero);
+
+ var playableWithoutInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo);
+ Assert.That(playableWithoutInvert.Breaks.Count, Is.Not.Zero);
+ Assert.That(playableWithoutInvert.Breaks[0], Is.EqualTo(new BreakPeriod(0, 1000)));
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs
new file mode 100644
index 0000000000..f6e79114de
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs
@@ -0,0 +1,643 @@
+// 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.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Mods
+{
+ public partial class TestSceneManiaModNoRelease : RateAdjustedBeatmapTestScene
+ {
+ private const double time_before_head = 250;
+ private const double time_head = 1500;
+ private const double time_during_hold_1 = 2500;
+ private const double time_tail = 4000;
+ private const double time_after_tail = 5250;
+
+ private List judgementResults = new List();
+
+ ///
+ /// -----[ ]-----
+ /// o o
+ ///
+ [Test]
+ public void TestNoInput()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ assertNoteJudgement(HitResult.IgnoreMiss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestCorrectInput()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Perfect);
+ assertNoteJudgement(HitResult.IgnoreHit);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestLateRelease()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Perfect);
+ assertNoteJudgement(HitResult.IgnoreHit);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestPressTooEarlyAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail, ManiaAction.Key1),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestPressTooEarlyAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o
+ ///
+ [Test]
+ public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_before_head + 10),
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Perfect);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o
+ ///
+ [Test]
+ public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_before_head + 10),
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Perfect);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAtStartAndBreak()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 10),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xox o
+ ///
+ [Test]
+ public void TestPressAtStartThenReleaseAndImmediatelyRepress()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 1),
+ new ManiaReplayFrame(time_head + 2, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertComboAtJudgement(0, 1);
+ assertTailJudgement(HitResult.Meh);
+ assertComboAtJudgement(1, 0);
+ assertComboAtJudgement(3, 1);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o
+ ///
+ [Test]
+ public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 10),
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o o
+ ///
+ [Test]
+ public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 10),
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestPressDuringNoteAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o o
+ ///
+ [Test]
+ public void TestPressDuringNoteAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ ///
+ /// -----[ ]--------------
+ /// xo
+ ///
+ [Test]
+ public void TestPressAndReleaseAfterTailWithCloseByHead()
+ {
+ const int duration = 30;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ // hold note is very short, to make the head still in range
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = duration,
+ Column = 0,
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head + duration + 60, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + duration + 70),
+ }, beatmap);
+
+ assertHeadJudgement(HitResult.Ok);
+ assertTailJudgement(HitResult.Perfect);
+ }
+
+ ///
+ /// -----[ ]-O-------------
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAndReleaseJustBeforeTailWithNearbyNoteAndCloseByHead()
+ {
+ Note note;
+
+ const int duration = 50;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ // hold note is very short, to make the head still in range
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = duration,
+ Column = 0,
+ },
+ {
+ // Next note within tail lenience
+ note = new Note
+ {
+ StartTime = time_head + duration + 10
+ }
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head + duration, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + duration + 10),
+ }, beatmap);
+
+ assertHeadJudgement(HitResult.Good);
+ assertTailJudgement(HitResult.Perfect);
+
+ assertHitObjectJudgement(note, HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]--O--
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAndReleaseJustBeforeTailWithNearbyNote()
+ {
+ Note note;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = time_tail - time_head,
+ Column = 0,
+ },
+ {
+ // Next note within tail lenience
+ note = new Note
+ {
+ StartTime = time_tail + 50
+ }
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_tail - 10, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ }, beatmap);
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+
+ assertHitObjectJudgement(note, HitResult.Good);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo
+ ///
+ [Test]
+ public void TestPressAndReleaseJustAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_tail + 20, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail + 30),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ ///
+ /// -----[ ]--O--
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAndReleaseJustAfterTailWithNearbyNote()
+ {
+ // Next note within tail lenience
+ Note note = new Note { StartTime = time_tail + 50 };
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = time_tail - time_head,
+ Column = 0,
+ },
+ note
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_tail + 10, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail + 20),
+ }, beatmap);
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+
+ assertHitObjectJudgement(note, HitResult.Great);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_tail, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail + 10),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ [Test]
+ public void TestMissReleaseAndHitSecondRelease()
+ {
+ var windows = new ManiaHitWindows();
+ windows.SetDifficulty(10);
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = 1000,
+ Duration = 500,
+ Column = 0,
+ },
+ new HoldNote
+ {
+ StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10,
+ Duration = 500,
+ Column = 0,
+ },
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty
+ {
+ SliderTickRate = 4,
+ OverallDifficulty = 10,
+ },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1),
+ new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()),
+ }, beatmap);
+
+ AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
+ .All(j => !j.Type.IsHit()));
+
+ AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
+ .All(j => j.Type.IsHit()));
+ }
+
+ [Test]
+ public void TestZeroLength()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = 1000,
+ Duration = 0,
+ Column = 0,
+ },
+ },
+ BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(beatmap.HitObjects[0].StartTime, ManiaAction.Key1),
+ new ManiaReplayFrame(beatmap.HitObjects[0].GetEndTime() + 1),
+ }, beatmap);
+
+ AddAssert("hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
+ .All(j => j.Type.IsHit()));
+ }
+
+ private void assertHitObjectJudgement(HitObject hitObject, HitResult result)
+ => AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result));
+
+ private void assertHeadJudgement(HitResult result)
+ => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type, () => Is.EqualTo(result));
+
+ private void assertTailJudgement(HitResult result)
+ => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type, () => Is.EqualTo(result));
+
+ private void assertNoteJudgement(HitResult result)
+ => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
+
+ private void assertComboAtJudgement(int judgementIndex, int combo)
+ => AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo));
+
+ private ScoreAccessibleReplayPlayer currentPlayer = null!;
+
+ private void performTest(List frames, Beatmap? beatmap = null)
+ {
+ if (beatmap == null)
+ {
+ beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = time_tail - time_head,
+ Column = 0,
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo,
+ },
+ };
+
+ beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
+ }
+
+ AddStep("load player", () =>
+ {
+ SelectedMods.Value = new List
+ {
+ new ManiaModNoRelease()
+ };
+
+ Beatmap.Value = CreateWorkingBeatmap(beatmap);
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults.Add(result);
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ judgementResults = new List();
+ });
+
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
+ }
+
+ private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, new PlayerConfiguration
+ {
+ AllowPause = false,
+ ShowResults = false,
+ })
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
index ab9f57ecc3..a5c18babe2 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
@@ -1,10 +1,8 @@
// 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.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
@@ -16,37 +14,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Test]
public void TestMinor()
{
- AddStep("Create barlines", () => recreate());
+ AddStep("Create barlines", recreate);
}
- private void recreate(Func>? createBarLines = null)
+ private void recreate()
{
var stageDefinitions = new List
{
new StageDefinition(4),
};
- SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s =>
+ SetContents(_ =>
{
- if (createBarLines != null)
+ var maniaPlayfield = new ManiaPlayfield(stageDefinitions);
+
+ // Must be scheduled so the pool is loaded before we try and retrieve from it.
+ Schedule(() =>
{
- var barLines = createBarLines();
-
- foreach (var b in barLines)
- s.Add(b);
-
- return;
- }
-
- for (int i = 0; i < 64; i++)
- {
- s.Add(new BarLine
+ for (int i = 0; i < 64; i++)
{
- StartTime = Time.Current + i * 500,
- Major = i % 4 == 0,
- });
- }
- }));
+ maniaPlayfield.Add(new BarLine
+ {
+ StartTime = Time.Current + i * 500,
+ Major = i % 4 == 0,
+ });
+ }
+ });
+
+ return maniaPlayfield;
+ });
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
index 51c2bac6d1..7a0abb9e64 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("Hold key", () =>
{
clock.CurrentTime = 0;
- note.OnPressed(new KeyBindingPressEvent(GetContainingInputManager().CurrentState, ManiaAction.Key1));
+ note.OnPressed(new KeyBindingPressEvent(GetContainingInputManager()!.CurrentState, ManiaAction.Key1));
});
AddStep("progress time", () => clock.CurrentTime = 500);
AddAssert("head is visible", () => note.Head.Alpha == 1);
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 5f299f419d..e328d23ed4 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -474,8 +474,8 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => !j.Type.IsHit()));
- AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
- .All(j => j.Type.IsHit()));
+ AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
+ .All(j => j.Type.IsHit()));
}
[Test]
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs
new file mode 100644
index 0000000000..30c0113bff
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs
@@ -0,0 +1,49 @@
+// 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.Graphics.Containers;
+using osu.Framework.Input;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public partial class TestSceneManiaTouchInputArea : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ [Test]
+ public void TestTouchAreaNotInitiallyVisible()
+ {
+ AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
+ }
+
+ [Test]
+ public void TestPressReceptors()
+ {
+ AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
+
+ for (int i = 0; i < 4; i++)
+ {
+ int index = i;
+
+ AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
+
+ AddAssert("action sent",
+ () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions),
+ () => Does.Contain(getReceptor(index).Action.Value));
+
+ AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
+
+ AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
+ }
+ }
+
+ private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault();
+
+ private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
index 31ff57395c..990f545ee4 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
@@ -8,7 +8,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}
private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor)
- => hitObject.Anchor.HasFlagFast(expectedAnchor) && hitObject.Origin.HasFlagFast(expectedAnchor);
+ => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor);
private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor)
=> verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor));
diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
index 2a8dc715f9..341d52afcf 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
@@ -39,18 +39,18 @@ namespace osu.Game.Rulesets.Mania.Tests
public void TestScrollingDownwards()
{
AddStep("set down scroll", () => scrollingContainer.Direction = ScrollingDirection.Down);
- AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
- AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
- AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
+ AddStep("set coverage = 0.5", () => cover.Coverage.Value = 0.5f);
+ AddStep("set coverage = 0.8f", () => cover.Coverage.Value = 0.8f);
+ AddStep("set coverage = 0.2f", () => cover.Coverage.Value = 0.2f);
}
[Test]
public void TestScrollingUpwards()
{
AddStep("set up scroll", () => scrollingContainer.Direction = ScrollingDirection.Up);
- AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
- AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
- AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
+ AddStep("set coverage = 0.5", () => cover.Coverage.Value = 0.5f);
+ AddStep("set coverage = 0.8f", () => cover.Coverage.Value = 0.8f);
+ AddStep("set coverage = 0.2f", () => cover.Coverage.Value = 0.2f);
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index fee3ba3e39..db04142915 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
foreach (var stage in stages)
{
- for (int i = 0; i < stage.Columns.Count; i++)
+ for (int i = 0; i < stage.Columns.Length; i++)
{
var obj = new Note { Column = i, StartTime = Time.Current + 2000 };
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
foreach (var stage in stages)
{
- for (int i = 0; i < stage.Columns.Count; i++)
+ for (int i = 0; i < stage.Columns.Length; i++)
{
var obj = new HoldNote { Column = i, StartTime = Time.Current + 2000, Duration = 500 };
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
index 81557c198d..b5b265792b 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
@@ -34,16 +34,21 @@ namespace osu.Game.Rulesets.Mania.Tests
[SetUpSteps]
public void SetUpSteps()
{
- AddStep("setup hierarchy", () => Child = new Container
+ AddStep("setup hierarchy", () =>
{
- Clock = new FramedClock(clock = new ManualClock()),
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new[]
+ Child = new Container
{
- drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
- }
+ Clock = new FramedClock(clock = new ManualClock()),
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new[]
+ {
+ drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
+ }
+ };
+
+ drawableRuleset.AllowBackwardsSeeks = true;
});
AddStep("retrieve config bindable", () =>
{
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index b991db408c..eee06acdb8 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,12 +2,12 @@
-
+
WinExe
- net6.0
+ net8.0
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
index 28cdf8907e..8222e5477d 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
@@ -22,11 +22,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
public int TotalColumns => Stages.Sum(g => g.Columns);
- ///
- /// The total number of columns that were present in this before any user adjustments.
- ///
- public readonly int OriginalTotalColumns;
-
///
/// Creates a new .
///
@@ -35,7 +30,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
{
Stages.Add(defaultStage);
- OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
}
public override IEnumerable GetStatistics()
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index def22608d6..39ee3d209b 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Mania.Objects;
using System;
using System.Linq;
@@ -14,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Utils;
using osuTK;
@@ -27,24 +26,42 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
private const int max_notes_for_density = 7;
+ ///
+ /// The total number of columns.
+ ///
+ public int TotalColumns => TargetColumns * (Dual ? 2 : 1);
+
+ ///
+ /// The number of columns per-stage.
+ ///
public int TargetColumns;
+
+ ///
+ /// Whether to double the number of stages.
+ ///
public bool Dual;
+
+ ///
+ /// Whether the beatmap instantiated with is for the mania ruleset.
+ ///
public readonly bool IsForCurrentRuleset;
- private readonly int originalTargetColumns;
-
// Internal for testing purposes
- internal LegacyRandom Random { get; private set; }
+ internal readonly LegacyRandom Random;
private Pattern lastPattern = new Pattern();
- private ManiaBeatmap beatmap;
-
public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
- : base(beatmap, ruleset)
+ : this(beatmap, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap), ruleset)
{
- IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
- TargetColumns = GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap));
+ }
+
+ private ManiaBeatmapConverter(IBeatmap? beatmap, LegacyBeatmapConversionDifficultyInfo difficulty, Ruleset ruleset)
+ : base(beatmap!, ruleset)
+ {
+ IsForCurrentRuleset = difficulty.SourceRuleset.Equals(ruleset.RulesetInfo);
+ Random = new LegacyRandom((int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate));
+ TargetColumns = getColumnCount(difficulty);
if (IsForCurrentRuleset && TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{
@@ -52,52 +69,53 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
Dual = true;
}
- originalTargetColumns = TargetColumns;
+ static int getColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
+ {
+ double roundedCircleSize = Math.Round(difficulty.CircleSize);
+
+ if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
+ return (int)Math.Max(1, roundedCircleSize);
+
+ double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
+
+ if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
+ {
+ int countSliderOrSpinner = difficulty.EndTimeObjectCount;
+
+ // In osu!stable, this division appears as if it happens on floats, but due to release-mode
+ // optimisations, it actually ends up happening on doubles.
+ double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
+
+ if (percentSpecialObjects < 0.2)
+ return 7;
+ if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
+ return roundedOverallDifficulty > 5 ? 7 : 6;
+ if (percentSpecialObjects > 0.6)
+ return roundedOverallDifficulty > 4 ? 5 : 4;
+ }
+
+ return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
+ }
}
- public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
+ public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyList? mods = null)
{
- double roundedCircleSize = Math.Round(difficulty.CircleSize);
+ var converter = new ManiaBeatmapConverter(null, difficulty, new ManiaRuleset());
- if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
- return (int)Math.Max(1, roundedCircleSize);
-
- double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
-
- if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
+ if (mods != null)
{
- int countSliderOrSpinner = difficulty.EndTimeObjectCount;
-
- // In osu!stable, this division appears as if it happens on floats, but due to release-mode
- // optimisations, it actually ends up happening on doubles.
- double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
-
- if (percentSpecialObjects < 0.2)
- return 7;
- if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
- return roundedOverallDifficulty > 5 ? 7 : 6;
- if (percentSpecialObjects > 0.6)
- return roundedOverallDifficulty > 4 ? 5 : 4;
+ foreach (var m in mods.OfType())
+ m.ApplyToBeatmapConverter(converter);
}
- return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
+ return converter.TotalColumns;
}
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
- protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
- {
- IBeatmapDifficultyInfo difficulty = original.Difficulty;
-
- int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
- Random = new LegacyRandom(seed);
-
- return base.ConvertBeatmap(original, cancellationToken);
- }
-
protected override Beatmap CreateBeatmap()
{
- beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns), originalTargetColumns);
+ ManiaBeatmap beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns));
if (Dual)
beatmap.Stages.Add(new StageDefinition(TargetColumns));
@@ -115,10 +133,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap);
-
- if (objects == null)
- yield break;
-
foreach (ManiaHitObject obj in objects)
yield return obj;
}
@@ -152,7 +166,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// The hit objects generated.
private IEnumerable generateSpecific(HitObject original, IBeatmap originalBeatmap)
{
- var generator = new SpecificBeatmapPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
+ var generator = new SpecificBeatmapPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
foreach (var newPattern in generator.Generate())
{
@@ -171,13 +185,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// The hit objects generated.
private IEnumerable generateConverted(HitObject original, IBeatmap originalBeatmap)
{
- Patterns.PatternGenerator conversion = null;
+ Patterns.PatternGenerator? conversion = null;
switch (original)
{
case IHasPath:
{
- var generator = new PathObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
+ var generator = new PathObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
conversion = generator;
var positionData = original as IHasPosition;
@@ -195,7 +209,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
case IHasDuration endTimeData:
{
- conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
+ conversion = new EndTimeObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime);
@@ -206,7 +220,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
computeDensity(original.StartTime);
- conversion = new HitObjectPatternGenerator(Random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap);
+ conversion = new HitObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair);
recordNote(original.StartTime, positionData.Position);
break;
@@ -231,8 +245,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
{
- public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
- : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
+ public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
+ : base(random, hitObject, beatmap, previousPattern, totalColumns)
{
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index 2265d3d347..52bb87ae19 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private readonly int endTime;
private readonly PatternType convertType;
- public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
- : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
+ public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
+ : base(random, hitObject, beatmap, previousPattern, totalColumns)
{
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index 27cb681300..9880369dfb 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Extensions.EnumExtensions;
using osuTK;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -23,9 +22,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private readonly PatternType convertType;
- public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density,
- PatternType lastStair, IBeatmap originalBeatmap)
- : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
+ public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition,
+ double density, PatternType lastStair)
+ : base(random, hitObject, beatmap, previousPattern, totalColumns)
{
StairType = lastStair;
@@ -79,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
else
convertType |= PatternType.LowProbability;
- if (!convertType.HasFlagFast(PatternType.KeepSingle))
+ if (!convertType.HasFlag(PatternType.KeepSingle))
{
if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8)
convertType |= PatternType.Mirror;
@@ -102,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0;
- if (convertType.HasFlagFast(PatternType.Reverse) && PreviousPattern.HitObjects.Any())
+ if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any())
{
// Generate a new pattern by copying the last hit objects in reverse-column order
for (int i = RandomStart; i < TotalColumns; i++)
@@ -114,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern;
}
- if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
+ if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
// If we convert to 7K + 1, let's not overload the special key
&& (TotalColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column
@@ -127,7 +126,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern;
}
- if (convertType.HasFlagFast(PatternType.ForceStack) && PreviousPattern.HitObjects.Any())
+ if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any())
{
// Generate a new pattern by placing on the already filled columns
for (int i = RandomStart; i < TotalColumns; i++)
@@ -141,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (PreviousPattern.HitObjects.Count() == 1)
{
- if (convertType.HasFlagFast(PatternType.Stair))
+ if (convertType.HasFlag(PatternType.Stair))
{
// Generate a new pattern by placing on the next column, cycling back to the start if there is no "next"
int targetColumn = lastColumn + 1;
@@ -152,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern;
}
- if (convertType.HasFlagFast(PatternType.ReverseStair))
+ if (convertType.HasFlag(PatternType.ReverseStair))
{
// Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous"
int targetColumn = lastColumn - 1;
@@ -164,10 +163,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
}
- if (convertType.HasFlagFast(PatternType.KeepSingle))
+ if (convertType.HasFlag(PatternType.KeepSingle))
return generateRandomNotes(1);
- if (convertType.HasFlagFast(PatternType.Mirror))
+ if (convertType.HasFlag(PatternType.Mirror))
{
if (ConversionDifficulty > 6.5)
return generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
@@ -179,7 +178,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 6.5)
{
- if (convertType.HasFlagFast(PatternType.LowProbability))
+ if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.78, 0.42, 0, 0);
return generateRandomPattern(1, 0.62, 0, 0);
@@ -187,7 +186,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 4)
{
- if (convertType.HasFlagFast(PatternType.LowProbability))
+ if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.35, 0.08, 0, 0);
return generateRandomPattern(0.52, 0.15, 0, 0);
@@ -195,7 +194,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 2)
{
- if (convertType.HasFlagFast(PatternType.LowProbability))
+ if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.18, 0, 0, 0);
return generateRandomPattern(0.45, 0, 0, 0);
@@ -208,9 +207,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in p.HitObjects)
{
- if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1)
+ if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
StairType = PatternType.ReverseStair;
- if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart)
+ if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
StairType = PatternType.Stair;
}
@@ -230,7 +229,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
var pattern = new Pattern();
- bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack);
+ bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack);
if (!allowStacking)
noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects);
@@ -250,7 +249,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int getNextColumn(int last)
{
- if (convertType.HasFlagFast(PatternType.Gathered))
+ if (convertType.HasFlag(PatternType.Gathered))
{
last++;
if (last == TotalColumns)
@@ -297,7 +296,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The containing the hit objects.
private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3)
{
- if (convertType.HasFlagFast(PatternType.ForceNotStack))
+ if (convertType.HasFlag(PatternType.ForceNotStack))
return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
var pattern = new Pattern();
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs
index 4922915c7d..c54da74424 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs
@@ -7,7 +7,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
-using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -31,8 +30,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private PatternType convertType;
- public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
- : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
+ public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
+ : base(random, hitObject, beatmap, previousPattern, totalColumns)
{
convertType = PatternType.None;
if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)
@@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 6.5)
{
- if (convertType.HasFlagFast(PatternType.LowProbability))
+ if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
@@ -147,7 +146,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 4)
{
- if (convertType.HasFlagFast(PatternType.LowProbability))
+ if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
@@ -155,13 +154,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 2.5)
{
- if (convertType.HasFlagFast(PatternType.LowProbability))
+ if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.3, 0, 0);
return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
}
- if (convertType.HasFlagFast(PatternType.LowProbability))
+ if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.17, 0, 0);
return generateNRandomNotes(StartTime, 0.27, 0, 0);
@@ -219,7 +218,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
- if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
+ if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
int lastColumn = nextColumn;
@@ -371,7 +370,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
- bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability);
+ bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes)
@@ -404,7 +403,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int endTime = startTime + SegmentDuration * SpanCount;
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
- if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
+ if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
for (int i = 0; i < columnRepeat; i++)
@@ -433,7 +432,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
- if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
+ if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
holdColumn = FindAvailableColumn(holdColumn, PreviousPattern);
// Create the hold note
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index 77f93b4ef9..48b8778501 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
@@ -27,20 +27,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
protected readonly LegacyRandom Random;
- ///
- /// The beatmap which is being converted from.
- ///
- protected readonly IBeatmap OriginalBeatmap;
-
- protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
- : base(hitObject, beatmap, previousPattern)
+ protected PatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns)
+ : base(hitObject, beatmap, totalColumns, previousPattern)
{
ArgumentNullException.ThrowIfNull(random);
- ArgumentNullException.ThrowIfNull(originalBeatmap);
Random = random;
- OriginalBeatmap = originalBeatmap;
-
RandomStart = TotalColumns == 8 ? 1 : 0;
}
@@ -104,17 +96,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (conversionDifficulty != null)
return conversionDifficulty.Value;
- HitObject lastObject = OriginalBeatmap.HitObjects.LastOrDefault();
- HitObject firstObject = OriginalBeatmap.HitObjects.FirstOrDefault();
+ HitObject lastObject = Beatmap.HitObjects.LastOrDefault();
+ HitObject firstObject = Beatmap.HitObjects.FirstOrDefault();
// Drain time in seconds
- int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - OriginalBeatmap.TotalBreakTime) / 1000);
+ int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000);
if (drainTime == 0)
drainTime = 10000;
- IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty;
- conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
+ IBeatmapDifficultyInfo difficulty = Beatmap.Difficulty;
+ conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
return conversionDifficulty.Value;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
index 3d3c35773b..8d98515fa4 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
@@ -25,11 +26,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
///
/// The beatmap which is a part of.
///
- protected readonly ManiaBeatmap Beatmap;
+ protected readonly IBeatmap Beatmap;
protected readonly int TotalColumns;
- protected PatternGenerator(HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern)
+ protected PatternGenerator(HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
{
ArgumentNullException.ThrowIfNull(hitObject);
ArgumentNullException.ThrowIfNull(beatmap);
@@ -38,8 +39,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
HitObject = hitObject;
Beatmap = beatmap;
PreviousPattern = previousPattern;
-
- TotalColumns = Beatmap.TotalColumns;
+ TotalColumns = totalColumns;
}
///
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index 6bb6879052..4190e74e51 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty;
- public override int Version => 20220902;
+ public override int Version => 20230817;
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs
index d9fd96ac6a..8a1b127265 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs
@@ -51,13 +51,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return multiplier;
// Apply key mod multipliers.
-
int originalColumns = ManiaBeatmapConverter.GetColumnCount(difficulty);
- int actualColumns = originalColumns;
-
- actualColumns = mods.OfType().SingleOrDefault()?.KeyCount ?? actualColumns;
- if (mods.Any(m => m is ManiaModDualStages))
- actualColumns *= 2;
+ int actualColumns = ManiaBeatmapConverter.GetColumnCount(difficulty, mods);
if (actualColumns > originalColumns)
multiplier *= 0.9;
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs
new file mode 100644
index 0000000000..0aa72c28b8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs
@@ -0,0 +1,81 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.Skinning.Default;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
+{
+ public partial class EditHoldNoteEndPiece : CompositeDrawable
+ {
+ public Action? DragStarted { get; init; }
+ public Action? Dragging { get; init; }
+ public Action? DragEnded { get; init; }
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Height = DefaultNotePiece.NOTE_HEIGHT;
+
+ CornerRadius = 5;
+ Masking = true;
+
+ InternalChild = new DefaultNotePiece();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateState();
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateState();
+ return true;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateState();
+ base.OnHoverLost(e);
+ }
+
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ DragStarted?.Invoke();
+ return true;
+ }
+
+ protected override void OnDrag(DragEvent e)
+ {
+ base.OnDrag(e);
+ Dragging?.Invoke(e.ScreenSpaceMousePosition);
+ }
+
+ protected override void OnDragEnd(DragEndEvent e)
+ {
+ base.OnDragEnd(e);
+ DragEnded?.Invoke();
+ }
+
+ private void updateState()
+ {
+ var colour = colours.Yellow;
+
+ if (IsHovered)
+ colour = colour.Lighten(1);
+
+ Colour = colour;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index 8ec5213d5f..b8e6aa26a0 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -1,16 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Screens.Edit;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@@ -18,10 +18,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public partial class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
{
[Resolved]
- private OsuColour colours { get; set; }
+ private OsuColour colours { get; set; } = null!;
- private EditNotePiece head;
- private EditNotePiece tail;
+ [Resolved]
+ private IEditorChangeHandler? changeHandler { get; set; }
+
+ [Resolved]
+ private EditorBeatmap? editorBeatmap { get; set; }
+
+ [Resolved]
+ private IPositionSnapProvider? positionSnapProvider { get; set; }
+
+ private EditHoldNoteEndPiece head = null!;
+ private EditHoldNoteEndPiece tail = null!;
public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold)
@@ -33,8 +42,43 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
InternalChildren = new Drawable[]
{
- head = new EditNotePiece { RelativeSizeAxes = Axes.X },
- tail = new EditNotePiece { RelativeSizeAxes = Axes.X },
+ head = new EditHoldNoteEndPiece
+ {
+ RelativeSizeAxes = Axes.X,
+ DragStarted = () => changeHandler?.BeginChange(),
+ Dragging = pos =>
+ {
+ double endTimeBeforeDrag = HitObject.EndTime;
+ double proposedStartTime = positionSnapProvider?.FindSnappedPositionAndTime(pos).Time ?? HitObjectContainer.TimeAtScreenSpacePosition(pos);
+ double proposedEndTime = endTimeBeforeDrag;
+
+ if (proposedStartTime >= proposedEndTime)
+ return;
+
+ HitObject.StartTime = proposedStartTime;
+ HitObject.EndTime = proposedEndTime;
+ editorBeatmap?.Update(HitObject);
+ },
+ DragEnded = () => changeHandler?.EndChange(),
+ },
+ tail = new EditHoldNoteEndPiece
+ {
+ RelativeSizeAxes = Axes.X,
+ DragStarted = () => changeHandler?.BeginChange(),
+ Dragging = pos =>
+ {
+ double proposedStartTime = HitObject.StartTime;
+ double proposedEndTime = positionSnapProvider?.FindSnappedPositionAndTime(pos).Time ?? HitObjectContainer.TimeAtScreenSpacePosition(pos);
+
+ if (proposedStartTime >= proposedEndTime)
+ return;
+
+ HitObject.StartTime = proposedStartTime;
+ HitObject.EndTime = proposedEndTime;
+ editorBeatmap?.Update(HitObject);
+ },
+ DragEnded = () => changeHandler?.EndChange(),
+ },
new Container
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index 1ae65dd8c0..c645ddd98d 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
@@ -17,9 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private Playfield playfield { get; set; } = null!;
- [Resolved]
- private IScrollingInfo scrollingInfo { get; set; } = null!;
-
protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
protected ManiaSelectionBlueprint(T hitObject)
@@ -28,14 +26,31 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
RelativeSizeAxes = Axes.None;
}
- protected override void Update()
- {
- base.Update();
+ private readonly IBindable directionBindable = new Bindable();
- var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ directionBindable.BindTo(scrollingInfo.Direction);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ directionBindable.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ var anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
Anchor = Origin = anchor;
foreach (var child in InternalChildren)
child.Anchor = child.Origin = anchor;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
Position = Parent!.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
Width = HitObjectContainer.DrawWidth;
diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs
new file mode 100644
index 0000000000..51ead5f423
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs
@@ -0,0 +1,39 @@
+// 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 osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Rulesets.Mania.Edit.Checks
+{
+ public class CheckKeyCount : ICheck
+ {
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Check mania keycount.");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateKeycountTooLow(this),
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var diff = context.Beatmap.Difficulty;
+
+ if (diff.CircleSize < 4)
+ {
+ yield return new IssueTemplateKeycountTooLow(this).Create(diff.CircleSize);
+ }
+ }
+
+ public class IssueTemplateKeycountTooLow : IssueTemplate
+ {
+ public IssueTemplateKeycountTooLow(ICheck check)
+ : base(check, IssueType.Problem, "Key count is {0} and must be 4 or higher.")
+ {
+ }
+
+ public Issue Create(float current) => new Issue(this, current);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs
new file mode 100644
index 0000000000..233c602c21
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Rulesets.Mania.Edit.Checks
+{
+ public class CheckManiaAbnormalDifficultySettings : CheckAbnormalDifficultySettings
+ {
+ public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks mania relevant settings");
+
+ public override IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var diff = context.Beatmap.Difficulty;
+ Issue? issue;
+
+ if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue))
+ yield return issue;
+
+ if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue))
+ yield return issue;
+
+ if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue))
+ yield return issue;
+
+ if (OutOfRange("Drain rate", diff.DrainRate, out issue))
+ yield return issue;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs
index 8d34373f82..4c4cf519ce 100644
--- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs
+++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{
public BindableBool ShowSpeedChanges { get; } = new BindableBool();
+ public double? TimelineTimeRange { get; set; }
+
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods)
@@ -38,5 +41,11 @@ namespace osu.Game.Rulesets.Mania.Edit
Origin = Anchor.Centre,
Size = Vector2.One
};
+
+ protected override void Update()
+ {
+ TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
+ base.Update();
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs
new file mode 100644
index 0000000000..4adabfa4d7
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs
@@ -0,0 +1,26 @@
+// 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 osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Rulesets.Mania.Edit.Checks;
+
+namespace osu.Game.Rulesets.Mania.Edit
+{
+ public class ManiaBeatmapVerifier : IBeatmapVerifier
+ {
+ private readonly List checks = new List
+ {
+ // Settings
+ new CheckKeyCount(),
+ new CheckManiaAbnormalDifficultySettings(),
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ return checks.SelectMany(check => check.Run(context));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index 967cdb0e54..02a4f3a022 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -1,11 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
+using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@@ -14,6 +13,7 @@ using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -21,14 +21,17 @@ namespace osu.Game.Rulesets.Mania.Edit
{
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer
{
- private DrawableManiaEditorRuleset drawableRuleset;
+ private DrawableManiaEditorRuleset drawableRuleset = null!;
+
+ [Resolved]
+ private EditorScreenWithTimeline? screenWithTimeline { get; set; }
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
}
- public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
+ public new ManiaPlayfield Playfield => drawableRuleset.Playfield;
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
@@ -72,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Edit
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
continue;
- ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
+ ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
if (current == null)
continue;
@@ -83,5 +86,13 @@ namespace osu.Game.Rulesets.Mania.Edit
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (screenWithTimeline?.TimelineArea.Timeline != null)
+ drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 8fdbada04f..9ae2112b30 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
@@ -16,6 +17,16 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved]
private HitObjectComposer composer { get; set; } = null!;
+ protected override void OnSelectionChanged()
+ {
+ base.OnSelectionChanged();
+
+ var selectedObjects = SelectedItems.OfType().ToArray();
+
+ SelectionBox.CanFlipX = canFlipX(selectedObjects);
+ SelectionBox.CanFlipY = canFlipY(selectedObjects);
+ }
+
public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
var hitObjectBlueprint = (HitObjectSelectionBlueprint)moveEvent.Blueprint;
@@ -26,6 +37,58 @@ namespace osu.Game.Rulesets.Mania.Edit
return true;
}
+ public override bool HandleFlip(Direction direction, bool flipOverOrigin)
+ {
+ var selectedObjects = SelectedItems.OfType().ToArray();
+ var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
+
+ if (selectedObjects.Length == 0)
+ return false;
+
+ switch (direction)
+ {
+ case Direction.Horizontal:
+ if (!canFlipX(selectedObjects))
+ return false;
+
+ int firstColumn = flipOverOrigin ? 0 : selectedObjects.Min(ho => ho.Column);
+ int lastColumn = flipOverOrigin ? (int)EditorBeatmap.BeatmapInfo.Difficulty.CircleSize - 1 : selectedObjects.Max(ho => ho.Column);
+
+ EditorBeatmap.PerformOnSelection(hitObject =>
+ {
+ var maniaObject = (ManiaHitObject)hitObject;
+ maniaPlayfield.Remove(maniaObject);
+ maniaObject.Column = firstColumn + (lastColumn - maniaObject.Column);
+ maniaPlayfield.Add(maniaObject);
+ });
+
+ return true;
+
+ case Direction.Vertical:
+ if (!canFlipY(selectedObjects))
+ return false;
+
+ double selectionStartTime = selectedObjects.Min(ho => ho.StartTime);
+ double selectionEndTime = selectedObjects.Max(ho => ho.GetEndTime());
+
+ EditorBeatmap.PerformOnSelection(hitObject =>
+ {
+ hitObject.StartTime = selectionStartTime + (selectionEndTime - hitObject.GetEndTime());
+ });
+
+ return true;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(direction), direction, "Cannot flip over the supplied direction.");
+ }
+ }
+
+ private static bool canFlipX(ManiaHitObject[] selectedObjects)
+ => selectedObjects.Select(ho => ho.Column).Distinct().Count() > 1;
+
+ private static bool canFlipY(ManiaHitObject[] selectedObjects)
+ => selectedObjects.Length > 1 && selectedObjects.Min(ho => ho.StartTime) < selectedObjects.Max(ho => ho.GetEndTime());
+
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
index 4f983debea..7168504309 100644
--- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
@@ -3,20 +3,164 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Mania.Edit.Setup
{
- public partial class ManiaDifficultySection : DifficultySection
+ public partial class ManiaDifficultySection : SetupSection
{
+ public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
+
+ private LabelledSliderBar keyCountSlider { get; set; } = null!;
+ private LabelledSwitchButton specialStyle { get; set; } = null!;
+ private LabelledSliderBar healthDrainSlider { get; set; } = null!;
+ private LabelledSliderBar overallDifficultySlider { get; set; } = null!;
+ private LabelledSliderBar baseVelocitySlider { get; set; } = null!;
+ private LabelledSliderBar tickRateSlider { get; set; } = null!;
+
+ [Resolved]
+ private Editor? editor { get; set; }
+
+ [Resolved]
+ private IEditorChangeHandler? changeHandler { get; set; }
+
[BackgroundDependencyLoader]
private void load()
{
- CircleSizeSlider.Label = BeatmapsetsStrings.ShowStatsCsMania;
- CircleSizeSlider.Description = "The number of columns in the beatmap";
- if (CircleSizeSlider.Current is BindableNumber circleSizeFloat)
- circleSizeFloat.Precision = 1;
+ Children = new Drawable[]
+ {
+ keyCountSlider = new LabelledSliderBar
+ {
+ Label = BeatmapsetsStrings.ShowStatsCsMania,
+ FixedLabelWidth = LABEL_WIDTH,
+ Description = "The number of columns in the beatmap",
+ Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
+ {
+ Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
+ MinValue = 0,
+ MaxValue = 10,
+ Precision = 1,
+ }
+ },
+ specialStyle = new LabelledSwitchButton
+ {
+ Label = "Use special (N+1) style",
+ FixedLabelWidth = LABEL_WIDTH,
+ Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
+ Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
+ },
+ healthDrainSlider = new LabelledSliderBar
+ {
+ Label = BeatmapsetsStrings.ShowStatsDrain,
+ FixedLabelWidth = LABEL_WIDTH,
+ Description = EditorSetupStrings.DrainRateDescription,
+ Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
+ {
+ Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
+ MinValue = 0,
+ MaxValue = 10,
+ Precision = 0.1f,
+ }
+ },
+ overallDifficultySlider = new LabelledSliderBar
+ {
+ Label = BeatmapsetsStrings.ShowStatsAccuracy,
+ FixedLabelWidth = LABEL_WIDTH,
+ Description = EditorSetupStrings.OverallDifficultyDescription,
+ Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
+ {
+ Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
+ MinValue = 0,
+ MaxValue = 10,
+ Precision = 0.1f,
+ }
+ },
+ baseVelocitySlider = new LabelledSliderBar
+ {
+ Label = EditorSetupStrings.BaseVelocity,
+ FixedLabelWidth = LABEL_WIDTH,
+ Description = EditorSetupStrings.BaseVelocityDescription,
+ Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
+ {
+ Default = 1.4,
+ MinValue = 0.4,
+ MaxValue = 3.6,
+ Precision = 0.01f,
+ }
+ },
+ tickRateSlider = new LabelledSliderBar
+ {
+ Label = EditorSetupStrings.TickRate,
+ FixedLabelWidth = LABEL_WIDTH,
+ Description = EditorSetupStrings.TickRateDescription,
+ Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
+ {
+ Default = 1,
+ MinValue = 1,
+ MaxValue = 4,
+ Precision = 1,
+ }
+ },
+ };
+
+ keyCountSlider.Current.BindValueChanged(updateKeyCount);
+ healthDrainSlider.Current.BindValueChanged(_ => updateValues());
+ overallDifficultySlider.Current.BindValueChanged(_ => updateValues());
+ baseVelocitySlider.Current.BindValueChanged(_ => updateValues());
+ tickRateSlider.Current.BindValueChanged(_ => updateValues());
+ }
+
+ private bool updatingKeyCount;
+
+ private void updateKeyCount(ValueChangedEvent keyCount)
+ {
+ if (updatingKeyCount) return;
+
+ updateValues();
+
+ if (editor == null) return;
+
+ updatingKeyCount = true;
+
+ editor.Reload().ContinueWith(t =>
+ {
+ if (!t.GetResultSafely())
+ {
+ Schedule(() =>
+ {
+ changeHandler!.RestoreState(-1);
+ Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value = keyCount.OldValue;
+ updatingKeyCount = false;
+ });
+ }
+ else
+ {
+ updatingKeyCount = false;
+ }
+ });
+ }
+
+ private void updateValues()
+ {
+ // for now, update these on commit rather than making BeatmapMetadata bindables.
+ // after switching database engines we can reconsider if switching to bindables is a good direction.
+ Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
+ Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
+ Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
+ Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
+ Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
+ Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
+
+ Beatmap.UpdateAllHitObjects();
+ Beatmap.SaveState();
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs
deleted file mode 100644
index d5a9a311bc..0000000000
--- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Game.Graphics.UserInterfaceV2;
-using osu.Game.Screens.Edit.Setup;
-
-namespace osu.Game.Rulesets.Mania.Edit.Setup
-{
- public partial class ManiaSetupSection : RulesetSetupSection
- {
- private LabelledSwitchButton specialStyle;
-
- public ManiaSetupSection()
- : base(new ManiaRuleset().RulesetInfo)
- {
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Children = new Drawable[]
- {
- specialStyle = new LabelledSwitchButton
- {
- Label = "Use special (N+1) style",
- Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
- Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
- }
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- specialStyle.Current.BindValueChanged(_ => updateBeatmap());
- }
-
- private void updateBeatmap()
- {
- Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
- Beatmap.SaveState();
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
index 930ca217cd..8c6efbc72d 100644
--- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
+++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
@@ -1,9 +1,14 @@
// 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 osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
@@ -14,9 +19,9 @@ namespace osu.Game.Rulesets.Mania
{
private FilterCriteria.OptionalRange keys;
- public bool Matches(BeatmapInfo beatmapInfo)
+ public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
{
- return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo)));
+ return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods));
}
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
@@ -30,5 +35,20 @@ namespace osu.Game.Rulesets.Mania
return false;
}
+
+ public bool FilterMayChangeFromMods(ValueChangedEvent> mods)
+ {
+ if (keys.HasFilter)
+ {
+ // Interpreting as the Mod type is required for equality comparison.
+ HashSet oldSet = mods.OldValue.OfType().AsEnumerable().ToHashSet();
+ HashSet newSet = mods.NewValue.OfType().AsEnumerable().ToHashSet();
+
+ if (!oldSet.SetEquals(newSet))
+ return true;
+ }
+
+ return false;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index c38d6519bd..c01fa508fe 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
@@ -65,6 +64,8 @@ namespace osu.Game.Rulesets.Mania
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
+ public override IBeatmapVerifier CreateBeatmapVerifier() => new ManiaBeatmapVerifier();
+
public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap)
{
switch (skin)
@@ -87,79 +88,79 @@ namespace osu.Game.Rulesets.Mania
public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
- if (mods.HasFlagFast(LegacyMods.Nightcore))
+ if (mods.HasFlag(LegacyMods.Nightcore))
yield return new ManiaModNightcore();
- else if (mods.HasFlagFast(LegacyMods.DoubleTime))
+ else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new ManiaModDoubleTime();
- if (mods.HasFlagFast(LegacyMods.Perfect))
+ if (mods.HasFlag(LegacyMods.Perfect))
yield return new ManiaModPerfect();
- else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
+ else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new ManiaModSuddenDeath();
- if (mods.HasFlagFast(LegacyMods.Cinema))
+ if (mods.HasFlag(LegacyMods.Cinema))
yield return new ManiaModCinema();
- else if (mods.HasFlagFast(LegacyMods.Autoplay))
+ else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new ManiaModAutoplay();
- if (mods.HasFlagFast(LegacyMods.Easy))
+ if (mods.HasFlag(LegacyMods.Easy))
yield return new ManiaModEasy();
- if (mods.HasFlagFast(LegacyMods.FadeIn))
+ if (mods.HasFlag(LegacyMods.FadeIn))
yield return new ManiaModFadeIn();
- if (mods.HasFlagFast(LegacyMods.Flashlight))
+ if (mods.HasFlag(LegacyMods.Flashlight))
yield return new ManiaModFlashlight();
- if (mods.HasFlagFast(LegacyMods.HalfTime))
+ if (mods.HasFlag(LegacyMods.HalfTime))
yield return new ManiaModHalfTime();
- if (mods.HasFlagFast(LegacyMods.HardRock))
+ if (mods.HasFlag(LegacyMods.HardRock))
yield return new ManiaModHardRock();
- if (mods.HasFlagFast(LegacyMods.Hidden))
+ if (mods.HasFlag(LegacyMods.Hidden))
yield return new ManiaModHidden();
- if (mods.HasFlagFast(LegacyMods.Key1))
+ if (mods.HasFlag(LegacyMods.Key1))
yield return new ManiaModKey1();
- if (mods.HasFlagFast(LegacyMods.Key2))
+ if (mods.HasFlag(LegacyMods.Key2))
yield return new ManiaModKey2();
- if (mods.HasFlagFast(LegacyMods.Key3))
+ if (mods.HasFlag(LegacyMods.Key3))
yield return new ManiaModKey3();
- if (mods.HasFlagFast(LegacyMods.Key4))
+ if (mods.HasFlag(LegacyMods.Key4))
yield return new ManiaModKey4();
- if (mods.HasFlagFast(LegacyMods.Key5))
+ if (mods.HasFlag(LegacyMods.Key5))
yield return new ManiaModKey5();
- if (mods.HasFlagFast(LegacyMods.Key6))
+ if (mods.HasFlag(LegacyMods.Key6))
yield return new ManiaModKey6();
- if (mods.HasFlagFast(LegacyMods.Key7))
+ if (mods.HasFlag(LegacyMods.Key7))
yield return new ManiaModKey7();
- if (mods.HasFlagFast(LegacyMods.Key8))
+ if (mods.HasFlag(LegacyMods.Key8))
yield return new ManiaModKey8();
- if (mods.HasFlagFast(LegacyMods.Key9))
+ if (mods.HasFlag(LegacyMods.Key9))
yield return new ManiaModKey9();
- if (mods.HasFlagFast(LegacyMods.KeyCoop))
+ if (mods.HasFlag(LegacyMods.KeyCoop))
yield return new ManiaModDualStages();
- if (mods.HasFlagFast(LegacyMods.NoFail))
+ if (mods.HasFlag(LegacyMods.NoFail))
yield return new ManiaModNoFail();
- if (mods.HasFlagFast(LegacyMods.Random))
+ if (mods.HasFlag(LegacyMods.Random))
yield return new ManiaModRandom();
- if (mods.HasFlagFast(LegacyMods.Mirror))
+ if (mods.HasFlag(LegacyMods.Mirror))
yield return new ManiaModMirror();
- if (mods.HasFlagFast(LegacyMods.ScoreV2))
+ if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
@@ -239,6 +240,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModEasy(),
new ManiaModNoFail(),
new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()),
+ new ManiaModNoRelease(),
};
case ModType.DifficultyIncrease:
@@ -247,7 +249,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModHardRock(),
new MultiMod(new ManiaModSuddenDeath(), new ManiaModPerfect()),
new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()),
- new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()),
+ new MultiMod(new ManiaModFadeIn(), new ManiaModHidden(), new ManiaModCover()),
new ManiaModFlashlight(),
new ModAccuracyChallenge(),
};
@@ -375,7 +377,7 @@ namespace osu.Game.Rulesets.Mania
/// The that corresponds to .
private PlayfieldType getPlayfieldType(int variant)
{
- return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v);
+ return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderDescending().First(v => variant >= v);
}
protected override IEnumerable GetValidHitResults()
@@ -417,12 +419,13 @@ namespace osu.Game.Rulesets.Mania
return new ManiaFilterCriteria();
}
- public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
+ public override IEnumerable CreateEditorSetupSections() =>
+ [
+ new ManiaDifficultySection(),
+ ];
- public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
-
- public int GetKeyCount(IBeatmapInfo beatmapInfo)
- => ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo));
+ public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList? mods = null)
+ => ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);
}
public enum PlayfieldType
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs
index 44120e16e6..046d1c5b34 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs
@@ -15,10 +15,6 @@ namespace osu.Game.Rulesets.Mania
: base(component)
{
}
-
- protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
-
- protected override string ComponentName => Component.ToString().ToLowerInvariant();
}
public enum ManiaSkinComponents
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
index 050b302bd8..88d6a19822 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public abstract int KeyCount { get; }
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
+ public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
{
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs
new file mode 100644
index 0000000000..eb243bfab7
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Bindables;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mania.UI;
+
+namespace osu.Game.Rulesets.Mania.Mods
+{
+ public class ManiaModCover : ManiaModWithPlayfieldCover
+ {
+ public override string Name => "Cover";
+ public override string Acronym => "CO";
+
+ public override LocalisableString Description => @"Decrease the playfield's viewing area.";
+
+ public override double ScoreMultiplier => 1;
+
+ protected override CoverExpandDirection ExpandDirection => Direction.Value;
+
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
+ {
+ typeof(ManiaModHidden),
+ typeof(ManiaModFadeIn)
+ }).ToArray();
+
+ public override bool Ranked => false;
+
+ [SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")]
+ public override BindableNumber Coverage { get; } = new BindableFloat(0.5f)
+ {
+ Precision = 0.1f,
+ MinValue = 0.2f,
+ MaxValue = 0.8f,
+ Default = 0.5f,
+ };
+
+ [SettingSource("Direction", "The direction on which the cover is applied")]
+ public Bindable Direction { get; } = new Bindable();
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
index 196514c7b1..54a0b8f36d 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
@@ -3,29 +3,24 @@
using System;
using System.Linq;
-using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModFadeIn : ManiaModPlayfieldCover
+ public class ManiaModFadeIn : ManiaModHidden
{
public override string Name => "Fade In";
public override string Acronym => "FI";
public override LocalisableString Description => @"Keys appear out of nowhere!";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
+ {
+ typeof(ManiaModHidden),
+ typeof(ManiaModCover)
+ }).ToArray();
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
-
- public override BindableNumber Coverage { get; } = new BindableFloat(0.5f)
- {
- Precision = 0.1f,
- MinValue = 0.1f,
- MaxValue = 0.7f,
- Default = 0.5f,
- };
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs
index d9de06a811..189c4b3a5f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs
@@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModHardRock : ModHardRock
{
public override double ScoreMultiplier => 1;
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
index f23cb335a5..3365b206cf 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
@@ -3,27 +3,113 @@
using System;
using System.Linq;
+using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Skinning;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModHidden : ManiaModPlayfieldCover
+ public partial class ManiaModHidden : ManiaModWithPlayfieldCover, IApplicableToPlayer, IUpdatableByPlayfield
{
+ ///
+ /// osu!stable is referenced to 768px.
+ ///
+ private const float reference_playfield_height = 768;
+
+ public const float MIN_COVERAGE = 160f;
+ public const float MAX_COVERAGE = 400f;
+ private const float coverage_increase_per_combo = 0.5f;
+
public override LocalisableString Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1;
- public override BindableNumber Coverage { get; } = new BindableFloat(0.5f)
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
{
- Precision = 0.1f,
- MinValue = 0.2f,
- MaxValue = 0.8f,
- Default = 0.5f,
- };
-
- public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
+ typeof(ManiaModFadeIn),
+ typeof(ManiaModCover)
+ }).ToArray();
+ public override BindableNumber Coverage { get; } = new BindableFloat(MIN_COVERAGE);
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
+
+ private readonly IBindable isBreakTime = new Bindable();
+ private readonly BindableInt combo = new BindableInt();
+
+ public override void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ {
+ base.ApplyToScoreProcessor(scoreProcessor);
+
+ combo.UnbindAll();
+ combo.BindTo(scoreProcessor.Combo);
+ }
+
+ public void ApplyToPlayer(Player player)
+ {
+ isBreakTime.UnbindAll();
+ isBreakTime.BindTo(player.IsBreakTime);
+ }
+
+ public void Update(Playfield playfield)
+ {
+ Coverage.Value = isBreakTime.Value
+ ? 0
+ : Math.Min(MAX_COVERAGE, MIN_COVERAGE + combo.Value * coverage_increase_per_combo) / reference_playfield_height;
+ }
+
+ protected override PlayfieldCoveringWrapper CreateCover(Drawable content) => new LegacyPlayfieldCover(content);
+
+ private partial class LegacyPlayfieldCover : PlayfieldCoveringWrapper
+ {
+ [Resolved]
+ private ISkinSource skin { get; set; } = null!;
+
+ private IBindable? hitPosition;
+
+ public LegacyPlayfieldCover(Drawable content)
+ : base(content)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ skin.SourceChanged += onSkinChanged;
+ onSkinChanged();
+ }
+
+ private void onSkinChanged()
+ {
+ hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition);
+ }
+
+ protected override float GetHeight(float coverage)
+ {
+ // In osu!stable, the cover is applied in absolute (x768) coordinates from the hit position.
+ float availablePlayfieldHeight = Math.Abs(reference_playfield_height - (hitPosition?.Value ?? Stage.HIT_TARGET_POSITION));
+
+ if (availablePlayfieldHeight == 0)
+ return base.GetHeight(coverage);
+
+ return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (skin.IsNotNull())
+ skin.SourceChanged -= onSkinChanged;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
index 4e6cc4f1d6..eba0b2effe 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion;
- public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) };
+ public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert), typeof(ManiaModNoRelease) };
public void ApplyToBeatmap(IBeatmap beatmap)
{
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
index 31f52610e9..7dd0c499da 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "One Key";
public override string Acronym => "1K";
public override LocalisableString Description => @"Play with one key.";
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
index 67e65b887a..a6c57d4597 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Ten Keys";
public override string Acronym => "10K";
public override LocalisableString Description => @"Play with ten keys.";
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
index 0f8148d252..0d04395a52 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Two Keys";
public override string Acronym => "2K";
public override LocalisableString Description => @"Play with two keys.";
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
index 0f8af7940c..c83b0979ee 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Three Keys";
public override string Acronym => "3K";
public override LocalisableString Description => @"Play with three keys.";
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
index f9690b4298..cc7e270dda 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
{
public override LocalisableString Description => "Notes are flipped horizontally.";
+ public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToBeatmap(IBeatmap beatmap)
{
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs
new file mode 100644
index 0000000000..b5490aa950
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs
@@ -0,0 +1,110 @@
+// 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 System.Threading;
+using osu.Framework.Localisation;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Mania.Mods
+{
+ public partial class ManiaModNoRelease : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableRuleset
+ {
+ public override string Name => "No Release";
+
+ public override string Acronym => "NR";
+
+ public override LocalisableString Description => "No more timing the end of hold notes.";
+
+ public override double ScoreMultiplier => 0.9;
+
+ public override ModType Type => ModType.DifficultyReduction;
+
+ public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };
+
+ public void ApplyToBeatmap(IBeatmap beatmap)
+ {
+ var maniaBeatmap = (ManiaBeatmap)beatmap;
+ var hitObjects = maniaBeatmap.HitObjects.Select(obj =>
+ {
+ if (obj is HoldNote hold)
+ return new NoReleaseHoldNote(hold);
+
+ return obj;
+ }).ToList();
+
+ maniaBeatmap.HitObjects = hitObjects;
+ }
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
+
+ foreach (var stage in maniaRuleset.Playfield.Stages)
+ {
+ foreach (var column in stage.Columns)
+ {
+ column.RegisterPool(10, 50);
+ }
+ }
+ }
+
+ private partial class NoReleaseDrawableHoldNoteTail : DrawableHoldNoteTail
+ {
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ // apply perfect once the tail is reached
+ if (HoldNote.HoldStartTime != null && timeOffset >= 0)
+ ApplyResult(GetCappedResult(HitResult.Perfect));
+ else
+ base.CheckForResult(userTriggered, timeOffset);
+ }
+ }
+
+ private class NoReleaseTailNote : TailNote
+ {
+ }
+
+ private class NoReleaseHoldNote : HoldNote
+ {
+ public NoReleaseHoldNote(HoldNote hold)
+ {
+ StartTime = hold.StartTime;
+ Duration = hold.Duration;
+ Column = hold.Column;
+ NodeSamples = hold.NodeSamples;
+ }
+
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ AddNested(Head = new HeadNote
+ {
+ StartTime = StartTime,
+ Column = Column,
+ Samples = GetNodeSamples(0),
+ });
+
+ AddNested(Tail = new NoReleaseTailNote
+ {
+ StartTime = EndTime,
+ Column = Column,
+ Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
+ });
+
+ AddNested(Body = new HoldNoteBody
+ {
+ StartTime = StartTime,
+ Column = Column
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs
similarity index 78%
rename from osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
rename to osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs
index bc76c5cfe9..864ef6c3d6 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs
@@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
@@ -15,7 +14,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
- public abstract class ManiaModPlayfieldCover : ModHidden, IApplicableToDrawableRuleset
+ public abstract class ManiaModWithPlayfieldCover : ModHidden, IApplicableToDrawableRuleset
{
public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
@@ -24,7 +23,9 @@ namespace osu.Game.Rulesets.Mania.Mods
///
protected abstract CoverExpandDirection ExpandDirection { get; }
- [SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")]
+ ///
+ /// The relative area that should be completely covered. This does not include the fade.
+ ///
public abstract BindableNumber Coverage { get; }
public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
@@ -37,15 +38,17 @@ namespace osu.Game.Rulesets.Mania.Mods
Container hocParent = (Container)hoc.Parent!;
hocParent.Remove(hoc, false);
- hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
+ hocParent.Add(CreateCover(hoc).With(c =>
{
c.RelativeSizeAxes = Axes.Both;
c.Direction = ExpandDirection;
- c.Coverage = Coverage.Value;
+ c.Coverage.BindTo(Coverage);
}));
}
}
+ protected virtual PlayfieldCoveringWrapper CreateCover(Drawable content) => new PlayfieldCoveringWrapper(content);
+
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 3490d50871..9c56f0473c 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -265,14 +265,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (Tail.AllJudged)
{
if (Tail.IsHit)
- ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ ApplyMaxResult();
else
MissForcefully();
- }
- // Make sure that the hold note is fully judged by giving the body a judgement.
- if (Tail.AllJudged && !Body.AllJudged)
- Body.TriggerResult(Tail.IsHit);
+ // Make sure that the hold note is fully judged by giving the body a judgement.
+ if (!Body.AllJudged)
+ Body.TriggerResult(Tail.IsHit);
+
+ // Important that this is always called when a result is applied.
+ endHold();
+ }
}
public override void MissForcefully()
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs
index 1b2efbafdf..6259033235 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs
@@ -25,7 +25,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
if (AllJudged) return;
- ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
+ if (hit)
+ ApplyMaxResult();
+ else
+ ApplyMinResult();
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 8498fd36de..e98622b8bf 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
/// Causes this to get missed, disregarding all conditions in implementations of .
///
- public virtual void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
+ public virtual void MissForcefully() => ApplyMinResult();
}
public abstract partial class DrawableManiaHitObject : DrawableManiaHitObject
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index 680009bc4c..f6b92ab405 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -89,18 +89,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = r.Judgement.MinResult);
+ ApplyMinResult();
return;
}
var result = HitObject.HitWindows.ResultFor(timeOffset);
+
if (result == HitResult.None)
return;
result = GetCappedResult(result);
-
- ApplyResult(r => r.Type = result);
+ ApplyResult(result);
}
///
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index 3f930a310b..6be0ee2d6b 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -72,18 +72,18 @@ namespace osu.Game.Rulesets.Mania.Objects
///
/// The head note of the hold.
///
- public HeadNote Head { get; private set; }
+ public HeadNote Head { get; protected set; }
///
/// The tail note of the hold.
///
- public TailNote Tail { get; private set; }
+ public TailNote Tail { get; protected set; }
///
/// The body of the hold.
/// This is an invisible and silent object that tracks the holding state of the .
///
- public HoldNoteBody Body { get; private set; }
+ public HoldNoteBody Body { get; protected set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 1947d86a97..0444394d87 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
}
protected override IEnumerable EnumerateHitObjects(IBeatmap beatmap)
- => base.EnumerateHitObjects(beatmap).OrderBy(ho => ho, JudgementOrderComparer.DEFAULT);
+ => base.EnumerateHitObjects(beatmap).Order(JudgementOrderComparer.DEFAULT);
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
index 4ce3c50f7c..0052fd8b78 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
@@ -17,8 +17,10 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
- public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
{
+ private const float judgement_y_position = 160;
+
private RingExplosion? ringExplosion;
[Resolved]
@@ -30,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
- Y = 160;
+ Y = judgement_y_position;
}
[BackgroundDependencyLoader]
@@ -76,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
- this.MoveTo(Vector2.Zero);
+ this.MoveToY(judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
index 07045b76ca..6de0752671 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
@@ -65,11 +65,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
- light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d =>
+ light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength)?.With(d =>
{
- if (d == null)
- return;
-
d.Origin = Anchor.Centre;
d.Blending = BlendingParameters.Additive;
d.Scale = new Vector2(lightScale);
@@ -91,11 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting);
- bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30).With(d =>
+ bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30)?.With(d =>
{
- if (d == null)
- return;
-
if (d is TextureAnimation animation)
animation.IsPlaying = false;
@@ -140,10 +134,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private void onIsHittingChanged(ValueChangedEvent isHitting)
{
if (bodySprite is TextureAnimation bodyAnimation)
- {
- bodyAnimation.GotoFrame(0);
bodyAnimation.IsPlaying = isHitting.NewValue;
- }
if (lightContainer == null)
return;
@@ -219,6 +210,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
base.Update();
+ if (!isHitting.Value)
+ (bodySprite as TextureAnimation)?.GotoFrame(0);
+
if (holdNote.Body.HasHoldBreak)
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
@@ -243,7 +237,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
bodySprite.FillMode = FillMode.Stretch;
// i dunno this looks about right??
- bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight);
+ // the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild.
+ if (sprite.DrawHeight > 0)
+ bodySprite.Scale = new Vector2(1, scaleDirection * MathF.Max(1, 32800 / sprite.DrawHeight));
}
break;
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
index 1ec218644c..95b00e32ea 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
@@ -43,11 +43,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
- explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength).With(d =>
+ explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength)?.With(d =>
{
- if (d == null)
- return;
-
d.Origin = Anchor.Centre;
d.Blending = BlendingParameters.Additive;
d.Scale = new Vector2(explosionScale);
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
index 48b92a8486..8f9a2d7e74 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.UI;
@@ -46,17 +47,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
+ // Key images are placed side-to-side on the playfield, therefore ClampToEdge must be used to prevent any gaps between each key.
upSprite = new Sprite
{
Origin = Anchor.BottomCentre,
- Texture = skin.GetTexture(upImage),
+ Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, default),
RelativeSizeAxes = Axes.X,
Width = 1
},
downSprite = new Sprite
{
Origin = Anchor.BottomCentre,
- Texture = skin.GetTexture(downImage),
+ Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, default),
RelativeSizeAxes = Axes.X,
Width = 1,
Alpha = 0
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs
index 1a47fe5076..680198c1a6 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs
@@ -28,13 +28,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
string bottomImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
?? "mania-stage-bottom";
- sprite = skin.GetAnimation(bottomImage, true, true)?.With(d =>
- {
- if (d == null)
- return;
-
- d.Scale = new Vector2(1.6f);
- });
+ sprite = skin.GetAnimation(bottomImage, true, true)?.With(d => d.Scale = new Vector2(1.6f));
if (sprite != null)
InternalChild = sprite;
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 6cd55bb099..c05a8f2a29 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -93,8 +93,7 @@ namespace osu.Game.Rulesets.Mania.UI
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
// (see `Stage.columnBackgrounds`).
BackgroundContainer,
- TopLevelContainer,
- new ColumnTouchInputArea(this)
+ TopLevelContainer
};
var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
@@ -181,38 +180,5 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
-
- public partial class ColumnTouchInputArea : Drawable
- {
- private readonly Column column;
-
- [Resolved(canBeNull: true)]
- private ManiaInputManager maniaInputManager { get; set; }
-
- private KeyBindingContainer keyBindingContainer;
-
- public ColumnTouchInputArea(Column column)
- {
- RelativeSizeAxes = Axes.Both;
-
- this.column = column;
- }
-
- protected override void LoadComplete()
- {
- keyBindingContainer = maniaInputManager?.KeyBindingContainer;
- }
-
- protected override bool OnTouchDown(TouchDownEvent e)
- {
- keyBindingContainer?.TriggerPressed(column.Action.Value);
- return true;
- }
-
- protected override void OnTouchUp(TouchUpEvent e)
- {
- keyBindingContainer?.TriggerReleased(column.Action.Value);
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
index 8734f8ac8a..f444448797 100644
--- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
+++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
@@ -3,17 +3,12 @@
#nullable disable
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Skinning;
-using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -28,20 +23,21 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// All contents added to this .
///
- public IReadOnlyList Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList();
+ public TContent[] Content { get; }
- private readonly FillFlowContainer columns;
+ private readonly FillFlowContainer> columns;
private readonly StageDefinition stageDefinition;
public ColumnFlow(StageDefinition stageDefinition)
{
this.stageDefinition = stageDefinition;
+ Content = new TContent[stageDefinition.Columns];
AutoSizeAxes = Axes.X;
Masking = true;
- InternalChild = columns = new FillFlowContainer
+ InternalChild = columns = new FillFlowContainer>
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
@@ -49,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.UI
};
for (int i = 0; i < stageDefinition.Columns; i++)
- columns.Add(new Container { RelativeSizeAxes = Axes.Y });
+ columns.Add(new Container { RelativeSizeAxes = Axes.Y });
}
private ISkinSource currentSkin;
@@ -63,12 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI
onSkinChanged();
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
- updateMobileSizing();
- }
-
private void onSkinChanged()
{
for (int i = 0; i < stageDefinition.Columns; i++)
@@ -93,8 +83,6 @@ namespace osu.Game.Rulesets.Mania.UI
columns[i].Width = width.Value;
}
-
- updateMobileSizing();
}
///
@@ -102,31 +90,9 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// The index of the column to set the content of.
/// The content.
- public void SetContentForColumn(int column, TContent content) => columns[column].Child = content;
-
- private void updateMobileSizing()
+ public void SetContentForColumn(int column, TContent content)
{
- if (!IsLoaded || !RuntimeInfo.IsMobile)
- return;
-
- // GridContainer+CellContainer containing this stage (gets split up for dual stages).
- Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize;
-
- // Will be null in tests.
- if (containingCell == null)
- return;
-
- float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
-
- // 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
- float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
- // 1.92 is a "reference" mobile screen aspect ratio for phones.
- // We should scale it back for cases like tablets which aren't so extreme.
- mobileAdjust *= aspectRatio / 1.92f;
-
- // Best effort until we have better mobile support.
- for (int i = 0; i < stageDefinition.Columns; i++)
- columns[i].Width *= mobileAdjust;
+ Content[column] = columns[column].Child = content;
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index bea536e4af..aed53e157a 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -1,16 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Graphics;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Input;
+using osu.Framework.Platform;
+using osu.Framework.Threading;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers;
@@ -26,10 +27,12 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
+using osu.Game.Screens.Play;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI
{
+ [Cached]
public partial class DrawableManiaRuleset : DrawableScrollingRuleset
{
///
@@ -42,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.UI
///
public const double MAX_TIME_RANGE = 11485;
- protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
+ public new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
@@ -54,15 +57,19 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly Bindable configDirection = new Bindable();
private readonly BindableInt configScrollSpeed = new BindableInt();
- private double smoothTimeRange;
+
+ private double currentTimeRange;
+ protected double TargetTimeRange;
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
- [Resolved]
- private ISkinSource skin { get; set; }
+ private ISkinSource currentSkin = null!;
- public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ [Resolved]
+ private GameHost gameHost { get; set; } = null!;
+
+ public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null)
: base(ruleset, beatmap, mods)
{
BarLines = new BarLineGenerator(Beatmap).BarLines;
@@ -72,8 +79,12 @@ namespace osu.Game.Rulesets.Mania.UI
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(ISkinSource source)
{
+ currentSkin = source;
+ currentSkin.SourceChanged += onSkinChange;
+ skinChanged();
+
foreach (var mod in Mods.OfType())
mod.ApplyToTrack(speedAdjustmentTrack);
@@ -96,9 +107,11 @@ namespace osu.Game.Rulesets.Mania.UI
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
- configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
+ configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
- TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
+ TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
+
+ KeyBindingInputManager.Add(new ManiaTouchInputArea());
}
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
@@ -109,19 +122,37 @@ namespace osu.Game.Rulesets.Mania.UI
updateTimeRange();
}
+ private ScheduledDelegate? pendingSkinChange;
+ private float hitPosition;
+
+ private void onSkinChange()
+ {
+ // schedule required to avoid calls after disposed.
+ // note that this has the side-effect of components only performing a skin change when they are alive.
+ pendingSkinChange?.Cancel();
+ pendingSkinChange = Scheduler.Add(skinChanged);
+ }
+
+ private void skinChanged()
+ {
+ hitPosition = currentSkin.GetConfig(
+ new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
+ ?? Stage.HIT_TARGET_POSITION;
+
+ pendingSkinChange = null;
+ }
+
private void updateTimeRange()
{
- float hitPosition = skin.GetConfig(
- new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
- ?? Stage.HIT_TARGET_POSITION;
-
const float length_to_default_hit_position = 768 - LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION;
float lengthToHitPosition = 768 - hitPosition;
// This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position.
float scale = lengthToHitPosition / length_to_default_hit_position;
- TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
+ // we're intentionally using the game host's update clock here to decouple the time range tween from the gameplay clock (which can be arbitrarily paused, or even rewinding)
+ currentTimeRange = Interpolation.DampContinuously(currentTimeRange, TargetTimeRange, 50, gameHost.UpdateThread.Clock.ElapsedFrameTime);
+ TimeRange.Value = currentTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
}
///
@@ -139,10 +170,20 @@ namespace osu.Game.Rulesets.Mania.UI
protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
- public override DrawableHitObject CreateDrawableRepresentation(ManiaHitObject h) => null;
+ public override DrawableHitObject? CreateDrawableRepresentation(ManiaHitObject h) => null;
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score);
+
+ protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay();
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (currentSkin.IsNotNull())
+ currentSkin.SourceChanged -= onSkinChange;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 314d199944..b3420c49f3 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -7,7 +7,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using System;
using System.Collections.Generic;
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -42,7 +41,16 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
+ {
+ foreach (var s in stages)
+ {
+ if (s.ReceivePositionalInputAt(screenSpacePos))
+ return true;
+ }
+
+ return false;
+ }
public ManiaPlayfield(List stageDefinitions)
{
@@ -71,7 +79,7 @@ namespace osu.Game.Rulesets.Mania.UI
stages.Add(newStage);
AddNested(newStage);
- firstColumnIndex += newStage.Columns.Count;
+ firstColumnIndex += newStage.Columns.Length;
}
}
@@ -125,9 +133,9 @@ namespace osu.Game.Rulesets.Mania.UI
foreach (var stage in stages)
{
- if (index >= stage.Columns.Count)
+ if (index >= stage.Columns.Length)
{
- index -= stage.Columns.Count;
+ index -= stage.Columns.Length;
continue;
}
@@ -140,7 +148,18 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// Retrieves the total amount of columns across all stages in this playfield.
///
- public int TotalColumns => stages.Sum(s => s.Columns.Count);
+ public int TotalColumns
+ {
+ get
+ {
+ int sum = 0;
+
+ foreach (var stage in stages)
+ sum += stage.Columns.Length;
+
+ return sum;
+ }
+ }
private Stage getStageByColumn(int column)
{
@@ -148,7 +167,7 @@ namespace osu.Game.Rulesets.Mania.UI
foreach (var stage in stages)
{
- sum += stage.Columns.Count;
+ sum += stage.Columns.Length;
if (sum > column)
return stage;
}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs
new file mode 100644
index 0000000000..453b75ac84
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.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.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Configuration;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// An overlay that captures and displays osu!mania mouse and touch input.
+ ///
+ public partial class ManiaTouchInputArea : VisibilityContainer
+ {
+ // visibility state affects our child. we always want to handle input.
+ public override bool PropagatePositionalInputSubTree => true;
+ public override bool PropagateNonPositionalInputSubTree => true;
+
+ [SettingSource("Spacing", "The spacing between receptors.")]
+ public BindableFloat Spacing { get; } = new BindableFloat(10)
+ {
+ Precision = 1,
+ MinValue = 0,
+ MaxValue = 100,
+ };
+
+ [SettingSource("Opacity", "The receptor opacity.")]
+ public BindableFloat Opacity { get; } = new BindableFloat(1)
+ {
+ Precision = 0.1f,
+ MinValue = 0,
+ MaxValue = 1
+ };
+
+ [Resolved]
+ private DrawableManiaRuleset drawableRuleset { get; set; } = null!;
+
+ private GridContainer gridContainer = null!;
+
+ public ManiaTouchInputArea()
+ {
+ Anchor = Anchor.BottomCentre;
+ Origin = Anchor.BottomCentre;
+
+ RelativeSizeAxes = Axes.Both;
+ Height = 0.5f;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ List receptorGridContent = new List();
+ List receptorGridDimensions = new List();
+
+ bool first = true;
+
+ foreach (var stage in drawableRuleset.Playfield.Stages)
+ {
+ foreach (var column in stage.Columns)
+ {
+ if (!first)
+ {
+ receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
+ receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
+ }
+
+ receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } });
+ receptorGridDimensions.Add(new Dimension());
+
+ first = false;
+ }
+ }
+
+ InternalChild = gridContainer = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ AlwaysPresent = true,
+ Content = new[] { receptorGridContent.ToArray() },
+ ColumnDimensions = receptorGridDimensions.ToArray()
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
+ }
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ // Hide whenever the keyboard is used.
+ Hide();
+ return false;
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ Show();
+ return true;
+ }
+
+ protected override bool OnTouchDown(TouchDownEvent e)
+ {
+ Show();
+ return true;
+ }
+
+ protected override void PopIn()
+ {
+ gridContainer.FadeIn(500, Easing.OutQuint);
+ }
+
+ protected override void PopOut()
+ {
+ gridContainer.FadeOut(300);
+ }
+
+ public partial class ColumnInputReceptor : CompositeDrawable
+ {
+ public readonly IBindable Action = new Bindable();
+
+ private readonly Box highlightOverlay;
+
+ [Resolved]
+ private ManiaInputManager? inputManager { get; set; }
+
+ private bool isPressed;
+
+ public ColumnInputReceptor()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = 10,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.15f,
+ },
+ highlightOverlay = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ Blending = BlendingParameters.Additive,
+ }
+ }
+ }
+ };
+ }
+
+ protected override bool OnTouchDown(TouchDownEvent e)
+ {
+ updateButton(true);
+ return false; // handled by parent container to show overlay.
+ }
+
+ protected override void OnTouchUp(TouchUpEvent e)
+ {
+ updateButton(false);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ updateButton(true);
+ return false; // handled by parent container to show overlay.
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ updateButton(false);
+ }
+
+ private void updateButton(bool press)
+ {
+ if (press == isPressed)
+ return;
+
+ isPressed = press;
+
+ if (press)
+ {
+ inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
+ highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
+ }
+ else
+ {
+ inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
+ highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
+ }
+ }
+ }
+
+ private partial class Gutter : Drawable
+ {
+ public readonly IBindable Spacing = new Bindable();
+
+ public Gutter()
+ {
+ Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs
index 92f471e36b..d8d9705530 100644
--- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs
+++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs
@@ -1,6 +1,8 @@
// 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.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -8,17 +10,24 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics;
+using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Rulesets.Mania.UI
{
///
- /// A that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield.
+ /// A that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield.
///
public partial class PlayfieldCoveringWrapper : CompositeDrawable
{
+ ///
+ /// The relative area that should be completely covered. This does not include the fade.
+ ///
+ public readonly BindableFloat Coverage = new BindableFloat();
+
///
/// The complete cover, including gradient and fill.
///
@@ -36,6 +45,8 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly IBindable scrollDirection = new Bindable();
+ private float currentCoverageHeight;
+
public PlayfieldCoveringWrapper(Drawable content)
{
InternalChild = new BufferedContainer
@@ -94,21 +105,46 @@ namespace osu.Game.Rulesets.Mania.UI
scrollDirection.BindValueChanged(onScrollDirectionChanged, true);
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateCoverSize(true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ updateCoverSize(false);
+ }
+
+ private void updateCoverSize(bool instant)
+ {
+ float targetCoverage;
+ float targetAlpha;
+
+ if (instant)
+ {
+ targetCoverage = Coverage.Value;
+ targetAlpha = Coverage.Value > 0 ? 1 : 0;
+ }
+ else
+ {
+ targetCoverage = (float)Interpolation.DampContinuously(currentCoverageHeight, Coverage.Value, 25, Math.Abs(Time.Elapsed));
+ targetAlpha = (float)Interpolation.DampContinuously(gradient.Alpha, Coverage.Value > 0 ? 1 : 0, 25, Math.Abs(Time.Elapsed));
+ }
+
+ filled.Height = GetHeight(targetCoverage);
+ gradient.Y = -GetHeight(targetCoverage);
+ gradient.Alpha = targetAlpha;
+
+ currentCoverageHeight = targetCoverage;
+ }
+
+ protected virtual float GetHeight(float coverage) => coverage;
+
private void onScrollDirectionChanged(ValueChangedEvent direction)
=> cover.Rotation = direction.NewValue == ScrollingDirection.Up ? 0 : 180f;
- ///
- /// The relative area that should be completely covered. This does not include the fade.
- ///
- public float Coverage
- {
- set
- {
- filled.Height = value;
- gradient.Y = -value;
- }
- }
-
///
/// The direction in which the cover expands.
///
@@ -123,11 +159,13 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// The cover expands along the scrolling direction.
///
+ [Description("Along scroll")]
AlongScroll,
///
/// The cover expands against the scrolling direction.
///
+ [Description("Against scroll")]
AgainstScroll
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index fa9af6d157..a4a09c9a82 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -1,22 +1,22 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using System.Collections.Generic;
+using System;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -36,18 +36,29 @@ namespace osu.Game.Rulesets.Mania.UI
public const float HIT_TARGET_POSITION = 110;
- public IReadOnlyList Columns => columnFlow.Content;
+ public Column[] Columns => columnFlow.Content;
private readonly ColumnFlow columnFlow;
private readonly JudgementContainer judgements;
- private readonly DrawablePool judgementPool;
+ private readonly JudgementPooler judgementPooler;
private readonly Drawable barLineContainer;
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Columns.Any(c => c.ReceivePositionalInputAt(screenSpacePos));
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
+ {
+ foreach (var c in Columns)
+ {
+ if (c.ReceivePositionalInputAt(screenSpacePos))
+ return true;
+ }
+
+ return false;
+ }
private readonly int firstColumnIndex;
+ private ISkinSource currentSkin = null!;
+
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
{
this.firstColumnIndex = firstColumnIndex;
@@ -65,7 +76,6 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new Drawable[]
{
- judgementPool = new DrawablePool(2),
new Container
{
Anchor = Anchor.TopCentre,
@@ -104,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
RelativeSizeAxes = Axes.Y,
},
- new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
+ new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground))
{
RelativeSizeAxes = Axes.Both
},
@@ -137,11 +147,13 @@ namespace osu.Game.Rulesets.Mania.UI
AddNested(column);
}
+ var hitWindows = new ManiaHitWindows();
+
+ AddInternal(judgementPooler = new JudgementPooler(Enum.GetValues().Where(r => hitWindows.IsHitResultAllowed(r))));
+
RegisterPool(50, 200);
}
- private ISkinSource currentSkin;
-
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
@@ -170,7 +182,7 @@ namespace osu.Game.Rulesets.Mania.UI
base.Dispose(isDisposing);
- if (currentSkin != null)
+ if (currentSkin.IsNotNull())
currentSkin.SourceChanged -= onSkinChanged;
}
@@ -180,13 +192,13 @@ namespace osu.Game.Rulesets.Mania.UI
NewResult += OnNewResult;
}
- public override void Add(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Add(hitObject);
+ public override void Add(HitObject hitObject) => Columns[((ManiaHitObject)hitObject).Column - firstColumnIndex].Add(hitObject);
- public override bool Remove(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Remove(hitObject);
+ public override bool Remove(HitObject hitObject) => Columns[((ManiaHitObject)hitObject).Column - firstColumnIndex].Remove(hitObject);
- public override void Add(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Add(h);
+ public override void Add(DrawableHitObject h) => Columns[((ManiaHitObject)h.HitObject).Column - firstColumnIndex].Add(h);
- public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
+ public override bool Remove(DrawableHitObject h) => Columns[((ManiaHitObject)h.HitObject).Column - firstColumnIndex].Remove(h);
public void Add(BarLine barLine) => base.Add(barLine);
@@ -196,13 +208,13 @@ namespace osu.Game.Rulesets.Mania.UI
return;
judgements.Clear(false);
- judgements.Add(judgementPool.Get(j =>
+ judgements.Add(judgementPooler.Get(result.Type, j =>
{
j.Apply(result, judgedObject);
j.Anchor = Anchor.Centre;
j.Origin = Anchor.Centre;
- }));
+ })!);
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
index 72f172188e..3bca938450 100644
--- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
+++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
@@ -1,6 +1,6 @@
- net6.0
+ net8.0
Library
true
smash the keys. to the beat.
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
index ed4725dd94..d0c3484cfd 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
index e8a46a9828..b79de6d40b 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
@@ -1,7 +1,7 @@
- net6.0-android
+ net8.0-android
Exe
osu.Game.Rulesets.Osu.Tests
osu.Game.Rulesets.Osu.Tests.Android
@@ -24,4 +24,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
index 7d50deb8ba..cc0233d7fd 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
@@ -1,7 +1,7 @@
Exe
- net6.0-ios
+ net8.0-ios
13.4
Exe
osu.Game.Rulesets.Osu.Tests
diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json
index 61be25b845..a68d6e12c0 100644
--- a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json
+++ b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Osu.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net8.0/osu.Game.Rulesets.Osu.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Osu.Tests.dll"
+ "${workspaceRoot}/bin/Release/net8.0/osu.Game.Rulesets.Osu.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs
new file mode 100644
index 0000000000..5f49714d93
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs
@@ -0,0 +1,194 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Osu.Edit.Checks;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Tests.Beatmaps;
+using System.Linq;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
+{
+ [TestFixture]
+ public class CheckOsuAbnormalDifficultySettingsTest
+ {
+ private CheckOsuAbnormalDifficultySettings check = null!;
+
+ private readonly IBeatmap beatmap = new Beatmap();
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckOsuAbnormalDifficultySettings();
+
+ beatmap.Difficulty = new BeatmapDifficulty
+ {
+ ApproachRate = 5,
+ CircleSize = 5,
+ DrainRate = 5,
+ OverallDifficulty = 5,
+ };
+ }
+
+ [Test]
+ public void TestNormalSettings()
+ {
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public void TestApproachRateTwoDecimals()
+ {
+ beatmap.Difficulty.ApproachRate = 5.55f;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
+ }
+
+ [Test]
+ public void TestCircleSizeTwoDecimals()
+ {
+ beatmap.Difficulty.CircleSize = 5.55f;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
+ }
+
+ [Test]
+ public void TestDrainRateTwoDecimals()
+ {
+ beatmap.Difficulty.DrainRate = 5.55f;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
+ }
+
+ [Test]
+ public void TestOverallDifficultyTwoDecimals()
+ {
+ beatmap.Difficulty.OverallDifficulty = 5.55f;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
+ }
+
+ [Test]
+ public void TestApproachRateUnder()
+ {
+ beatmap.Difficulty.ApproachRate = -10;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestCircleSizeUnder()
+ {
+ beatmap.Difficulty.CircleSize = -10;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestDrainRateUnder()
+ {
+ beatmap.Difficulty.DrainRate = -10;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestOverallDifficultyUnder()
+ {
+ beatmap.Difficulty.OverallDifficulty = -10;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestApproachRateOver()
+ {
+ beatmap.Difficulty.ApproachRate = 20;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestCircleSizeOver()
+ {
+ beatmap.Difficulty.CircleSize = 20;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestDrainRateOver()
+ {
+ beatmap.Difficulty.DrainRate = 20;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ [Test]
+ public void TestOverallDifficultyOver()
+ {
+ beatmap.Difficulty.OverallDifficulty = 20;
+
+ var context = getContext();
+ var issues = check.Run(context).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
+ }
+
+ private BeatmapVerifierContext getContext()
+ {
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
index 3d35ab79f7..fd711e543c 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
moveMouseToHitObject(1);
- AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true);
+ AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
mergeSelection();
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider created", () =>
{
- if (circle1 is null || circle2 is null || slider is null)
+ if (circle1 == null || circle2 == null || slider == null)
return false;
var controlPoints = slider.Path.ControlPoints;
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider created", () =>
{
- if (slider1 is null || slider2 is null || slider1Path is null)
+ if (slider1 == null || slider2 == null || slider1Path == null)
return false;
var controlPoints1 = slider1Path.ControlPoints;
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider end is at same completion for last slider", () =>
{
- if (slider1Path is null || slider2 is null)
+ if (slider1Path == null || slider2 == null)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
@@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
moveMouseToHitObject(1);
- AddAssert("merge option not available", () => selectionHandler.ContextMenuItems?.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
+ AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
mergeSelection();
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
@@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
moveMouseToHitObject(1);
- AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true);
+ AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
mergeSelection();
@@ -231,6 +231,137 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
(pos: circle2.Position, pathType: null)));
}
+ [Test]
+ public void TestMergeSliderSliderSameStartTime()
+ {
+ Slider? slider1 = null;
+ SliderPath? slider1Path = null;
+ Slider? slider2 = null;
+
+ AddStep("select two sliders", () =>
+ {
+ slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
+ slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
+ slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
+ EditorClock.Seek(slider1.StartTime);
+ EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
+ });
+
+ AddStep("move sliders to the same start time", () =>
+ {
+ slider2!.StartTime = slider1!.StartTime;
+ });
+
+ mergeSelection();
+
+ AddAssert("slider created", () =>
+ {
+ if (slider1 == null || slider2 == null || slider1Path == null)
+ return false;
+
+ var controlPoints1 = slider1Path.ControlPoints;
+ var controlPoints2 = slider2.Path.ControlPoints;
+ (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
+
+ for (int i = 0; i < controlPoints1.Count - 1; i++)
+ {
+ args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
+ }
+
+ for (int i = 0; i < controlPoints2.Count; i++)
+ {
+ args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
+ }
+
+ return sliderCreatedFor(args);
+ });
+
+ AddAssert("samples exist", sliderSampleExist);
+
+ AddAssert("merged slider matches first slider", () =>
+ {
+ var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
+ return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
+ && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
+ && mergedSlider.Samples.SequenceEqual(slider1.Samples);
+ });
+
+ AddAssert("slider end is at same completion for last slider", () =>
+ {
+ if (slider1Path == null || slider2 == null)
+ return false;
+
+ var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
+ return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
+ });
+ }
+
+ [Test]
+ public void TestMergeSliderSliderSameStartAndEndTime()
+ {
+ Slider? slider1 = null;
+ SliderPath? slider1Path = null;
+ Slider? slider2 = null;
+
+ AddStep("select two sliders", () =>
+ {
+ slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
+ slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
+ slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
+ EditorClock.Seek(slider1.StartTime);
+ EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
+ });
+
+ AddStep("move sliders to the same start & end time", () =>
+ {
+ slider2!.StartTime = slider1!.StartTime;
+ slider2.Path = slider1.Path;
+ });
+
+ mergeSelection();
+
+ AddAssert("slider created", () =>
+ {
+ if (slider1 == null || slider2 == null || slider1Path == null)
+ return false;
+
+ var controlPoints1 = slider1Path.ControlPoints;
+ var controlPoints2 = slider2.Path.ControlPoints;
+ (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
+
+ for (int i = 0; i < controlPoints1.Count - 1; i++)
+ {
+ args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
+ }
+
+ for (int i = 0; i < controlPoints2.Count; i++)
+ {
+ args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
+ }
+
+ return sliderCreatedFor(args);
+ });
+
+ AddAssert("samples exist", sliderSampleExist);
+
+ AddAssert("merged slider matches first slider", () =>
+ {
+ var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
+ return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
+ && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
+ && mergedSlider.Samples.SequenceEqual(slider1.Samples);
+ });
+
+ AddAssert("slider end is at same completion for last slider", () =>
+ {
+ if (slider1Path == null || slider2 == null)
+ return false;
+
+ var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
+ return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
+ });
+ }
+
private void mergeSelection()
{
AddStep("merge selection", () =>
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs
index 623cefff6b..b97fe5c5a8 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs
@@ -124,6 +124,113 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count == 2);
}
+ [Test]
+ public void TestControlClickAddsControlPointsIfSingleSliderSelected()
+ {
+ var firstSlider = new Slider
+ {
+ StartTime = 0,
+ Position = new Vector2(0, 0),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100))
+ }
+ }
+ };
+ var secondSlider = new Slider
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 200),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100, -100))
+ }
+ }
+ };
+
+ AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
+ AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange(new HitObject[] { secondSlider }));
+
+ AddStep("move mouse to middle of slider", () =>
+ {
+ var pos = blueprintContainer.SelectionBlueprints
+ .First(s => s.Item == secondSlider)
+ .ChildrenOfType().First()
+ .ScreenSpaceDrawQuad.Centre;
+
+ InputManager.MoveMouseTo(pos);
+ });
+ AddStep("control-click left mouse", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
+ AddAssert("slider has 3 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(3));
+ }
+
+ [Test]
+ public void TestControlClickDoesNotAddSliderControlPointsIfMultipleObjectsSelected()
+ {
+ var firstSlider = new Slider
+ {
+ StartTime = 0,
+ Position = new Vector2(0, 0),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100))
+ }
+ }
+ };
+ var secondSlider = new Slider
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 200),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100, -100))
+ }
+ }
+ };
+
+ AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
+ AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange(new HitObject[] { firstSlider, secondSlider }));
+
+ AddStep("move mouse to middle of slider", () =>
+ {
+ var pos = blueprintContainer.SelectionBlueprints
+ .First(s => s.Item == secondSlider)
+ .ChildrenOfType().First()
+ .ScreenSpaceDrawQuad.Centre;
+
+ InputManager.MoveMouseTo(pos);
+ });
+ AddStep("control-click left mouse", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddAssert("selection not preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
+ AddAssert("second slider not selected",
+ () => blueprintContainer.SelectionBlueprints.First(s => s.Item == secondSlider).IsSelected,
+ () => Is.False);
+ AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2));
+ }
+
private ComposeBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType().First();
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
index d14e593587..b17f4e7487 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
@@ -1,13 +1,17 @@
// 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.Testing;
using osu.Framework.Utils;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
+using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual;
+using osu.Game.Utils;
using osuTK;
using osuTK.Input;
@@ -24,22 +28,22 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
- rectangularGridActive(false);
+ gridActive(false);
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType().Any());
- rectangularGridActive(true);
+ gridActive(true);
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any());
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
- rectangularGridActive(true);
+ gridActive(true);
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType().Any());
- rectangularGridActive(false);
+ gridActive(false);
}
[Test]
@@ -52,38 +56,122 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any());
+
+ AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
+ AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
+ AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
+ AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any());
+ AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
+ AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestDistanceSnapAdjustDoesNotHideTheGridIfStartingEnabled()
+ {
+ double distanceSnap = double.PositiveInfinity;
+
+ AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
+
+ AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
+ AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
+ AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType().First().DistanceSpacingMultiplier.Value);
+
+ AddStep("increase distance", () =>
+ {
+ InputManager.PressKey(Key.AltLeft);
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.ScrollVerticalBy(1);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ InputManager.ReleaseKey(Key.AltLeft);
+ });
+
+ AddUntilStep("distance snap increased", () => this.ChildrenOfType().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap));
+ AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestDistanceSnapAdjustShowsGridMomentarilyIfStartingDisabled()
+ {
+ double distanceSnap = double.PositiveInfinity;
+
+ AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
+ AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any());
+ AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType().First().DistanceSpacingMultiplier.Value);
+
+ AddStep("start increasing distance", () =>
+ {
+ InputManager.PressKey(Key.AltLeft);
+ InputManager.PressKey(Key.ControlLeft);
+ });
+
+ AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
+
+ AddStep("finish increasing distance", () =>
+ {
+ InputManager.ScrollVerticalBy(1);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ InputManager.ReleaseKey(Key.AltLeft);
+ });
+
+ AddUntilStep("distance snap increased", () => this.ChildrenOfType().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap));
+ AddUntilStep("distance snap hidden in the end", () => !this.ChildrenOfType().Any());
}
[Test]
public void TestGridSnapMomentaryToggle()
{
- rectangularGridActive(false);
+ gridActive(false);
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
- rectangularGridActive(true);
+ gridActive(true);
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
- rectangularGridActive(false);
+ gridActive(false);
}
- private void rectangularGridActive(bool active)
+ private void gridActive(bool active) where T : PositionSnapGrid
{
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
- AddStep("move cursor to (1, 1)", () =>
+ AddStep("move cursor to spacing + (1, 1)", () =>
{
- var composer = Editor.ChildrenOfType().Single();
- InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1)));
+ var composer = Editor.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(composer.ToScreenSpace(uniqueSnappingPosition(composer) + new Vector2(1, 1)));
});
if (active)
- AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(0, 0)));
+ {
+ AddAssert("placement blueprint at spacing + (0, 0)", () =>
+ {
+ var composer = Editor.ChildrenOfType().Single();
+ return Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position,
+ uniqueSnappingPosition(composer));
+ });
+ }
else
- AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(1, 1)));
+ {
+ AddAssert("placement blueprint at spacing + (1, 1)", () =>
+ {
+ var composer = Editor.ChildrenOfType().Single();
+ return Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position,
+ uniqueSnappingPosition(composer) + new Vector2(1, 1));
+ });
+ }
+ }
+
+ private Vector2 uniqueSnappingPosition(PositionSnapGrid grid)
+ {
+ return grid switch
+ {
+ RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
+ TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
+ CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
+ _ => Vector2.Zero
+ };
}
[Test]
public void TestGridSizeToggling()
{
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
- AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any());
+ AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any());
gridSizeIs(4);
nextGridSizeIs(8);
@@ -99,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}
private void gridSizeIs(int size)
- => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing == new Vector2(size)
+ => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing.Value == new Vector2(size)
&& EditorBeatmap.BeatmapInfo.GridSize == size);
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs
new file mode 100644
index 0000000000..28c1577fcb
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs
@@ -0,0 +1,300 @@
+// 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;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ [TestFixture]
+ public partial class TestSceneOsuReverseSelection : TestSceneOsuEditor
+ {
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ [Test]
+ public void TestReverseSelectionTwoCircles()
+ {
+ OsuHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ AddStep("Add circles", () =>
+ {
+ var circle1 = new HitCircle
+ {
+ StartTime = 0,
+ Position = new Vector2(208, 240)
+ };
+ var circle2 = new HitCircle
+ {
+ StartTime = 200,
+ Position = new Vector2(256, 144)
+ };
+
+ EditorBeatmap.AddRange([circle1, circle2]);
+ });
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+
+ AddStep("Reverse selection", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.Key(Key.G);
+ InputManager.ReleaseKey(Key.LControl);
+ });
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+ }
+
+ [Test]
+ public void TestReverseSelectionThreeCircles()
+ {
+ OsuHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ AddStep("Add circles", () =>
+ {
+ var circle1 = new HitCircle
+ {
+ StartTime = 0,
+ Position = new Vector2(208, 240)
+ };
+ var circle2 = new HitCircle
+ {
+ StartTime = 200,
+ Position = new Vector2(256, 144)
+ };
+ var circle3 = new HitCircle
+ {
+ StartTime = 400,
+ Position = new Vector2(304, 240)
+ };
+
+ EditorBeatmap.AddRange([circle1, circle2, circle3]);
+ });
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+
+ AddStep("Reverse selection", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.Key(Key.G);
+ InputManager.ReleaseKey(Key.LControl);
+ });
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+ }
+
+ [Test]
+ public void TestReverseSelectionCircleAndSlider()
+ {
+ OsuHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ Vector2 sliderHeadOldPosition = default;
+ Vector2 sliderTailOldPosition = default;
+
+ AddStep("Add objects", () =>
+ {
+ var circle = new HitCircle
+ {
+ StartTime = 0,
+ Position = new Vector2(208, 240)
+ };
+ var slider = new Slider
+ {
+ StartTime = 200,
+ Position = sliderHeadOldPosition = new Vector2(257, 144),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100))
+ }
+ }
+ };
+
+ sliderTailOldPosition = slider.EndPosition;
+
+ EditorBeatmap.AddRange([circle, slider]);
+ });
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ AddStep("Select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+
+ AddStep("Reverse selection", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.Key(Key.G);
+ InputManager.ReleaseKey(Key.LControl);
+ });
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+
+ AddAssert("Slider head is at slider tail", () =>
+ Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, sliderTailOldPosition) < 1);
+
+ AddAssert("Slider tail is at slider head", () =>
+ Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).EndPosition, sliderHeadOldPosition) < 1);
+ }
+
+ [Test]
+ public void TestReverseSelectionTwoCirclesAndSlider()
+ {
+ OsuHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ Vector2 sliderHeadOldPosition = default;
+ Vector2 sliderTailOldPosition = default;
+
+ AddStep("Add objects", () =>
+ {
+ var circle1 = new HitCircle
+ {
+ StartTime = 0,
+ Position = new Vector2(208, 240)
+ };
+ var circle2 = new HitCircle
+ {
+ StartTime = 200,
+ Position = new Vector2(256, 144)
+ };
+ var slider = new Slider
+ {
+ StartTime = 200,
+ Position = sliderHeadOldPosition = new Vector2(304, 240),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100))
+ }
+ }
+ };
+
+ sliderTailOldPosition = slider.EndPosition;
+
+ EditorBeatmap.AddRange([circle1, circle2, slider]);
+ });
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ AddStep("Select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+
+ AddStep("Reverse selection", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.Key(Key.G);
+ InputManager.ReleaseKey(Key.LControl);
+ });
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+
+ AddAssert("Slider head is at slider tail", () =>
+ Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, sliderTailOldPosition) < 1);
+
+ AddAssert("Slider tail is at slider head", () =>
+ Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).EndPosition, sliderHeadOldPosition) < 1);
+ }
+
+ [Test]
+ public void TestReverseSelectionTwoCombos()
+ {
+ OsuHitObject[] objects = null!;
+ bool[] newCombos = null!;
+
+ AddStep("Add circles", () =>
+ {
+ var circle1 = new HitCircle
+ {
+ StartTime = 0,
+ Position = new Vector2(216, 240)
+ };
+ var circle2 = new HitCircle
+ {
+ StartTime = 200,
+ Position = new Vector2(120, 192)
+ };
+ var circle3 = new HitCircle
+ {
+ StartTime = 400,
+ Position = new Vector2(216, 144)
+ };
+
+ var circle4 = new HitCircle
+ {
+ StartTime = 646,
+ NewCombo = true,
+ Position = new Vector2(296, 240)
+ };
+ var circle5 = new HitCircle
+ {
+ StartTime = 846,
+ Position = new Vector2(392, 162)
+ };
+ var circle6 = new HitCircle
+ {
+ StartTime = 1046,
+ Position = new Vector2(296, 144)
+ };
+
+ EditorBeatmap.AddRange([circle1, circle2, circle3, circle4, circle5, circle6]);
+ });
+
+ AddStep("store objects & new combo data", () =>
+ {
+ objects = getObjects().ToArray();
+ newCombos = getObjectNewCombos().ToArray();
+ });
+
+ AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+
+ AddStep("Reverse selection", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.Key(Key.G);
+ InputManager.ReleaseKey(Key.LControl);
+ });
+
+ AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
+ AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
+ }
+
+ private IEnumerable getObjects() => EditorBeatmap.HitObjects.OfType();
+
+ private IEnumerable getObjectNewCombos() => getObjects().Select(ho => ho.NewCombo);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
index 8234381283..93eb76aba6 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
@@ -30,23 +31,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
});
- [Test]
- public void TestAddOverlappingControlPoints()
- {
- createVisualiser(true);
-
- addControlPointStep(new Vector2(200));
- addControlPointStep(new Vector2(300));
- addControlPointStep(new Vector2(300));
- addControlPointStep(new Vector2(500, 300));
-
- AddAssert("last connection displayed", () =>
- {
- var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position == new Vector2(300));
- return lastConnection.DrawWidth > 50;
- });
- }
-
[Test]
public void TestPerfectCurveTooManyPoints()
{
@@ -172,6 +156,109 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPathType(4, null);
}
+ [Test]
+ public void TestStackingUpdatesPointsPosition()
+ {
+ createVisualiser(true);
+
+ Vector2[] points =
+ [
+ new Vector2(200),
+ new Vector2(300),
+ new Vector2(500, 300),
+ new Vector2(700, 200),
+ new Vector2(500, 100)
+ ];
+
+ foreach (var point in points) addControlPointStep(point);
+
+ AddStep("apply stacking", () => slider.StackHeightBindable.Value += 1);
+
+ for (int i = 0; i < points.Length; i++)
+ addAssertPointPositionChanged(points, i);
+ }
+
+ [Test]
+ public void TestChangingControlPointTypeViaTab()
+ {
+ createVisualiser(true);
+
+ addControlPointStep(new Vector2(200), PathType.LINEAR);
+ addControlPointStep(new Vector2(300));
+ addControlPointStep(new Vector2(500, 300));
+ addControlPointStep(new Vector2(700, 200));
+ addControlPointStep(new Vector2(500, 100));
+
+ AddStep("select first control point", () => visualiser.Pieces[0].IsSelected.Value = true);
+ AddStep("press tab", () => InputManager.Key(Key.Tab));
+ assertControlPointPathType(0, PathType.BEZIER);
+
+ AddStep("press shift-tab", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.Tab);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ assertControlPointPathType(0, PathType.LINEAR);
+
+ AddStep("press shift-tab", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.Tab);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ assertControlPointPathType(0, PathType.BSpline(4));
+
+ AddStep("press shift-tab", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.Tab);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ assertControlPointPathType(0, PathType.PERFECT_CURVE);
+ assertControlPointPathType(2, PathType.BSpline(4));
+
+ AddStep("select third last control point", () =>
+ {
+ visualiser.Pieces[0].IsSelected.Value = false;
+ visualiser.Pieces[2].IsSelected.Value = true;
+ });
+
+ AddStep("press shift-tab", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.Tab);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ assertControlPointPathType(2, PathType.PERFECT_CURVE);
+
+ AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
+ assertControlPointPathType(0, PathType.BEZIER);
+ assertControlPointPathType(2, null);
+
+ AddStep("select first and third control points", () =>
+ {
+ visualiser.Pieces[0].IsSelected.Value = true;
+ visualiser.Pieces[2].IsSelected.Value = true;
+ });
+ AddStep("press alt-1", () =>
+ {
+ InputManager.PressKey(Key.AltLeft);
+ InputManager.Key(Key.Number1);
+ InputManager.ReleaseKey(Key.AltLeft);
+ });
+ assertControlPointPathType(0, PathType.LINEAR);
+ assertControlPointPathType(2, PathType.LINEAR);
+ }
+
+ private void addAssertPointPositionChanged(Vector2[] points, int index)
+ {
+ AddAssert($"Point at {points.ElementAt(index)} changed",
+ () => visualiser.Pieces[index].Position,
+ () => !Is.EqualTo(points.ElementAt(index))
+ );
+ }
+
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection)
{
Anchor = Anchor.Centre,
@@ -206,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"click context menu item \"{contextMenuText}\"", () =>
{
- MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
+ MenuItem item = visualiser.ContextMenuItems!.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
item?.Action.Value?.Invoke();
});
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs
index d7dd30d608..30e0dbbf2e 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs
@@ -24,14 +24,38 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestHotkeyHandling()
{
- AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First()));
+ AddStep("deselect everything", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
- AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero);
+ AddUntilStep("no popover present", getPopover, () => Is.Null);
+
+ AddStep("select single circle",
+ () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First()));
+ AddStep("press rotate hotkey", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.R);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddUntilStep("popover present", getPopover, () => Is.Not.Null);
+ AddAssert("only playfield centre origin rotation available", () =>
+ {
+ var popover = getPopover();
+ var buttons = popover.ChildrenOfType();
+ return buttons.Any(btn => btn.Text == "Selection centre" && !btn.Enabled.Value)
+ && buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
+ });
+ AddStep("press rotate hotkey", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.R);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddUntilStep("no popover present", getPopover, () => Is.Null);
AddStep("select first three objects", () =>
{
@@ -44,14 +68,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
- AddUntilStep("popover present", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1));
+ AddUntilStep("popover present", getPopover, () => Is.Not.Null);
+ AddAssert("both origin rotation available", () =>
+ {
+ var popover = getPopover();
+ var buttons = popover.ChildrenOfType();
+ return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value)
+ && buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
+ });
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
- AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero);
+ AddUntilStep("no popover present", getPopover, () => Is.Null);
+
+ PreciseRotationPopover? getPopover() => this.ChildrenOfType().SingleOrDefault();
}
[Test]
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
index bbded55732..bc1e4f9864 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
@@ -2,13 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
@@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(200);
assertControlPointCount(2);
- assertControlPointType(0, PathType.LINEAR);
+ assertFinalControlPointType(0, PathType.LINEAR);
}
[Test]
@@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
- assertControlPointType(0, PathType.LINEAR);
+ assertFinalControlPointType(0, PathType.LINEAR);
}
[Test]
@@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
- assertControlPointType(0, PathType.PERFECT_CURVE);
+ assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100, 100));
- assertControlPointType(0, PathType.BEZIER);
+ assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
- assertControlPointType(0, PathType.LINEAR);
- assertControlPointType(1, PathType.LINEAR);
+ assertFinalControlPointType(0, PathType.LINEAR);
+ assertFinalControlPointType(1, PathType.LINEAR);
}
[Test]
@@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
- assertControlPointType(0, PathType.LINEAR);
+ assertFinalControlPointType(0, PathType.LINEAR);
assertLength(100);
}
@@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
- assertControlPointType(0, PathType.PERFECT_CURVE);
+ assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(4);
- assertControlPointType(0, PathType.BEZIER);
+ assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
- assertControlPointType(0, PathType.LINEAR);
- assertControlPointType(1, PathType.LINEAR);
+ assertFinalControlPointType(0, PathType.LINEAR);
+ assertFinalControlPointType(1, PathType.LINEAR);
}
[Test]
@@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
- assertControlPointType(0, PathType.LINEAR);
- assertControlPointType(1, PathType.PERFECT_CURVE);
+ assertFinalControlPointType(0, PathType.LINEAR);
+ assertFinalControlPointType(1, PathType.PERFECT_CURVE);
}
[Test]
@@ -268,8 +271,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPosition(2, new Vector2(100));
assertControlPointPosition(3, new Vector2(200, 100));
assertControlPointPosition(4, new Vector2(200));
- assertControlPointType(0, PathType.PERFECT_CURVE);
- assertControlPointType(2, PathType.PERFECT_CURVE);
+ assertFinalControlPointType(0, PathType.PERFECT_CURVE);
+ assertFinalControlPointType(2, PathType.PERFECT_CURVE);
+ }
+
+ [Test]
+ public void TestManualPathTypeControlViaKeyboard()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300));
+
+ assertControlPointTypeDuringPlacement(0, PathType.PERFECT_CURVE);
+
+ AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
+ assertControlPointTypeDuringPlacement(0, PathType.LINEAR);
+
+ AddStep("press shift-tab", () =>
+ {
+ InputManager.PressKey(Key.ShiftLeft);
+ InputManager.Key(Key.Tab);
+ InputManager.ReleaseKey(Key.ShiftLeft);
+ });
+ assertControlPointTypeDuringPlacement(0, PathType.BSpline(4));
+
+ AddStep("start new segment via S", () => InputManager.Key(Key.S));
+ assertControlPointTypeDuringPlacement(2, PathType.LINEAR);
+
+ addMovementStep(new Vector2(400, 300));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertFinalControlPointType(0, PathType.BSpline(4));
+ assertFinalControlPointType(2, PathType.PERFECT_CURVE);
}
[Test]
@@ -293,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addClickStep(MouseButton.Right);
assertPlaced(true);
- assertControlPointType(0, PathType.BEZIER);
+ assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -312,11 +353,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(808, tolerance: 10);
assertControlPointCount(5);
- assertControlPointType(0, PathType.BSpline(4));
- assertControlPointType(1, null);
- assertControlPointType(2, null);
- assertControlPointType(3, null);
- assertControlPointType(4, null);
+ assertFinalControlPointType(0, PathType.BSpline(4));
+ assertFinalControlPointType(1, null);
+ assertFinalControlPointType(2, null);
+ assertFinalControlPointType(3, null);
+ assertFinalControlPointType(4, null);
}
[Test]
@@ -337,10 +378,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(600, tolerance: 10);
assertControlPointCount(4);
- assertControlPointType(0, PathType.BSpline(4));
- assertControlPointType(1, PathType.BSpline(4));
- assertControlPointType(2, PathType.BSpline(4));
- assertControlPointType(3, null);
+ assertFinalControlPointType(0, PathType.BSpline(4));
+ assertFinalControlPointType(1, PathType.BSpline(4));
+ assertFinalControlPointType(2, PathType.BSpline(4));
+ assertFinalControlPointType(3, null);
}
[Test]
@@ -359,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
- assertControlPointType(0, PathType.BEZIER);
+ assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -379,7 +420,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
- assertControlPointType(0, PathType.PERFECT_CURVE);
+ assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@@ -400,7 +441,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
- assertControlPointType(0, PathType.PERFECT_CURVE);
+ assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@@ -421,7 +462,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
- assertControlPointType(0, PathType.BEZIER);
+ assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -438,7 +479,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
- assertControlPointType(0, PathType.PERFECT_CURVE);
+ assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
@@ -454,7 +495,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected));
- private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
+ private void assertControlPointTypeDuringPlacement(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}",
+ () => this.ChildrenOfType>().ElementAt(index).ControlPoint.Type, () => Is.EqualTo(type));
+
+ private void assertFinalControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1));
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
index 6c7733e68a..d68cbe6265 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
@@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("add hitsounds", () =>
{
- if (slider is null) return;
+ if (slider == null) return;
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70);
slider.Samples.Add(sample.With());
@@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"move mouse to control point {index}", () =>
{
- if (slider is null || visualiser is null) return;
+ if (slider == null || visualiser == null) return;
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position));
@@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"click context menu item \"{contextMenuText}\"", () =>
{
- if (visualiser is null) return;
+ if (visualiser == null) return;
MenuItem? item = visualiser.ContextMenuItems?.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs
index 021fdba225..52a170b84e 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs
@@ -3,9 +3,12 @@
#nullable disable
+using System;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -71,4 +74,120 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void moveMouse(Vector2 pos) =>
AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
}
+
+ [TestFixture]
+ public class TestSliderNearLinearScaling
+ {
+ private readonly Random rng = new Random(1337);
+
+ [Test]
+ public void TestScalingSliderFlat()
+ {
+ SliderPath sliderPathPerfect = new SliderPath(
+ [
+ new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE),
+ new PathControlPoint(new Vector2(50, 25)),
+ new PathControlPoint(new Vector2(25, 100)),
+ ]);
+
+ SliderPath sliderPathBezier = new SliderPath(
+ [
+ new PathControlPoint(new Vector2(0), PathType.BEZIER),
+ new PathControlPoint(new Vector2(50, 25)),
+ new PathControlPoint(new Vector2(25, 100)),
+ ]);
+
+ scaleSlider(sliderPathPerfect, new Vector2(0.000001f, 1));
+ scaleSlider(sliderPathBezier, new Vector2(0.000001f, 1));
+
+ for (int i = 0; i < 100; i++)
+ {
+ Assert.True(Precision.AlmostEquals(sliderPathPerfect.PositionAt(i / 100.0f), sliderPathBezier.PositionAt(i / 100.0f)));
+ }
+ }
+
+ [Test]
+ public void TestPerfectCurveMatchesTheoretical()
+ {
+ for (int i = 0; i < 20000; i++)
+ {
+ //Only test points that are in the screen's bounds
+ float p1X = 640.0f * (float)rng.NextDouble();
+ float p2X = 640.0f * (float)rng.NextDouble();
+
+ float p1Y = 480.0f * (float)rng.NextDouble();
+ float p2Y = 480.0f * (float)rng.NextDouble();
+ SliderPath sliderPathPerfect = new SliderPath(
+ [
+ new PathControlPoint(new Vector2(0, 0), PathType.PERFECT_CURVE),
+ new PathControlPoint(new Vector2(p1X, p1Y)),
+ new PathControlPoint(new Vector2(p2X, p2Y)),
+ ]);
+
+ assertMatchesPerfectCircle(sliderPathPerfect);
+
+ scaleSlider(sliderPathPerfect, new Vector2(0.00001f, 1));
+
+ assertMatchesPerfectCircle(sliderPathPerfect);
+ }
+ }
+
+ private void assertMatchesPerfectCircle(SliderPath path)
+ {
+ if (path.ControlPoints.Count != 3)
+ return;
+
+ //Replication of PathApproximator.CircularArcToPiecewiseLinear
+ CircularArcProperties circularArcProperties = new CircularArcProperties(path.ControlPoints.Select(x => x.Position).ToArray());
+
+ if (!circularArcProperties.IsValid)
+ return;
+
+ //Addresses cases where circularArcProperties.ThetaRange>0.5
+ //Occurs in code in PathControlPointVisualiser.ensureValidPathType
+ RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(path.ControlPoints.Select(x => x.Position).ToArray());
+ if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
+ return;
+
+ int subpoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius)))));
+
+ //ignore cases where subpoints is int.MaxValue, result will be garbage
+ //as well, having this many subpoints will cause an out of memory error, so can't happen during normal useage
+ if (subpoints == int.MaxValue)
+ return;
+
+ for (int i = 0; i < Math.Min(subpoints, 100); i++)
+ {
+ float progress = (float)rng.NextDouble();
+
+ //To avoid errors from interpolating points, ensure we check only positions that would be subpoints.
+ progress = (float)Math.Ceiling(progress * (subpoints - 1)) / (subpoints - 1);
+
+ //Special case - if few subpoints, ensure checking every single one rather than randomly
+ if (subpoints < 100)
+ progress = i / (float)(subpoints - 1);
+
+ //edge points cause issue with interpolation, so ignore the last two points and first
+ if (progress == 0.0f || progress >= (subpoints - 2) / (float)(subpoints - 1))
+ continue;
+
+ double theta = circularArcProperties.ThetaStart + (circularArcProperties.Direction * progress * circularArcProperties.ThetaRange);
+ Vector2 vector = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * circularArcProperties.Radius;
+
+ Assert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f),
+ "A perfect circle with points " + string.Join(", ", path.ControlPoints.Select(x => x.Position)) + " and radius" + circularArcProperties.Radius + "from SliderPath does not almost equal a theoretical perfect circle with " + subpoints + " subpoints"
+ + ": " + (circularArcProperties.Centre + vector) + " - " + path.PositionAt(progress)
+ + " = " + (circularArcProperties.Centre + vector - path.PositionAt(progress))
+ );
+ }
+ }
+
+ private void scaleSlider(SliderPath path, Vector2 scale)
+ {
+ for (int i = 0; i < path.ControlPoints.Count; i++)
+ {
+ path.ControlPoints[i].Position *= scale;
+ }
+ }
+ }
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
index 88c81c7a39..7375617aa8 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
@@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Autoplay = false,
Beatmap = new Beatmap
{
- Breaks = new List
+ Breaks =
{
new BreakPeriod(500, 2000),
},
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs
index a353914cd5..075fdd88ca 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs
@@ -1,8 +1,19 @@
// 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.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
@@ -21,5 +32,150 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
+
+ [Test]
+ public void TestPlayfieldBasedSize()
+ {
+ ModFlashlight mod = new OsuModFlashlight();
+ CreateModTest(new ModTestData
+ {
+ Mod = mod,
+ PassCondition = () =>
+ {
+ var flashlightOverlay = Player.DrawableRuleset.Overlays
+ .ChildrenOfType.Flashlight>()
+ .First();
+
+ return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize());
+ }
+ });
+
+ AddStep("adjust playfield scale", () =>
+ Player.DrawableRuleset.Playfield.Scale = new Vector2(.5f));
+ }
+
+ [Test]
+ public void TestSliderDimsOnlyAfterStartTime()
+ {
+ bool sliderDimmedBeforeStartTime = false;
+
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModFlashlight(),
+ PassCondition = () =>
+ {
+ sliderDimmedBeforeStartTime |=
+ Player.GameplayClockContainer.CurrentTime < 1000 && Player.ChildrenOfType.Flashlight>().Single().FlashlightDim > 0;
+ return Player.GameplayState.HasPassed && !sliderDimmedBeforeStartTime;
+ },
+ Beatmap = new OsuBeatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle { StartTime = 0, },
+ new Slider
+ {
+ StartTime = 1000,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100))
+ })
+ }
+ },
+ BeatmapInfo =
+ {
+ StackLeniency = 0,
+ }
+ },
+ ReplayFrames = new List