diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index b8dc201559f1..ace7db82f8e5 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.517.0",
"commands": [
"localisation"
]
diff --git a/.editorconfig b/.editorconfig
index c249e5e9b36f..7aecde95eee9 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 ff6d869e7200..a8a5d5e64b20 100644
--- a/.github/ISSUE_TEMPLATE/bug-issue.yml
+++ b/.github/ISSUE_TEMPLATE/bug-issue.yml
@@ -11,6 +11,10 @@ body:
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful.
+ # ATTENTION LINUX USERS
+
+ If you are having an issue and it is hardware related, **please open a [q&a discussion](https://github.com/ppy/osu/discussions/categories/q-a)** instead of an issue. There's a high chance your issue is due to your system configuration, and not our software.
+
- type: dropdown
attributes:
label: Type
@@ -38,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
@@ -46,22 +50,16 @@ body:
value: |
## Logs
- Attaching log files is required for every reported bug. See instructions below on how to find them.
-
- **Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
+ Attaching log files is required for **every** issue, regardless of whether you deem them required or not. See instructions below on how to find them.
### Desktop platforms
If the game has not yet been closed since you found the bug:
- 1. Head on to game settings and click on "Open osu! folder"
- 2. Then open the `logs` folder located there
+ 1. Head on to game settings and click on "Export logs"
+ 2. Click the notification to locate the file
+ 3. Drag the generated `.zip` files into the github issue window
- The default places to find the logs on desktop platforms are as follows:
- - `%AppData%/osu/logs` *on Windows*
- - `~/.local/share/osu/logs` *on Linux*
- - `~/Library/Application Support/osu/logs` *on macOS*
-
- If you have selected a custom location for the game files, you can find the `logs` folder there.
+ ![export logs button](https://github.com/ppy/osu/assets/191335/cbfa5550-b7ed-4c5c-8dd0-8b87cc90ad9b)
### Mobile platforms
@@ -69,10 +67,6 @@ body:
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
- ---
-
- After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
-
- type: textarea
attributes:
label: Logs
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 103e4dbc309d..1ea46545632f 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') }}
@@ -77,12 +70,12 @@ 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: Compile
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
@@ -106,18 +99,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 +126,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 5f16e090409d..7fd0f798cd33 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)
+ 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 99e39f6f5611..c44f46d70a60 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 ff4165c41486..be104d0fd3f2 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 32d3d37ffec3..b19f03ad7dd3 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 525b3418cd1a..a51ad09d6c86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -265,6 +265,7 @@ __pycache__/
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
+.idea/*/.idea/projectSettingsUpdater.xml
# Generated files
.idea/**/contentModel.xml
@@ -340,4 +341,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 a7b652c4545b..a4d4707f9b7a 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 4bb9f4d2a0dc..000000000000
--- 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/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
deleted file mode 100644
index 4bb9f4d2a0dc..000000000000
--- 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 d500c595c0d9..a7a6649a4ff6 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 6da760dead15..39e6cb10c569 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 741679707a3e..88367387aefe 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 104b1266ca12..cdc980e019df 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 f58f9d4ae2c0..d91cf014795a 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 0f2c3903280f..daafff7dfb2c 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 898aec880ceb..62f587410f50 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 dae6e032b13f..f6e2c26d1994 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 519107b5e39e..c437dcf0f0a5 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 4bb9f4d2a0dc..000000000000
--- 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 4bb9f4d2a0dc..000000000000
--- 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 9a471df90206..d72e7abee21f 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 d93fddf42dc2..7c5225cff701 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 4106641adb5b..0fe6b6fb4d52 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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/Directory.Build.props b/Directory.Build.props
index 734374c84004..5ba12b845ba0 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 d3e7537cef72..3bb8b62d5d74 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 d5dc0723afc0..cb722e5df3ad 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 000000000000..74d05ff69064
--- /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 000000000000..1c1e5f540c09
--- /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 b43381934688..0d7203739372 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 03ee7c9204b3..63c481a62350 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 @@ public static class VisualTestRunner
[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 2baa7ee0e012..7d43eb2b05f9 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 744e207b5742..e53fe011579b 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 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
{
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 d09e7647e073..89abd5665c1c 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 000000000000..74d05ff69064
--- /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 000000000000..1c1e5f540c09
--- /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 d60bc2571daa..ec832d9a72c7 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 55c0cf6a3bff..c44cbb845b08 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 @@ public static class VisualTestRunner
[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 a2308e6dfca0..7dc8a1336b01 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 c5ada4288dcc..b1be25727f96 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.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 @@ private void load(TextureStore textures)
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 9c8867f4eff6..165b6b6c6b89 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 000000000000..74d05ff69064
--- /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 000000000000..1c1e5f540c09
--- /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 f1f37f63638b..a60979073b5f 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 b45505678c0d..5beb6616a7e9 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 @@ public static class VisualTestRunner
[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 e839d2657cca..9c4c8217f043 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 a3c3b891057d..adcbd36485cb 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 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
{
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 5bf3884f53c0..6d9565a6f2ee 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 000000000000..74d05ff69064
--- /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 000000000000..1c1e5f540c09
--- /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 d60bc2571daa..ec832d9a72c7 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 55c0cf6a3bff..c44cbb845b08 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 @@ public static class VisualTestRunner
[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 a2308e6dfca0..7dc8a1336b01 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 d198fa81cb12..3ad636a601a6 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.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 @@ private void load(PippidonPlayfield playfield, TextureStore textures)
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 9c8867f4eff6..165b6b6c6b89 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 b8c3ad373a59..186a6093f5ce 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 c2a587fdc267..fabfcc223e74 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 1e40e844ccb4..f564b93d6f58 100644
Binary files a/assets/lazer.png and b/assets/lazer.png differ
diff --git a/global.json b/global.json
index 5dcd5f425a82..789bff3bd0c2 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 6609db302759..349829555dcc 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 fd01b1116485..000000000000
--- 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 33ffed432e68..bbee491d90a0 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -72,9 +72,9 @@ protected override void OnCreate(Bundle? savedInstanceState)
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 52cfb67f428c..a235913ef3aa 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -5,13 +5,9 @@
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 @@ public override void SetHost(GameHost host)
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 000000000000..1af30228ecaa
--- /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 fc7aa8a092cb..000000000000
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 000000000000..600c070c3e1a
--- /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 000000000000..7133c9c861db
--- /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 000000000000..a1f717cf6c41
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 000000000000..36651206e8c7
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 000000000000..c68ed7a2d558
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 000000000000..85f308442952
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 000000000000..28be07512ad6
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 000000000000..613b817f0a54
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 000000000000..4330a939d9f1
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 000000000000..ec7d60283808
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 000000000000..2e722ee884f6
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 000000000000..f22db0c61680
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 1507bfaa290e..be2e66972801 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 503e5935f594..606988ccdfde 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 f990fd55fc87..780d367900ea 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -5,14 +5,21 @@
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);
+
+ 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.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
+ client.Initialize();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
@@ -67,36 +113,59 @@ private void load(OsuConfigManager config)
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()
- {
- if (!client.IsInitialized)
- return;
+ private void onRoomUpdated() => schedulePresenceUpdate();
- if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
+ private ScheduledDelegate? presenceUpdateDelegate;
+
+ private void schedulePresenceUpdate()
+ {
+ presenceUpdateDelegate?.Cancel();
+ presenceUpdateDelegate = Scheduler.AddDelayed(() =>
{
- client.ClearPresence();
+ 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.Online && activity.Value != null)
+ // user activity
+ if (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 @@ private void updateStatus()
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 @@ private void updateStatus()
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 @@ private string truncate(string str)
});
}
- 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 @@ private string truncate(string str)
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 000000000000..0b09613ba0c3
--- /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 a0db896f46da..3e06dad4c54b 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -7,6 +7,7 @@
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.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 @@ internal partial class OsuGameDesktop : OsuGame
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 @@ public OsuGameDesktop(string[]? args = null)
[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 @@ public override void SetHost(GameHost host)
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 @@ protected override void Dispose(bool isDisposing)
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 000000000000..0df87ab007f5
--- /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 a33e845f5b10..23e56cdce9c6 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -5,6 +5,7 @@
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.Game;
using osu.Game.IPC;
using osu.Game.Tournament;
-using SDL2;
+using SDL;
using Squirrel;
namespace osu.Desktop
@@ -30,30 +31,51 @@ public static class Program
[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 @@ public static void Main(string[] args)
}
}
- 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 @@ private static void setupSquirrel()
{
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 000000000000..9ca2dc3a5c82
--- /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 000000000000..89084b5a15db
--- /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/Windows/Icons.cs b/osu.Desktop/Windows/Icons.cs
new file mode 100644
index 000000000000..67915c101a05
--- /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 000000000000..b32c01433de9
--- /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 a6aa8abb9f3f..d5dbf933c1c6
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 f37cfdc5f17c..e7a63bd921a9 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,15 @@
-
+
-
+
+
+
+
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index db58c325bd3b..66b3970351f1 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 000000000000..d40b92db5f06
--- /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 000000000000..aa229c7d06da
--- /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 5de21a68d0e0..af84ee47f13a 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 52b34959b97d..b6ab91ed5c14 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 4ee321944295..4b2e54be6736 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 acf12bb0ac01..9c262a752a98 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 201343a03619..7b9291c87055 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 7572c6670f13..f4c36d518800 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -52,6 +52,9 @@ public class CatchBeatmapConversionTest : BeatmapConversionTest
[TestCase("3644427", new[] { typeof(CatchModEasy), typeof(CatchModFlashlight) })]
[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 000000000000..1b46be01fb69
--- /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/CatchRateAdjustedDisplayDifficultyTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.cs
new file mode 100644
index 000000000000..f77ec64df31f
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ [TestFixture]
+ public class CatchRateAdjustedDisplayDifficultyTest
+ {
+ private static IEnumerable difficultyValuesToTest()
+ {
+ for (float i = 0; i <= 10; i += 0.5f)
+ yield return i;
+ }
+
+ [TestCaseSource(nameof(difficultyValuesToTest))]
+ public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
+ {
+ var ruleset = new CatchRuleset();
+ var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
+
+ var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
+
+ Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
+ }
+
+ [Test]
+ public void TestRateBelowOne()
+ {
+ var ruleset = new CatchRuleset();
+ var difficulty = new BeatmapDifficulty();
+
+ var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
+
+ Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
+ }
+
+ [Test]
+ public void TestRateAboveOne()
+ {
+ var ruleset = new CatchRuleset();
+ var difficulty = new BeatmapDifficulty();
+
+ var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
+
+ Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
+ }
+ }
+}
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 000000000000..33aa4cba5db7
--- /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/TestSceneCatchReverseSelection.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs
new file mode 100644
index 000000000000..36a0e3388e1d
--- /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/Mods/TestSceneCatchModNoScope.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
index c48bf7adc959..c8f7da1aaed8 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
@@ -74,7 +74,7 @@ public void TestVisibleDuringBreak()
StartTime = 5000,
}
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(2000, 4000),
}
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
index 45e7d7aa280e..7d539f91e437 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
@@ -11,7 +11,7 @@
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
- public partial class TestSceneCatchModPerfect : ModPerfectTestScene
+ public partial class TestSceneCatchModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
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 000000000000..01150e701d9a
--- /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 000000000000..a0cecc1b180e
--- /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/112643-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643-expected-conversion.json
new file mode 100644
index 000000000000..7d6e29b6c1d4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643-expected-conversion.json
@@ -0,0 +1 @@
+{"Mappings":[{"StartTime":2375.0,"Objects":[{"StartTime":2375.0,"Position":64.0,"HyperDash":false}]},{"StartTime":2625.0,"Objects":[{"StartTime":2625.0,"Position":172.0,"HyperDash":false}]},{"StartTime":2875.0,"Objects":[{"StartTime":2875.0,"Position":152.0,"HyperDash":false}]},{"StartTime":3125.0,"Objects":[{"StartTime":3125.0,"Position":80.0,"HyperDash":false}]},{"StartTime":3375.0,"Objects":[{"StartTime":3375.0,"Position":224.0,"HyperDash":false}]},{"StartTime":3625.0,"Objects":[{"StartTime":3625.0,"Position":192.0,"HyperDash":false}]},{"StartTime":3875.0,"Objects":[{"StartTime":3875.0,"Position":136.0,"HyperDash":false}]},{"StartTime":4125.0,"Objects":[{"StartTime":4125.0,"Position":272.0,"HyperDash":false},{"StartTime":4187.0,"Position":295.965057,"HyperDash":false},{"StartTime":4250.0,"Position":339.30658,"HyperDash":false},{"StartTime":4312.0,"Position":372.55603,"HyperDash":false},{"StartTime":4375.0,"Position":372.509583,"HyperDash":false},{"StartTime":4437.0,"Position":372.203644,"HyperDash":false},{"StartTime":4500.0,"Position":340.885864,"HyperDash":false},{"StartTime":4562.0,"Position":348.843384,"HyperDash":false},{"StartTime":4625.0,"Position":384.566772,"HyperDash":false},{"StartTime":4749.0,"Position":462.643433,"HyperDash":false}]},{"StartTime":4875.0,"Objects":[{"StartTime":4875.0,"Position":504.0,"HyperDash":false},{"StartTime":4937.0,"Position":456.809235,"HyperDash":false},{"StartTime":5000.0,"Position":413.577362,"HyperDash":false},{"StartTime":5062.0,"Position":384.032623,"HyperDash":false},{"StartTime":5125.0,"Position":351.76297,"HyperDash":false},{"StartTime":5178.0,"Position":327.56488,"HyperDash":false},{"StartTime":5232.0,"Position":288.905457,"HyperDash":false},{"StartTime":5285.0,"Position":281.458923,"HyperDash":false},{"StartTime":5375.0,"Position":249.3499,"HyperDash":false}]},{"StartTime":5625.0,"Objects":[{"StartTime":5625.0,"Position":384.0,"HyperDash":false}]},{"StartTime":5875.0,"Objects":[{"StartTime":5875.0,"Position":272.0,"HyperDash":false}]},{"StartTime":6000.0,"Objects":[{"StartTime":6000.0,"Position":272.0,"HyperDash":false}]},{"StartTime":6125.0,"Objects":[{"StartTime":6125.0,"Position":272.0,"HyperDash":false}]},{"StartTime":6375.0,"Objects":[{"StartTime":6375.0,"Position":92.0,"HyperDash":false}]},{"StartTime":6625.0,"Objects":[{"StartTime":6625.0,"Position":124.0,"HyperDash":false}]},{"StartTime":6875.0,"Objects":[{"StartTime":6875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":7125.0,"Objects":[{"StartTime":7125.0,"Position":388.0,"HyperDash":false}]},{"StartTime":7375.0,"Objects":[{"StartTime":7375.0,"Position":420.0,"HyperDash":false}]},{"StartTime":7625.0,"Objects":[{"StartTime":7625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":7875.0,"Objects":[{"StartTime":7875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":8125.0,"Objects":[{"StartTime":8125.0,"Position":443.0,"HyperDash":false},{"StartTime":8187.0,"Position":392.598877,"HyperDash":false},{"StartTime":8250.0,"Position":365.1502,"HyperDash":false},{"StartTime":8312.0,"Position":352.954926,"HyperDash":false},{"StartTime":8375.0,"Position":294.614716,"HyperDash":false},{"StartTime":8437.0,"Position":268.171936,"HyperDash":false},{"StartTime":8500.0,"Position":207.09552,"HyperDash":false},{"StartTime":8562.0,"Position":158.395874,"HyperDash":false},{"StartTime":8625.0,"Position":135.590256,"HyperDash":false},{"StartTime":8749.0,"Position":67.66239,"HyperDash":false}]},{"StartTime":8875.0,"Objects":[{"StartTime":8875.0,"Position":24.0,"HyperDash":false},{"StartTime":8937.0,"Position":54.41505,"HyperDash":false},{"StartTime":9000.0,"Position":92.0854,"HyperDash":false},{"StartTime":9062.0,"Position":91.62684,"HyperDash":false},{"StartTime":9125.0,"Position":114.961037,"HyperDash":false},{"StartTime":9178.0,"Position":112.725426,"HyperDash":false},{"StartTime":9232.0,"Position":118.526962,"HyperDash":false},{"StartTime":9285.0,"Position":72.53759,"HyperDash":false},{"StartTime":9374.0,"Position":43.35332,"HyperDash":false}]},{"StartTime":9625.0,"Objects":[{"StartTime":9625.0,"Position":16.0,"HyperDash":false}]},{"StartTime":9875.0,"Objects":[{"StartTime":9875.0,"Position":136.0,"HyperDash":false}]},{"StartTime":10000.0,"Objects":[{"StartTime":10000.0,"Position":136.0,"HyperDash":false}]},{"StartTime":10125.0,"Objects":[{"StartTime":10125.0,"Position":136.0,"HyperDash":false}]},{"StartTime":10375.0,"Objects":[{"StartTime":10375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":10625.0,"Objects":[{"StartTime":10625.0,"Position":368.0,"HyperDash":false}]},{"StartTime":10875.0,"Objects":[{"StartTime":10875.0,"Position":196.0,"HyperDash":false}]},{"StartTime":11125.0,"Objects":[{"StartTime":11125.0,"Position":316.0,"HyperDash":false}]},{"StartTime":11375.0,"Objects":[{"StartTime":11375.0,"Position":144.0,"HyperDash":false}]},{"StartTime":11625.0,"Objects":[{"StartTime":11625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":11875.0,"Objects":[{"StartTime":11875.0,"Position":112.0,"HyperDash":false}]},{"StartTime":12125.0,"Objects":[{"StartTime":12125.0,"Position":164.0,"HyperDash":false},{"StartTime":12250.0,"Position":238.49942,"HyperDash":false}]},{"StartTime":12500.0,"Objects":[{"StartTime":12500.0,"Position":100.0,"HyperDash":false},{"StartTime":12625.0,"Position":25.50058,"HyperDash":false}]},{"StartTime":12875.0,"Objects":[{"StartTime":12875.0,"Position":144.0,"HyperDash":false},{"StartTime":13000.0,"Position":69.50058,"HyperDash":false}]},{"StartTime":13250.0,"Objects":[{"StartTime":13250.0,"Position":208.0,"HyperDash":false},{"StartTime":13375.0,"Position":282.49942,"HyperDash":false}]},{"StartTime":13625.0,"Objects":[{"StartTime":13625.0,"Position":332.0,"HyperDash":false}]},{"StartTime":13875.0,"Objects":[{"StartTime":13875.0,"Position":180.0,"HyperDash":false}]},{"StartTime":14125.0,"Objects":[{"StartTime":14125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":14250.0,"Objects":[{"StartTime":14250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":14500.0,"Objects":[{"StartTime":14500.0,"Position":324.0,"HyperDash":false}]},{"StartTime":14625.0,"Objects":[{"StartTime":14625.0,"Position":324.0,"HyperDash":false}]},{"StartTime":14875.0,"Objects":[{"StartTime":14875.0,"Position":192.0,"HyperDash":false}]},{"StartTime":15000.0,"Objects":[{"StartTime":15000.0,"Position":192.0,"HyperDash":false}]},{"StartTime":15250.0,"Objects":[{"StartTime":15250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":15375.0,"Objects":[{"StartTime":15375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":15625.0,"Objects":[{"StartTime":15625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":15875.0,"Objects":[{"StartTime":15875.0,"Position":120.0,"HyperDash":false}]},{"StartTime":16125.0,"Objects":[{"StartTime":16125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":18375.0,"Objects":[{"StartTime":18375.0,"Position":20.0,"HyperDash":false}]},{"StartTime":18625.0,"Objects":[{"StartTime":18625.0,"Position":180.0,"HyperDash":false}]},{"StartTime":18875.0,"Objects":[{"StartTime":18875.0,"Position":52.0,"HyperDash":false}]},{"StartTime":19125.0,"Objects":[{"StartTime":19125.0,"Position":120.0,"HyperDash":false}]},{"StartTime":19375.0,"Objects":[{"StartTime":19375.0,"Position":128.0,"HyperDash":false}]},{"StartTime":19625.0,"Objects":[{"StartTime":19625.0,"Position":48.0,"HyperDash":false}]},{"StartTime":19875.0,"Objects":[{"StartTime":19875.0,"Position":192.0,"HyperDash":false}]},{"StartTime":20125.0,"Objects":[{"StartTime":20125.0,"Position":300.0,"HyperDash":false},{"StartTime":20187.0,"Position":319.510284,"HyperDash":false},{"StartTime":20250.0,"Position":361.959717,"HyperDash":false},{"StartTime":20312.0,"Position":410.823639,"HyperDash":false},{"StartTime":20375.0,"Position":393.9937,"HyperDash":false},{"StartTime":20428.0,"Position":389.407,"HyperDash":false},{"StartTime":20482.0,"Position":394.563232,"HyperDash":false},{"StartTime":20535.0,"Position":430.098541,"HyperDash":false},{"StartTime":20624.0,"Position":486.9303,"HyperDash":false}]},{"StartTime":20875.0,"Objects":[{"StartTime":20875.0,"Position":472.0,"HyperDash":false},{"StartTime":20937.0,"Position":454.614349,"HyperDash":false},{"StartTime":21000.0,"Position":395.812744,"HyperDash":false},{"StartTime":21062.0,"Position":377.009979,"HyperDash":false},{"StartTime":21125.0,"Position":345.3677,"HyperDash":false},{"StartTime":21178.0,"Position":342.8652,"HyperDash":false},{"StartTime":21232.0,"Position":325.856567,"HyperDash":false},{"StartTime":21285.0,"Position":310.223846,"HyperDash":false},{"StartTime":21374.0,"Position":280.7244,"HyperDash":false}]},{"StartTime":21625.0,"Objects":[{"StartTime":21625.0,"Position":404.0,"HyperDash":false}]},{"StartTime":21875.0,"Objects":[{"StartTime":21875.0,"Position":432.0,"HyperDash":false}]},{"StartTime":22000.0,"Objects":[{"StartTime":22000.0,"Position":432.0,"HyperDash":false}]},{"StartTime":22125.0,"Objects":[{"StartTime":22125.0,"Position":432.0,"HyperDash":false}]},{"StartTime":22375.0,"Objects":[{"StartTime":22375.0,"Position":296.0,"HyperDash":false}]},{"StartTime":22625.0,"Objects":[{"StartTime":22625.0,"Position":168.0,"HyperDash":false},{"StartTime":22678.0,"Position":157.672318,"HyperDash":false},{"StartTime":22732.0,"Position":121.82901,"HyperDash":false},{"StartTime":22785.0,"Position":68.50134,"HyperDash":false},{"StartTime":22875.0,"Position":39.09584,"HyperDash":false}]},{"StartTime":23125.0,"Objects":[{"StartTime":23125.0,"Position":268.0,"HyperDash":false},{"StartTime":23178.0,"Position":252.906113,"HyperDash":false},{"StartTime":23232.0,"Position":215.4331,"HyperDash":false},{"StartTime":23285.0,"Position":192.339218,"HyperDash":false},{"StartTime":23375.0,"Position":173.217529,"HyperDash":false}]},{"StartTime":23625.0,"Objects":[{"StartTime":23625.0,"Position":252.0,"HyperDash":false},{"StartTime":23678.0,"Position":297.327667,"HyperDash":false},{"StartTime":23732.0,"Position":299.171,"HyperDash":false},{"StartTime":23785.0,"Position":350.498657,"HyperDash":false},{"StartTime":23875.0,"Position":380.904175,"HyperDash":false}]},{"StartTime":24125.0,"Objects":[{"StartTime":24125.0,"Position":484.0,"HyperDash":false},{"StartTime":24187.0,"Position":459.330444,"HyperDash":false},{"StartTime":24250.0,"Position":410.3108,"HyperDash":false},{"StartTime":24312.0,"Position":381.927948,"HyperDash":false},{"StartTime":24375.0,"Position":342.702942,"HyperDash":false},{"StartTime":24437.0,"Position":307.727,"HyperDash":false},{"StartTime":24500.0,"Position":254.618744,"HyperDash":false},{"StartTime":24562.0,"Position":219.823792,"HyperDash":false},{"StartTime":24625.0,"Position":195.842667,"HyperDash":false},{"StartTime":24750.0,"Position":124.114441,"HyperDash":false}]},{"StartTime":24875.0,"Objects":[{"StartTime":24875.0,"Position":72.0,"HyperDash":false},{"StartTime":24937.0,"Position":90.6446,"HyperDash":false},{"StartTime":25000.0,"Position":102.976662,"HyperDash":false},{"StartTime":25062.0,"Position":121.259918,"HyperDash":false},{"StartTime":25125.0,"Position":115.072632,"HyperDash":false},{"StartTime":25178.0,"Position":104.017952,"HyperDash":false},{"StartTime":25232.0,"Position":66.87554,"HyperDash":false},{"StartTime":25285.0,"Position":53.7148743,"HyperDash":false},{"StartTime":25374.0,"Position":0.0,"HyperDash":false}]},{"StartTime":25625.0,"Objects":[{"StartTime":25625.0,"Position":56.0,"HyperDash":false}]},{"StartTime":25875.0,"Objects":[{"StartTime":25875.0,"Position":176.0,"HyperDash":false}]},{"StartTime":26000.0,"Objects":[{"StartTime":26000.0,"Position":176.0,"HyperDash":false}]},{"StartTime":26125.0,"Objects":[{"StartTime":26125.0,"Position":176.0,"HyperDash":false}]},{"StartTime":26375.0,"Objects":[{"StartTime":26375.0,"Position":316.0,"HyperDash":false}]},{"StartTime":26625.0,"Objects":[{"StartTime":26625.0,"Position":464.0,"HyperDash":false},{"StartTime":26678.0,"Position":423.678864,"HyperDash":false},{"StartTime":26732.0,"Position":428.026764,"HyperDash":false},{"StartTime":26785.0,"Position":431.558746,"HyperDash":false},{"StartTime":26875.0,"Position":408.8022,"HyperDash":false}]},{"StartTime":27125.0,"Objects":[{"StartTime":27125.0,"Position":232.0,"HyperDash":false},{"StartTime":27178.0,"Position":266.0937,"HyperDash":false},{"StartTime":27232.0,"Position":284.472229,"HyperDash":false},{"StartTime":27285.0,"Position":289.223022,"HyperDash":false},{"StartTime":27374.0,"Position":288.2113,"HyperDash":false}]},{"StartTime":27625.0,"Objects":[{"StartTime":27625.0,"Position":136.0,"HyperDash":false}]},{"StartTime":27875.0,"Objects":[{"StartTime":27875.0,"Position":60.0,"HyperDash":false}]},{"StartTime":28125.0,"Objects":[{"StartTime":28125.0,"Position":212.0,"HyperDash":false},{"StartTime":28250.0,"Position":244.219086,"HyperDash":false}]},{"StartTime":28500.0,"Objects":[{"StartTime":28500.0,"Position":340.0,"HyperDash":false},{"StartTime":28625.0,"Position":372.2191,"HyperDash":false}]},{"StartTime":28875.0,"Objects":[{"StartTime":28875.0,"Position":256.0,"HyperDash":false},{"StartTime":29000.0,"Position":223.780914,"HyperDash":false}]},{"StartTime":29250.0,"Objects":[{"StartTime":29250.0,"Position":128.0,"HyperDash":false},{"StartTime":29375.0,"Position":95.7809143,"HyperDash":false}]},{"StartTime":29625.0,"Objects":[{"StartTime":29625.0,"Position":238.0,"HyperDash":false},{"StartTime":29678.0,"Position":279.04657,"HyperDash":false},{"StartTime":29731.0,"Position":322.09314,"HyperDash":false},{"StartTime":29784.0,"Position":325.1397,"HyperDash":false},{"StartTime":29874.0,"Position":397.954651,"HyperDash":false}]},{"StartTime":30125.0,"Objects":[{"StartTime":30125.0,"Position":512.0,"HyperDash":false}]},{"StartTime":30250.0,"Objects":[{"StartTime":30250.0,"Position":512.0,"HyperDash":false}]},{"StartTime":30500.0,"Objects":[{"StartTime":30500.0,"Position":416.0,"HyperDash":false}]},{"StartTime":30625.0,"Objects":[{"StartTime":30625.0,"Position":416.0,"HyperDash":false}]},{"StartTime":30875.0,"Objects":[{"StartTime":30875.0,"Position":300.0,"HyperDash":false}]},{"StartTime":31000.0,"Objects":[{"StartTime":31000.0,"Position":300.0,"HyperDash":false}]},{"StartTime":31250.0,"Objects":[{"StartTime":31250.0,"Position":236.0,"HyperDash":false}]},{"StartTime":31375.0,"Objects":[{"StartTime":31375.0,"Position":236.0,"HyperDash":false}]},{"StartTime":31625.0,"Objects":[{"StartTime":31625.0,"Position":152.0,"HyperDash":false}]},{"StartTime":31875.0,"Objects":[{"StartTime":31875.0,"Position":300.0,"HyperDash":false}]},{"StartTime":32125.0,"Objects":[{"StartTime":32125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":34625.0,"Objects":[{"StartTime":34625.0,"Position":52.0,"HyperDash":false}]},{"StartTime":34875.0,"Objects":[{"StartTime":34875.0,"Position":152.0,"HyperDash":false}]},{"StartTime":35125.0,"Objects":[{"StartTime":35125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":35625.0,"Objects":[{"StartTime":35625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":36125.0,"Objects":[{"StartTime":36125.0,"Position":256.0,"HyperDash":false},{"StartTime":36178.0,"Position":285.74295,"HyperDash":false},{"StartTime":36232.0,"Position":306.695557,"HyperDash":false},{"StartTime":36285.0,"Position":338.9461,"HyperDash":false},{"StartTime":36375.0,"Position":338.0262,"HyperDash":false}]},{"StartTime":36625.0,"Objects":[{"StartTime":36625.0,"Position":320.0,"HyperDash":false}]},{"StartTime":36875.0,"Objects":[{"StartTime":36875.0,"Position":204.0,"HyperDash":false}]},{"StartTime":37125.0,"Objects":[{"StartTime":37125.0,"Position":104.0,"HyperDash":false},{"StartTime":37178.0,"Position":84.88513,"HyperDash":false},{"StartTime":37232.0,"Position":58.02897,"HyperDash":false},{"StartTime":37285.0,"Position":32.3897247,"HyperDash":false},{"StartTime":37375.0,"Position":42.93435,"HyperDash":false}]},{"StartTime":37625.0,"Objects":[{"StartTime":37625.0,"Position":92.0,"HyperDash":false}]},{"StartTime":37875.0,"Objects":[{"StartTime":37875.0,"Position":212.0,"HyperDash":false}]},{"StartTime":38000.0,"Objects":[{"StartTime":38000.0,"Position":268.0,"HyperDash":false}]},{"StartTime":38125.0,"Objects":[{"StartTime":38125.0,"Position":324.0,"HyperDash":false},{"StartTime":38178.0,"Position":338.3627,"HyperDash":false},{"StartTime":38232.0,"Position":380.1851,"HyperDash":false},{"StartTime":38285.0,"Position":411.5478,"HyperDash":false},{"StartTime":38375.0,"Position":438.918457,"HyperDash":false}]},{"StartTime":38625.0,"Objects":[{"StartTime":38625.0,"Position":504.0,"HyperDash":false}]},{"StartTime":38875.0,"Objects":[{"StartTime":38875.0,"Position":364.0,"HyperDash":false}]},{"StartTime":39125.0,"Objects":[{"StartTime":39125.0,"Position":232.0,"HyperDash":false},{"StartTime":39187.0,"Position":199.986359,"HyperDash":false},{"StartTime":39250.0,"Position":169.811844,"HyperDash":false},{"StartTime":39312.0,"Position":133.274048,"HyperDash":false},{"StartTime":39375.0,"Position":115.502953,"HyperDash":false},{"StartTime":39437.0,"Position":95.79658,"HyperDash":false},{"StartTime":39500.0,"Position":126.272606,"HyperDash":false},{"StartTime":39562.0,"Position":153.43367,"HyperDash":false},{"StartTime":39625.0,"Position":177.594223,"HyperDash":false},{"StartTime":39687.0,"Position":138.43367,"HyperDash":false},{"StartTime":39750.0,"Position":126.007256,"HyperDash":false},{"StartTime":39812.0,"Position":110.796577,"HyperDash":false},{"StartTime":39875.0,"Position":115.652954,"HyperDash":false},{"StartTime":39928.0,"Position":111.270706,"HyperDash":false},{"StartTime":39982.0,"Position":160.599289,"HyperDash":false},{"StartTime":40035.0,"Position":158.120911,"HyperDash":false},{"StartTime":40124.0,"Position":232.0,"HyperDash":false}]},{"StartTime":40375.0,"Objects":[{"StartTime":40375.0,"Position":280.0,"HyperDash":false}]},{"StartTime":40625.0,"Objects":[{"StartTime":40625.0,"Position":400.0,"HyperDash":false},{"StartTime":40678.0,"Position":429.074829,"HyperDash":false},{"StartTime":40732.0,"Position":455.5662,"HyperDash":false},{"StartTime":40785.0,"Position":457.641022,"HyperDash":false},{"StartTime":40875.0,"Position":504.126617,"HyperDash":false}]},{"StartTime":41125.0,"Objects":[{"StartTime":41125.0,"Position":480.0,"HyperDash":false}]},{"StartTime":41375.0,"Objects":[{"StartTime":41375.0,"Position":324.0,"HyperDash":false}]},{"StartTime":41625.0,"Objects":[{"StartTime":41625.0,"Position":168.0,"HyperDash":false}]},{"StartTime":41875.0,"Objects":[{"StartTime":41875.0,"Position":72.0,"HyperDash":false}]},{"StartTime":42000.0,"Objects":[{"StartTime":42000.0,"Position":48.0,"HyperDash":false}]},{"StartTime":42125.0,"Objects":[{"StartTime":42125.0,"Position":96.0,"HyperDash":false},{"StartTime":42178.0,"Position":114.931221,"HyperDash":false},{"StartTime":42232.0,"Position":153.604843,"HyperDash":false},{"StartTime":42285.0,"Position":193.4396,"HyperDash":false},{"StartTime":42374.0,"Position":240.778946,"HyperDash":false}]},{"StartTime":42625.0,"Objects":[{"StartTime":42625.0,"Position":400.0,"HyperDash":false}]},{"StartTime":42875.0,"Objects":[{"StartTime":42875.0,"Position":440.0,"HyperDash":false}]},{"StartTime":43000.0,"Objects":[{"StartTime":43000.0,"Position":464.0,"HyperDash":false}]},{"StartTime":43125.0,"Objects":[{"StartTime":43125.0,"Position":416.0,"HyperDash":false},{"StartTime":43178.0,"Position":375.182983,"HyperDash":false},{"StartTime":43232.0,"Position":366.663025,"HyperDash":false},{"StartTime":43285.0,"Position":335.968475,"HyperDash":false},{"StartTime":43375.0,"Position":271.221039,"HyperDash":false}]},{"StartTime":43625.0,"Objects":[{"StartTime":43625.0,"Position":112.0,"HyperDash":false}]},{"StartTime":43875.0,"Objects":[{"StartTime":43875.0,"Position":140.0,"HyperDash":false}]},{"StartTime":44125.0,"Objects":[{"StartTime":44125.0,"Position":52.0,"HyperDash":false}]},{"StartTime":44375.0,"Objects":[{"StartTime":44375.0,"Position":208.0,"HyperDash":false}]},{"StartTime":44625.0,"Objects":[{"StartTime":44625.0,"Position":344.0,"HyperDash":false}]},{"StartTime":44875.0,"Objects":[{"StartTime":44875.0,"Position":448.0,"HyperDash":false},{"StartTime":44937.0,"Position":411.344635,"HyperDash":false},{"StartTime":45000.0,"Position":386.572845,"HyperDash":false},{"StartTime":45062.0,"Position":355.1799,"HyperDash":false},{"StartTime":45125.0,"Position":304.139374,"HyperDash":false},{"StartTime":45187.0,"Position":271.8332,"HyperDash":false},{"StartTime":45250.0,"Position":232.840988,"HyperDash":false},{"StartTime":45312.0,"Position":235.629944,"HyperDash":false},{"StartTime":45375.0,"Position":232.882874,"HyperDash":false},{"StartTime":45437.0,"Position":251.629944,"HyperDash":false},{"StartTime":45500.0,"Position":243.152222,"HyperDash":false},{"StartTime":45562.0,"Position":270.8332,"HyperDash":false},{"StartTime":45625.0,"Position":304.729126,"HyperDash":false},{"StartTime":45678.0,"Position":323.441345,"HyperDash":false},{"StartTime":45732.0,"Position":370.914246,"HyperDash":false},{"StartTime":45785.0,"Position":421.2586,"HyperDash":false},{"StartTime":45874.0,"Position":448.0,"HyperDash":false}]},{"StartTime":46125.0,"Objects":[{"StartTime":46125.0,"Position":326.0,"HyperDash":false},{"StartTime":46187.0,"Position":309.377716,"HyperDash":false},{"StartTime":46250.0,"Position":271.650543,"HyperDash":false},{"StartTime":46312.0,"Position":219.299332,"HyperDash":false},{"StartTime":46375.0,"Position":182.286819,"HyperDash":false},{"StartTime":46428.0,"Position":144.357529,"HyperDash":false},{"StartTime":46482.0,"Position":145.0256,"HyperDash":false},{"StartTime":46535.0,"Position":101.934631,"HyperDash":false},{"StartTime":46625.0,"Position":110.882874,"HyperDash":false}]},{"StartTime":46875.0,"Objects":[{"StartTime":46875.0,"Position":230.0,"HyperDash":false},{"StartTime":46937.0,"Position":247.622284,"HyperDash":false},{"StartTime":47000.0,"Position":299.3495,"HyperDash":false},{"StartTime":47062.0,"Position":322.700653,"HyperDash":false},{"StartTime":47125.0,"Position":373.7132,"HyperDash":false},{"StartTime":47178.0,"Position":390.642456,"HyperDash":false},{"StartTime":47232.0,"Position":424.974426,"HyperDash":false},{"StartTime":47285.0,"Position":428.065369,"HyperDash":false},{"StartTime":47375.0,"Position":445.1171,"HyperDash":false}]},{"StartTime":47625.0,"Objects":[{"StartTime":47625.0,"Position":376.0,"HyperDash":false}]},{"StartTime":48125.0,"Objects":[{"StartTime":48125.0,"Position":376.0,"HyperDash":false},{"StartTime":48178.0,"Position":340.223816,"HyperDash":false},{"StartTime":48232.0,"Position":305.204224,"HyperDash":false},{"StartTime":48285.0,"Position":270.449249,"HyperDash":false},{"StartTime":48375.0,"Position":222.9901,"HyperDash":false}]},{"StartTime":48625.0,"Objects":[{"StartTime":48625.0,"Position":84.0,"HyperDash":false}]},{"StartTime":48875.0,"Objects":[{"StartTime":48875.0,"Position":152.0,"HyperDash":false}]},{"StartTime":49125.0,"Objects":[{"StartTime":49125.0,"Position":44.0,"HyperDash":false},{"StartTime":49178.0,"Position":69.96314,"HyperDash":false},{"StartTime":49232.0,"Position":103.8065,"HyperDash":false},{"StartTime":49285.0,"Position":156.7781,"HyperDash":false},{"StartTime":49374.0,"Position":197.1017,"HyperDash":false}]},{"StartTime":49625.0,"Objects":[{"StartTime":49625.0,"Position":336.0,"HyperDash":false}]},{"StartTime":49875.0,"Objects":[{"StartTime":49875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":50125.0,"Objects":[{"StartTime":50125.0,"Position":176.0,"HyperDash":false}]},{"StartTime":50625.0,"Objects":[{"StartTime":50625.0,"Position":340.0,"HyperDash":false}]},{"StartTime":50875.0,"Objects":[{"StartTime":50875.0,"Position":420.0,"HyperDash":false}]},{"StartTime":51125.0,"Objects":[{"StartTime":51125.0,"Position":500.0,"HyperDash":false}]},{"StartTime":51625.0,"Objects":[{"StartTime":51625.0,"Position":172.0,"HyperDash":false}]},{"StartTime":51875.0,"Objects":[{"StartTime":51875.0,"Position":92.0,"HyperDash":false}]},{"StartTime":52125.0,"Objects":[{"StartTime":52125.0,"Position":12.0,"HyperDash":false},{"StartTime":52178.0,"Position":43.4575653,"HyperDash":false},{"StartTime":52232.0,"Position":57.4520721,"HyperDash":false},{"StartTime":52285.0,"Position":85.90964,"HyperDash":false},{"StartTime":52375.0,"Position":146.23381,"HyperDash":false}]},{"StartTime":52625.0,"Objects":[{"StartTime":52625.0,"Position":304.0,"HyperDash":false}]},{"StartTime":52875.0,"Objects":[{"StartTime":52875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":53125.0,"Objects":[{"StartTime":53125.0,"Position":216.0,"HyperDash":false},{"StartTime":53178.0,"Position":229.457565,"HyperDash":false},{"StartTime":53232.0,"Position":269.452057,"HyperDash":false},{"StartTime":53285.0,"Position":304.909637,"HyperDash":false},{"StartTime":53375.0,"Position":350.233826,"HyperDash":false}]},{"StartTime":53625.0,"Objects":[{"StartTime":53625.0,"Position":508.0,"HyperDash":false}]},{"StartTime":53875.0,"Objects":[{"StartTime":53875.0,"Position":460.0,"HyperDash":false}]},{"StartTime":54125.0,"Objects":[{"StartTime":54125.0,"Position":344.0,"HyperDash":false}]},{"StartTime":54375.0,"Objects":[{"StartTime":54375.0,"Position":228.0,"HyperDash":false}]},{"StartTime":54625.0,"Objects":[{"StartTime":54625.0,"Position":153.0,"HyperDash":false}]},{"StartTime":54875.0,"Objects":[{"StartTime":54875.0,"Position":72.0,"HyperDash":false}]},{"StartTime":55125.0,"Objects":[{"StartTime":55125.0,"Position":180.0,"HyperDash":false}]},{"StartTime":55375.0,"Objects":[{"StartTime":55375.0,"Position":284.0,"HyperDash":false}]},{"StartTime":55625.0,"Objects":[{"StartTime":55625.0,"Position":359.0,"HyperDash":false}]},{"StartTime":55875.0,"Objects":[{"StartTime":55875.0,"Position":440.0,"HyperDash":false}]},{"StartTime":56125.0,"Objects":[{"StartTime":56125.0,"Position":352.0,"HyperDash":false},{"StartTime":56178.0,"Position":355.0677,"HyperDash":false},{"StartTime":56231.0,"Position":396.135376,"HyperDash":false},{"StartTime":56284.0,"Position":431.2031,"HyperDash":false},{"StartTime":56374.0,"Position":455.6765,"HyperDash":false}]},{"StartTime":56625.0,"Objects":[{"StartTime":56625.0,"Position":312.0,"HyperDash":false}]},{"StartTime":56875.0,"Objects":[{"StartTime":56875.0,"Position":200.0,"HyperDash":false}]},{"StartTime":57125.0,"Objects":[{"StartTime":57125.0,"Position":160.0,"HyperDash":false},{"StartTime":57178.0,"Position":134.932312,"HyperDash":false},{"StartTime":57231.0,"Position":131.864609,"HyperDash":false},{"StartTime":57284.0,"Position":84.7969055,"HyperDash":false},{"StartTime":57374.0,"Position":56.32347,"HyperDash":false}]},{"StartTime":57625.0,"Objects":[{"StartTime":57625.0,"Position":200.0,"HyperDash":false}]},{"StartTime":57875.0,"Objects":[{"StartTime":57875.0,"Position":312.0,"HyperDash":false}]},{"StartTime":58125.0,"Objects":[{"StartTime":58125.0,"Position":444.0,"HyperDash":false},{"StartTime":58178.0,"Position":405.081421,"HyperDash":false},{"StartTime":58232.0,"Position":380.062256,"HyperDash":false},{"StartTime":58285.0,"Position":399.193085,"HyperDash":false},{"StartTime":58374.0,"Position":377.6735,"HyperDash":false}]},{"StartTime":58500.0,"Objects":[{"StartTime":58500.0,"Position":344.0,"HyperDash":false}]},{"StartTime":58625.0,"Objects":[{"StartTime":58625.0,"Position":272.0,"HyperDash":false},{"StartTime":58678.0,"Position":263.870544,"HyperDash":false},{"StartTime":58732.0,"Position":246.779541,"HyperDash":false},{"StartTime":58785.0,"Position":179.497513,"HyperDash":false},{"StartTime":58875.0,"Position":139.25528,"HyperDash":false}]},{"StartTime":59125.0,"Objects":[{"StartTime":59125.0,"Position":68.0,"HyperDash":false},{"StartTime":59178.0,"Position":89.57149,"HyperDash":false},{"StartTime":59232.0,"Position":123.207489,"HyperDash":false},{"StartTime":59285.0,"Position":141.936157,"HyperDash":false},{"StartTime":59375.0,"Position":133.961975,"HyperDash":false}]},{"StartTime":59500.0,"Objects":[{"StartTime":59500.0,"Position":168.0,"HyperDash":false}]},{"StartTime":59625.0,"Objects":[{"StartTime":59625.0,"Position":240.0,"HyperDash":false},{"StartTime":59678.0,"Position":245.129486,"HyperDash":false},{"StartTime":59732.0,"Position":270.220459,"HyperDash":false},{"StartTime":59785.0,"Position":296.5025,"HyperDash":false},{"StartTime":59875.0,"Position":372.74472,"HyperDash":false}]},{"StartTime":60125.0,"Objects":[{"StartTime":60125.0,"Position":456.0,"HyperDash":false}]},{"StartTime":60375.0,"Objects":[{"StartTime":60375.0,"Position":328.0,"HyperDash":false}]},{"StartTime":60625.0,"Objects":[{"StartTime":60625.0,"Position":216.0,"HyperDash":false}]},{"StartTime":60875.0,"Objects":[{"StartTime":60875.0,"Position":72.0,"HyperDash":false},{"StartTime":60937.0,"Position":71.25553,"HyperDash":false},{"StartTime":61000.0,"Position":61.5583878,"HyperDash":false},{"StartTime":61062.0,"Position":98.84126,"HyperDash":false},{"StartTime":61125.0,"Position":119.510284,"HyperDash":false},{"StartTime":61187.0,"Position":142.845825,"HyperDash":false},{"StartTime":61250.0,"Position":184.319992,"HyperDash":false},{"StartTime":61312.0,"Position":240.90744,"HyperDash":false},{"StartTime":61375.0,"Position":269.728363,"HyperDash":false},{"StartTime":61437.0,"Position":239.90744,"HyperDash":false},{"StartTime":61500.0,"Position":197.687851,"HyperDash":false},{"StartTime":61562.0,"Position":150.845825,"HyperDash":false},{"StartTime":61625.0,"Position":119.024872,"HyperDash":false},{"StartTime":61678.0,"Position":90.12531,"HyperDash":false},{"StartTime":61732.0,"Position":72.3374557,"HyperDash":false},{"StartTime":61785.0,"Position":89.06496,"HyperDash":false},{"StartTime":61874.0,"Position":72.0,"HyperDash":false}]},{"StartTime":62125.0,"Objects":[{"StartTime":62125.0,"Position":200.0,"HyperDash":false},{"StartTime":62187.0,"Position":191.234039,"HyperDash":false},{"StartTime":62250.0,"Position":203.319962,"HyperDash":false},{"StartTime":62312.0,"Position":235.3192,"HyperDash":false},{"StartTime":62375.0,"Position":246.7092,"HyperDash":false},{"StartTime":62428.0,"Position":291.675018,"HyperDash":false},{"StartTime":62482.0,"Position":309.9024,"HyperDash":false},{"StartTime":62535.0,"Position":336.449463,"HyperDash":false},{"StartTime":62625.0,"Position":396.8608,"HyperDash":false}]},{"StartTime":62875.0,"Objects":[{"StartTime":62875.0,"Position":480.0,"HyperDash":false},{"StartTime":62937.0,"Position":492.1737,"HyperDash":false},{"StartTime":63000.0,"Position":476.1641,"HyperDash":false},{"StartTime":63062.0,"Position":475.045135,"HyperDash":false},{"StartTime":63125.0,"Position":433.461975,"HyperDash":false},{"StartTime":63178.0,"Position":389.354034,"HyperDash":false},{"StartTime":63232.0,"Position":366.034546,"HyperDash":false},{"StartTime":63285.0,"Position":321.454956,"HyperDash":false},{"StartTime":63375.0,"Position":283.111176,"HyperDash":false}]},{"StartTime":63625.0,"Objects":[{"StartTime":63625.0,"Position":136.0,"HyperDash":false},{"StartTime":63678.0,"Position":111.887825,"HyperDash":false},{"StartTime":63732.0,"Position":108.904541,"HyperDash":false},{"StartTime":63785.0,"Position":105.234535,"HyperDash":false},{"StartTime":63874.0,"Position":128.127991,"HyperDash":false}]},{"StartTime":64125.0,"Objects":[{"StartTime":64125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":64375.0,"Objects":[{"StartTime":64375.0,"Position":284.0,"HyperDash":false}]},{"StartTime":64625.0,"Objects":[{"StartTime":64625.0,"Position":440.0,"HyperDash":false}]},{"StartTime":64875.0,"Objects":[{"StartTime":64875.0,"Position":420.0,"HyperDash":false}]},{"StartTime":65125.0,"Objects":[{"StartTime":65125.0,"Position":300.0,"HyperDash":false}]},{"StartTime":65375.0,"Objects":[{"StartTime":65375.0,"Position":272.0,"HyperDash":false}]},{"StartTime":65625.0,"Objects":[{"StartTime":65625.0,"Position":116.0,"HyperDash":false}]},{"StartTime":65875.0,"Objects":[{"StartTime":65875.0,"Position":136.0,"HyperDash":false}]},{"StartTime":66125.0,"Objects":[{"StartTime":66125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":68125.0,"Objects":[{"StartTime":68125.0,"Position":256.0,"HyperDash":false},{"StartTime":68187.0,"Position":266.157,"HyperDash":false},{"StartTime":68250.0,"Position":280.344269,"HyperDash":false},{"StartTime":68312.0,"Position":243.4508,"HyperDash":false},{"StartTime":68375.0,"Position":216.9601,"HyperDash":false},{"StartTime":68428.0,"Position":173.102234,"HyperDash":false},{"StartTime":68482.0,"Position":150.915558,"HyperDash":false},{"StartTime":68535.0,"Position":106.794662,"HyperDash":false},{"StartTime":68625.0,"Position":73.61266,"HyperDash":false}]},{"StartTime":68875.0,"Objects":[{"StartTime":68875.0,"Position":132.0,"HyperDash":false},{"StartTime":68937.0,"Position":160.783325,"HyperDash":false},{"StartTime":69000.0,"Position":193.9825,"HyperDash":false},{"StartTime":69062.0,"Position":205.765823,"HyperDash":false},{"StartTime":69125.0,"Position":235.965,"HyperDash":false},{"StartTime":69178.0,"Position":262.005585,"HyperDash":false},{"StartTime":69232.0,"Position":285.462,"HyperDash":false},{"StartTime":69285.0,"Position":302.5026,"HyperDash":false},{"StartTime":69375.0,"Position":339.93,"HyperDash":false}]},{"StartTime":69625.0,"Objects":[{"StartTime":69625.0,"Position":456.0,"HyperDash":false}]},{"StartTime":69875.0,"Objects":[{"StartTime":69875.0,"Position":340.0,"HyperDash":false}]},{"StartTime":70000.0,"Objects":[{"StartTime":70000.0,"Position":340.0,"HyperDash":false}]},{"StartTime":70125.0,"Objects":[{"StartTime":70125.0,"Position":340.0,"HyperDash":false}]},{"StartTime":70375.0,"Objects":[{"StartTime":70375.0,"Position":228.0,"HyperDash":false}]},{"StartTime":70625.0,"Objects":[{"StartTime":70625.0,"Position":256.0,"HyperDash":false},{"StartTime":70678.0,"Position":210.6065,"HyperDash":false},{"StartTime":70732.0,"Position":177.325424,"HyperDash":false},{"StartTime":70785.0,"Position":151.573288,"HyperDash":false},{"StartTime":70875.0,"Position":107.425896,"HyperDash":false}]},{"StartTime":71125.0,"Objects":[{"StartTime":71125.0,"Position":148.0,"HyperDash":false},{"StartTime":71178.0,"Position":184.328445,"HyperDash":false},{"StartTime":71232.0,"Position":200.780228,"HyperDash":false},{"StartTime":71285.0,"Position":257.6842,"HyperDash":false},{"StartTime":71374.0,"Position":296.433563,"HyperDash":false}]},{"StartTime":71625.0,"Objects":[{"StartTime":71625.0,"Position":424.0,"HyperDash":false}]},{"StartTime":71875.0,"Objects":[{"StartTime":71875.0,"Position":336.0,"HyperDash":false}]},{"StartTime":72000.0,"Objects":[{"StartTime":72000.0,"Position":336.0,"HyperDash":false}]},{"StartTime":72125.0,"Objects":[{"StartTime":72125.0,"Position":336.0,"HyperDash":false}]},{"StartTime":72375.0,"Objects":[{"StartTime":72375.0,"Position":228.0,"HyperDash":false},{"StartTime":72428.0,"Position":211.104858,"HyperDash":false},{"StartTime":72482.0,"Position":163.608932,"HyperDash":false},{"StartTime":72535.0,"Position":134.045914,"HyperDash":false},{"StartTime":72625.0,"Position":143.764755,"HyperDash":false}]},{"StartTime":72875.0,"Objects":[{"StartTime":72875.0,"Position":268.0,"HyperDash":false},{"StartTime":72937.0,"Position":248.6492,"HyperDash":false},{"StartTime":73000.0,"Position":273.503021,"HyperDash":false},{"StartTime":73062.0,"Position":247.768143,"HyperDash":false},{"StartTime":73125.0,"Position":228.062622,"HyperDash":false},{"StartTime":73178.0,"Position":204.959824,"HyperDash":false},{"StartTime":73232.0,"Position":170.633987,"HyperDash":false},{"StartTime":73285.0,"Position":155.368179,"HyperDash":false},{"StartTime":73375.0,"Position":103.8164,"HyperDash":false}]},{"StartTime":73625.0,"Objects":[{"StartTime":73625.0,"Position":24.0,"HyperDash":false}]},{"StartTime":73875.0,"Objects":[{"StartTime":73875.0,"Position":92.0,"HyperDash":false}]},{"StartTime":74000.0,"Objects":[{"StartTime":74000.0,"Position":92.0,"HyperDash":false}]},{"StartTime":74125.0,"Objects":[{"StartTime":74125.0,"Position":92.0,"HyperDash":false}]},{"StartTime":74375.0,"Objects":[{"StartTime":74375.0,"Position":224.0,"HyperDash":false}]},{"StartTime":74625.0,"Objects":[{"StartTime":74625.0,"Position":340.0,"HyperDash":false},{"StartTime":74678.0,"Position":381.308228,"HyperDash":false},{"StartTime":74732.0,"Position":376.477844,"HyperDash":false},{"StartTime":74785.0,"Position":399.771942,"HyperDash":false},{"StartTime":74875.0,"Position":387.2963,"HyperDash":false}]},{"StartTime":75125.0,"Objects":[{"StartTime":75125.0,"Position":268.0,"HyperDash":false},{"StartTime":75178.0,"Position":219.691772,"HyperDash":false},{"StartTime":75232.0,"Position":224.522156,"HyperDash":false},{"StartTime":75285.0,"Position":185.228043,"HyperDash":false},{"StartTime":75375.0,"Position":220.70369,"HyperDash":false}]},{"StartTime":75625.0,"Objects":[{"StartTime":75625.0,"Position":268.0,"HyperDash":false},{"StartTime":75678.0,"Position":251.437485,"HyperDash":false},{"StartTime":75732.0,"Position":209.2417,"HyperDash":false},{"StartTime":75785.0,"Position":166.6792,"HyperDash":false},{"StartTime":75875.0,"Position":109.686234,"HyperDash":false}]},{"StartTime":76125.0,"Objects":[{"StartTime":76125.0,"Position":24.0,"HyperDash":false},{"StartTime":76250.0,"Position":103.510704,"HyperDash":false}]},{"StartTime":76375.0,"Objects":[{"StartTime":76375.0,"Position":176.0,"HyperDash":false}]},{"StartTime":76625.0,"Objects":[{"StartTime":76625.0,"Position":348.0,"HyperDash":false}]},{"StartTime":76875.0,"Objects":[{"StartTime":76875.0,"Position":248.0,"HyperDash":false}]},{"StartTime":77125.0,"Objects":[{"StartTime":77125.0,"Position":264.0,"HyperDash":false}]},{"StartTime":77375.0,"Objects":[{"StartTime":77375.0,"Position":324.0,"HyperDash":false}]},{"StartTime":77625.0,"Objects":[{"StartTime":77625.0,"Position":180.0,"HyperDash":false}]},{"StartTime":77875.0,"Objects":[{"StartTime":77875.0,"Position":240.0,"HyperDash":false}]},{"StartTime":78125.0,"Objects":[{"StartTime":78125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":78375.0,"Objects":[{"StartTime":78375.0,"Position":100.0,"HyperDash":false}]},{"StartTime":78625.0,"Objects":[{"StartTime":78625.0,"Position":8.0,"HyperDash":false},{"StartTime":78678.0,"Position":30.0805969,"HyperDash":false},{"StartTime":78732.0,"Position":72.26928,"HyperDash":false},{"StartTime":78785.0,"Position":94.77067,"HyperDash":false},{"StartTime":78874.0,"Position":149.724487,"HyperDash":false}]},{"StartTime":79125.0,"Objects":[{"StartTime":79125.0,"Position":304.0,"HyperDash":false},{"StartTime":79178.0,"Position":282.0235,"HyperDash":false},{"StartTime":79232.0,"Position":238.981018,"HyperDash":false},{"StartTime":79285.0,"Position":222.634567,"HyperDash":false},{"StartTime":79375.0,"Position":162.2755,"HyperDash":false}]},{"StartTime":79625.0,"Objects":[{"StartTime":79625.0,"Position":304.0,"HyperDash":false}]},{"StartTime":79875.0,"Objects":[{"StartTime":79875.0,"Position":460.0,"HyperDash":false}]},{"StartTime":80125.0,"Objects":[{"StartTime":80125.0,"Position":420.0,"HyperDash":false},{"StartTime":80250.0,"Position":340.0,"HyperDash":false}]},{"StartTime":80375.0,"Objects":[{"StartTime":80375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":80625.0,"Objects":[{"StartTime":80625.0,"Position":344.0,"HyperDash":false}]},{"StartTime":80875.0,"Objects":[{"StartTime":80875.0,"Position":168.0,"HyperDash":false}]},{"StartTime":81125.0,"Objects":[{"StartTime":81125.0,"Position":384.0,"HyperDash":false}]},{"StartTime":81375.0,"Objects":[{"StartTime":81375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":81625.0,"Objects":[{"StartTime":81625.0,"Position":168.0,"HyperDash":false}]},{"StartTime":81875.0,"Objects":[{"StartTime":81875.0,"Position":344.0,"HyperDash":false}]},{"StartTime":82125.0,"Objects":[{"StartTime":82125.0,"Position":128.0,"HyperDash":false}]},{"StartTime":82250.0,"Objects":[{"StartTime":82250.0,"Position":48.0,"HyperDash":false},{"StartTime":82303.0,"Position":38.86482,"HyperDash":false},{"StartTime":82357.0,"Position":53.93512,"HyperDash":false},{"StartTime":82410.0,"Position":60.0134125,"HyperDash":false},{"StartTime":82500.0,"Position":124.821884,"HyperDash":false}]},{"StartTime":82625.0,"Objects":[{"StartTime":82625.0,"Position":204.0,"HyperDash":false},{"StartTime":82678.0,"Position":208.888657,"HyperDash":false},{"StartTime":82731.0,"Position":211.78508,"HyperDash":false},{"StartTime":82784.0,"Position":215.863892,"HyperDash":false},{"StartTime":82874.0,"Position":280.821869,"HyperDash":false}]},{"StartTime":83000.0,"Objects":[{"StartTime":83000.0,"Position":352.0,"HyperDash":false},{"StartTime":83053.0,"Position":303.246552,"HyperDash":false},{"StartTime":83107.0,"Position":291.2771,"HyperDash":false},{"StartTime":83160.0,"Position":254.710571,"HyperDash":false},{"StartTime":83250.0,"Position":222.496063,"HyperDash":false}]},{"StartTime":83375.0,"Objects":[{"StartTime":83375.0,"Position":192.0,"HyperDash":false},{"StartTime":83428.0,"Position":152.246567,"HyperDash":false},{"StartTime":83482.0,"Position":112.277092,"HyperDash":false},{"StartTime":83535.0,"Position":87.71058,"HyperDash":false},{"StartTime":83625.0,"Position":62.496067,"HyperDash":false}]},{"StartTime":83875.0,"Objects":[{"StartTime":83875.0,"Position":32.0,"HyperDash":false}]},{"StartTime":84125.0,"Objects":[{"StartTime":84125.0,"Position":172.0,"HyperDash":false}]},{"StartTime":84250.0,"Objects":[{"StartTime":84250.0,"Position":179.0,"HyperDash":false},{"StartTime":84308.0,"Position":278.0,"HyperDash":false},{"StartTime":84367.0,"Position":474.0,"HyperDash":false},{"StartTime":84425.0,"Position":50.0,"HyperDash":false},{"StartTime":84484.0,"Position":458.0,"HyperDash":false},{"StartTime":84542.0,"Position":425.0,"HyperDash":false},{"StartTime":84601.0,"Position":466.0,"HyperDash":false},{"StartTime":84660.0,"Position":56.0,"HyperDash":false},{"StartTime":84718.0,"Position":109.0,"HyperDash":false},{"StartTime":84777.0,"Position":482.0,"HyperDash":false},{"StartTime":84835.0,"Position":147.0,"HyperDash":false},{"StartTime":84894.0,"Position":285.0,"HyperDash":false},{"StartTime":84953.0,"Position":452.0,"HyperDash":false},{"StartTime":85011.0,"Position":419.0,"HyperDash":false},{"StartTime":85070.0,"Position":269.0,"HyperDash":false},{"StartTime":85128.0,"Position":249.0,"HyperDash":false},{"StartTime":85187.0,"Position":233.0,"HyperDash":false},{"StartTime":85246.0,"Position":449.0,"HyperDash":false},{"StartTime":85304.0,"Position":411.0,"HyperDash":false},{"StartTime":85363.0,"Position":75.0,"HyperDash":false},{"StartTime":85421.0,"Position":474.0,"HyperDash":false},{"StartTime":85480.0,"Position":176.0,"HyperDash":false},{"StartTime":85539.0,"Position":1.0,"HyperDash":false},{"StartTime":85597.0,"Position":37.0,"HyperDash":false},{"StartTime":85656.0,"Position":481.0,"HyperDash":false},{"StartTime":85714.0,"Position":375.0,"HyperDash":false},{"StartTime":85773.0,"Position":407.0,"HyperDash":false},{"StartTime":85832.0,"Position":231.0,"HyperDash":false},{"StartTime":85890.0,"Position":338.0,"HyperDash":false},{"StartTime":85949.0,"Position":322.0,"HyperDash":false},{"StartTime":86007.0,"Position":347.0,"HyperDash":false},{"StartTime":86066.0,"Position":365.0,"HyperDash":false},{"StartTime":86125.0,"Position":453.0,"HyperDash":false}]},{"StartTime":86250.0,"Objects":[{"StartTime":86250.0,"Position":486.0,"HyperDash":false},{"StartTime":86304.0,"Position":68.0,"HyperDash":false},{"StartTime":86359.0,"Position":498.0,"HyperDash":false},{"StartTime":86414.0,"Position":164.0,"HyperDash":false},{"StartTime":86468.0,"Position":1.0,"HyperDash":false},{"StartTime":86523.0,"Position":501.0,"HyperDash":false},{"StartTime":86578.0,"Position":82.0,"HyperDash":false},{"StartTime":86632.0,"Position":494.0,"HyperDash":false},{"StartTime":86687.0,"Position":479.0,"HyperDash":false},{"StartTime":86742.0,"Position":373.0,"HyperDash":false},{"StartTime":86796.0,"Position":450.0,"HyperDash":false},{"StartTime":86851.0,"Position":144.0,"HyperDash":false},{"StartTime":86906.0,"Position":365.0,"HyperDash":false},{"StartTime":86960.0,"Position":285.0,"HyperDash":false},{"StartTime":87015.0,"Position":45.0,"HyperDash":false},{"StartTime":87070.0,"Position":65.0,"HyperDash":false},{"StartTime":87125.0,"Position":337.0,"HyperDash":false}]},{"StartTime":88125.0,"Objects":[{"StartTime":88125.0,"Position":256.0,"HyperDash":false},{"StartTime":88178.0,"Position":292.30423,"HyperDash":false},{"StartTime":88232.0,"Position":341.450134,"HyperDash":false},{"StartTime":88285.0,"Position":358.591034,"HyperDash":false},{"StartTime":88375.0,"Position":390.822968,"HyperDash":false}]},{"StartTime":88625.0,"Objects":[{"StartTime":88625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":88875.0,"Objects":[{"StartTime":88875.0,"Position":136.0,"HyperDash":false}]},{"StartTime":89125.0,"Objects":[{"StartTime":89125.0,"Position":8.0,"HyperDash":false},{"StartTime":89178.0,"Position":0.0,"HyperDash":false},{"StartTime":89232.0,"Position":12.7492714,"HyperDash":false},{"StartTime":89285.0,"Position":7.342363,"HyperDash":false},{"StartTime":89375.0,"Position":41.059124,"HyperDash":false}]},{"StartTime":89625.0,"Objects":[{"StartTime":89625.0,"Position":164.0,"HyperDash":false}]},{"StartTime":89875.0,"Objects":[{"StartTime":89875.0,"Position":288.0,"HyperDash":false}]},{"StartTime":90000.0,"Objects":[{"StartTime":90000.0,"Position":288.0,"HyperDash":false}]},{"StartTime":90125.0,"Objects":[{"StartTime":90125.0,"Position":288.0,"HyperDash":false},{"StartTime":90178.0,"Position":307.058655,"HyperDash":false},{"StartTime":90232.0,"Position":366.7033,"HyperDash":false},{"StartTime":90285.0,"Position":400.761932,"HyperDash":false},{"StartTime":90375.0,"Position":434.503052,"HyperDash":false}]},{"StartTime":90625.0,"Objects":[{"StartTime":90625.0,"Position":476.0,"HyperDash":false}]},{"StartTime":90875.0,"Objects":[{"StartTime":90875.0,"Position":332.0,"HyperDash":false}]},{"StartTime":91125.0,"Objects":[{"StartTime":91125.0,"Position":180.0,"HyperDash":false}]},{"StartTime":91375.0,"Objects":[{"StartTime":91375.0,"Position":36.0,"HyperDash":false}]},{"StartTime":91625.0,"Objects":[{"StartTime":91625.0,"Position":56.0,"HyperDash":false}]},{"StartTime":92125.0,"Objects":[{"StartTime":92125.0,"Position":56.0,"HyperDash":false},{"StartTime":92178.0,"Position":78.15752,"HyperDash":false},{"StartTime":92232.0,"Position":134.940643,"HyperDash":false},{"StartTime":92285.0,"Position":142.098145,"HyperDash":false},{"StartTime":92375.0,"Position":212.403366,"HyperDash":false}]},{"StartTime":92625.0,"Objects":[{"StartTime":92625.0,"Position":84.0,"HyperDash":false}]},{"StartTime":92875.0,"Objects":[{"StartTime":92875.0,"Position":220.0,"HyperDash":false}]},{"StartTime":93125.0,"Objects":[{"StartTime":93125.0,"Position":320.0,"HyperDash":false},{"StartTime":93178.0,"Position":369.1575,"HyperDash":false},{"StartTime":93232.0,"Position":398.940643,"HyperDash":false},{"StartTime":93285.0,"Position":408.098145,"HyperDash":false},{"StartTime":93375.0,"Position":476.403381,"HyperDash":false}]},{"StartTime":93625.0,"Objects":[{"StartTime":93625.0,"Position":432.0,"HyperDash":false}]},{"StartTime":93875.0,"Objects":[{"StartTime":93875.0,"Position":296.0,"HyperDash":false}]},{"StartTime":94000.0,"Objects":[{"StartTime":94000.0,"Position":296.0,"HyperDash":false}]},{"StartTime":94125.0,"Objects":[{"StartTime":94125.0,"Position":296.0,"HyperDash":false},{"StartTime":94178.0,"Position":273.1039,"HyperDash":false},{"StartTime":94232.0,"Position":244.445969,"HyperDash":false},{"StartTime":94285.0,"Position":219.02182,"HyperDash":false},{"StartTime":94374.0,"Position":170.471848,"HyperDash":false}]},{"StartTime":94625.0,"Objects":[{"StartTime":94625.0,"Position":216.0,"HyperDash":false},{"StartTime":94678.0,"Position":259.7602,"HyperDash":false},{"StartTime":94732.0,"Position":282.299927,"HyperDash":false},{"StartTime":94785.0,"Position":294.678436,"HyperDash":false},{"StartTime":94875.0,"Position":341.528168,"HyperDash":false}]},{"StartTime":95000.0,"Objects":[{"StartTime":95000.0,"Position":341.0,"HyperDash":false}]},{"StartTime":95125.0,"Objects":[{"StartTime":95125.0,"Position":341.0,"HyperDash":false},{"StartTime":95178.0,"Position":347.282532,"HyperDash":false},{"StartTime":95232.0,"Position":344.6459,"HyperDash":false},{"StartTime":95285.0,"Position":339.928436,"HyperDash":false},{"StartTime":95375.0,"Position":361.200684,"HyperDash":false}]},{"StartTime":95625.0,"Objects":[{"StartTime":95625.0,"Position":171.0,"HyperDash":false},{"StartTime":95678.0,"Position":158.717453,"HyperDash":false},{"StartTime":95732.0,"Position":169.354111,"HyperDash":false},{"StartTime":95785.0,"Position":172.071564,"HyperDash":false},{"StartTime":95875.0,"Position":150.799316,"HyperDash":false}]},{"StartTime":96125.0,"Objects":[{"StartTime":96125.0,"Position":43.0,"HyperDash":false}]},{"StartTime":96375.0,"Objects":[{"StartTime":96375.0,"Position":81.0,"HyperDash":false}]},{"StartTime":96625.0,"Objects":[{"StartTime":96625.0,"Position":169.0,"HyperDash":false}]},{"StartTime":96875.0,"Objects":[{"StartTime":96875.0,"Position":304.0,"HyperDash":false},{"StartTime":96937.0,"Position":333.433136,"HyperDash":false},{"StartTime":97000.0,"Position":385.325043,"HyperDash":false},{"StartTime":97062.0,"Position":379.667,"HyperDash":false},{"StartTime":97125.0,"Position":401.778076,"HyperDash":false},{"StartTime":97187.0,"Position":418.125366,"HyperDash":false},{"StartTime":97250.0,"Position":403.005768,"HyperDash":false},{"StartTime":97312.0,"Position":375.9013,"HyperDash":false},{"StartTime":97375.0,"Position":343.426239,"HyperDash":false},{"StartTime":97437.0,"Position":382.9013,"HyperDash":false},{"StartTime":97499.0,"Position":392.005768,"HyperDash":false},{"StartTime":97561.0,"Position":388.066345,"HyperDash":false},{"StartTime":97624.0,"Position":401.778076,"HyperDash":false},{"StartTime":97677.0,"Position":380.074066,"HyperDash":false},{"StartTime":97731.0,"Position":366.190063,"HyperDash":false},{"StartTime":97785.0,"Position":348.305481,"HyperDash":false},{"StartTime":97874.0,"Position":304.0,"HyperDash":false}]},{"StartTime":98125.0,"Objects":[{"StartTime":98125.0,"Position":240.0,"HyperDash":false},{"StartTime":98187.0,"Position":220.193451,"HyperDash":false},{"StartTime":98250.0,"Position":179.67662,"HyperDash":false},{"StartTime":98312.0,"Position":167.455551,"HyperDash":false},{"StartTime":98375.0,"Position":115.407051,"HyperDash":false},{"StartTime":98428.0,"Position":97.24337,"HyperDash":false},{"StartTime":98482.0,"Position":115.416969,"HyperDash":false},{"StartTime":98535.0,"Position":122.237556,"HyperDash":false},{"StartTime":98624.0,"Position":166.963364,"HyperDash":false}]},{"StartTime":98875.0,"Objects":[{"StartTime":98875.0,"Position":240.0,"HyperDash":false},{"StartTime":98937.0,"Position":273.329651,"HyperDash":false},{"StartTime":99000.0,"Position":306.601349,"HyperDash":false},{"StartTime":99062.0,"Position":324.816467,"HyperDash":false},{"StartTime":99125.0,"Position":363.818481,"HyperDash":false},{"StartTime":99178.0,"Position":391.8492,"HyperDash":false},{"StartTime":99232.0,"Position":363.507568,"HyperDash":false},{"StartTime":99285.0,"Position":349.543182,"HyperDash":false},{"StartTime":99374.0,"Position":311.711731,"HyperDash":false}]},{"StartTime":99625.0,"Objects":[{"StartTime":99625.0,"Position":180.0,"HyperDash":false},{"StartTime":99678.0,"Position":143.011124,"HyperDash":false},{"StartTime":99732.0,"Position":113.192444,"HyperDash":false},{"StartTime":99785.0,"Position":79.4256439,"HyperDash":false},{"StartTime":99874.0,"Position":45.3982735,"HyperDash":false}]},{"StartTime":100125.0,"Objects":[{"StartTime":100125.0,"Position":48.0,"HyperDash":false},{"StartTime":100178.0,"Position":75.85622,"HyperDash":false},{"StartTime":100231.0,"Position":116.712425,"HyperDash":false},{"StartTime":100284.0,"Position":156.568634,"HyperDash":false},{"StartTime":100374.0,"Position":202.3622,"HyperDash":false}]},{"StartTime":100625.0,"Objects":[{"StartTime":100625.0,"Position":348.0,"HyperDash":false},{"StartTime":100678.0,"Position":383.8562,"HyperDash":false},{"StartTime":100731.0,"Position":402.712433,"HyperDash":false},{"StartTime":100784.0,"Position":456.568634,"HyperDash":false},{"StartTime":100874.0,"Position":502.362183,"HyperDash":false}]},{"StartTime":101125.0,"Objects":[{"StartTime":101125.0,"Position":504.0,"HyperDash":false},{"StartTime":101178.0,"Position":488.1438,"HyperDash":false},{"StartTime":101231.0,"Position":446.287567,"HyperDash":false},{"StartTime":101284.0,"Position":423.431366,"HyperDash":false},{"StartTime":101374.0,"Position":349.637817,"HyperDash":false}]},{"StartTime":101625.0,"Objects":[{"StartTime":101625.0,"Position":204.0,"HyperDash":false},{"StartTime":101678.0,"Position":156.143784,"HyperDash":false},{"StartTime":101731.0,"Position":133.287567,"HyperDash":false},{"StartTime":101784.0,"Position":117.431358,"HyperDash":false},{"StartTime":101874.0,"Position":49.6378021,"HyperDash":false}]},{"StartTime":102000.0,"Objects":[{"StartTime":102000.0,"Position":49.0,"HyperDash":false}]},{"StartTime":102125.0,"Objects":[{"StartTime":102125.0,"Position":49.0,"HyperDash":false}]},{"StartTime":102625.0,"Objects":[{"StartTime":102625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":102875.0,"Objects":[{"StartTime":102875.0,"Position":384.0,"HyperDash":false}]},{"StartTime":103125.0,"Objects":[{"StartTime":103125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":103625.0,"Objects":[{"StartTime":103625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":103875.0,"Objects":[{"StartTime":103875.0,"Position":128.0,"HyperDash":false}]},{"StartTime":104125.0,"Objects":[{"StartTime":104125.0,"Position":256.0,"HyperDash":false},{"StartTime":104178.0,"Position":272.994537,"HyperDash":false},{"StartTime":104232.0,"Position":332.6524,"HyperDash":false},{"StartTime":104285.0,"Position":355.331024,"HyperDash":false},{"StartTime":104375.0,"Position":402.7333,"HyperDash":false}]},{"StartTime":104625.0,"Objects":[{"StartTime":104625.0,"Position":492.0,"HyperDash":false}]},{"StartTime":104875.0,"Objects":[{"StartTime":104875.0,"Position":332.0,"HyperDash":false}]},{"StartTime":105125.0,"Objects":[{"StartTime":105125.0,"Position":256.0,"HyperDash":false},{"StartTime":105178.0,"Position":241.889923,"HyperDash":false},{"StartTime":105232.0,"Position":211.080078,"HyperDash":false},{"StartTime":105285.0,"Position":144.258759,"HyperDash":false},{"StartTime":105374.0,"Position":109.266708,"HyperDash":false}]},{"StartTime":105625.0,"Objects":[{"StartTime":105625.0,"Position":20.0,"HyperDash":false}]},{"StartTime":105875.0,"Objects":[{"StartTime":105875.0,"Position":180.0,"HyperDash":false}]},{"StartTime":106125.0,"Objects":[{"StartTime":106125.0,"Position":368.0,"HyperDash":false},{"StartTime":106178.0,"Position":376.172974,"HyperDash":false},{"StartTime":106231.0,"Position":410.418945,"HyperDash":false},{"StartTime":106284.0,"Position":421.151764,"HyperDash":false},{"StartTime":106374.0,"Position":416.485779,"HyperDash":false}]},{"StartTime":106625.0,"Objects":[{"StartTime":106625.0,"Position":220.0,"HyperDash":false},{"StartTime":106678.0,"Position":241.054184,"HyperDash":false},{"StartTime":106731.0,"Position":282.792328,"HyperDash":false},{"StartTime":106784.0,"Position":301.047577,"HyperDash":false},{"StartTime":106874.0,"Position":349.247284,"HyperDash":false}]},{"StartTime":107125.0,"Objects":[{"StartTime":107125.0,"Position":144.0,"HyperDash":false},{"StartTime":107178.0,"Position":125.932152,"HyperDash":false},{"StartTime":107232.0,"Position":88.3730545,"HyperDash":false},{"StartTime":107285.0,"Position":97.8173752,"HyperDash":false},{"StartTime":107375.0,"Position":95.51424,"HyperDash":false}]},{"StartTime":107625.0,"Objects":[{"StartTime":107625.0,"Position":292.0,"HyperDash":false},{"StartTime":107678.0,"Position":279.032471,"HyperDash":false},{"StartTime":107732.0,"Position":263.904663,"HyperDash":false},{"StartTime":107785.0,"Position":230.72316,"HyperDash":false},{"StartTime":107875.0,"Position":162.752716,"HyperDash":false}]},{"StartTime":108125.0,"Objects":[{"StartTime":108125.0,"Position":44.0,"HyperDash":false},{"StartTime":108178.0,"Position":95.98508,"HyperDash":false},{"StartTime":108232.0,"Position":110.3604,"HyperDash":false},{"StartTime":108285.0,"Position":123.706589,"HyperDash":false},{"StartTime":108374.0,"Position":169.6919,"HyperDash":false}]},{"StartTime":108625.0,"Objects":[{"StartTime":108625.0,"Position":304.0,"HyperDash":false}]},{"StartTime":108875.0,"Objects":[{"StartTime":108875.0,"Position":408.0,"HyperDash":false}]},{"StartTime":109125.0,"Objects":[{"StartTime":109125.0,"Position":468.0,"HyperDash":false},{"StartTime":109178.0,"Position":439.149963,"HyperDash":false},{"StartTime":109232.0,"Position":396.891418,"HyperDash":false},{"StartTime":109285.0,"Position":370.5935,"HyperDash":false},{"StartTime":109375.0,"Position":342.308075,"HyperDash":false}]},{"StartTime":109625.0,"Objects":[{"StartTime":109625.0,"Position":208.0,"HyperDash":false}]},{"StartTime":109875.0,"Objects":[{"StartTime":109875.0,"Position":104.0,"HyperDash":false}]},{"StartTime":110125.0,"Objects":[{"StartTime":110125.0,"Position":256.0,"HyperDash":false},{"StartTime":110178.0,"Position":239.263885,"HyperDash":false},{"StartTime":110232.0,"Position":204.098785,"HyperDash":false},{"StartTime":110285.0,"Position":187.362686,"HyperDash":false},{"StartTime":110375.0,"Position":148.7542,"HyperDash":false}]},{"StartTime":110625.0,"Objects":[{"StartTime":110625.0,"Position":256.0,"HyperDash":false},{"StartTime":110678.0,"Position":283.827423,"HyperDash":false},{"StartTime":110731.0,"Position":319.654846,"HyperDash":false},{"StartTime":110784.0,"Position":325.482239,"HyperDash":false},{"StartTime":110874.0,"Position":363.2458,"HyperDash":false}]},{"StartTime":111125.0,"Objects":[{"StartTime":111125.0,"Position":208.0,"HyperDash":false},{"StartTime":111178.0,"Position":185.263885,"HyperDash":false},{"StartTime":111232.0,"Position":170.098785,"HyperDash":false},{"StartTime":111285.0,"Position":123.362686,"HyperDash":false},{"StartTime":111375.0,"Position":100.754196,"HyperDash":false}]},{"StartTime":111625.0,"Objects":[{"StartTime":111625.0,"Position":304.0,"HyperDash":false},{"StartTime":111678.0,"Position":318.7361,"HyperDash":false},{"StartTime":111732.0,"Position":353.901184,"HyperDash":false},{"StartTime":111785.0,"Position":357.6373,"HyperDash":false},{"StartTime":111875.0,"Position":411.2458,"HyperDash":false}]},{"StartTime":112125.0,"Objects":[{"StartTime":112125.0,"Position":252.0,"HyperDash":false}]},{"StartTime":112375.0,"Objects":[{"StartTime":112375.0,"Position":112.0,"HyperDash":false}]},{"StartTime":112625.0,"Objects":[{"StartTime":112625.0,"Position":72.0,"HyperDash":false}]},{"StartTime":112875.0,"Objects":[{"StartTime":112875.0,"Position":158.0,"HyperDash":false},{"StartTime":112937.0,"Position":180.39856,"HyperDash":false},{"StartTime":113000.0,"Position":253.684036,"HyperDash":false},{"StartTime":113062.0,"Position":263.862976,"HyperDash":false},{"StartTime":113125.0,"Position":289.459473,"HyperDash":false},{"StartTime":113187.0,"Position":294.857574,"HyperDash":false},{"StartTime":113250.0,"Position":301.491974,"HyperDash":false},{"StartTime":113312.0,"Position":306.150818,"HyperDash":false},{"StartTime":113375.0,"Position":278.112,"HyperDash":false},{"StartTime":113437.0,"Position":308.150818,"HyperDash":false},{"StartTime":113500.0,"Position":291.538177,"HyperDash":false},{"StartTime":113562.0,"Position":288.857574,"HyperDash":false},{"StartTime":113625.0,"Position":289.160065,"HyperDash":false},{"StartTime":113678.0,"Position":275.785217,"HyperDash":false},{"StartTime":113732.0,"Position":261.88623,"HyperDash":false},{"StartTime":113785.0,"Position":219.895935,"HyperDash":false},{"StartTime":113874.0,"Position":158.0,"HyperDash":false}]},{"StartTime":114125.0,"Objects":[{"StartTime":114125.0,"Position":176.0,"HyperDash":false},{"StartTime":114187.0,"Position":215.46962,"HyperDash":false},{"StartTime":114250.0,"Position":243.459351,"HyperDash":false},{"StartTime":114312.0,"Position":280.9655,"HyperDash":false},{"StartTime":114375.0,"Position":311.184082,"HyperDash":false},{"StartTime":114428.0,"Position":345.321442,"HyperDash":false},{"StartTime":114482.0,"Position":372.3753,"HyperDash":false},{"StartTime":114535.0,"Position":414.472534,"HyperDash":false},{"StartTime":114624.0,"Position":431.115143,"HyperDash":false}]},{"StartTime":114875.0,"Objects":[{"StartTime":114875.0,"Position":328.0,"HyperDash":false},{"StartTime":114937.0,"Position":303.669556,"HyperDash":false},{"StartTime":115000.0,"Position":279.312225,"HyperDash":false},{"StartTime":115062.0,"Position":265.2286,"HyperDash":false},{"StartTime":115125.0,"Position":258.051422,"HyperDash":false},{"StartTime":115178.0,"Position":262.0706,"HyperDash":false},{"StartTime":115231.0,"Position":286.7301,"HyperDash":false},{"StartTime":115284.0,"Position":315.1607,"HyperDash":false},{"StartTime":115374.0,"Position":349.780029,"HyperDash":false}]},{"StartTime":115625.0,"Objects":[{"StartTime":115625.0,"Position":488.0,"HyperDash":false},{"StartTime":115678.0,"Position":480.653168,"HyperDash":false},{"StartTime":115732.0,"Position":483.186554,"HyperDash":false},{"StartTime":115785.0,"Position":463.839722,"HyperDash":false},{"StartTime":115875.0,"Position":458.062073,"HyperDash":false}]},{"StartTime":116125.0,"Objects":[{"StartTime":116125.0,"Position":416.0,"HyperDash":false}]},{"StartTime":116375.0,"Objects":[{"StartTime":116375.0,"Position":288.0,"HyperDash":false}]},{"StartTime":116625.0,"Objects":[{"StartTime":116625.0,"Position":164.0,"HyperDash":false}]},{"StartTime":116875.0,"Objects":[{"StartTime":116875.0,"Position":36.0,"HyperDash":false}]},{"StartTime":117125.0,"Objects":[{"StartTime":117125.0,"Position":104.0,"HyperDash":false}]},{"StartTime":117375.0,"Objects":[{"StartTime":117375.0,"Position":232.0,"HyperDash":false}]},{"StartTime":117625.0,"Objects":[{"StartTime":117625.0,"Position":356.0,"HyperDash":false}]},{"StartTime":117875.0,"Objects":[{"StartTime":117875.0,"Position":484.0,"HyperDash":false}]},{"StartTime":118125.0,"Objects":[{"StartTime":118125.0,"Position":356.0,"HyperDash":false}]},{"StartTime":128125.0,"Objects":[{"StartTime":128125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":128250.0,"Objects":[{"StartTime":128250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":128500.0,"Objects":[{"StartTime":128500.0,"Position":336.0,"HyperDash":false}]},{"StartTime":128625.0,"Objects":[{"StartTime":128625.0,"Position":336.0,"HyperDash":false}]},{"StartTime":128875.0,"Objects":[{"StartTime":128875.0,"Position":400.0,"HyperDash":false}]},{"StartTime":129000.0,"Objects":[{"StartTime":129000.0,"Position":400.0,"HyperDash":false}]},{"StartTime":129250.0,"Objects":[{"StartTime":129250.0,"Position":492.0,"HyperDash":false}]},{"StartTime":129375.0,"Objects":[{"StartTime":129375.0,"Position":492.0,"HyperDash":false}]},{"StartTime":129625.0,"Objects":[{"StartTime":129625.0,"Position":440.0,"HyperDash":false},{"StartTime":129678.0,"Position":420.699738,"HyperDash":false},{"StartTime":129731.0,"Position":376.399475,"HyperDash":false},{"StartTime":129784.0,"Position":327.099243,"HyperDash":false},{"StartTime":129874.0,"Position":283.551636,"HyperDash":false}]},{"StartTime":130125.0,"Objects":[{"StartTime":130125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":130250.0,"Objects":[{"StartTime":130250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":130500.0,"Objects":[{"StartTime":130500.0,"Position":176.0,"HyperDash":false}]},{"StartTime":130625.0,"Objects":[{"StartTime":130625.0,"Position":176.0,"HyperDash":false}]},{"StartTime":130875.0,"Objects":[{"StartTime":130875.0,"Position":112.0,"HyperDash":false}]},{"StartTime":131000.0,"Objects":[{"StartTime":131000.0,"Position":112.0,"HyperDash":false}]},{"StartTime":131250.0,"Objects":[{"StartTime":131250.0,"Position":20.0,"HyperDash":false}]},{"StartTime":131375.0,"Objects":[{"StartTime":131375.0,"Position":20.0,"HyperDash":false}]},{"StartTime":131625.0,"Objects":[{"StartTime":131625.0,"Position":72.0,"HyperDash":false},{"StartTime":131678.0,"Position":85.16705,"HyperDash":false},{"StartTime":131732.0,"Position":139.959915,"HyperDash":false},{"StartTime":131785.0,"Position":179.126953,"HyperDash":false},{"StartTime":131875.0,"Position":228.44838,"HyperDash":false}]},{"StartTime":132125.0,"Objects":[{"StartTime":132125.0,"Position":408.0,"HyperDash":false},{"StartTime":132187.0,"Position":432.7211,"HyperDash":false},{"StartTime":132250.0,"Position":463.48645,"HyperDash":false},{"StartTime":132312.0,"Position":484.605652,"HyperDash":false},{"StartTime":132375.0,"Position":511.913147,"HyperDash":false},{"StartTime":132437.0,"Position":511.3131,"HyperDash":false},{"StartTime":132500.0,"Position":512.0,"HyperDash":false},{"StartTime":132562.0,"Position":512.0,"HyperDash":false},{"StartTime":132625.0,"Position":491.9296,"HyperDash":false},{"StartTime":132687.0,"Position":477.671265,"HyperDash":false},{"StartTime":132750.0,"Position":455.869171,"HyperDash":false},{"StartTime":132812.0,"Position":413.826355,"HyperDash":false},{"StartTime":132875.0,"Position":366.962769,"HyperDash":false},{"StartTime":132937.0,"Position":340.888336,"HyperDash":false},{"StartTime":133000.0,"Position":273.617157,"HyperDash":false},{"StartTime":133062.0,"Position":263.5604,"HyperDash":false},{"StartTime":133125.0,"Position":210.586578,"HyperDash":false},{"StartTime":133187.0,"Position":176.064163,"HyperDash":false},{"StartTime":133250.0,"Position":127.187744,"HyperDash":false},{"StartTime":133312.0,"Position":131.32103,"HyperDash":false},{"StartTime":133375.0,"Position":102.106659,"HyperDash":false},{"StartTime":133437.0,"Position":101.403084,"HyperDash":false},{"StartTime":133500.0,"Position":84.85893,"HyperDash":false},{"StartTime":133562.0,"Position":83.863945,"HyperDash":false},{"StartTime":133625.0,"Position":119.323433,"HyperDash":false},{"StartTime":133687.0,"Position":159.490738,"HyperDash":false},{"StartTime":133750.0,"Position":179.476852,"HyperDash":false},{"StartTime":133812.0,"Position":207.3787,"HyperDash":false},{"StartTime":133875.0,"Position":256.6099,"HyperDash":false},{"StartTime":133937.0,"Position":289.899384,"HyperDash":false},{"StartTime":134000.0,"Position":322.431061,"HyperDash":false},{"StartTime":134062.0,"Position":371.9527,"HyperDash":false},{"StartTime":134125.0,"Position":392.617126,"HyperDash":false},{"StartTime":134187.0,"Position":422.877838,"HyperDash":false},{"StartTime":134250.0,"Position":425.129883,"HyperDash":false},{"StartTime":134312.0,"Position":404.693054,"HyperDash":false},{"StartTime":134375.0,"Position":409.929779,"HyperDash":false},{"StartTime":134437.0,"Position":384.0832,"HyperDash":false},{"StartTime":134500.0,"Position":354.885651,"HyperDash":false},{"StartTime":134562.0,"Position":326.547424,"HyperDash":false},{"StartTime":134625.0,"Position":301.508575,"HyperDash":false},{"StartTime":134687.0,"Position":255.1601,"HyperDash":false},{"StartTime":134750.0,"Position":222.486877,"HyperDash":false},{"StartTime":134812.0,"Position":183.853729,"HyperDash":false},{"StartTime":134875.0,"Position":145.138245,"HyperDash":false},{"StartTime":134937.0,"Position":107.848343,"HyperDash":false},{"StartTime":135000.0,"Position":58.21479,"HyperDash":false},{"StartTime":135062.0,"Position":57.82658,"HyperDash":false},{"StartTime":135125.0,"Position":20.1227779,"HyperDash":false},{"StartTime":135187.0,"Position":0.0,"HyperDash":false},{"StartTime":135250.0,"Position":0.0,"HyperDash":false},{"StartTime":135312.0,"Position":0.0,"HyperDash":false},{"StartTime":135375.0,"Position":0.05981236,"HyperDash":false},{"StartTime":135428.0,"Position":14.5409756,"HyperDash":false},{"StartTime":135482.0,"Position":36.1827965,"HyperDash":false},{"StartTime":135535.0,"Position":37.5372772,"HyperDash":false},{"StartTime":135625.0,"Position":103.892265,"HyperDash":false}]},{"StartTime":135875.0,"Objects":[{"StartTime":135875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":136000.0,"Objects":[{"StartTime":136000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":136125.0,"Objects":[{"StartTime":136125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":136375.0,"Objects":[{"StartTime":136375.0,"Position":136.0,"HyperDash":false}]},{"StartTime":136625.0,"Objects":[{"StartTime":136625.0,"Position":132.0,"HyperDash":false}]},{"StartTime":136750.0,"Objects":[{"StartTime":136750.0,"Position":133.0,"HyperDash":false}]},{"StartTime":137000.0,"Objects":[{"StartTime":137000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":137125.0,"Objects":[{"StartTime":137125.0,"Position":255.0,"HyperDash":false}]},{"StartTime":137250.0,"Objects":[{"StartTime":137250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":137375.0,"Objects":[{"StartTime":137375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":137625.0,"Objects":[{"StartTime":137625.0,"Position":380.0,"HyperDash":false}]},{"StartTime":137875.0,"Objects":[{"StartTime":137875.0,"Position":376.0,"HyperDash":false}]},{"StartTime":138125.0,"Objects":[{"StartTime":138125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":138375.0,"Objects":[{"StartTime":138375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":138625.0,"Objects":[{"StartTime":138625.0,"Position":144.0,"HyperDash":false}]},{"StartTime":138750.0,"Objects":[{"StartTime":138750.0,"Position":144.0,"HyperDash":false}]},{"StartTime":139000.0,"Objects":[{"StartTime":139000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139125.0,"Objects":[{"StartTime":139125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139250.0,"Objects":[{"StartTime":139250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139375.0,"Objects":[{"StartTime":139375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139625.0,"Objects":[{"StartTime":139625.0,"Position":368.0,"HyperDash":false}]},{"StartTime":139875.0,"Objects":[{"StartTime":139875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":140000.0,"Objects":[{"StartTime":140000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":140125.0,"Objects":[{"StartTime":140125.0,"Position":256.0,"HyperDash":false},{"StartTime":140178.0,"Position":227.121277,"HyperDash":false},{"StartTime":140232.0,"Position":201.278854,"HyperDash":false},{"StartTime":140285.0,"Position":210.432343,"HyperDash":false},{"StartTime":140374.0,"Position":256.095947,"HyperDash":false}]},{"StartTime":140625.0,"Objects":[{"StartTime":140625.0,"Position":332.0,"HyperDash":false}]},{"StartTime":140750.0,"Objects":[{"StartTime":140750.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141000.0,"Objects":[{"StartTime":141000.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141125.0,"Objects":[{"StartTime":141125.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141250.0,"Objects":[{"StartTime":141250.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141375.0,"Objects":[{"StartTime":141375.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141625.0,"Objects":[{"StartTime":141625.0,"Position":180.0,"HyperDash":false}]},{"StartTime":141875.0,"Objects":[{"StartTime":141875.0,"Position":180.0,"HyperDash":false}]},{"StartTime":142125.0,"Objects":[{"StartTime":142125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":142375.0,"Objects":[{"StartTime":142375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":142625.0,"Objects":[{"StartTime":142625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":142750.0,"Objects":[{"StartTime":142750.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143000.0,"Objects":[{"StartTime":143000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143125.0,"Objects":[{"StartTime":143125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143250.0,"Objects":[{"StartTime":143250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143375.0,"Objects":[{"StartTime":143375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143625.0,"Objects":[{"StartTime":143625.0,"Position":188.0,"HyperDash":false}]},{"StartTime":143875.0,"Objects":[{"StartTime":143875.0,"Position":324.0,"HyperDash":false}]},{"StartTime":144000.0,"Objects":[{"StartTime":144000.0,"Position":324.0,"HyperDash":false}]},{"StartTime":144125.0,"Objects":[{"StartTime":144125.0,"Position":324.0,"HyperDash":false},{"StartTime":144178.0,"Position":375.919983,"HyperDash":false},{"StartTime":144232.0,"Position":388.48,"HyperDash":false},{"StartTime":144285.0,"Position":424.4,"HyperDash":false},{"StartTime":144375.0,"Position":484.0,"HyperDash":false}]},{"StartTime":144625.0,"Objects":[{"StartTime":144625.0,"Position":392.0,"HyperDash":false}]},{"StartTime":144750.0,"Objects":[{"StartTime":144750.0,"Position":392.0,"HyperDash":false}]},{"StartTime":145000.0,"Objects":[{"StartTime":145000.0,"Position":324.0,"HyperDash":false}]},{"StartTime":145125.0,"Objects":[{"StartTime":145125.0,"Position":324.0,"HyperDash":false}]},{"StartTime":145250.0,"Objects":[{"StartTime":145250.0,"Position":324.0,"HyperDash":false}]},{"StartTime":145375.0,"Objects":[{"StartTime":145375.0,"Position":324.0,"HyperDash":false}]},{"StartTime":145625.0,"Objects":[{"StartTime":145625.0,"Position":188.0,"HyperDash":false}]},{"StartTime":145875.0,"Objects":[{"StartTime":145875.0,"Position":120.0,"HyperDash":false}]},{"StartTime":146125.0,"Objects":[{"StartTime":146125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":146375.0,"Objects":[{"StartTime":146375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":146625.0,"Objects":[{"StartTime":146625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":146750.0,"Objects":[{"StartTime":146750.0,"Position":256.0,"HyperDash":false}]},{"StartTime":147000.0,"Objects":[{"StartTime":147000.0,"Position":176.0,"HyperDash":false}]},{"StartTime":147125.0,"Objects":[{"StartTime":147125.0,"Position":176.0,"HyperDash":false}]},{"StartTime":147250.0,"Objects":[{"StartTime":147250.0,"Position":176.0,"HyperDash":false}]},{"StartTime":147375.0,"Objects":[{"StartTime":147375.0,"Position":176.0,"HyperDash":false}]},{"StartTime":147625.0,"Objects":[{"StartTime":147625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":147875.0,"Objects":[{"StartTime":147875.0,"Position":336.0,"HyperDash":false}]},{"StartTime":148000.0,"Objects":[{"StartTime":148000.0,"Position":336.0,"HyperDash":false}]},{"StartTime":148125.0,"Objects":[{"StartTime":148125.0,"Position":336.0,"HyperDash":false},{"StartTime":148178.0,"Position":375.538025,"HyperDash":false},{"StartTime":148231.0,"Position":390.979462,"HyperDash":false},{"StartTime":148284.0,"Position":386.895447,"HyperDash":false},{"StartTime":148374.0,"Position":370.6822,"HyperDash":false}]},{"StartTime":148625.0,"Objects":[{"StartTime":148625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":148750.0,"Objects":[{"StartTime":148750.0,"Position":240.0,"HyperDash":false}]},{"StartTime":149000.0,"Objects":[{"StartTime":149000.0,"Position":240.0,"HyperDash":false}]},{"StartTime":149125.0,"Objects":[{"StartTime":149125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":149250.0,"Objects":[{"StartTime":149250.0,"Position":272.0,"HyperDash":false}]},{"StartTime":149375.0,"Objects":[{"StartTime":149375.0,"Position":288.0,"HyperDash":false}]},{"StartTime":149625.0,"Objects":[{"StartTime":149625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":149875.0,"Objects":[{"StartTime":149875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":150125.0,"Objects":[{"StartTime":150125.0,"Position":116.0,"HyperDash":false}]},{"StartTime":150250.0,"Objects":[{"StartTime":150250.0,"Position":120.0,"HyperDash":false}]},{"StartTime":150375.0,"Objects":[{"StartTime":150375.0,"Position":132.0,"HyperDash":false}]},{"StartTime":150500.0,"Objects":[{"StartTime":150500.0,"Position":152.0,"HyperDash":false}]},{"StartTime":150625.0,"Objects":[{"StartTime":150625.0,"Position":176.0,"HyperDash":false}]},{"StartTime":150750.0,"Objects":[{"StartTime":150750.0,"Position":208.0,"HyperDash":false}]},{"StartTime":150875.0,"Objects":[{"StartTime":150875.0,"Position":232.0,"HyperDash":false}]},{"StartTime":151000.0,"Objects":[{"StartTime":151000.0,"Position":248.0,"HyperDash":false}]},{"StartTime":151125.0,"Objects":[{"StartTime":151125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":151250.0,"Objects":[{"StartTime":151250.0,"Position":260.0,"HyperDash":false}]},{"StartTime":151375.0,"Objects":[{"StartTime":151375.0,"Position":272.0,"HyperDash":false}]},{"StartTime":151500.0,"Objects":[{"StartTime":151500.0,"Position":292.0,"HyperDash":false}]},{"StartTime":151625.0,"Objects":[{"StartTime":151625.0,"Position":316.0,"HyperDash":false}]},{"StartTime":151750.0,"Objects":[{"StartTime":151750.0,"Position":348.0,"HyperDash":false}]},{"StartTime":151875.0,"Objects":[{"StartTime":151875.0,"Position":372.0,"HyperDash":false}]},{"StartTime":152000.0,"Objects":[{"StartTime":152000.0,"Position":388.0,"HyperDash":false}]},{"StartTime":152125.0,"Objects":[{"StartTime":152125.0,"Position":404.0,"HyperDash":false},{"StartTime":152178.0,"Position":429.642151,"HyperDash":false},{"StartTime":152232.0,"Position":425.184479,"HyperDash":false},{"StartTime":152285.0,"Position":392.507416,"HyperDash":false},{"StartTime":152375.0,"Position":342.072266,"HyperDash":false}]},{"StartTime":152625.0,"Objects":[{"StartTime":152625.0,"Position":108.0,"HyperDash":false},{"StartTime":152678.0,"Position":112.349617,"HyperDash":false},{"StartTime":152732.0,"Position":112.903786,"HyperDash":false},{"StartTime":152785.0,"Position":119.761673,"HyperDash":false},{"StartTime":152874.0,"Position":169.927719,"HyperDash":false}]},{"StartTime":153125.0,"Objects":[{"StartTime":153125.0,"Position":256.0,"HyperDash":false},{"StartTime":153250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":153375.0,"Objects":[{"StartTime":153375.0,"Position":256.0,"HyperDash":false},{"StartTime":153437.0,"Position":269.0,"HyperDash":false},{"StartTime":153500.0,"Position":241.0,"HyperDash":false},{"StartTime":153562.0,"Position":247.0,"HyperDash":false},{"StartTime":153625.0,"Position":256.0,"HyperDash":false},{"StartTime":153678.0,"Position":244.0,"HyperDash":false},{"StartTime":153732.0,"Position":258.0,"HyperDash":false},{"StartTime":153785.0,"Position":240.0,"HyperDash":false},{"StartTime":153875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":154125.0,"Objects":[{"StartTime":154125.0,"Position":360.0,"HyperDash":false}]},{"StartTime":154250.0,"Objects":[{"StartTime":154250.0,"Position":360.0,"HyperDash":false}]},{"StartTime":154375.0,"Objects":[{"StartTime":154375.0,"Position":360.0,"HyperDash":false}]},{"StartTime":154625.0,"Objects":[{"StartTime":154625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":154750.0,"Objects":[{"StartTime":154750.0,"Position":256.0,"HyperDash":false}]},{"StartTime":154875.0,"Objects":[{"StartTime":154875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":155125.0,"Objects":[{"StartTime":155125.0,"Position":154.0,"HyperDash":false}]},{"StartTime":155250.0,"Objects":[{"StartTime":155250.0,"Position":154.0,"HyperDash":false}]},{"StartTime":155375.0,"Objects":[{"StartTime":155375.0,"Position":155.0,"HyperDash":false},{"StartTime":155437.0,"Position":134.040146,"HyperDash":false},{"StartTime":155500.0,"Position":134.7992,"HyperDash":false},{"StartTime":155562.0,"Position":112.444061,"HyperDash":false},{"StartTime":155625.0,"Position":165.451813,"HyperDash":false},{"StartTime":155678.0,"Position":137.274612,"HyperDash":false},{"StartTime":155732.0,"Position":123.117592,"HyperDash":false},{"StartTime":155785.0,"Position":139.026031,"HyperDash":false},{"StartTime":155874.0,"Position":155.0,"HyperDash":false}]},{"StartTime":156000.0,"Objects":[{"StartTime":156000.0,"Position":163.0,"HyperDash":false}]},{"StartTime":156125.0,"Objects":[{"StartTime":156125.0,"Position":163.0,"HyperDash":false}]},{"StartTime":156250.0,"Objects":[{"StartTime":156250.0,"Position":163.0,"HyperDash":false},{"StartTime":156312.0,"Position":183.5915,"HyperDash":false},{"StartTime":156375.0,"Position":203.198776,"HyperDash":false},{"StartTime":156437.0,"Position":232.230286,"HyperDash":false},{"StartTime":156500.0,"Position":268.618439,"HyperDash":false},{"StartTime":156562.0,"Position":314.36087,"HyperDash":false},{"StartTime":156625.0,"Position":335.6506,"HyperDash":false},{"StartTime":156687.0,"Position":380.1404,"HyperDash":false},{"StartTime":156750.0,"Position":422.05127,"HyperDash":false},{"StartTime":156874.0,"Position":473.144562,"HyperDash":false}]},{"StartTime":157125.0,"Objects":[{"StartTime":157125.0,"Position":320.0,"HyperDash":false},{"StartTime":157187.0,"Position":278.6829,"HyperDash":false},{"StartTime":157250.0,"Position":245.222778,"HyperDash":false},{"StartTime":157312.0,"Position":218.502289,"HyperDash":false},{"StartTime":157375.0,"Position":221.008591,"HyperDash":false},{"StartTime":157428.0,"Position":240.596039,"HyperDash":false},{"StartTime":157482.0,"Position":248.418259,"HyperDash":false},{"StartTime":157535.0,"Position":260.110321,"HyperDash":false},{"StartTime":157624.0,"Position":317.0907,"HyperDash":false}]},{"StartTime":157750.0,"Objects":[{"StartTime":157750.0,"Position":348.0,"HyperDash":false}]},{"StartTime":157875.0,"Objects":[{"StartTime":157875.0,"Position":380.0,"HyperDash":false}]},{"StartTime":158000.0,"Objects":[{"StartTime":158000.0,"Position":404.0,"HyperDash":false}]},{"StartTime":158125.0,"Objects":[{"StartTime":158125.0,"Position":412.0,"HyperDash":false}]},{"StartTime":158250.0,"Objects":[{"StartTime":158250.0,"Position":412.0,"HyperDash":false}]},{"StartTime":158375.0,"Objects":[{"StartTime":158375.0,"Position":404.0,"HyperDash":false}]},{"StartTime":158625.0,"Objects":[{"StartTime":158625.0,"Position":264.0,"HyperDash":false},{"StartTime":158687.0,"Position":234.814957,"HyperDash":false},{"StartTime":158750.0,"Position":191.04628,"HyperDash":false},{"StartTime":158875.0,"Position":264.0,"HyperDash":false}]},{"StartTime":159125.0,"Objects":[{"StartTime":159125.0,"Position":164.0,"HyperDash":false},{"StartTime":159187.0,"Position":197.185043,"HyperDash":false},{"StartTime":159250.0,"Position":236.95372,"HyperDash":false},{"StartTime":159375.0,"Position":164.0,"HyperDash":false}]},{"StartTime":159625.0,"Objects":[{"StartTime":159625.0,"Position":56.0,"HyperDash":false}]},{"StartTime":159875.0,"Objects":[{"StartTime":159875.0,"Position":64.0,"HyperDash":false}]},{"StartTime":160000.0,"Objects":[{"StartTime":160000.0,"Position":64.0,"HyperDash":false}]},{"StartTime":160125.0,"Objects":[{"StartTime":160125.0,"Position":64.0,"HyperDash":false},{"StartTime":160187.0,"Position":64.51918,"HyperDash":false},{"StartTime":160250.0,"Position":26.7402878,"HyperDash":false},{"StartTime":160375.0,"Position":64.0,"HyperDash":false}]},{"StartTime":160500.0,"Objects":[{"StartTime":160500.0,"Position":128.0,"HyperDash":false},{"StartTime":160562.0,"Position":132.164291,"HyperDash":false},{"StartTime":160625.0,"Position":134.379623,"HyperDash":false},{"StartTime":160750.0,"Position":128.0,"HyperDash":false}]},{"StartTime":160875.0,"Objects":[{"StartTime":160875.0,"Position":192.0,"HyperDash":false},{"StartTime":160937.0,"Position":189.164291,"HyperDash":false},{"StartTime":161000.0,"Position":198.379623,"HyperDash":false},{"StartTime":161125.0,"Position":192.0,"HyperDash":false}]},{"StartTime":161250.0,"Objects":[{"StartTime":161250.0,"Position":240.0,"HyperDash":false},{"StartTime":161312.0,"Position":248.7879,"HyperDash":false},{"StartTime":161375.0,"Position":289.975616,"HyperDash":false},{"StartTime":161500.0,"Position":240.0,"HyperDash":false}]},{"StartTime":161625.0,"Objects":[{"StartTime":161625.0,"Position":284.0,"HyperDash":false},{"StartTime":161687.0,"Position":327.2897,"HyperDash":false},{"StartTime":161750.0,"Position":339.019562,"HyperDash":false},{"StartTime":161875.0,"Position":284.0,"HyperDash":false}]},{"StartTime":162000.0,"Objects":[{"StartTime":162000.0,"Position":328.0,"HyperDash":false},{"StartTime":162062.0,"Position":364.361755,"HyperDash":false},{"StartTime":162124.0,"Position":407.040955,"HyperDash":false},{"StartTime":162249.0,"Position":328.0,"HyperDash":false}]},{"StartTime":162375.0,"Objects":[{"StartTime":162375.0,"Position":308.0,"HyperDash":false},{"StartTime":162437.0,"Position":269.638245,"HyperDash":false},{"StartTime":162499.0,"Position":228.959045,"HyperDash":false},{"StartTime":162624.0,"Position":308.0,"HyperDash":false}]},{"StartTime":162750.0,"Objects":[{"StartTime":162750.0,"Position":340.0,"HyperDash":false},{"StartTime":162812.0,"Position":374.361755,"HyperDash":false},{"StartTime":162874.0,"Position":419.040955,"HyperDash":false},{"StartTime":162999.0,"Position":340.0,"HyperDash":false}]},{"StartTime":163125.0,"Objects":[{"StartTime":163125.0,"Position":284.0,"HyperDash":false},{"StartTime":163187.0,"Position":280.849731,"HyperDash":false},{"StartTime":163249.0,"Position":271.649841,"HyperDash":false},{"StartTime":163374.0,"Position":284.0,"HyperDash":false}]},{"StartTime":163500.0,"Objects":[{"StartTime":163500.0,"Position":224.0,"HyperDash":false},{"StartTime":163562.0,"Position":227.849731,"HyperDash":false},{"StartTime":163624.0,"Position":211.649857,"HyperDash":false},{"StartTime":163749.0,"Position":224.0,"HyperDash":false}]},{"StartTime":163875.0,"Objects":[{"StartTime":163875.0,"Position":180.0,"HyperDash":false},{"StartTime":163937.0,"Position":134.564423,"HyperDash":false},{"StartTime":163999.0,"Position":102.8189,"HyperDash":false},{"StartTime":164124.0,"Position":180.0,"HyperDash":false}]},{"StartTime":164250.0,"Objects":[{"StartTime":164250.0,"Position":144.0,"HyperDash":false},{"StartTime":164312.0,"Position":107.832245,"HyperDash":false},{"StartTime":164375.0,"Position":79.14566,"HyperDash":false},{"StartTime":164500.0,"Position":144.0,"HyperDash":false}]},{"StartTime":164625.0,"Objects":[{"StartTime":164625.0,"Position":168.0,"HyperDash":false},{"StartTime":164687.0,"Position":182.167755,"HyperDash":false},{"StartTime":164750.0,"Position":232.85434,"HyperDash":false},{"StartTime":164875.0,"Position":168.0,"HyperDash":false}]},{"StartTime":165000.0,"Objects":[{"StartTime":165000.0,"Position":136.0,"HyperDash":false},{"StartTime":165062.0,"Position":117.871719,"HyperDash":false},{"StartTime":165124.0,"Position":101.605316,"HyperDash":false},{"StartTime":165249.0,"Position":136.0,"HyperDash":false}]},{"StartTime":165375.0,"Objects":[{"StartTime":165375.0,"Position":188.0,"HyperDash":false},{"StartTime":165437.0,"Position":220.128281,"HyperDash":false},{"StartTime":165499.0,"Position":222.394684,"HyperDash":false},{"StartTime":165624.0,"Position":188.0,"HyperDash":false}]},{"StartTime":165750.0,"Objects":[{"StartTime":165750.0,"Position":236.0,"HyperDash":false}]},{"StartTime":165875.0,"Objects":[{"StartTime":165875.0,"Position":236.0,"HyperDash":false}]},{"StartTime":166125.0,"Objects":[{"StartTime":166125.0,"Position":364.0,"HyperDash":false},{"StartTime":166187.0,"Position":369.388123,"HyperDash":false},{"StartTime":166250.0,"Position":391.656616,"HyperDash":false},{"StartTime":166312.0,"Position":357.6028,"HyperDash":false},{"StartTime":166375.0,"Position":309.5534,"HyperDash":false},{"StartTime":166499.0,"Position":282.373474,"HyperDash":false}]},{"StartTime":166625.0,"Objects":[{"StartTime":166625.0,"Position":264.0,"HyperDash":false},{"StartTime":166687.0,"Position":284.388123,"HyperDash":false},{"StartTime":166750.0,"Position":283.656616,"HyperDash":false},{"StartTime":166812.0,"Position":260.602844,"HyperDash":false},{"StartTime":166875.0,"Position":209.5534,"HyperDash":false},{"StartTime":166999.0,"Position":182.373489,"HyperDash":false}]},{"StartTime":167125.0,"Objects":[{"StartTime":167125.0,"Position":192.0,"HyperDash":false}]},{"StartTime":167375.0,"Objects":[{"StartTime":167375.0,"Position":320.0,"HyperDash":false}]},{"StartTime":167625.0,"Objects":[{"StartTime":167625.0,"Position":192.0,"HyperDash":false}]},{"StartTime":167750.0,"Objects":[{"StartTime":167750.0,"Position":256.0,"HyperDash":false}]},{"StartTime":167875.0,"Objects":[{"StartTime":167875.0,"Position":320.0,"HyperDash":false}]},{"StartTime":168125.0,"Objects":[{"StartTime":168125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":168250.0,"Objects":[{"StartTime":168250.0,"Position":193.0,"HyperDash":false},{"StartTime":168308.0,"Position":488.0,"HyperDash":false},{"StartTime":168367.0,"Position":314.0,"HyperDash":false},{"StartTime":168425.0,"Position":135.0,"HyperDash":false},{"StartTime":168484.0,"Position":399.0,"HyperDash":false},{"StartTime":168542.0,"Position":404.0,"HyperDash":false},{"StartTime":168601.0,"Position":152.0,"HyperDash":false},{"StartTime":168660.0,"Position":353.0,"HyperDash":false},{"StartTime":168718.0,"Position":358.0,"HyperDash":false},{"StartTime":168777.0,"Position":447.0,"HyperDash":false},{"StartTime":168835.0,"Position":222.0,"HyperDash":false},{"StartTime":168894.0,"Position":382.0,"HyperDash":false},{"StartTime":168953.0,"Position":433.0,"HyperDash":false},{"StartTime":169011.0,"Position":450.0,"HyperDash":false},{"StartTime":169070.0,"Position":326.0,"HyperDash":false},{"StartTime":169128.0,"Position":414.0,"HyperDash":false},{"StartTime":169187.0,"Position":285.0,"HyperDash":false},{"StartTime":169246.0,"Position":336.0,"HyperDash":false},{"StartTime":169304.0,"Position":509.0,"HyperDash":false},{"StartTime":169363.0,"Position":334.0,"HyperDash":false},{"StartTime":169421.0,"Position":72.0,"HyperDash":false},{"StartTime":169480.0,"Position":425.0,"HyperDash":false},{"StartTime":169539.0,"Position":451.0,"HyperDash":false},{"StartTime":169597.0,"Position":220.0,"HyperDash":false},{"StartTime":169656.0,"Position":25.0,"HyperDash":false},{"StartTime":169714.0,"Position":77.0,"HyperDash":false},{"StartTime":169773.0,"Position":509.0,"HyperDash":false},{"StartTime":169832.0,"Position":90.0,"HyperDash":false},{"StartTime":169890.0,"Position":118.0,"HyperDash":false},{"StartTime":169949.0,"Position":58.0,"HyperDash":false},{"StartTime":170007.0,"Position":12.0,"HyperDash":false},{"StartTime":170066.0,"Position":215.0,"HyperDash":false},{"StartTime":170125.0,"Position":487.0,"HyperDash":false}]},{"StartTime":171125.0,"Objects":[{"StartTime":171125.0,"Position":446.0,"HyperDash":false},{"StartTime":171187.0,"Position":491.0,"HyperDash":false},{"StartTime":171250.0,"Position":459.0,"HyperDash":false},{"StartTime":171312.0,"Position":37.0,"HyperDash":false},{"StartTime":171375.0,"Position":291.0,"HyperDash":false},{"StartTime":171437.0,"Position":315.0,"HyperDash":false},{"StartTime":171500.0,"Position":35.0,"HyperDash":false},{"StartTime":171562.0,"Position":208.0,"HyperDash":false},{"StartTime":171625.0,"Position":504.0,"HyperDash":false},{"StartTime":171687.0,"Position":296.0,"HyperDash":false},{"StartTime":171750.0,"Position":105.0,"HyperDash":false},{"StartTime":171812.0,"Position":488.0,"HyperDash":false},{"StartTime":171875.0,"Position":230.0,"HyperDash":false},{"StartTime":171937.0,"Position":446.0,"HyperDash":false},{"StartTime":172000.0,"Position":241.0,"HyperDash":false},{"StartTime":172062.0,"Position":413.0,"HyperDash":false},{"StartTime":172125.0,"Position":357.0,"HyperDash":false}]},{"StartTime":172375.0,"Objects":[{"StartTime":172375.0,"Position":48.0,"HyperDash":false}]},{"StartTime":172625.0,"Objects":[{"StartTime":172625.0,"Position":20.0,"HyperDash":false},{"StartTime":172678.0,"Position":23.1916313,"HyperDash":false},{"StartTime":172732.0,"Position":25.55497,"HyperDash":false},{"StartTime":172785.0,"Position":75.26404,"HyperDash":false},{"StartTime":172875.0,"Position":108.478035,"HyperDash":false}]},{"StartTime":173125.0,"Objects":[{"StartTime":173125.0,"Position":240.0,"HyperDash":false}]},{"StartTime":173375.0,"Objects":[{"StartTime":173375.0,"Position":200.0,"HyperDash":false}]},{"StartTime":173625.0,"Objects":[{"StartTime":173625.0,"Position":324.0,"HyperDash":false},{"StartTime":173678.0,"Position":349.476471,"HyperDash":false},{"StartTime":173732.0,"Position":378.649323,"HyperDash":false},{"StartTime":173785.0,"Position":384.945282,"HyperDash":false},{"StartTime":173875.0,"Position":412.1426,"HyperDash":false}]},{"StartTime":174000.0,"Objects":[{"StartTime":174000.0,"Position":412.0,"HyperDash":false}]},{"StartTime":174125.0,"Objects":[{"StartTime":174125.0,"Position":412.0,"HyperDash":false},{"StartTime":174178.0,"Position":426.1397,"HyperDash":false},{"StartTime":174232.0,"Position":445.433044,"HyperDash":false},{"StartTime":174285.0,"Position":425.572754,"HyperDash":false},{"StartTime":174375.0,"Position":450.394928,"HyperDash":false}]},{"StartTime":174625.0,"Objects":[{"StartTime":174625.0,"Position":398.0,"HyperDash":false},{"StartTime":174678.0,"Position":380.028442,"HyperDash":false},{"StartTime":174732.0,"Position":327.434753,"HyperDash":false},{"StartTime":174785.0,"Position":306.4632,"HyperDash":false},{"StartTime":174875.0,"Position":242.473724,"HyperDash":false}]},{"StartTime":175000.0,"Objects":[{"StartTime":175000.0,"Position":245.0,"HyperDash":false}]},{"StartTime":175125.0,"Objects":[{"StartTime":175125.0,"Position":245.0,"HyperDash":false},{"StartTime":175178.0,"Position":247.860275,"HyperDash":false},{"StartTime":175232.0,"Position":229.566971,"HyperDash":false},{"StartTime":175285.0,"Position":219.427246,"HyperDash":false},{"StartTime":175375.0,"Position":206.605072,"HyperDash":false}]},{"StartTime":175625.0,"Objects":[{"StartTime":175625.0,"Position":259.0,"HyperDash":false},{"StartTime":175678.0,"Position":271.971558,"HyperDash":false},{"StartTime":175732.0,"Position":338.565247,"HyperDash":false},{"StartTime":175785.0,"Position":339.5368,"HyperDash":false},{"StartTime":175875.0,"Position":414.526276,"HyperDash":false}]},{"StartTime":176125.0,"Objects":[{"StartTime":176125.0,"Position":424.0,"HyperDash":false}]},{"StartTime":176375.0,"Objects":[{"StartTime":176375.0,"Position":272.0,"HyperDash":false}]},{"StartTime":176625.0,"Objects":[{"StartTime":176625.0,"Position":116.0,"HyperDash":false}]},{"StartTime":176875.0,"Objects":[{"StartTime":176875.0,"Position":173.0,"HyperDash":false},{"StartTime":176937.0,"Position":220.433136,"HyperDash":false},{"StartTime":177000.0,"Position":248.325027,"HyperDash":false},{"StartTime":177062.0,"Position":256.667,"HyperDash":false},{"StartTime":177125.0,"Position":270.778076,"HyperDash":false},{"StartTime":177187.0,"Position":271.125366,"HyperDash":false},{"StartTime":177250.0,"Position":267.005768,"HyperDash":false},{"StartTime":177312.0,"Position":259.9013,"HyperDash":false},{"StartTime":177375.0,"Position":212.426208,"HyperDash":false},{"StartTime":177437.0,"Position":239.901321,"HyperDash":false},{"StartTime":177500.0,"Position":249.239349,"HyperDash":false},{"StartTime":177562.0,"Position":285.125366,"HyperDash":false},{"StartTime":177625.0,"Position":270.676758,"HyperDash":false},{"StartTime":177678.0,"Position":275.8453,"HyperDash":false},{"StartTime":177732.0,"Position":255.82901,"HyperDash":false},{"StartTime":177785.0,"Position":207.305466,"HyperDash":false},{"StartTime":177874.0,"Position":173.0,"HyperDash":false}]},{"StartTime":178125.0,"Objects":[{"StartTime":178125.0,"Position":28.0,"HyperDash":false},{"StartTime":178187.0,"Position":78.55116,"HyperDash":false},{"StartTime":178250.0,"Position":102.707985,"HyperDash":false},{"StartTime":178312.0,"Position":129.259155,"HyperDash":false},{"StartTime":178375.0,"Position":179.41597,"HyperDash":false},{"StartTime":178428.0,"Position":226.516159,"HyperDash":false},{"StartTime":178482.0,"Position":240.222,"HyperDash":false},{"StartTime":178535.0,"Position":283.3222,"HyperDash":false},{"StartTime":178625.0,"Position":330.83194,"HyperDash":false}]},{"StartTime":178875.0,"Objects":[{"StartTime":178875.0,"Position":172.0,"HyperDash":false},{"StartTime":178937.0,"Position":221.551163,"HyperDash":false},{"StartTime":179000.0,"Position":253.707977,"HyperDash":false},{"StartTime":179062.0,"Position":274.259155,"HyperDash":false},{"StartTime":179125.0,"Position":323.415955,"HyperDash":false},{"StartTime":179178.0,"Position":344.516174,"HyperDash":false},{"StartTime":179232.0,"Position":379.222,"HyperDash":false},{"StartTime":179285.0,"Position":429.3222,"HyperDash":false},{"StartTime":179375.0,"Position":474.83194,"HyperDash":false}]},{"StartTime":179625.0,"Objects":[{"StartTime":179625.0,"Position":384.0,"HyperDash":false},{"StartTime":179678.0,"Position":348.327026,"HyperDash":false},{"StartTime":179732.0,"Position":316.224579,"HyperDash":false},{"StartTime":179785.0,"Position":267.12973,"HyperDash":false},{"StartTime":179875.0,"Position":244.098541,"HyperDash":false}]},{"StartTime":180000.0,"Objects":[{"StartTime":180000.0,"Position":244.0,"HyperDash":false}]},{"StartTime":180125.0,"Objects":[{"StartTime":180125.0,"Position":244.0,"HyperDash":false},{"StartTime":180178.0,"Position":217.455292,"HyperDash":false},{"StartTime":180232.0,"Position":186.277634,"HyperDash":false},{"StartTime":180285.0,"Position":129.732925,"HyperDash":false},{"StartTime":180375.0,"Position":85.77019,"HyperDash":false}]},{"StartTime":180625.0,"Objects":[{"StartTime":180625.0,"Position":100.0,"HyperDash":false},{"StartTime":180678.0,"Position":146.386475,"HyperDash":false},{"StartTime":180732.0,"Position":185.4029,"HyperDash":false},{"StartTime":180785.0,"Position":189.789368,"HyperDash":false},{"StartTime":180875.0,"Position":257.4834,"HyperDash":false}]},{"StartTime":181000.0,"Objects":[{"StartTime":181000.0,"Position":257.0,"HyperDash":false}]},{"StartTime":181125.0,"Objects":[{"StartTime":181125.0,"Position":256.0,"HyperDash":false},{"StartTime":181178.0,"Position":273.4897,"HyperDash":false},{"StartTime":181231.0,"Position":332.9794,"HyperDash":false},{"StartTime":181284.0,"Position":358.4691,"HyperDash":false},{"StartTime":181374.0,"Position":413.338379,"HyperDash":false}]},{"StartTime":181625.0,"Objects":[{"StartTime":181625.0,"Position":426.0,"HyperDash":false},{"StartTime":181678.0,"Position":383.4294,"HyperDash":false},{"StartTime":181732.0,"Position":353.2254,"HyperDash":false},{"StartTime":181785.0,"Position":325.654816,"HyperDash":false},{"StartTime":181875.0,"Position":267.648163,"HyperDash":false}]},{"StartTime":182000.0,"Objects":[{"StartTime":182000.0,"Position":267.0,"HyperDash":false}]},{"StartTime":182125.0,"Objects":[{"StartTime":182125.0,"Position":267.0,"HyperDash":false},{"StartTime":182178.0,"Position":226.982559,"HyperDash":false},{"StartTime":182232.0,"Position":205.749466,"HyperDash":false},{"StartTime":182285.0,"Position":176.327576,"HyperDash":false},{"StartTime":182375.0,"Position":168.9247,"HyperDash":false}]},{"StartTime":182625.0,"Objects":[{"StartTime":182625.0,"Position":140.0,"HyperDash":false},{"StartTime":182678.0,"Position":155.139557,"HyperDash":false},{"StartTime":182731.0,"Position":203.985977,"HyperDash":false},{"StartTime":182784.0,"Position":216.5605,"HyperDash":false},{"StartTime":182874.0,"Position":238.0753,"HyperDash":false}]},{"StartTime":183125.0,"Objects":[{"StartTime":183125.0,"Position":62.0,"HyperDash":false},{"StartTime":183178.0,"Position":70.6348648,"HyperDash":false},{"StartTime":183232.0,"Position":74.16411,"HyperDash":false},{"StartTime":183285.0,"Position":122.076561,"HyperDash":false},{"StartTime":183375.0,"Position":173.7103,"HyperDash":false}]},{"StartTime":183625.0,"Objects":[{"StartTime":183625.0,"Position":348.0,"HyperDash":false},{"StartTime":183678.0,"Position":324.143158,"HyperDash":false},{"StartTime":183732.0,"Position":333.585327,"HyperDash":false},{"StartTime":183785.0,"Position":270.711,"HyperDash":false},{"StartTime":183874.0,"Position":236.603912,"HyperDash":false}]},{"StartTime":184125.0,"Objects":[{"StartTime":184125.0,"Position":64.0,"HyperDash":false}]},{"StartTime":184250.0,"Objects":[{"StartTime":184250.0,"Position":488.0,"HyperDash":false},{"StartTime":184335.0,"Position":482.0,"HyperDash":false},{"StartTime":184421.0,"Position":321.0,"HyperDash":false},{"StartTime":184507.0,"Position":474.0,"HyperDash":false},{"StartTime":184593.0,"Position":252.0,"HyperDash":false},{"StartTime":184679.0,"Position":247.0,"HyperDash":false},{"StartTime":184765.0,"Position":406.0,"HyperDash":false},{"StartTime":184851.0,"Position":319.0,"HyperDash":false},{"StartTime":184937.0,"Position":253.0,"HyperDash":false},{"StartTime":185023.0,"Position":411.0,"HyperDash":false},{"StartTime":185109.0,"Position":205.0,"HyperDash":false},{"StartTime":185195.0,"Position":54.0,"HyperDash":false},{"StartTime":185281.0,"Position":224.0,"HyperDash":false},{"StartTime":185367.0,"Position":465.0,"HyperDash":false},{"StartTime":185453.0,"Position":432.0,"HyperDash":false},{"StartTime":185539.0,"Position":108.0,"HyperDash":false},{"StartTime":185625.0,"Position":95.0,"HyperDash":false}]},{"StartTime":186125.0,"Objects":[{"StartTime":186125.0,"Position":48.0,"HyperDash":false},{"StartTime":186187.0,"Position":89.47744,"HyperDash":false},{"StartTime":186250.0,"Position":93.06244,"HyperDash":false},{"StartTime":186312.0,"Position":160.382751,"HyperDash":false},{"StartTime":186375.0,"Position":190.718857,"HyperDash":false},{"StartTime":186437.0,"Position":209.265518,"HyperDash":false},{"StartTime":186500.0,"Position":273.188416,"HyperDash":false},{"StartTime":186562.0,"Position":294.6259,"HyperDash":false},{"StartTime":186625.0,"Position":321.75354,"HyperDash":false},{"StartTime":186678.0,"Position":352.728241,"HyperDash":false},{"StartTime":186732.0,"Position":377.1885,"HyperDash":false},{"StartTime":186785.0,"Position":409.4063,"HyperDash":false},{"StartTime":186874.0,"Position":463.955,"HyperDash":false}]},{"StartTime":187125.0,"Objects":[{"StartTime":187125.0,"Position":328.0,"HyperDash":false},{"StartTime":187178.0,"Position":313.795776,"HyperDash":false},{"StartTime":187232.0,"Position":325.474457,"HyperDash":false},{"StartTime":187285.0,"Position":313.270233,"HyperDash":false},{"StartTime":187375.0,"Position":298.734741,"HyperDash":false}]},{"StartTime":187625.0,"Objects":[{"StartTime":187625.0,"Position":184.0,"HyperDash":false},{"StartTime":187678.0,"Position":198.204239,"HyperDash":false},{"StartTime":187732.0,"Position":213.525543,"HyperDash":false},{"StartTime":187785.0,"Position":188.729767,"HyperDash":false},{"StartTime":187875.0,"Position":213.265274,"HyperDash":false}]},{"StartTime":188125.0,"Objects":[{"StartTime":188125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":188250.0,"Objects":[{"StartTime":188250.0,"Position":175.0,"HyperDash":false},{"StartTime":188335.0,"Position":48.0,"HyperDash":false},{"StartTime":188421.0,"Position":307.0,"HyperDash":false},{"StartTime":188507.0,"Position":375.0,"HyperDash":false},{"StartTime":188593.0,"Position":149.0,"HyperDash":false},{"StartTime":188679.0,"Position":250.0,"HyperDash":false},{"StartTime":188765.0,"Position":142.0,"HyperDash":false},{"StartTime":188851.0,"Position":170.0,"HyperDash":false},{"StartTime":188937.0,"Position":281.0,"HyperDash":false},{"StartTime":189023.0,"Position":444.0,"HyperDash":false},{"StartTime":189109.0,"Position":414.0,"HyperDash":false},{"StartTime":189195.0,"Position":321.0,"HyperDash":false},{"StartTime":189281.0,"Position":328.0,"HyperDash":false},{"StartTime":189367.0,"Position":32.0,"HyperDash":false},{"StartTime":189453.0,"Position":259.0,"HyperDash":false},{"StartTime":189539.0,"Position":169.0,"HyperDash":false},{"StartTime":189625.0,"Position":207.0,"HyperDash":false}]},{"StartTime":190125.0,"Objects":[{"StartTime":190125.0,"Position":464.0,"HyperDash":false},{"StartTime":190187.0,"Position":452.522552,"HyperDash":false},{"StartTime":190250.0,"Position":408.937561,"HyperDash":false},{"StartTime":190312.0,"Position":358.617249,"HyperDash":false},{"StartTime":190375.0,"Position":321.281128,"HyperDash":false},{"StartTime":190437.0,"Position":281.7345,"HyperDash":false},{"StartTime":190500.0,"Position":240.811569,"HyperDash":false},{"StartTime":190562.0,"Position":234.374115,"HyperDash":false},{"StartTime":190625.0,"Position":190.246445,"HyperDash":false},{"StartTime":190678.0,"Position":161.271759,"HyperDash":false},{"StartTime":190732.0,"Position":103.811485,"HyperDash":false},{"StartTime":190785.0,"Position":84.59368,"HyperDash":false},{"StartTime":190874.0,"Position":48.04496,"HyperDash":false}]},{"StartTime":191125.0,"Objects":[{"StartTime":191125.0,"Position":184.0,"HyperDash":false},{"StartTime":191178.0,"Position":177.204239,"HyperDash":false},{"StartTime":191232.0,"Position":205.525543,"HyperDash":false},{"StartTime":191285.0,"Position":211.729767,"HyperDash":false},{"StartTime":191375.0,"Position":213.265274,"HyperDash":false}]},{"StartTime":191625.0,"Objects":[{"StartTime":191625.0,"Position":328.0,"HyperDash":false},{"StartTime":191678.0,"Position":303.795776,"HyperDash":false},{"StartTime":191732.0,"Position":318.474457,"HyperDash":false},{"StartTime":191785.0,"Position":296.270233,"HyperDash":false},{"StartTime":191875.0,"Position":298.734741,"HyperDash":false}]},{"StartTime":192125.0,"Objects":[{"StartTime":192125.0,"Position":164.0,"HyperDash":false}]},{"StartTime":192375.0,"Objects":[{"StartTime":192375.0,"Position":28.0,"HyperDash":false}]},{"StartTime":192625.0,"Objects":[{"StartTime":192625.0,"Position":28.0,"HyperDash":false}]},{"StartTime":192875.0,"Objects":[{"StartTime":192875.0,"Position":128.0,"HyperDash":false},{"StartTime":192937.0,"Position":126.887405,"HyperDash":false},{"StartTime":193000.0,"Position":175.597244,"HyperDash":false},{"StartTime":193062.0,"Position":198.553162,"HyperDash":false},{"StartTime":193125.0,"Position":235.7683,"HyperDash":false},{"StartTime":193187.0,"Position":291.259583,"HyperDash":false},{"StartTime":193250.0,"Position":330.488678,"HyperDash":false},{"StartTime":193312.0,"Position":338.450653,"HyperDash":false},{"StartTime":193375.0,"Position":390.71225,"HyperDash":false},{"StartTime":193437.0,"Position":356.065,"HyperDash":false},{"StartTime":193500.0,"Position":315.488678,"HyperDash":false},{"StartTime":193562.0,"Position":279.894,"HyperDash":false},{"StartTime":193625.0,"Position":235.7683,"HyperDash":false},{"StartTime":193678.0,"Position":221.0309,"HyperDash":false},{"StartTime":193732.0,"Position":168.99295,"HyperDash":false},{"StartTime":193785.0,"Position":164.902176,"HyperDash":false},{"StartTime":193875.0,"Position":128.0,"HyperDash":false}]},{"StartTime":194125.0,"Objects":[{"StartTime":194125.0,"Position":276.0,"HyperDash":false},{"StartTime":194187.0,"Position":324.316467,"HyperDash":false},{"StartTime":194250.0,"Position":328.094818,"HyperDash":false},{"StartTime":194312.0,"Position":373.795776,"HyperDash":false},{"StartTime":194375.0,"Position":386.318756,"HyperDash":false},{"StartTime":194428.0,"Position":376.7576,"HyperDash":false},{"StartTime":194482.0,"Position":404.218842,"HyperDash":false},{"StartTime":194535.0,"Position":384.551483,"HyperDash":false},{"StartTime":194624.0,"Position":374.339844,"HyperDash":false}]},{"StartTime":194875.0,"Objects":[{"StartTime":194875.0,"Position":236.0,"HyperDash":false},{"StartTime":194937.0,"Position":201.752014,"HyperDash":false},{"StartTime":195000.0,"Position":162.019058,"HyperDash":false},{"StartTime":195062.0,"Position":146.331146,"HyperDash":false},{"StartTime":195125.0,"Position":125.789307,"HyperDash":false},{"StartTime":195178.0,"Position":127.304863,"HyperDash":false},{"StartTime":195232.0,"Position":133.772476,"HyperDash":false},{"StartTime":195285.0,"Position":111.34684,"HyperDash":false},{"StartTime":195375.0,"Position":137.660187,"HyperDash":false}]},{"StartTime":195625.0,"Objects":[{"StartTime":195625.0,"Position":280.0,"HyperDash":false},{"StartTime":195678.0,"Position":279.7856,"HyperDash":false},{"StartTime":195732.0,"Position":250.37854,"HyperDash":false},{"StartTime":195785.0,"Position":235.164154,"HyperDash":false},{"StartTime":195875.0,"Position":231.818985,"HyperDash":false}]},{"StartTime":196125.0,"Objects":[{"StartTime":196125.0,"Position":104.0,"HyperDash":false}]},{"StartTime":196375.0,"Objects":[{"StartTime":196375.0,"Position":136.0,"HyperDash":false}]},{"StartTime":196625.0,"Objects":[{"StartTime":196625.0,"Position":116.0,"HyperDash":false}]},{"StartTime":196875.0,"Objects":[{"StartTime":196875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":197000.0,"Objects":[{"StartTime":197000.0,"Position":332.0,"HyperDash":false}]},{"StartTime":197125.0,"Objects":[{"StartTime":197125.0,"Position":408.0,"HyperDash":false}]},{"StartTime":197250.0,"Objects":[{"StartTime":197250.0,"Position":392.0,"HyperDash":false}]},{"StartTime":197375.0,"Objects":[{"StartTime":197375.0,"Position":376.0,"HyperDash":false}]},{"StartTime":197625.0,"Objects":[{"StartTime":197625.0,"Position":396.0,"HyperDash":false}]},{"StartTime":197875.0,"Objects":[{"StartTime":197875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":198000.0,"Objects":[{"StartTime":198000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":198125.0,"Objects":[{"StartTime":198125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":198625.0,"Objects":[{"StartTime":198625.0,"Position":136.0,"HyperDash":false}]},{"StartTime":199125.0,"Objects":[{"StartTime":199125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":199625.0,"Objects":[{"StartTime":199625.0,"Position":376.0,"HyperDash":false}]},{"StartTime":200125.0,"Objects":[{"StartTime":200125.0,"Position":256.0,"HyperDash":false}]}]}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643.osu
new file mode 100644
index 000000000000..35ef17ae342b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643.osu
@@ -0,0 +1,582 @@
+osu file format v9
+
+[General]
+StackLeniency: 0.7
+Mode: 0
+
+[Difficulty]
+HPDrainRate:7
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.2
+SliderTickRate:2
+
+[Events]
+//Background and Video events
+//Break Periods
+2,16325,17625
+2,32325,33875
+2,66325,67375
+2,120135,127375
+//Storyboard Layer 0 (Background)
+//Storyboard Layer 1 (Fail)
+//Storyboard Layer 2 (Pass)
+//Storyboard Layer 3 (Foreground)
+//Storyboard Sound Samples
+//Background Colour Transformations
+3,100,163,162,255
+
+[TimingPoints]
+125,500,4,1,0,50,1,0
+36125,-100,4,1,0,50,0,1
+66125,-100,4,1,0,50,0,0
+88125,-100,4,1,0,50,0,1
+120125,-100,4,1,0,50,0,0
+170125,-100,4,2,0,5,0,0
+170250,-100,4,1,0,50,0,0
+172125,-100,4,1,0,50,0,1
+200125,-100,4,1,0,50,0,0
+
+[HitObjects]
+64,80,2375,5,0
+172,192,2625,1,2
+152,36,2875,1,0
+80,176,3125,1,2
+224,112,3375,1,0
+192,256,3625,1,8
+136,116,3875,1,0
+272,32,4125,2,2,B|376:0|408:56|412:125|320:144|304:176|328:216|368:272|496:208,1,400,6|0
+504,216,4875,2,2,B|376:232|288:280|248:384,1,320
+384,344,5625,1,8
+272,216,5875,1,0
+272,216,6000,1,0
+272,216,6125,1,4
+92,280,6375,5,0
+124,108,6625,1,8
+256,8,6875,1,0
+388,108,7125,1,2
+420,280,7375,1,8
+256,296,7625,1,8
+256,120,7875,1,0
+443,152,8125,2,2,B|397:202|305:219|256:192|203:163|114:181|68:231,1,400,2|0
+24,256,8875,2,2,B|112:227|141:134|122:36|37:1,1,320
+16,132,9625,1,8
+136,280,9875,1,0
+136,280,10000,1,0
+136,280,10125,1,4
+256,172,10375,5,0
+368,56,10625,1,8
+196,116,10875,1,0
+316,116,11125,1,2
+144,56,11375,1,0
+256,0,11625,1,8
+112,128,11875,1,0
+164,280,12125,6,0,B|256:316,1,80,4|2
+100,348,12500,2,0,B|8:312,1,80,0|2
+144,212,12875,2,0,B|52:176,1,80,0|2
+208,144,13250,2,0,B|300:180,1,80,0|2
+332,324,13625,1,8
+180,324,13875,1,0
+256,240,14125,5,4
+256,240,14250,1,2
+324,112,14500,1,0
+324,112,14625,1,2
+192,56,14875,1,4
+192,56,15000,1,2
+256,164,15250,1,0
+256,164,15375,1,2
+256,20,15625,1,8
+120,56,15875,1,0
+256,92,16125,1,6
+20,152,18375,5,0
+180,136,18625,1,8
+52,228,18875,1,0
+120,84,19125,1,2
+128,244,19375,1,0
+48,84,19625,1,8
+192,212,19875,1,0
+300,72,20125,2,4,B|396:36|444:84|396:144|352:184|372:224|416:260|532:224|528:164,1,320,4|0
+472,40,20875,2,2,B|376:72|304:164|272:260|280:320,1,320
+404,352,21625,1,8
+432,196,21875,1,0
+432,196,22000,1,0
+432,196,22125,1,4
+296,100,22375,5,0
+168,196,22625,2,0,B|32:296,1,160,8|0
+268,212,23125,2,0,B|168:76,1,160,2|8
+252,312,23625,2,0,B|388:212,1,160,8|0
+484,96,24125,2,2,B|412:0|320:36|288:120|240:136|200:132|156:116|132:96|80:44,1,400,2|0
+72,24,24875,2,2,B|158:66|148:177|67:253|-19:210,1,320
+56,108,25625,1,8
+176,200,25875,1,0
+176,200,26000,1,0
+176,200,26125,1,4
+316,92,26375,5,0
+464,164,26625,2,0,B|394:224|412:336,1,160,2|0
+232,316,27125,2,0,B|306:256|284:144,1,160,2|8
+136,88,27625,1,8
+60,224,27875,1,0
+212,132,28125,6,0,B|256:32,1,80,4|2
+340,228,28500,2,0,B|384:128,1,80,0|2
+256,284,28875,2,0,B|212:184,1,80,4|2
+128,380,29250,2,0,B|84:280,1,80,0|2
+238,383,29625,2,0,B|406:379,1,160,8|0
+512,267,30125,5,4
+512,267,30250,1,2
+416,152,30500,1,0
+416,152,30625,1,2
+300,264,30875,1,4
+300,264,31000,1,2
+236,100,31250,1,0
+236,100,31375,1,2
+152,256,31625,1,8
+300,160,31875,1,0
+256,332,32125,1,6
+52,52,34625,5,0
+152,164,34875,1,0
+256,56,35125,1,4
+256,56,35625,1,2
+256,56,36125,2,4,B|331:63|364:136|320:224,1,160,4|0
+320,312,36625,1,8
+204,228,36875,1,0
+104,328,37125,2,2,B|24:287|44:188,1,160
+92,60,37625,1,8
+212,148,37875,1,0
+268,104,38000,1,0
+324,60,38125,2,0,B|452:184,1,160,4|0
+504,300,38625,1,8
+364,340,38875,1,0
+232,280,39125,6,2,B|150:282|69:198|105:87|179:53,2,320,2|2|6
+280,148,40375,1,0
+400,228,40625,2,0,B|520:368,1,160,8|0
+480,192,41125,1,2
+324,220,41375,1,2
+168,256,41625,1,8
+72,148,41875,1,2
+48,84,42000,1,2
+96,36,42125,2,0,B|164:108|256:44,1,160,6|0
+400,72,42625,1,2
+440,236,42875,1,2
+464,300,43000,1,2
+416,348,43125,2,0,B|348:276|256:340,1,160,6|0
+112,312,43625,1,2
+140,188,43875,1,0
+52,64,44125,5,6
+208,48,44375,1,0
+344,132,44625,1,8
+448,256,44875,2,2,B|401:321|285:337|217:242|233:163,2,320,2|2|0
+326,211,46125,2,2,B|279:146|163:130|95:225|111:304,1,320,6|0
+230,287,46875,2,2,B|277:352|393:368|461:273|445:194,1,320,6|8
+376,80,47625,1,8
+376,80,48125,6,0,B|304:128|216:96,1,160,4|0
+84,56,48625,1,8
+152,200,48875,1,0
+44,320,49125,2,0,B|121:364|204:320,1,160,4|0
+336,240,49625,5,8
+256,148,49875,1,0
+176,240,50125,1,0
+340,144,50625,1,0
+420,236,50875,1,0
+500,144,51125,1,2
+172,144,51625,1,2
+92,236,51875,1,0
+12,144,52125,6,0,B|160:48,1,160,4|0
+304,76,52625,1,8
+256,228,52875,1,0
+216,112,53125,2,0,B|364:208,1,160,2|0
+508,180,53625,1,8
+460,28,53875,1,0
+344,96,54125,1,2
+228,8,54375,1,0
+153,116,54625,1,2
+72,220,54875,1,0
+180,295,55125,1,2
+284,376,55375,1,0
+359,268,55625,1,2
+440,164,55875,1,0
+352,160,56125,6,0,B|466:294,1,160,4|0
+312,228,56625,1,8
+200,300,56875,1,0
+160,160,57125,2,0,B|46:294,1,160,4|0
+200,228,57625,1,8
+312,300,57875,1,0
+444,208,58125,2,0,B|362:164|380:56,1,160,2|0
+344,12,58500,1,0
+272,4,58625,2,0,B|232:88|120:68,1,160,2|0
+68,176,59125,2,0,B|148:220|132:328,1,160,2|0
+168,372,59500,1,0
+240,380,59625,2,0,B|280:296|392:316,1,160,2|0
+456,176,60125,5,6
+328,80,60375,1,0
+216,196,60625,1,8
+72,136,60875,2,2,B|54:209|91:305|191:336|269:306,2,320,2|2|0
+200,224,62125,2,2,B|182:150|219:54|319:23|397:53,1,320,2|0
+480,179,62875,2,2,B|499:252|462:348|362:379|284:349,1,320,2|0
+136,296,63625,2,0,B|67:220|140:136,1,160,8|0
+256,56,64125,5,6
+284,212,64375,1,0
+440,180,64625,1,8
+420,24,64875,1,0
+300,132,65125,1,6
+272,288,65375,1,0
+116,256,65625,1,8
+136,100,65875,1,0
+256,8,66125,1,4
+256,56,68125,6,0,B|298:128|244:237|123:241|74:173,1,320
+132,80,68875,2,2,B|344:328,1,320
+456,224,69625,1,8
+340,116,69875,1,0
+340,116,70000,1,0
+340,116,70125,1,4
+228,4,70375,5,0
+256,160,70625,2,0,B|186:224|88:168,1,160,2|0
+148,332,71125,2,0,B|216:396|316:340,1,160,2|8
+424,248,71625,1,8
+336,112,71875,1,0
+336,112,72000,1,0
+336,112,72125,1,4
+228,208,72375,2,0,B|139:179|144:80,1,160,0|8
+268,56,72875,2,2,B|272:164|220:272|120:308|72:308,1,320
+24,192,73625,1,8
+92,64,73875,1,0
+92,64,74000,1,0
+92,64,74125,1,4
+224,140,74375,5,0
+340,224,74625,2,0,B|412:211|428:121|363:77,1,160,2|0
+268,192,75125,2,0,B|196:205|180:295|245:339,1,160,2|0
+268,192,75625,2,0,B|104:168,1,160,8|0
+24,52,76125,6,0,B|132:40,1,80
+176,32,76375,1,2
+348,60,76625,1,2
+248,164,76875,1,2
+264,20,77125,1,2
+324,140,77375,1,2
+180,116,77625,1,2
+240,240,77875,1,0
+256,92,78125,1,4
+100,124,78375,5,0
+8,256,78625,2,0,B|64:332|176:304,1,160,8|0
+304,260,79125,2,0,B|248:184|136:212,1,160,2|0
+304,260,79625,1,8
+460,284,79875,1,2
+420,128,80125,6,0,B|332:128,1,80,4|0
+256,124,80375,1,2
+344,260,80625,1,2
+168,260,80875,1,2
+384,192,81125,1,2
+256,260,81375,1,2
+168,124,81625,1,2
+344,124,81875,1,2
+128,192,82125,1,4
+48,192,82250,6,0,B|48:84|152:52,1,160,2|0
+204,44,82625,2,0,B|204:152|308:184,1,160,2|0
+352,160,83000,2,0,B|244:160|212:264,1,160,2|0
+192,316,83375,2,0,B|84:316|52:212,1,160,2|2
+32,88,83875,1,2
+172,8,84125,1,4
+256,192,84250,12,6,86125
+256,192,86250,12,4,87125
+256,100,88125,6,2,B|308:116|368:104|404:16,1,160,6|0
+256,100,88625,1,8
+136,180,88875,1,0
+8,96,89125,2,0,B|-28:168|16:232|68:256,1,160,2|0
+164,312,89625,1,8
+288,236,89875,1,2
+288,236,90000,1,2
+288,236,90125,2,2,B|452:164,1,160,6|0
+476,32,90625,1,8
+332,104,90875,1,0
+180,104,91125,5,6
+36,32,91375,1,8
+56,164,91625,1,8
+56,164,92125,2,0,B|260:208,1,160,6|0
+84,296,92625,1,8
+220,376,92875,1,0
+320,268,93125,2,0,B|524:224,1,160,6|0
+432,80,93625,1,8
+296,152,93875,1,2
+296,152,94000,1,2
+296,152,94125,2,2,B|232:164|176:132|164:52,1,160,6|0
+216,232,94625,2,2,B|280:220|336:252|348:332,1,160,2|0
+341,304,95000,1,0
+341,304,95125,2,0,B|369:84,1,160,2|0
+171,80,95625,2,0,B|143:300,1,160,2|0
+43,358,96125,5,6
+81,219,96375,1,0
+169,332,96625,1,8
+304,272,96875,2,2,B|388:252|426:161|418:63|344:19,2,320,2|2|0
+240,144,98125,2,2,B|219:244|50:229|65:60|168:58,1,320
+240,144,98875,2,2,B|260:43|429:58|414:227|311:229,1,320,2|0
+180,292,99625,2,0,B|80:304|36:208,1,160,2|0
+48,64,100125,6,0,B|224:112,1,160,4|0
+348,52,100625,2,0,B|524:4,1,160,2|0
+504,172,101125,2,0,B|328:124,1,160,2|0
+204,184,101625,2,0,B|28:232,1,160,2|0
+49,226,102000,1,0
+49,226,102125,1,2
+256,324,102625,5,8
+384,256,102875,1,0
+256,188,103125,1,6
+256,188,103625,1,2
+128,256,103875,1,0
+256,324,104125,6,0,B|324:252|432:316,1,160,6|0
+492,168,104625,1,8
+332,188,104875,1,0
+256,60,105125,2,0,B|188:132|80:68,1,160,6|0
+20,216,105625,1,8
+180,196,105875,1,0
+368,156,106125,2,0,B|418:184|462:234|408:296,1,160,2|0
+220,80,106625,2,0,B|248:30|298:-14|360:40,1,160,2|0
+144,228,107125,2,0,B|94:200|50:150|104:88,1,160,2|0
+292,304,107625,2,0,B|264:354|214:398|152:344,1,160,2|0
+44,216,108125,6,0,B|145:221|172:132,1,160,6|0
+304,224,108625,1,8
+408,104,108875,1,0
+468,216,109125,2,0,B|367:221|340:132,1,160,6|0
+208,224,109625,1,8
+104,104,109875,1,0
+256,56,110125,2,0,B|144:180,1,160,2|0
+256,328,110625,2,0,B|368:204,1,160,2|0
+208,244,111125,2,0,B|96:368,1,160,2|0
+304,140,111625,2,0,B|416:16,1,160,2|0
+252,20,112125,5,6
+112,60,112375,1,0
+72,200,112625,1,8
+158,316,112875,2,2,B|236:321|324:259|326:152|278:89,2,320,2|2|0
+176,168,114125,2,2,B|214:236|313:276|405:220|431:145,1,320,2|0
+328,64,114875,2,2,B|259:102|219:201|275:293|350:319,1,320,2|0
+488,340,115625,2,0,B|456:172,1,160,2|0
+416,72,116125,5,6
+288,140,116375,1,0
+164,68,116625,1,8
+36,136,116875,1,0
+104,264,117125,1,6
+232,332,117375,1,0
+356,260,117625,1,8
+484,328,117875,1,0
+356,384,118125,1,6
+256,12,128125,5,4
+256,12,128250,1,2
+336,128,128500,1,0
+336,128,128625,1,2
+400,0,128875,1,0
+400,0,129000,1,2
+492,112,129250,1,0
+492,112,129375,1,2
+440,248,129625,2,2,B|272:284,1,160
+256,108,130125,5,4
+256,108,130250,1,2
+176,224,130500,1,0
+176,224,130625,1,2
+112,96,130875,1,0
+112,96,131000,1,2
+20,208,131250,1,0
+20,208,131375,1,2
+72,344,131625,2,2,B|240:380,1,160
+408,376,132125,6,0,B|512:352|584:248|592:-32|416:-48|256:-80|96:-16|56:88|8:224|88:304|144:336|184:368|256:368|256:368|328:368|368:336|424:304|504:224|456:88|416:-16|256:-80|96:-48|-80:-32|-72:248|0:352|104:376,1,2240,6|0
+256,192,135875,5,2
+256,192,136000,1,0
+256,192,136125,1,4
+136,104,136375,1,0
+132,240,136625,1,8
+133,240,136750,1,0
+256,280,137000,1,0
+255,280,137125,1,8
+256,280,137250,1,0
+256,280,137375,1,0
+380,240,137625,1,8
+376,104,137875,1,0
+256,124,138125,5,4
+256,124,138375,1,0
+144,192,138625,1,8
+144,192,138750,1,0
+256,260,139000,1,0
+256,260,139125,1,8
+256,260,139250,1,0
+256,260,139375,1,0
+368,192,139625,1,8
+256,124,139875,1,0
+256,124,140000,1,0
+256,124,140125,2,2,B|188:112|212:76|188:36|256:20,1,160,6|2
+332,128,140625,5,8
+332,128,140750,1,0
+332,256,141000,1,0
+332,256,141125,1,8
+332,256,141250,1,0
+332,256,141375,1,0
+180,256,141625,1,8
+180,128,141875,1,0
+256,56,142125,5,4
+256,56,142375,1,0
+256,160,142625,1,8
+256,160,142750,1,0
+256,264,143000,1,0
+256,264,143125,1,8
+256,264,143250,1,0
+256,264,143375,1,0
+188,352,143625,1,8
+324,352,143875,1,0
+324,352,144000,1,0
+324,352,144125,2,0,B|492:352,1,160,6|2
+392,280,144625,5,8
+392,280,144750,1,0
+324,192,145000,1,0
+324,192,145125,1,8
+324,192,145250,1,0
+324,192,145375,1,0
+188,192,145625,1,8
+120,280,145875,1,0
+256,288,146125,5,4
+256,288,146375,1,0
+256,176,146625,1,8
+256,176,146750,1,0
+176,96,147000,1,0
+176,96,147125,1,8
+176,96,147250,1,0
+176,96,147375,1,0
+256,16,147625,1,8
+336,96,147875,1,0
+336,96,148000,1,0
+336,96,148125,2,6,B|400:156|388:224|364:248,1,160,6|2
+256,272,148625,5,8
+240,264,148750,1,0
+240,180,149000,1,0
+256,172,149125,1,8
+272,164,149250,1,0
+288,156,149375,1,0
+256,64,149625,1,8
+256,64,149875,1,0
+116,180,150125,5,0
+120,200,150250,1,0
+132,224,150375,1,0
+152,236,150500,1,0
+176,240,150625,1,8
+208,240,150750,1,0
+232,236,150875,1,0
+248,216,151000,1,0
+256,192,151125,1,8
+260,168,151250,1,0
+272,144,151375,1,8
+292,132,151500,1,0
+316,128,151625,1,8
+348,128,151750,1,8
+372,132,151875,1,8
+388,152,152000,1,0
+404,184,152125,6,0,B|436:250|377:334|292:300,1,160,6|0
+108,200,152625,2,0,B|76:134|135:50|220:84,1,160,6|0
+256,192,153125,2,0,B|256:100,1,80,2|0
+256,192,153375,2,0,B|256:368,2,160,2|8|0
+360,60,154125,5,0
+360,60,154250,1,0
+360,60,154375,1,2
+256,12,154625,1,0
+256,12,154750,1,0
+256,12,154875,1,2
+154,64,155125,1,0
+154,64,155250,1,2
+155,63,155375,2,0,B|87:119|115:191|179:211|227:179,2,160,0|8|0
+163,74,156000,5,0
+163,74,156125,1,0
+163,74,156250,2,2,B|174:151|299:265|445:180|473:106,1,400,2|0
+320,80,157125,2,2,B|224:88|184:188|224:288|320:295,1,320
+348,292,157750,1,0
+380,280,157875,1,0
+404,260,158000,1,0
+412,236,158125,1,0
+412,208,158250,1,0
+404,180,158375,1,0
+264,68,158625,2,0,B|184:104,2,80,2|0|2
+164,216,159125,2,0,B|244:180,2,80,2|0|2
+56,144,159625,5,8
+64,276,159875,1,8
+64,276,160000,1,8
+64,276,160125,2,0,B|24:352,2,80,2|0|0
+128,288,160500,2,0,B|136:188,2,80,2|0|0
+192,300,160875,2,0,B|200:400,2,80,2|0|0
+240,256,161250,2,0,B|304:176,2,80,2|0|0
+284,304,161625,2,0,B|356:380,2,80,2|0|0
+328,256,162000,6,0,B|456:236,2,80,0|2|0
+308,192,162375,2,0,B|180:172,2,80,0|2|0
+340,136,162750,2,0,B|468:116,2,80,0|2|0
+284,100,163125,2,0,B|264:-28,2,80,0|2|0
+224,128,163500,2,0,B|204:256,2,80,0|2|0
+180,76,163875,6,0,B|92:52,2,80,2|0|0
+144,132,164250,2,0,B|72:184,2,80,2|0|0
+168,196,164625,2,0,B|240:248,2,80,2|0|0
+136,256,165000,2,0,B|96:340,2,80,2|0|0
+188,296,165375,2,0,B|228:380,2,80,2|0|0
+236,252,165750,1,0
+236,252,165875,1,2
+364,276,166125,6,2,B|408:176|360:156|320:168|296:176|268:132|264:112|272:76|304:52|328:40,1,240,2|0
+264,24,166625,2,2,B|308:124|260:144|220:132|196:124|168:168|164:188|172:224|204:248|228:260,1,240,2|0
+192,280,167125,1,0
+320,376,167375,1,0
+192,376,167625,1,0
+256,328,167750,1,0
+320,280,167875,1,0
+256,124,168125,1,6
+256,192,168250,12,0,170125
+256,192,171125,12,6,172125
+48,56,172375,5,0
+20,184,172625,2,0,B|16:264|92:316|152:304,1,160,8|0
+240,300,173125,1,2
+200,176,173375,1,0
+324,220,173625,2,0,B|360:220|416:258|412:338,1,160,8|0
+412,334,174000,1,0
+412,334,174125,2,0,B|456:156,1,160,6|0
+398,35,174625,2,0,B|220:-8,1,160,2|0
+245,0,175000,1,0
+245,0,175125,2,0,B|201:178,1,160,6|0
+259,299,175625,2,0,B|437:342,1,160,2|0
+424,176,176125,5,6
+272,128,176375,1,0
+116,152,176625,1,8
+173,253,176875,2,2,B|257:233|295:142|287:44|213:0,2,320,2|2|0
+28,204,178125,2,2,B|356:316,1,320
+172,360,178875,2,2,B|500:248,1,320,2|0
+384,148,179625,2,0,B|292:168|224:96|232:44,1,160,2|0
+244,93,180000,1,0
+244,93,180125,6,0,B|64:120,1,160,6|0
+100,268,180625,2,0,B|256:296,1,160,8|0
+257,296,181000,1,0
+256,296,181125,2,0,B|413:267,1,160,6|0
+426,116,181625,2,0,B|267:93,1,160,8|2
+267,93,182000,5,2
+267,93,182125,2,2,B|180:112|168:212,1,160,2|0
+140,380,182625,2,0,B|227:361|239:261,1,160,8|0
+62,169,183125,2,2,B|80:256|180:268,1,160,2|0
+348,296,183625,2,0,B|329:208|229:196,1,160,8|0
+64,172,184125,1,6
+256,192,184250,12,2,185625
+48,188,186125,6,2,B|96:108|256:108|256:192|256:276|416:276|464:196,1,480,2|0
+328,144,187125,2,0,B|296:316,1,160,2|0
+184,240,187625,2,0,B|216:68,1,160,2|0
+256,192,188125,1,6
+256,192,188250,12,2,189625
+464,188,190125,6,2,B|416:108|256:108|256:192|256:276|96:276|48:196,1,480,2|0
+184,144,191125,2,0,B|216:316,1,160,2|0
+328,240,191625,2,0,B|296:68,1,160,2|0
+164,32,192125,5,6
+28,84,192375,1,0
+28,228,192625,1,8
+128,332,192875,2,2,B|160:224|300:172|408:244,2,320,2|2|0
+276,356,194125,2,2,B|384:324|436:184|364:76,1,320
+236,28,194875,2,2,B|128:60|76:200|148:308,1,320,2|0
+280,268,195625,2,0,B|232:116,1,160,2|0
+104,52,196125,5,6
+136,192,196375,1,0
+116,344,196625,1,8
+256,312,196875,1,0
+332,312,197000,1,0
+408,332,197125,1,6
+392,264,197250,1,0
+376,192,197375,1,0
+396,40,197625,1,8
+256,72,197875,5,0
+256,72,198000,1,0
+256,72,198125,1,6
+136,192,198625,1,6
+256,312,199125,1,6
+376,192,199625,1,6
+256,192,200125,1,6
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 000000000000..a562074fe94c
--- /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 000000000000..ff641d9b0a9a
--- /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 419a846ec354..825e8c697c10 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
@@ -39,7 +39,7 @@ public void TestJuiceStream()
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/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
deleted file mode 100644
index e2fc31d86930..000000000000
--- 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 f60ae29f7781..b03fa00f76da 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -293,7 +293,7 @@ private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult
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/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index 8c179fe9a945..3f26647f86aa 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -49,7 +49,7 @@ public void TestHyperDash()
AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
- for (int i = 0; i < 9; i++)
+ for (int i = 0; i < 11; i++)
{
int count = i + 1;
AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count);
@@ -104,12 +104,22 @@ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
})
}, 1);
+ createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0, spacingAfterGroup: 400);
+ createObjects(() => new TestJuiceStream(left_x)
+ {
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(Vector2.Zero),
+ new PathControlPoint(new Vector2(0, 300))
+ })
+ }, count: 1, spacingAfterGroup: 150);
+ createObjects(() => new Fruit { X = left_x }, count: 1, spacing: 0, spacingAfterGroup: 400);
+ createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0);
+
return beatmap;
- void createObjects(Func createObject, int count = 3)
+ void createObjects(Func createObject, int count = 3, float spacing = 140, float spacingAfterGroup = 700)
{
- const float spacing = 140;
-
for (int i = 0; i < count; i++)
{
var hitObject = createObject();
@@ -117,7 +127,7 @@ void createObjects(Func createObject, int count = 3)
beatmap.HitObjects.Add(hitObject);
}
- startTime += 700;
+ startTime += spacingAfterGroup;
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.cs
new file mode 100644
index 000000000000..951f5d1ca126
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.cs
@@ -0,0 +1,72 @@
+// 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.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public partial class TestSceneOutOfBoundsObjects : TestSceneCatchPlayer
+ {
+ protected override bool Autoplay => true;
+
+ [Test]
+ public void TestNoOutOfBoundsObjects()
+ {
+ bool anyObjectOutOfBounds = false;
+
+ AddStep("reset flag", () => anyObjectOutOfBounds = false);
+
+ AddUntilStep("check for out-of-bounds objects",
+ () =>
+ {
+ anyObjectOutOfBounds |= Player.ChildrenOfType().Any(dho => dho.X < 0 || dho.X > CatchPlayfield.WIDTH);
+ return Player.ScoreProcessor.HasCompleted.Value;
+ });
+
+ AddAssert("no out of bound objects found", () => !anyObjectOutOfBounds);
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ Ruleset = ruleset,
+ },
+ HitObjects = new List
+ {
+ new Fruit { StartTime = 1000, X = -50 },
+ new Fruit { StartTime = 1200, X = CatchPlayfield.WIDTH + 50 },
+ new JuiceStream
+ {
+ StartTime = 1500,
+ X = 10,
+ Path = new SliderPath(PathType.LINEAR, new[]
+ {
+ Vector2.Zero,
+ new Vector2(-200, 0)
+ })
+ },
+ new JuiceStream
+ {
+ StartTime = 3000,
+ X = CatchPlayfield.WIDTH - 10,
+ Path = new SliderPath(PathType.LINEAR, new[]
+ {
+ Vector2.Zero,
+ new Vector2(200, 0)
+ })
+ },
+ }
+ };
+ }
+}
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 c45c85833c81..619081c754cd 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/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
index f009c10a9c42..1f05d66b86ac 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
@@ -5,6 +5,7 @@
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
@@ -38,5 +39,25 @@ public override IEnumerable GetStatistics()
}
};
}
+
+ ///
+ /// Enumerate all s, sorted by their start times.
+ ///
+ ///
+ /// If multiple objects have the same start time, the ordering is preserved (it is a stable sorting).
+ ///
+ public static IEnumerable GetPalpableObjects(IEnumerable hitObjects)
+ {
+ return hitObjects.SelectMany(selectPalpableObjects).OrderBy(h => h.StartTime);
+
+ IEnumerable selectPalpableObjects(HitObject h)
+ {
+ if (h is PalpableCatchHitObject palpable)
+ yield return palpable;
+
+ foreach (var nested in h.NestedHitObjects.OfType())
+ yield return nested;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index 02d4cdbb94d2..198f8f59c60f 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@@ -119,7 +118,11 @@ private static void applyHardRockOffset(CatchHitObject hitObject, ref float? las
float offsetPosition = hitObject.OriginalX;
double startTime = hitObject.StartTime;
- if (lastPosition == null)
+ if (lastPosition == null ||
+ // some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here.
+ // reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50
+ // todo: should be revisited and corrected later probably.
+ lastPosition == 0)
{
lastPosition = offsetPosition;
lastStartTime = startTime;
@@ -208,24 +211,9 @@ private static void applyOffset(ref float position, float amount)
private static void initialiseHyperDash(IBeatmap beatmap)
{
- List palpableObjects = new List();
-
- foreach (var currentObject in beatmap.HitObjects)
- {
- if (currentObject is Fruit fruitObject)
- palpableObjects.Add(fruitObject);
-
- if (currentObject is JuiceStream)
- {
- foreach (var juice in currentObject.NestedHitObjects)
- {
- if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet))
- palpableObjects.Add(palpableObject);
- }
- }
- }
-
- palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
+ var palpableObjects = CatchBeatmap.GetPalpableObjects(beatmap.HitObjects)
+ .Where(h => h is Fruit || (h is Droplet && h is not TinyDroplet))
+ .ToArray();
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2;
@@ -237,7 +225,7 @@ private static void initialiseHyperDash(IBeatmap beatmap)
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
- for (int i = 0; i < palpableObjects.Count - 1; i++)
+ for (int i = 0; i < palpableObjects.Length - 1; i++)
{
var currentObject = palpableObjects[i];
var nextObject = palpableObjects[i + 1];
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 013a70966380..72d1a161ddb4 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -15,6 +15,7 @@
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Catch.Skinning.Argon;
@@ -235,5 +236,17 @@ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatm
}),
};
}
+
+ ///
+ public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
+ {
+ BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
+
+ double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
+ preempt /= rate;
+ adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
+
+ return adjustedDifficulty;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs b/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs
index 149aae1cb490..596b102ac575 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs
@@ -11,9 +11,5 @@ public CatchSkinComponentLookup(CatchSkinComponents component)
: 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/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index b826c1f546aa..f12c41a4152a 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Catch.Difficulty.Skills;
using osu.Game.Rulesets.Catch.Mods;
@@ -56,13 +57,10 @@ protected override IEnumerable CreateDifficultyHitObjects(I
List objects = new List();
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
- foreach (var hitObject in beatmap.HitObjects
- .SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj })
- .Cast()
- .OrderBy(x => x.StartTime))
+ foreach (var hitObject in CatchBeatmap.GetPalpableObjects(beatmap.HitObjects))
{
// We want to only consider fruits that contribute to the combo.
- if (hitObject is BananaShower || hitObject is TinyDroplet)
+ if (hitObject is Banana || hitObject is TinyDroplet)
continue;
if (lastObject != null)
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
index 746f5713e44f..f931795ff27b 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
@@ -7,9 +7,10 @@
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Judgements;
+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;
@@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
{
+ private readonly ScoreProcessor scoreProcessor = new CatchScoreProcessor();
+
private int legacyBonusScore;
private int standardisedBonusScore;
private int combo;
@@ -60,13 +63,7 @@ public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap p
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();
@@ -74,6 +71,8 @@ public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap p
simulateHit(obj, ref attributes);
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
+ attributes.BonusScore = legacyBonusScore;
+ attributes.MaxCombo = combo;
return attributes;
}
@@ -132,7 +131,7 @@ private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attribut
if (isBonus)
{
legacyBonusScore += scoreIncrease;
- standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
+ standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
}
else
attributes.AccuracyScore += scoreIncrease;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index b30b85be2de6..55232a9598c8 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 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
{
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 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
(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 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
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 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
}
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/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
index 1e63d32c4179..6902f7817234 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Input.Events;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -17,7 +18,7 @@ public partial class BananaShowerPlacementBlueprint : CatchPlacementBlueprint HitObject.Duration > 0;
+ protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
public BananaShowerPlacementBlueprint()
{
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
index 9e50b5a80f7b..7b57dac36edc 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
@@ -4,6 +4,7 @@
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -24,7 +25,7 @@ public partial class JuiceStreamPlacementBlueprint : CatchPlacementBlueprint HitObject.Duration > 0;
+ protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
public JuiceStreamPlacementBlueprint()
{
@@ -48,7 +49,7 @@ protected override void LoadComplete()
{
base.LoadComplete();
- inputManager = GetContainingInputManager();
+ inputManager = GetContainingInputManager()!;
BeginPlacement();
}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs
index c7a41a4e22f7..71da6d5014ae 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs
@@ -13,7 +13,8 @@ public class CatchBeatmapVerifier : IBeatmapVerifier
{
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/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
index 418351e2f31e..a2784126ebb4 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
@@ -76,21 +76,38 @@ public override bool HandleFlip(Direction direction, bool flipOverOrigin)
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 =>
+ // 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();
+
+ foreach (var h in hitObjects)
{
- hitObject.StartTime = selectionEndTime - (hitObject.GetEndTime() - selectionStartTime);
+ h.StartTime = selectionEndTime - (h.GetEndTime() - selectionStartTime);
- if (hitObject is JuiceStream juiceStream)
+ 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 000000000000..d2c3df087262
--- /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 9d88c90576a8..f933b9a28f13 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
@@ -21,7 +21,6 @@ public class CatchModFloatingFruits : Mod, IApplicableToDrawableRuleset 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 17ff8afb8760..52c42dfddbdc 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -150,7 +150,7 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
- TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
+ TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
}
@@ -189,6 +189,21 @@ public void UpdateComboInformation(IHasComboInformation? lastObj)
// The half of the height of the osu! playfield.
public const float DEFAULT_LEGACY_CONVERT_Y = 192;
+ ///
+ /// Minimum preempt time at AR=10.
+ ///
+ public const double PREEMPT_MIN = 450;
+
+ ///
+ /// Median preempt time at AR=5.
+ ///
+ public const double PREEMPT_MID = 1200;
+
+ ///
+ /// Maximum preempt time at AR=0.
+ ///
+ public const double PREEMPT_MAX = 1800;
+
///
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
index 03adbce88583..9ee4a151821c 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
@@ -22,7 +22,7 @@ public DrawableBananaShower(BananaShower? s)
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 7f8c17861d93..64705f990999 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -63,7 +63,12 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
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 41ecf592769f..677b61df4765 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
@@ -22,7 +22,7 @@ public DrawableJuiceStream(JuiceStream? s)
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/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
index 4a9661f10891..ade00918ab09 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.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;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Catch.UI;
using osuTK;
using osuTK.Graphics;
@@ -70,7 +72,10 @@ private void load()
private void updateXPosition(ValueChangedEvent _)
{
- X = OriginalXBindable.Value + XOffsetBindable.Value;
+ // same as `CatchHitObject.EffectiveX`.
+ // not using that property directly to support scenarios where `HitObject` may not necessarily be present
+ // for this pooled drawable.
+ X = Math.Clamp(OriginalXBindable.Value + XOffsetBindable.Value, 0, CatchPlayfield.WIDTH);
}
protected override void OnApply()
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 000000000000..90bdb0237e1b
--- /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 90f034cf2d33..9e7e0b604237 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -10,7 +10,6 @@
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
@@ -77,6 +76,8 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
{
base.CreateNestedHitObjects(cancellationToken);
+ this.PopulateNodeSamples();
+
var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();
int nodeIndex = 0;
@@ -102,8 +103,7 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
AddNested(new TinyDroplet
{
StartTime = t + lastEvent.Value.Time,
- X = ClampToPlayfield(EffectiveX + Path.PositionAt(
- lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X),
+ X = EffectiveX + Path.PositionAt(lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
});
}
}
@@ -120,7 +120,7 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
{
Samples = dropletSamples,
StartTime = e.Time,
- X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
+ X = EffectiveX + Path.PositionAt(e.PathProgress).X,
});
break;
@@ -131,16 +131,14 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
{
Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
- X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
+ X = EffectiveX + Path.PositionAt(e.PathProgress).X,
});
break;
}
}
}
- public float EndX => ClampToPlayfield(EffectiveX + this.CurvePositionAt(1).X);
-
- public float ClampToPlayfield(float value) => Math.Clamp(value, 0, CatchPlayfield.WIDTH);
+ public float EndX => EffectiveX + this.CurvePositionAt(1).X;
[JsonIgnore]
public double Duration
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
index c3cc488941ec..b2509091fe2e 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
@@ -5,6 +5,7 @@
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 @@ public CatchHealthProcessor(double drainStartTime)
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 66c76f9b17c8..12a4182bf1af 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 @@ public partial class CatchScoreProcessor : ScoreProcessor
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;
}
+ 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;
+
+ case HitResult.LargeBonus:
+ return 200;
+ }
+
+ return base.GetBaseScoreForResult(result);
+ }
+
protected override double GetComboScoreChange(JudgementResult result)
- => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
+ {
+ 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 RankFromAccuracy(double accuracy)
+ public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results)
{
if (accuracy == accuracy_cutoff_x)
return ScoreRank.X;
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs
deleted file mode 100644
index ea8d742b1ad8..000000000000
--- 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 fb8af9bdb69b..d1ef47cf17a1 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -122,19 +122,6 @@ public CatchLegacySkinTransformer(ISkin skin)
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 11531011eece..74dfa6c1fd10 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
@@ -17,24 +17,36 @@ public partial class CatchPlayfieldAdjustmentContainer : PlayfieldAdjustmentCont
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 0c2c157d106d..dca01fc61a4e 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -112,11 +112,6 @@ public CatcherAnimationState CurrentState
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.
///
@@ -126,6 +121,7 @@ public CatcherAnimationState CurrentState
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
+ private double? lastHyperDashStartTime;
private double hyperDashModifier = 1;
private int hyperDashDirection;
private float hyperDashTargetPosition;
@@ -233,16 +229,23 @@ public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult r
// droplet doesn't affect the catcher state
if (hitObject is TinyDroplet) return;
- if (result.IsHit && hitObject.HyperDashTarget is CatchHitObject target)
+ // if a hyper fruit was already handled this frame, just go where it says to go.
+ // this special-cases some aspire maps that have doubled-up objects (one hyper, one not) at the same time instant.
+ // handling this "properly" elsewhere is impossible as there is no feasible way to ensure
+ // that the hyperfruit gets judged second (especially if it coincides with a last fruit in a juice stream).
+ if (lastHyperDashStartTime != Time.Current)
{
- double timeDifference = target.StartTime - hitObject.StartTime;
- double positionDifference = target.EffectiveX - X;
- double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
+ if (result.IsHit && hitObject.HyperDashTarget is CatchHitObject target)
+ {
+ double timeDifference = target.StartTime - hitObject.StartTime;
+ double positionDifference = target.EffectiveX - X;
+ double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
- SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX);
+ SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX);
+ }
+ else
+ SetHyperDashState();
}
- else
- SetHyperDashState();
if (result.IsHit)
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
@@ -292,6 +295,8 @@ public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
if (wasHyperDashing)
runHyperDashStateTransition(false);
+
+ lastHyperDashStartTime = null;
}
else
{
@@ -301,6 +306,8 @@ public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
if (!wasHyperDashing)
runHyperDashStateTransition(true);
+
+ lastHyperDashStartTime = Time.Current;
}
}
@@ -327,8 +334,6 @@ protected override void SkinChanged(ISkinSource skin)
skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
DEFAULT_HYPER_DASH_COLOUR;
- flipCatcherPlate = skin.GetConfig(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
-
runHyperDashStateTransition(HyperDashing);
}
@@ -340,8 +345,7 @@ protected override void Update()
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 567c288b4786..21faec56de48 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -84,7 +84,7 @@ protected override void Update()
{
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 f0a327d7acda..32ebdc11598e 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.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 @@ private void load()
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 ecce7c1b3fa8..a5138ffb3903 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 f5a49210ea11..df4930419c33 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 25335754d234..2866508a0227 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 51e07dd6c109..d51e541e95bb 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 f6a067a83148..b8dafda8b55a 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 000000000000..b40a62176c1d
--- /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 000000000000..da5ab037e5b6
--- /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/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index fbc0ed178550..127beed83e93 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.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 @@ protected override bool OnMouseMove(MouseMoveEvent e)
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 @@ public Drawable Child
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 000000000000..9765648f4492
--- /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 8e0b51dcf8ba..d88f48858230 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
@@ -186,8 +186,106 @@ public void TestDragHoldNoteSelectionVertically()
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 000000000000..b48f579ec0b6
--- /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/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
index 435d5e737e9a..609c2e895368 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
@@ -24,6 +24,7 @@ public class ManiaBeatmapConversionTest : BeatmapConversionTest base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs
index 315849f7de49..a9771a46f338 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 @@ public void TestNoDrain()
// 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/TestSceneManiaModAutoplay.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModAutoplay.cs
new file mode 100644
index 000000000000..f653f209c198
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModAutoplay.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Mods
+{
+ public partial class TestSceneManiaModAutoplay : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ [Test]
+ public void TestPerfectScoreOnShortHoldNote()
+ {
+ CreateModTest(new ModTestData
+ {
+ Autoplay = true,
+ Beatmap = new ManiaBeatmap(new StageDefinition(1))
+ {
+ HitObjects = new List
+ {
+ new HoldNote
+ {
+ StartTime = 100,
+ EndTime = 100,
+ },
+ new HoldNote
+ {
+ StartTime = 100.1,
+ EndTime = 150,
+ },
+ }
+ },
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 4
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs
index c717f03f5121..975e43ec08b0 100644
--- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using NUnit.Framework;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
@@ -25,8 +26,8 @@ public partial class TestSceneManiaModDoubleTime : ModTestScene
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
{
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
- && Player.ScoreProcessor.Accuracy.Value == 1
- && Player.ScoreProcessor.TotalScore.Value == 1_000_000,
+ && Precision.AlmostEquals(Player.ScoreProcessor.Accuracy.Value, 0.9836, 0.01)
+ && Player.ScoreProcessor.TotalScore.Value == 946_049,
Autoplay = false,
Beatmap = new Beatmap
{
@@ -53,7 +54,7 @@ public void TestHitWindowWithDoubleTime()
Mod = doubleTime,
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
&& Player.ScoreProcessor.Accuracy.Value == 1
- && Player.ScoreProcessor.TotalScore.Value == (long)(1_000_010 * doubleTime.ScoreMultiplier),
+ && Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * doubleTime.ScoreMultiplier),
Autoplay = false,
Beatmap = new Beatmap
{
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs
index 2c8c151e7fe1..962089798377 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 @@ public partial class TestSceneManiaModFadeIn : ModTestScene
{
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 204f26f15157..ae23c4573c94 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 @@ public partial class TestSceneManiaModHidden : ModTestScene
{
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/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs
index 97a6ee28f4b6..51730e2b434a 100644
--- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs
@@ -1,14 +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 NUnit.Framework;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
- public partial class TestSceneManiaModPerfect : ModPerfectTestScene
+ public partial class TestSceneManiaModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
@@ -24,5 +29,52 @@ public TestSceneManiaModPerfect()
[TestCase(false)]
[TestCase(true)]
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
+
+ [Test]
+ public void TestGreatHit() => CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModPerfect(),
+ PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Note
+ {
+ StartTime = 1000,
+ }
+ },
+ },
+ ReplayFrames = new List
+ {
+ new ManiaReplayFrame(1020, ManiaAction.Key1),
+ new ManiaReplayFrame(2000)
+ }
+ });
+
+ [Test]
+ public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModPerfect(),
+ PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HoldNote
+ {
+ StartTime = 1000,
+ EndTime = 3000,
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new ManiaReplayFrame(1000, ManiaAction.Key1),
+ new ManiaReplayFrame(2000)
+ }
+ });
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.cs
new file mode 100644
index 000000000000..619816a815fd
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.cs
@@ -0,0 +1,72 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Mods
+{
+ public partial class TestSceneManiaModSuddenDeath : ModFailConditionTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ public TestSceneManiaModSuddenDeath()
+ : base(new ManiaModSuddenDeath())
+ {
+ }
+
+ [Test]
+ public void TestGreatHit() => CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModSuddenDeath(),
+ PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Note
+ {
+ StartTime = 1000,
+ }
+ },
+ },
+ ReplayFrames = new List
+ {
+ new ManiaReplayFrame(1020, ManiaAction.Key1),
+ new ManiaReplayFrame(2000)
+ }
+ });
+
+ [Test]
+ public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModSuddenDeath(),
+ PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HoldNote
+ {
+ StartTime = 1000,
+ EndTime = 3000,
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new ManiaReplayFrame(1000, ManiaAction.Key1),
+ new ManiaReplayFrame(2000)
+ }
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162-expected-conversion.json
new file mode 100644
index 000000000000..498195126795
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162-expected-conversion.json
@@ -0,0 +1 @@
+{"Mappings":[{"RandomW":2659430625,"RandomX":3579807591,"RandomY":273326509,"RandomZ":272911513,"StartTime":1107.0,"Objects":[{"StartTime":1107.0,"EndTime":1838.0,"Column":1}]},{"RandomW":4073513076,"RandomX":272911513,"RandomY":2659430625,"RandomZ":3083761897,"StartTime":2570.0,"Objects":[{"StartTime":2570.0,"EndTime":2935.0,"Column":6}]},{"RandomW":1129971314,"RandomX":3083761897,"RandomY":4073513076,"RandomZ":3235797552,"StartTime":3302.0,"Objects":[{"StartTime":3302.0,"EndTime":3667.0,"Column":3}]},{"RandomW":315510790,"RandomX":3235797552,"RandomY":1129971314,"RandomZ":2274676672,"StartTime":4033.0,"Objects":[{"StartTime":4033.0,"EndTime":4764.0,"Column":1}]},{"RandomW":2899658679,"RandomX":2274676672,"RandomY":315510790,"RandomZ":552830901,"StartTime":5497.0,"Objects":[{"StartTime":5497.0,"EndTime":5862.0,"Column":2}]},{"RandomW":3979364583,"RandomX":552830901,"RandomY":2899658679,"RandomZ":2367584034,"StartTime":6228.0,"Objects":[{"StartTime":6228.0,"EndTime":6593.0,"Column":5}]},{"RandomW":1470933435,"RandomX":2367584034,"RandomY":3979364583,"RandomZ":1363326171,"StartTime":6960.0,"Objects":[{"StartTime":6960.0,"EndTime":7142.0,"Column":4}]},{"RandomW":695558923,"RandomX":3979364583,"RandomY":1363326171,"RandomZ":1470933435,"StartTime":7326.0,"Objects":[{"StartTime":7326.0,"EndTime":7326.0,"Column":2},{"StartTime":7326.0,"EndTime":7326.0,"Column":3}]},{"RandomW":47047112,"RandomX":1470933435,"RandomY":695558923,"RandomZ":1181573554,"StartTime":7509.0,"Objects":[{"StartTime":7509.0,"EndTime":7691.0,"Column":0}]},{"RandomW":807301467,"RandomX":695558923,"RandomY":1181573554,"RandomZ":47047112,"StartTime":7875.0,"Objects":[{"StartTime":7875.0,"EndTime":7875.0,"Column":5}]},{"RandomW":2679940725,"RandomX":47047112,"RandomY":807301467,"RandomZ":3002147176,"StartTime":8058.0,"Objects":[{"StartTime":8058.0,"EndTime":8240.0,"Column":1}]},{"RandomW":176449914,"RandomX":2679940725,"RandomY":4061321195,"RandomZ":826668123,"StartTime":8424.0,"Objects":[{"StartTime":8424.0,"EndTime":8789.0,"Column":2},{"StartTime":8424.0,"EndTime":8789.0,"Column":0}]},{"RandomW":3697485076,"RandomX":347653435,"RandomY":172035291,"RandomZ":598178640,"StartTime":8972.0,"Objects":[{"StartTime":8972.0,"EndTime":9154.0,"Column":1},{"StartTime":8972.0,"EndTime":9154.0,"Column":5}]},{"RandomW":237023934,"RandomX":172035291,"RandomY":598178640,"RandomZ":3697485076,"StartTime":9338.0,"Objects":[{"StartTime":9338.0,"EndTime":9338.0,"Column":4},{"StartTime":9338.0,"EndTime":9338.0,"Column":5}]},{"RandomW":201670773,"RandomX":598178640,"RandomY":3697485076,"RandomZ":237023934,"StartTime":9521.0,"Objects":[{"StartTime":9521.0,"EndTime":9521.0,"Column":3}]},{"RandomW":3522038595,"RandomX":237023934,"RandomY":201670773,"RandomZ":341886814,"StartTime":9887.0,"Objects":[{"StartTime":9887.0,"EndTime":10069.0,"Column":4}]},{"RandomW":3662734978,"RandomX":201670773,"RandomY":341886814,"RandomZ":3522038595,"StartTime":10253.0,"Objects":[{"StartTime":10253.0,"EndTime":10253.0,"Column":3},{"StartTime":10253.0,"EndTime":10253.0,"Column":4}]},{"RandomW":4235203413,"RandomX":341886814,"RandomY":3522038595,"RandomZ":3662734978,"StartTime":10436.0,"Objects":[{"StartTime":10436.0,"EndTime":10436.0,"Column":2},{"StartTime":10436.0,"EndTime":10436.0,"Column":3}]},{"RandomW":3996672434,"RandomX":3522038595,"RandomY":3662734978,"RandomZ":4235203413,"StartTime":10619.0,"Objects":[{"StartTime":10619.0,"EndTime":10619.0,"Column":1},{"StartTime":10619.0,"EndTime":10619.0,"Column":2}]},{"RandomW":1328405285,"RandomX":3662734978,"RandomY":4235203413,"RandomZ":3996672434,"StartTime":10802.0,"Objects":[{"StartTime":10802.0,"EndTime":10802.0,"Column":0},{"StartTime":10802.0,"EndTime":10802.0,"Column":1}]},{"RandomW":303317172,"RandomX":4235203413,"RandomY":3996672434,"RandomZ":1328405285,"StartTime":10985.0,"Objects":[{"StartTime":10985.0,"EndTime":10985.0,"Column":1},{"StartTime":10985.0,"EndTime":10985.0,"Column":2}]},{"RandomW":1854018328,"RandomX":3996672434,"RandomY":1328405285,"RandomZ":303317172,"StartTime":11167.0,"Objects":[{"StartTime":11167.0,"EndTime":11167.0,"Column":2},{"StartTime":11167.0,"EndTime":11167.0,"Column":3}]},{"RandomW":1134221963,"RandomX":1328405285,"RandomY":303317172,"RandomZ":1854018328,"StartTime":12814.0,"Objects":[{"StartTime":12814.0,"EndTime":12814.0,"Column":1}]},{"RandomW":2894789541,"RandomX":1134221963,"RandomY":1649399086,"RandomZ":3538823219,"StartTime":13180.0,"Objects":[{"StartTime":13180.0,"EndTime":13362.0,"Column":4},{"StartTime":13180.0,"EndTime":13362.0,"Column":2}]},{"RandomW":2259123626,"RandomX":2894789541,"RandomY":961618493,"RandomZ":631989916,"StartTime":13546.0,"Objects":[{"StartTime":13546.0,"EndTime":13728.0,"Column":0}]},{"RandomW":3004853499,"RandomX":2259123626,"RandomY":2097932552,"RandomZ":3455806558,"StartTime":13911.0,"Objects":[{"StartTime":13911.0,"EndTime":14093.0,"Column":4},{"StartTime":13911.0,"EndTime":14093.0,"Column":2}]},{"RandomW":1511929919,"RandomX":250420511,"RandomY":747435619,"RandomZ":973338160,"StartTime":14277.0,"Objects":[{"StartTime":14277.0,"EndTime":14277.0,"Column":5},{"StartTime":14277.0,"EndTime":14277.0,"Column":6},{"StartTime":14459.0,"EndTime":14459.0,"Column":2},{"StartTime":14459.0,"EndTime":14459.0,"Column":3},{"StartTime":14641.0,"EndTime":14641.0,"Column":3},{"StartTime":14641.0,"EndTime":14641.0,"Column":4}]},{"RandomW":1997079940,"RandomX":973338160,"RandomY":1511929919,"RandomZ":1014879110,"StartTime":14826.0,"Objects":[{"StartTime":14826.0,"EndTime":15191.0,"Column":6}]},{"RandomW":735692759,"RandomX":1997079940,"RandomY":1386139427,"RandomZ":4192918159,"StartTime":15375.0,"Objects":[{"StartTime":15375.0,"EndTime":15557.0,"Column":2}]},{"RandomW":348373517,"RandomX":1386139427,"RandomY":4192918159,"RandomZ":735692759,"StartTime":15741.0,"Objects":[{"StartTime":15741.0,"EndTime":15741.0,"Column":5},{"StartTime":15741.0,"EndTime":15741.0,"Column":6}]},{"RandomW":521239132,"RandomX":735692759,"RandomY":348373517,"RandomZ":2961240161,"StartTime":16106.0,"Objects":[{"StartTime":16106.0,"EndTime":16288.0,"Column":1}]},{"RandomW":1199465075,"RandomX":521239132,"RandomY":4195606806,"RandomZ":4039804915,"StartTime":16472.0,"Objects":[{"StartTime":16472.0,"EndTime":16654.0,"Column":6},{"StartTime":16472.0,"EndTime":16654.0,"Column":3}]},{"RandomW":3059180408,"RandomX":4039804915,"RandomY":1199465075,"RandomZ":3542692698,"StartTime":16838.0,"Objects":[{"StartTime":16838.0,"EndTime":17020.0,"Column":2}]},{"RandomW":834119344,"RandomX":302423902,"RandomY":2799635095,"RandomZ":1022775029,"StartTime":17204.0,"Objects":[{"StartTime":17204.0,"EndTime":17204.0,"Column":4},{"StartTime":17204.0,"EndTime":17204.0,"Column":5},{"StartTime":17386.0,"EndTime":17386.0,"Column":2},{"StartTime":17386.0,"EndTime":17386.0,"Column":3},{"StartTime":17568.0,"EndTime":17568.0,"Column":3},{"StartTime":17568.0,"EndTime":17568.0,"Column":4}]},{"RandomW":1236797567,"RandomX":1022775029,"RandomY":834119344,"RandomZ":393032631,"StartTime":17753.0,"Objects":[{"StartTime":17753.0,"EndTime":18118.0,"Column":4}]},{"RandomW":892840048,"RandomX":1236797567,"RandomY":3350685275,"RandomZ":1270471227,"StartTime":18302.0,"Objects":[{"StartTime":18302.0,"EndTime":18484.0,"Column":2}]},{"RandomW":3233581364,"RandomX":1270471227,"RandomY":892840048,"RandomZ":3158680921,"StartTime":18667.0,"Objects":[{"StartTime":18667.0,"EndTime":19032.0,"Column":3}]},{"RandomW":1163000602,"RandomX":892840048,"RandomY":3158680921,"RandomZ":3233581364,"StartTime":19216.0,"Objects":[{"StartTime":19216.0,"EndTime":19216.0,"Column":2}]},{"RandomW":1548989545,"RandomX":3233581364,"RandomY":1163000602,"RandomZ":3450712040,"StartTime":19399.0,"Objects":[{"StartTime":19399.0,"EndTime":19581.0,"Column":5}]},{"RandomW":313779584,"RandomX":1548989545,"RandomY":2021811198,"RandomZ":2999045855,"StartTime":19765.0,"Objects":[{"StartTime":19765.0,"EndTime":19947.0,"Column":2},{"StartTime":19765.0,"EndTime":19947.0,"Column":1}]},{"RandomW":3548572483,"RandomX":2021811198,"RandomY":2999045855,"RandomZ":313779584,"StartTime":20131.0,"Objects":[{"StartTime":20131.0,"EndTime":20131.0,"Column":6}]},{"RandomW":75459001,"RandomX":313779584,"RandomY":3548572483,"RandomZ":3094675294,"StartTime":20314.0,"Objects":[{"StartTime":20314.0,"EndTime":20496.0,"Column":0}]},{"RandomW":1299261902,"RandomX":3094675294,"RandomY":75459001,"RandomZ":2305626963,"StartTime":20680.0,"Objects":[{"StartTime":20680.0,"EndTime":21045.0,"Column":4}]},{"RandomW":2905421941,"RandomX":2305626963,"RandomY":1299261902,"RandomZ":1390453041,"StartTime":21228.0,"Objects":[{"StartTime":21228.0,"EndTime":21410.0,"Column":2}]},{"RandomW":2294300184,"RandomX":1390453041,"RandomY":2905421941,"RandomZ":1278955784,"StartTime":21594.0,"Objects":[{"StartTime":21594.0,"EndTime":22325.0,"Column":6},{"StartTime":21594.0,"EndTime":21594.0,"Column":4},{"StartTime":21959.0,"EndTime":21959.0,"Column":4},{"StartTime":22324.0,"EndTime":22324.0,"Column":4}]},{"RandomW":3749637912,"RandomX":2905421941,"RandomY":1278955784,"RandomZ":2294300184,"StartTime":22509.0,"Objects":[{"StartTime":22509.0,"EndTime":22509.0,"Column":6},{"StartTime":22509.0,"EndTime":22509.0,"Column":0}]},{"RandomW":753327495,"RandomX":458525202,"RandomY":2373004129,"RandomZ":80278569,"StartTime":22692.0,"Objects":[{"StartTime":22692.0,"EndTime":22874.0,"Column":2}]},{"RandomW":3562609217,"RandomX":753327495,"RandomY":2472396307,"RandomZ":2540952890,"StartTime":23058.0,"Objects":[{"StartTime":23058.0,"EndTime":23058.0,"Column":1},{"StartTime":23058.0,"EndTime":23058.0,"Column":5}]},{"RandomW":3562609217,"RandomX":753327495,"RandomY":2472396307,"RandomZ":2540952890,"StartTime":23241.0,"Objects":[{"StartTime":23241.0,"EndTime":23241.0,"Column":5},{"StartTime":23241.0,"EndTime":23241.0,"Column":1}]},{"RandomW":3009004844,"RandomX":2540952890,"RandomY":3562609217,"RandomZ":3460951976,"StartTime":23606.0,"Objects":[{"StartTime":23606.0,"EndTime":23971.0,"Column":2}]},{"RandomW":1524995266,"RandomX":3133817968,"RandomY":2791164538,"RandomZ":669533622,"StartTime":24155.0,"Objects":[{"StartTime":24155.0,"EndTime":24337.0,"Column":4}]},{"RandomW":2667749121,"RandomX":1524995266,"RandomY":3001332266,"RandomZ":4204965910,"StartTime":24521.0,"Objects":[{"StartTime":24521.0,"EndTime":24521.0,"Column":0},{"StartTime":24521.0,"EndTime":24521.0,"Column":6}]},{"RandomW":1230014889,"RandomX":2127937865,"RandomY":2434329733,"RandomZ":443126576,"StartTime":24704.0,"Objects":[{"StartTime":24704.0,"EndTime":25069.0,"Column":1},{"StartTime":24704.0,"EndTime":25069.0,"Column":4}]},{"RandomW":1409501366,"RandomX":2573194819,"RandomY":3480583465,"RandomZ":2580776932,"StartTime":25253.0,"Objects":[{"StartTime":25253.0,"EndTime":25253.0,"Column":0},{"StartTime":25253.0,"EndTime":25253.0,"Column":1},{"StartTime":25435.0,"EndTime":25435.0,"Column":4},{"StartTime":25435.0,"EndTime":25435.0,"Column":5},{"StartTime":25617.0,"EndTime":25617.0,"Column":1},{"StartTime":25617.0,"EndTime":25617.0,"Column":2}]},{"RandomW":864641467,"RandomX":3480583465,"RandomY":2580776932,"RandomZ":1409501366,"StartTime":25802.0,"Objects":[{"StartTime":25802.0,"EndTime":25802.0,"Column":1}]},{"RandomW":1467076310,"RandomX":2580776932,"RandomY":1409501366,"RandomZ":864641467,"StartTime":25985.0,"Objects":[{"StartTime":25985.0,"EndTime":25985.0,"Column":2}]},{"RandomW":479214438,"RandomX":864641467,"RandomY":1467076310,"RandomZ":1385729915,"StartTime":26167.0,"Objects":[{"StartTime":26167.0,"EndTime":26532.0,"Column":1}]},{"RandomW":3054605916,"RandomX":1684801014,"RandomY":3182588115,"RandomZ":734516041,"StartTime":26716.0,"Objects":[{"StartTime":26716.0,"EndTime":26716.0,"Column":4},{"StartTime":26716.0,"EndTime":26716.0,"Column":2},{"StartTime":26898.0,"EndTime":26898.0,"Column":3},{"StartTime":26898.0,"EndTime":26898.0,"Column":1},{"StartTime":27080.0,"EndTime":27080.0,"Column":2},{"StartTime":27080.0,"EndTime":27080.0,"Column":6}]},{"RandomW":2992010973,"RandomX":3182588115,"RandomY":734516041,"RandomZ":3054605916,"StartTime":27265.0,"Objects":[{"StartTime":27265.0,"EndTime":27265.0,"Column":2}]},{"RandomW":2622274732,"RandomX":734516041,"RandomY":3054605916,"RandomZ":2992010973,"StartTime":27448.0,"Objects":[{"StartTime":27448.0,"EndTime":27448.0,"Column":3}]},{"RandomW":3013455357,"RandomX":2992010973,"RandomY":2622274732,"RandomZ":2298767863,"StartTime":27631.0,"Objects":[{"StartTime":27631.0,"EndTime":27996.0,"Column":2}]},{"RandomW":2994521549,"RandomX":2622274732,"RandomY":2298767863,"RandomZ":3013455357,"StartTime":28180.0,"Objects":[{"StartTime":28180.0,"EndTime":28180.0,"Column":5}]},{"RandomW":3949426364,"RandomX":1261217522,"RandomY":3788322225,"RandomZ":3210845744,"StartTime":28363.0,"Objects":[{"StartTime":28363.0,"EndTime":28363.0,"Column":5},{"StartTime":28363.0,"EndTime":28363.0,"Column":2},{"StartTime":28545.0,"EndTime":28545.0,"Column":5},{"StartTime":28545.0,"EndTime":28545.0,"Column":2},{"StartTime":28727.0,"EndTime":28727.0,"Column":3},{"StartTime":28727.0,"EndTime":28727.0,"Column":6}]},{"RandomW":1304069042,"RandomX":3210845744,"RandomY":3949426364,"RandomZ":3310503444,"StartTime":28911.0,"Objects":[{"StartTime":28911.0,"EndTime":29276.0,"Column":4}]},{"RandomW":781934546,"RandomX":3310503444,"RandomY":1304069042,"RandomZ":4271440939,"StartTime":29460.0,"Objects":[{"StartTime":29460.0,"EndTime":29460.0,"Column":6},{"StartTime":29460.0,"EndTime":29460.0,"Column":2}]},{"RandomW":3592330498,"RandomX":781934546,"RandomY":2041503475,"RandomZ":3767559527,"StartTime":29643.0,"Objects":[{"StartTime":29643.0,"EndTime":30008.0,"Column":5},{"StartTime":29643.0,"EndTime":30008.0,"Column":4}]},{"RandomW":579808732,"RandomX":2041503475,"RandomY":3767559527,"RandomZ":3592330498,"StartTime":30192.0,"Objects":[{"StartTime":30192.0,"EndTime":30192.0,"Column":4}]},{"RandomW":769209912,"RandomX":3767559527,"RandomY":3592330498,"RandomZ":579808732,"StartTime":30375.0,"Objects":[{"StartTime":30375.0,"EndTime":30375.0,"Column":4}]},{"RandomW":1825941494,"RandomX":579808732,"RandomY":769209912,"RandomZ":1308743097,"StartTime":30558.0,"Objects":[{"StartTime":30558.0,"EndTime":30923.0,"Column":5}]},{"RandomW":1378114054,"RandomX":930055805,"RandomY":3554877022,"RandomZ":2467280262,"StartTime":31106.0,"Objects":[{"StartTime":31106.0,"EndTime":31106.0,"Column":2},{"StartTime":31106.0,"EndTime":31106.0,"Column":5},{"StartTime":31288.0,"EndTime":31288.0,"Column":4},{"StartTime":31288.0,"EndTime":31288.0,"Column":1},{"StartTime":31470.0,"EndTime":31470.0,"Column":1},{"StartTime":31470.0,"EndTime":31470.0,"Column":4}]},{"RandomW":422797905,"RandomX":3554877022,"RandomY":2467280262,"RandomZ":1378114054,"StartTime":31655.0,"Objects":[{"StartTime":31655.0,"EndTime":31655.0,"Column":1}]},{"RandomW":3538525895,"RandomX":2467280262,"RandomY":1378114054,"RandomZ":422797905,"StartTime":31838.0,"Objects":[{"StartTime":31838.0,"EndTime":31838.0,"Column":0}]},{"RandomW":1277180769,"RandomX":422797905,"RandomY":3538525895,"RandomZ":1017422489,"StartTime":32021.0,"Objects":[{"StartTime":32021.0,"EndTime":32386.0,"Column":4}]},{"RandomW":69027963,"RandomX":3464755550,"RandomY":1342331375,"RandomZ":1235978524,"StartTime":32570.0,"Objects":[{"StartTime":32570.0,"EndTime":32752.0,"Column":0}]},{"RandomW":3582265519,"RandomX":1342331375,"RandomY":1235978524,"RandomZ":69027963,"StartTime":32936.0,"Objects":[{"StartTime":32936.0,"EndTime":32936.0,"Column":2},{"StartTime":32936.0,"EndTime":32936.0,"Column":3}]},{"RandomW":2197579333,"RandomX":69027963,"RandomY":3582265519,"RandomZ":2534080209,"StartTime":33302.0,"Objects":[{"StartTime":33302.0,"EndTime":33667.0,"Column":0}]},{"RandomW":820123404,"RandomX":1816967409,"RandomY":2440103335,"RandomZ":1364041006,"StartTime":33850.0,"Objects":[{"StartTime":33850.0,"EndTime":34215.0,"Column":4},{"StartTime":33850.0,"EndTime":34215.0,"Column":2}]},{"RandomW":962636497,"RandomX":2440103335,"RandomY":1364041006,"RandomZ":820123404,"StartTime":34399.0,"Objects":[{"StartTime":34399.0,"EndTime":34399.0,"Column":3},{"StartTime":34399.0,"EndTime":34399.0,"Column":4}]},{"RandomW":539348071,"RandomX":1364041006,"RandomY":820123404,"RandomZ":962636497,"StartTime":34582.0,"Objects":[{"StartTime":34582.0,"EndTime":34582.0,"Column":4}]},{"RandomW":1036431212,"RandomX":962636497,"RandomY":539348071,"RandomZ":498893216,"StartTime":34765.0,"Objects":[{"StartTime":34765.0,"EndTime":34947.0,"Column":3}]},{"RandomW":30194727,"RandomX":539348071,"RandomY":498893216,"RandomZ":1036431212,"StartTime":35131.0,"Objects":[{"StartTime":35131.0,"EndTime":35131.0,"Column":4},{"StartTime":35131.0,"EndTime":35131.0,"Column":5}]},{"RandomW":4140580700,"RandomX":1036431212,"RandomY":30194727,"RandomZ":260312717,"StartTime":35314.0,"Objects":[{"StartTime":35314.0,"EndTime":35496.0,"Column":6}]},{"RandomW":4269364006,"RandomX":30194727,"RandomY":260312717,"RandomZ":4140580700,"StartTime":35680.0,"Objects":[{"StartTime":35680.0,"EndTime":35680.0,"Column":5},{"StartTime":35680.0,"EndTime":35680.0,"Column":6}]},{"RandomW":3052364007,"RandomX":4140580700,"RandomY":4269364006,"RandomZ":2586895690,"StartTime":35863.0,"Objects":[{"StartTime":35863.0,"EndTime":36045.0,"Column":2}]},{"RandomW":575578073,"RandomX":4269364006,"RandomY":2586895690,"RandomZ":3052364007,"StartTime":36228.0,"Objects":[{"StartTime":36228.0,"EndTime":36228.0,"Column":4}]},{"RandomW":379197653,"RandomX":2586895690,"RandomY":3052364007,"RandomZ":575578073,"StartTime":36411.0,"Objects":[{"StartTime":36411.0,"EndTime":36411.0,"Column":3}]},{"RandomW":2472409868,"RandomX":379197653,"RandomY":194885113,"RandomZ":3317367861,"StartTime":36594.0,"Objects":[{"StartTime":36594.0,"EndTime":36776.0,"Column":1}]},{"RandomW":3530386304,"RandomX":1439106306,"RandomY":3004383294,"RandomZ":2928959685,"StartTime":36960.0,"Objects":[{"StartTime":36960.0,"EndTime":36960.0,"Column":1},{"StartTime":36960.0,"EndTime":36960.0,"Column":5},{"StartTime":37142.0,"EndTime":37142.0,"Column":2},{"StartTime":37142.0,"EndTime":37142.0,"Column":6},{"StartTime":37324.0,"EndTime":37324.0,"Column":2},{"StartTime":37324.0,"EndTime":37324.0,"Column":6}]},{"RandomW":3220147162,"RandomX":3004383294,"RandomY":2928959685,"RandomZ":3530386304,"StartTime":37509.0,"Objects":[{"StartTime":37509.0,"EndTime":37509.0,"Column":2}]},{"RandomW":2530492073,"RandomX":2928959685,"RandomY":3530386304,"RandomZ":3220147162,"StartTime":37692.0,"Objects":[{"StartTime":37692.0,"EndTime":37692.0,"Column":1}]},{"RandomW":2605446910,"RandomX":3530386304,"RandomY":3220147162,"RandomZ":2530492073,"StartTime":37875.0,"Objects":[{"StartTime":37875.0,"EndTime":37875.0,"Column":2}]},{"RandomW":3786494373,"RandomX":2530492073,"RandomY":2605446910,"RandomZ":583253884,"StartTime":38058.0,"Objects":[{"StartTime":38058.0,"EndTime":38240.0,"Column":5}]},{"RandomW":1188028287,"RandomX":3601275468,"RandomY":312474208,"RandomZ":764976912,"StartTime":38424.0,"Objects":[{"StartTime":38424.0,"EndTime":38424.0,"Column":4},{"StartTime":38424.0,"EndTime":38424.0,"Column":2},{"StartTime":38606.0,"EndTime":38606.0,"Column":1},{"StartTime":38606.0,"EndTime":38606.0,"Column":5},{"StartTime":38788.0,"EndTime":38788.0,"Column":2},{"StartTime":38788.0,"EndTime":38788.0,"Column":6}]},{"RandomW":2824132752,"RandomX":312474208,"RandomY":764976912,"RandomZ":1188028287,"StartTime":38972.0,"Objects":[{"StartTime":38972.0,"EndTime":38972.0,"Column":3}]},{"RandomW":1173715712,"RandomX":764976912,"RandomY":1188028287,"RandomZ":2824132752,"StartTime":39155.0,"Objects":[{"StartTime":39155.0,"EndTime":39155.0,"Column":4}]},{"RandomW":2490370662,"RandomX":2824132752,"RandomY":1173715712,"RandomZ":2893810865,"StartTime":39338.0,"Objects":[{"StartTime":39338.0,"EndTime":39520.0,"Column":1}]},{"RandomW":1949144326,"RandomX":2893810865,"RandomY":2490370662,"RandomZ":2599342112,"StartTime":39704.0,"Objects":[{"StartTime":39704.0,"EndTime":40069.0,"Column":6}]},{"RandomW":743381221,"RandomX":2599342112,"RandomY":1949144326,"RandomZ":947390134,"StartTime":40253.0,"Objects":[{"StartTime":40253.0,"EndTime":40435.0,"Column":2}]},{"RandomW":3629226534,"RandomX":947390134,"RandomY":743381221,"RandomZ":3234636444,"StartTime":40619.0,"Objects":[{"StartTime":40619.0,"EndTime":40984.0,"Column":4}]},{"RandomW":551844396,"RandomX":743381221,"RandomY":3234636444,"RandomZ":3629226534,"StartTime":41167.0,"Objects":[{"StartTime":41167.0,"EndTime":41167.0,"Column":3}]},{"RandomW":2240897560,"RandomX":551844396,"RandomY":1949877989,"RandomZ":3510981308,"StartTime":41350.0,"Objects":[{"StartTime":41350.0,"EndTime":41532.0,"Column":4},{"StartTime":41350.0,"EndTime":41532.0,"Column":0}]},{"RandomW":874163267,"RandomX":3510981308,"RandomY":2240897560,"RandomZ":2259115420,"StartTime":41716.0,"Objects":[{"StartTime":41716.0,"EndTime":41898.0,"Column":2}]},{"RandomW":3476146382,"RandomX":2240897560,"RandomY":2259115420,"RandomZ":874163267,"StartTime":42082.0,"Objects":[{"StartTime":42082.0,"EndTime":42082.0,"Column":4}]},{"RandomW":2101943428,"RandomX":874163267,"RandomY":3476146382,"RandomZ":3250516626,"StartTime":42265.0,"Objects":[{"StartTime":42265.0,"EndTime":42447.0,"Column":6}]},{"RandomW":2630934490,"RandomX":3476146382,"RandomY":3250516626,"RandomZ":2101943428,"StartTime":42631.0,"Objects":[{"StartTime":42631.0,"EndTime":42631.0,"Column":4}]},{"RandomW":3294029476,"RandomX":3722838838,"RandomY":3959050362,"RandomZ":3731020989,"StartTime":42814.0,"Objects":[{"StartTime":42814.0,"EndTime":42814.0,"Column":6},{"StartTime":42814.0,"EndTime":42814.0,"Column":4},{"StartTime":42996.0,"EndTime":42996.0,"Column":5},{"StartTime":42996.0,"EndTime":42996.0,"Column":3},{"StartTime":43178.0,"EndTime":43178.0,"Column":5},{"StartTime":43178.0,"EndTime":43178.0,"Column":3}]},{"RandomW":692368043,"RandomX":3959050362,"RandomY":3731020989,"RandomZ":3294029476,"StartTime":43363.0,"Objects":[{"StartTime":43363.0,"EndTime":43363.0,"Column":5}]},{"RandomW":268717689,"RandomX":3731020989,"RandomY":3294029476,"RandomZ":692368043,"StartTime":43546.0,"Objects":[{"StartTime":43546.0,"EndTime":43546.0,"Column":4}]},{"RandomW":3628859376,"RandomX":3294029476,"RandomY":692368043,"RandomZ":268717689,"StartTime":43728.0,"Objects":[{"StartTime":43728.0,"EndTime":43728.0,"Column":3}]},{"RandomW":2810605489,"RandomX":268717689,"RandomY":3628859376,"RandomZ":2874884507,"StartTime":43911.0,"Objects":[{"StartTime":43911.0,"EndTime":44093.0,"Column":2}]},{"RandomW":317739913,"RandomX":2874884507,"RandomY":2810605489,"RandomZ":2512620222,"StartTime":44277.0,"Objects":[{"StartTime":44277.0,"EndTime":44459.0,"Column":1}]},{"RandomW":967116709,"RandomX":4156133369,"RandomY":2124840394,"RandomZ":3998877068,"StartTime":44643.0,"Objects":[{"StartTime":44643.0,"EndTime":44825.0,"Column":6},{"StartTime":44643.0,"EndTime":44825.0,"Column":3}]},{"RandomW":1331553411,"RandomX":3998877068,"RandomY":967116709,"RandomZ":39354671,"StartTime":45009.0,"Objects":[{"StartTime":45009.0,"EndTime":45374.0,"Column":4}]},{"RandomW":2785797100,"RandomX":1331553411,"RandomY":1897266817,"RandomZ":1620854569,"StartTime":45558.0,"Objects":[{"StartTime":45558.0,"EndTime":45923.0,"Column":5},{"StartTime":45558.0,"EndTime":45923.0,"Column":2}]},{"RandomW":114455122,"RandomX":1897266817,"RandomY":1620854569,"RandomZ":2785797100,"StartTime":46106.0,"Objects":[{"StartTime":46106.0,"EndTime":46106.0,"Column":3},{"StartTime":46106.0,"EndTime":46106.0,"Column":4}]},{"RandomW":3639436799,"RandomX":1620854569,"RandomY":2785797100,"RandomZ":114455122,"StartTime":46289.0,"Objects":[{"StartTime":46289.0,"EndTime":46289.0,"Column":4}]},{"RandomW":2239997850,"RandomX":1523242180,"RandomY":2737260786,"RandomZ":921894438,"StartTime":46472.0,"Objects":[{"StartTime":46472.0,"EndTime":46472.0,"Column":5},{"StartTime":46472.0,"EndTime":46472.0,"Column":3},{"StartTime":46654.0,"EndTime":46654.0,"Column":1},{"StartTime":46654.0,"EndTime":46654.0,"Column":5},{"StartTime":46836.0,"EndTime":46836.0,"Column":3},{"StartTime":46836.0,"EndTime":46836.0,"Column":1}]},{"RandomW":270173708,"RandomX":921894438,"RandomY":2239997850,"RandomZ":2313367322,"StartTime":47021.0,"Objects":[{"StartTime":47021.0,"EndTime":47386.0,"Column":0}]},{"RandomW":2981644775,"RandomX":2239997850,"RandomY":2313367322,"RandomZ":270173708,"StartTime":47570.0,"Objects":[{"StartTime":47570.0,"EndTime":47570.0,"Column":5}]},{"RandomW":698324797,"RandomX":2313367322,"RandomY":270173708,"RandomZ":2981644775,"StartTime":47936.0,"Objects":[{"StartTime":47936.0,"EndTime":47936.0,"Column":2}]},{"RandomW":2105158963,"RandomX":2981644775,"RandomY":698324797,"RandomZ":3113547499,"StartTime":48119.0,"Objects":[{"StartTime":48119.0,"EndTime":48667.0,"Column":6}]},{"RandomW":3675126935,"RandomX":3113547499,"RandomY":2105158963,"RandomZ":251569162,"StartTime":48850.0,"Objects":[{"StartTime":48850.0,"EndTime":49398.0,"Column":4}]},{"RandomW":1771033747,"RandomX":251569162,"RandomY":3675126935,"RandomZ":3308284595,"StartTime":49582.0,"Objects":[{"StartTime":49582.0,"EndTime":50130.0,"Column":5}]},{"RandomW":653741274,"RandomX":3308284595,"RandomY":1771033747,"RandomZ":2460676956,"StartTime":50314.0,"Objects":[{"StartTime":50314.0,"EndTime":50862.0,"Column":2}]},{"RandomW":3908591175,"RandomX":2011739264,"RandomY":2988284210,"RandomZ":772833847,"StartTime":51046.0,"Objects":[{"StartTime":51046.0,"EndTime":51594.0,"Column":6},{"StartTime":51046.0,"EndTime":51594.0,"Column":5}]},{"RandomW":782718603,"RandomX":3908591175,"RandomY":3666262892,"RandomZ":2215410951,"StartTime":51777.0,"Objects":[{"StartTime":51777.0,"EndTime":51959.0,"Column":0},{"StartTime":51777.0,"EndTime":51959.0,"Column":2}]},{"RandomW":3946166617,"RandomX":2215410951,"RandomY":782718603,"RandomZ":75972478,"StartTime":52143.0,"Objects":[{"StartTime":52143.0,"EndTime":52508.0,"Column":5}]},{"RandomW":204866941,"RandomX":782718603,"RandomY":75972478,"RandomZ":3946166617,"StartTime":52692.0,"Objects":[{"StartTime":52692.0,"EndTime":52692.0,"Column":3},{"StartTime":52692.0,"EndTime":52692.0,"Column":4}]},{"RandomW":628140489,"RandomX":3946166617,"RandomY":204866941,"RandomZ":405870974,"StartTime":52875.0,"Objects":[{"StartTime":52875.0,"EndTime":53240.0,"Column":2}]},{"RandomW":1325586396,"RandomX":628140489,"RandomY":1674126159,"RandomZ":3748192166,"StartTime":53424.0,"Objects":[{"StartTime":53424.0,"EndTime":53606.0,"Column":5},{"StartTime":53424.0,"EndTime":53606.0,"Column":4}]},{"RandomW":3311768819,"RandomX":3748192166,"RandomY":1325586396,"RandomZ":4019978516,"StartTime":53789.0,"Objects":[{"StartTime":53789.0,"EndTime":53789.0,"Column":4},{"StartTime":53789.0,"EndTime":53789.0,"Column":3}]},{"RandomW":1550448150,"RandomX":1325586396,"RandomY":4019978516,"RandomZ":3311768819,"StartTime":53972.0,"Objects":[{"StartTime":53972.0,"EndTime":53972.0,"Column":5}]},{"RandomW":169296756,"RandomX":3311768819,"RandomY":1550448150,"RandomZ":93091440,"StartTime":54155.0,"Objects":[{"StartTime":54155.0,"EndTime":54337.0,"Column":0}]},{"RandomW":2528106598,"RandomX":169296756,"RandomY":3812396233,"RandomZ":4042657790,"StartTime":54521.0,"Objects":[{"StartTime":54521.0,"EndTime":54703.0,"Column":6},{"StartTime":54521.0,"EndTime":54703.0,"Column":1}]},{"RandomW":1636289987,"RandomX":2528106598,"RandomY":638788900,"RandomZ":558809067,"StartTime":54887.0,"Objects":[{"StartTime":54887.0,"EndTime":55069.0,"Column":5}]},{"RandomW":914779004,"RandomX":558809067,"RandomY":1636289987,"RandomZ":2298692989,"StartTime":55253.0,"Objects":[{"StartTime":55253.0,"EndTime":55618.0,"Column":2}]},{"RandomW":1650670496,"RandomX":1636289987,"RandomY":2298692989,"RandomZ":914779004,"StartTime":55802.0,"Objects":[{"StartTime":55802.0,"EndTime":55802.0,"Column":2}]},{"RandomW":3497220679,"RandomX":1037372410,"RandomY":2926479760,"RandomZ":2880883370,"StartTime":55985.0,"Objects":[{"StartTime":55985.0,"EndTime":56350.0,"Column":4}]},{"RandomW":1164710248,"RandomX":2926479760,"RandomY":2880883370,"RandomZ":3497220679,"StartTime":56533.0,"Objects":[{"StartTime":56533.0,"EndTime":56533.0,"Column":5}]},{"RandomW":2188007582,"RandomX":3497220679,"RandomY":1164710248,"RandomZ":2677289564,"StartTime":56716.0,"Objects":[{"StartTime":56716.0,"EndTime":57081.0,"Column":0}]},{"RandomW":3363933174,"RandomX":1164710248,"RandomY":2677289564,"RandomZ":2188007582,"StartTime":57265.0,"Objects":[{"StartTime":57265.0,"EndTime":57265.0,"Column":3}]},{"RandomW":50721184,"RandomX":3363933174,"RandomY":3980600543,"RandomZ":3548114425,"StartTime":57448.0,"Objects":[{"StartTime":57448.0,"EndTime":57630.0,"Column":4},{"StartTime":57448.0,"EndTime":57630.0,"Column":0}]},{"RandomW":864990701,"RandomX":3548114425,"RandomY":50721184,"RandomZ":3340702733,"StartTime":57814.0,"Objects":[{"StartTime":57814.0,"EndTime":57996.0,"Column":2}]},{"RandomW":322108643,"RandomX":3340702733,"RandomY":864990701,"RandomZ":1066828352,"StartTime":58180.0,"Objects":[{"StartTime":58180.0,"EndTime":58362.0,"Column":1}]},{"RandomW":1792394322,"RandomX":1066828352,"RandomY":322108643,"RandomZ":749878772,"StartTime":58546.0,"Objects":[{"StartTime":58546.0,"EndTime":58728.0,"Column":5}]},{"RandomW":475567653,"RandomX":3789213642,"RandomY":1703666422,"RandomZ":3630902830,"StartTime":58911.0,"Objects":[{"StartTime":58911.0,"EndTime":59093.0,"Column":4},{"StartTime":58911.0,"EndTime":59093.0,"Column":1}]},{"RandomW":292381990,"RandomX":3630902830,"RandomY":475567653,"RandomZ":734768891,"StartTime":59277.0,"Objects":[{"StartTime":59277.0,"EndTime":59459.0,"Column":0}]},{"RandomW":1221027582,"RandomX":734768891,"RandomY":292381990,"RandomZ":2432050043,"StartTime":59643.0,"Objects":[{"StartTime":59643.0,"EndTime":60008.0,"Column":3}]},{"RandomW":1041081707,"RandomX":292381990,"RandomY":2432050043,"RandomZ":1221027582,"StartTime":60192.0,"Objects":[{"StartTime":60192.0,"EndTime":60192.0,"Column":0}]},{"RandomW":1144239065,"RandomX":2432050043,"RandomY":1221027582,"RandomZ":1041081707,"StartTime":60375.0,"Objects":[{"StartTime":60375.0,"EndTime":60375.0,"Column":1}]},{"RandomW":1711255007,"RandomX":1221027582,"RandomY":1041081707,"RandomZ":1144239065,"StartTime":60558.0,"Objects":[{"StartTime":60558.0,"EndTime":60558.0,"Column":2},{"StartTime":60558.0,"EndTime":60558.0,"Column":3}]},{"RandomW":377276168,"RandomX":1041081707,"RandomY":1144239065,"RandomZ":1711255007,"StartTime":60649.0,"Objects":[{"StartTime":60649.0,"EndTime":60649.0,"Column":1}]},{"RandomW":377276168,"RandomX":1041081707,"RandomY":1144239065,"RandomZ":1711255007,"StartTime":60741.0,"Objects":[{"StartTime":60741.0,"EndTime":60741.0,"Column":2}]},{"RandomW":1158225489,"RandomX":1144239065,"RandomY":1711255007,"RandomZ":377276168,"StartTime":60924.0,"Objects":[{"StartTime":60924.0,"EndTime":60924.0,"Column":3}]},{"RandomW":74717015,"RandomX":377276168,"RandomY":1158225489,"RandomZ":2625486930,"StartTime":61106.0,"Objects":[{"StartTime":61106.0,"EndTime":61288.0,"Column":0}]},{"RandomW":4106277974,"RandomX":1158225489,"RandomY":2625486930,"RandomZ":74717015,"StartTime":61472.0,"Objects":[{"StartTime":61472.0,"EndTime":61472.0,"Column":2},{"StartTime":61472.0,"EndTime":61472.0,"Column":3}]},{"RandomW":3720471658,"RandomX":4181108489,"RandomY":2335938349,"RandomZ":793896882,"StartTime":61655.0,"Objects":[{"StartTime":61655.0,"EndTime":61655.0,"Column":6},{"StartTime":61746.0,"EndTime":61746.0,"Column":0},{"StartTime":61837.0,"EndTime":61837.0,"Column":2}]},{"RandomW":3031050452,"RandomX":2441289268,"RandomY":3327554006,"RandomZ":1721397977,"StartTime":62021.0,"Objects":[{"StartTime":62021.0,"EndTime":62021.0,"Column":0},{"StartTime":62112.0,"EndTime":62112.0,"Column":3},{"StartTime":62203.0,"EndTime":62203.0,"Column":5}]},{"RandomW":1028780747,"RandomX":3327554006,"RandomY":1721397977,"RandomZ":3031050452,"StartTime":62387.0,"Objects":[{"StartTime":62387.0,"EndTime":62387.0,"Column":1}]},{"RandomW":4249178890,"RandomX":3031050452,"RandomY":1028780747,"RandomZ":1224535158,"StartTime":62570.0,"Objects":[{"StartTime":62570.0,"EndTime":62935.0,"Column":6}]},{"RandomW":407644414,"RandomX":1028780747,"RandomY":1224535158,"RandomZ":4249178890,"StartTime":63119.0,"Objects":[{"StartTime":63119.0,"EndTime":63119.0,"Column":4}]},{"RandomW":84513019,"RandomX":4249178890,"RandomY":407644414,"RandomZ":2855880342,"StartTime":63302.0,"Objects":[{"StartTime":63302.0,"EndTime":63667.0,"Column":0}]},{"RandomW":2876344117,"RandomX":2855880342,"RandomY":84513019,"RandomZ":3523432019,"StartTime":63850.0,"Objects":[{"StartTime":63850.0,"EndTime":63850.0,"Column":5},{"StartTime":63850.0,"EndTime":63850.0,"Column":2}]},{"RandomW":1247936821,"RandomX":2876344117,"RandomY":3407636795,"RandomZ":2195437291,"StartTime":64033.0,"Objects":[{"StartTime":64033.0,"EndTime":64033.0,"Column":1},{"StartTime":64033.0,"EndTime":64033.0,"Column":5}]},{"RandomW":1165002312,"RandomX":2195437291,"RandomY":1247936821,"RandomZ":1829597027,"StartTime":64216.0,"Objects":[{"StartTime":64216.0,"EndTime":64216.0,"Column":6},{"StartTime":64216.0,"EndTime":64216.0,"Column":3}]},{"RandomW":440601827,"RandomX":1247936821,"RandomY":1829597027,"RandomZ":1165002312,"StartTime":64399.0,"Objects":[{"StartTime":64399.0,"EndTime":64399.0,"Column":6},{"StartTime":64399.0,"EndTime":64399.0,"Column":0}]},{"RandomW":1174586413,"RandomX":1165002312,"RandomY":440601827,"RandomZ":1081265463,"StartTime":64582.0,"Objects":[{"StartTime":64582.0,"EndTime":64947.0,"Column":3}]},{"RandomW":1399461522,"RandomX":1174586413,"RandomY":2273396835,"RandomZ":2242340964,"StartTime":65131.0,"Objects":[{"StartTime":65131.0,"EndTime":65313.0,"Column":0},{"StartTime":65131.0,"EndTime":65313.0,"Column":4}]},{"RandomW":806141128,"RandomX":1399461522,"RandomY":52007806,"RandomZ":2388001070,"StartTime":65497.0,"Objects":[{"StartTime":65497.0,"EndTime":65862.0,"Column":2}]},{"RandomW":869393117,"RandomX":52007806,"RandomY":2388001070,"RandomZ":806141128,"StartTime":66046.0,"Objects":[{"StartTime":66046.0,"EndTime":66046.0,"Column":2}]},{"RandomW":2114055664,"RandomX":932480042,"RandomY":484530218,"RandomZ":2599754617,"StartTime":66228.0,"Objects":[{"StartTime":66228.0,"EndTime":66593.0,"Column":3},{"StartTime":66228.0,"EndTime":66593.0,"Column":1},{"StartTime":66228.0,"EndTime":66593.0,"Column":6}]},{"RandomW":4212241992,"RandomX":2599754617,"RandomY":2114055664,"RandomZ":3978789838,"StartTime":66777.0,"Objects":[{"StartTime":66777.0,"EndTime":66777.0,"Column":0},{"StartTime":66777.0,"EndTime":66777.0,"Column":6}]},{"RandomW":1778029315,"RandomX":4212241992,"RandomY":3373094016,"RandomZ":3088207420,"StartTime":66960.0,"Objects":[{"StartTime":66960.0,"EndTime":67142.0,"Column":3},{"StartTime":66960.0,"EndTime":67142.0,"Column":5}]},{"RandomW":523225986,"RandomX":3373094016,"RandomY":3088207420,"RandomZ":1778029315,"StartTime":67326.0,"Objects":[{"StartTime":67326.0,"EndTime":67326.0,"Column":2},{"StartTime":67326.0,"EndTime":67326.0,"Column":3}]},{"RandomW":2523721637,"RandomX":1778029315,"RandomY":523225986,"RandomZ":3156555187,"StartTime":67509.0,"Objects":[{"StartTime":67509.0,"EndTime":67874.0,"Column":1}]},{"RandomW":3753678213,"RandomX":2523721637,"RandomY":733156576,"RandomZ":1252112847,"StartTime":68058.0,"Objects":[{"StartTime":68058.0,"EndTime":68240.0,"Column":4},{"StartTime":68058.0,"EndTime":68240.0,"Column":5}]},{"RandomW":765988363,"RandomX":2650496303,"RandomY":3671318686,"RandomZ":3791148796,"StartTime":68424.0,"Objects":[{"StartTime":68424.0,"EndTime":68789.0,"Column":1},{"StartTime":68424.0,"EndTime":68789.0,"Column":2}]},{"RandomW":1639351583,"RandomX":1794981044,"RandomY":795866725,"RandomZ":201525954,"StartTime":68972.0,"Objects":[{"StartTime":68972.0,"EndTime":69337.0,"Column":0},{"StartTime":68972.0,"EndTime":69337.0,"Column":5}]},{"RandomW":3794603265,"RandomX":795866725,"RandomY":201525954,"RandomZ":1639351583,"StartTime":69521.0,"Objects":[{"StartTime":69521.0,"EndTime":69521.0,"Column":4},{"StartTime":69521.0,"EndTime":69521.0,"Column":5}]},{"RandomW":2799716979,"RandomX":1639351583,"RandomY":3794603265,"RandomZ":2996900863,"StartTime":69704.0,"Objects":[{"StartTime":69704.0,"EndTime":69886.0,"Column":2}]},{"RandomW":1138768260,"RandomX":2799716979,"RandomY":1940635085,"RandomZ":4184142780,"StartTime":70070.0,"Objects":[{"StartTime":70070.0,"EndTime":70252.0,"Column":6},{"StartTime":70070.0,"EndTime":70252.0,"Column":3}]},{"RandomW":3382500543,"RandomX":4184142780,"RandomY":1138768260,"RandomZ":3891744857,"StartTime":70436.0,"Objects":[{"StartTime":70436.0,"EndTime":70436.0,"Column":3},{"StartTime":70436.0,"EndTime":70436.0,"Column":4}]},{"RandomW":665559990,"RandomX":398143267,"RandomY":1440028745,"RandomZ":150863666,"StartTime":70619.0,"Objects":[{"StartTime":70619.0,"EndTime":70984.0,"Column":0},{"StartTime":70619.0,"EndTime":70984.0,"Column":2}]},{"RandomW":340592762,"RandomX":150863666,"RandomY":665559990,"RandomZ":3920056919,"StartTime":71167.0,"Objects":[{"StartTime":71167.0,"EndTime":71167.0,"Column":5},{"StartTime":71167.0,"EndTime":71167.0,"Column":1}]},{"RandomW":1518605551,"RandomX":340592762,"RandomY":4088291758,"RandomZ":2304957054,"StartTime":71350.0,"Objects":[{"StartTime":71350.0,"EndTime":71532.0,"Column":0},{"StartTime":71350.0,"EndTime":71532.0,"Column":4}]},{"RandomW":972812530,"RandomX":1518605551,"RandomY":653707549,"RandomZ":2799009660,"StartTime":71716.0,"Objects":[{"StartTime":71716.0,"EndTime":71898.0,"Column":2},{"StartTime":71716.0,"EndTime":71898.0,"Column":3}]},{"RandomW":3736044692,"RandomX":972812530,"RandomY":1134737486,"RandomZ":3549179654,"StartTime":72082.0,"Objects":[{"StartTime":72082.0,"EndTime":72264.0,"Column":4},{"StartTime":72082.0,"EndTime":72264.0,"Column":5}]},{"RandomW":2646968586,"RandomX":3695561354,"RandomY":2121039538,"RandomZ":3939713463,"StartTime":72448.0,"Objects":[{"StartTime":72448.0,"EndTime":72630.0,"Column":6},{"StartTime":72448.0,"EndTime":72630.0,"Column":1}]},{"RandomW":34357760,"RandomX":2646968586,"RandomY":1864765858,"RandomZ":1923246874,"StartTime":72814.0,"Objects":[{"StartTime":72814.0,"EndTime":72814.0,"Column":0},{"StartTime":72814.0,"EndTime":72814.0,"Column":6}]},{"RandomW":34357760,"RandomX":2646968586,"RandomY":1864765858,"RandomZ":1923246874,"StartTime":72997.0,"Objects":[{"StartTime":72997.0,"EndTime":72997.0,"Column":6},{"StartTime":72997.0,"EndTime":72997.0,"Column":0}]},{"RandomW":3006273170,"RandomX":1864765858,"RandomY":1923246874,"RandomZ":34357760,"StartTime":73363.0,"Objects":[{"StartTime":73363.0,"EndTime":73363.0,"Column":4},{"StartTime":73363.0,"EndTime":73363.0,"Column":5}]},{"RandomW":3978541447,"RandomX":3006273170,"RandomY":3972311639,"RandomZ":2371876462,"StartTime":73728.0,"Objects":[{"StartTime":73728.0,"EndTime":73728.0,"Column":2},{"StartTime":73819.0,"EndTime":73819.0,"Column":5},{"StartTime":73910.0,"EndTime":73910.0,"Column":0}]},{"RandomW":399194528,"RandomX":2371876462,"RandomY":3978541447,"RandomZ":3734283831,"StartTime":74094.0,"Objects":[{"StartTime":74094.0,"EndTime":74094.0,"Column":3},{"StartTime":74094.0,"EndTime":74094.0,"Column":1}]},{"RandomW":2883430810,"RandomX":4002716317,"RandomY":2698819798,"RandomZ":1875619237,"StartTime":74277.0,"Objects":[{"StartTime":74277.0,"EndTime":74642.0,"Column":6},{"StartTime":74277.0,"EndTime":74642.0,"Column":2}]},{"RandomW":3502984571,"RandomX":3789000206,"RandomY":2760409322,"RandomZ":2518464347,"StartTime":74826.0,"Objects":[{"StartTime":74826.0,"EndTime":74826.0,"Column":1},{"StartTime":74826.0,"EndTime":74826.0,"Column":4}]},{"RandomW":2447473462,"RandomX":1834893326,"RandomY":512459921,"RandomZ":2493625006,"StartTime":75009.0,"Objects":[{"StartTime":75009.0,"EndTime":75374.0,"Column":5},{"StartTime":75009.0,"EndTime":75374.0,"Column":0}]},{"RandomW":236980020,"RandomX":512459921,"RandomY":2493625006,"RandomZ":2447473462,"StartTime":75558.0,"Objects":[{"StartTime":75558.0,"EndTime":75558.0,"Column":4}]},{"RandomW":1338160073,"RandomX":236980020,"RandomY":1288545645,"RandomZ":3579861656,"StartTime":75741.0,"Objects":[{"StartTime":75741.0,"EndTime":75741.0,"Column":1},{"StartTime":75741.0,"EndTime":75741.0,"Column":5}]},{"RandomW":1104479394,"RandomX":1288545645,"RandomY":3579861656,"RandomZ":1338160073,"StartTime":75924.0,"Objects":[{"StartTime":75924.0,"EndTime":75924.0,"Column":6}]},{"RandomW":1611802424,"RandomX":3579861656,"RandomY":1338160073,"RandomZ":1104479394,"StartTime":76106.0,"Objects":[{"StartTime":76106.0,"EndTime":76106.0,"Column":5},{"StartTime":76106.0,"EndTime":76106.0,"Column":6}]},{"RandomW":74337788,"RandomX":1611802424,"RandomY":3077637432,"RandomZ":3984045284,"StartTime":76289.0,"Objects":[{"StartTime":76289.0,"EndTime":76654.0,"Column":0}]},{"RandomW":2589155279,"RandomX":74337788,"RandomY":4122247598,"RandomZ":3402826469,"StartTime":76838.0,"Objects":[{"StartTime":76838.0,"EndTime":77020.0,"Column":4},{"StartTime":76838.0,"EndTime":77020.0,"Column":1}]},{"RandomW":4015672441,"RandomX":2589155279,"RandomY":3961839828,"RandomZ":3184309519,"StartTime":77204.0,"Objects":[{"StartTime":77204.0,"EndTime":77569.0,"Column":3},{"StartTime":77204.0,"EndTime":77569.0,"Column":6}]},{"RandomW":605987856,"RandomX":3184309519,"RandomY":4015672441,"RandomZ":4025998202,"StartTime":77753.0,"Objects":[{"StartTime":77753.0,"EndTime":77753.0,"Column":0},{"StartTime":77753.0,"EndTime":77753.0,"Column":1}]},{"RandomW":1497070673,"RandomX":2430309501,"RandomY":1093966930,"RandomZ":2905669028,"StartTime":77936.0,"Objects":[{"StartTime":77936.0,"EndTime":78301.0,"Column":3},{"StartTime":77936.0,"EndTime":78301.0,"Column":2},{"StartTime":77936.0,"EndTime":78301.0,"Column":4}]},{"RandomW":353334135,"RandomX":1093966930,"RandomY":2905669028,"RandomZ":1497070673,"StartTime":78485.0,"Objects":[{"StartTime":78485.0,"EndTime":78485.0,"Column":1}]},{"RandomW":912971684,"RandomX":4030507912,"RandomY":3670783478,"RandomZ":1485865738,"StartTime":78667.0,"Objects":[{"StartTime":78667.0,"EndTime":78849.0,"Column":4},{"StartTime":78667.0,"EndTime":78849.0,"Column":2}]},{"RandomW":589257226,"RandomX":3670783478,"RandomY":1485865738,"RandomZ":912971684,"StartTime":79033.0,"Objects":[{"StartTime":79033.0,"EndTime":79033.0,"Column":3},{"StartTime":79033.0,"EndTime":79033.0,"Column":4}]},{"RandomW":2024304860,"RandomX":912971684,"RandomY":589257226,"RandomZ":2767994778,"StartTime":79216.0,"Objects":[{"StartTime":79216.0,"EndTime":79581.0,"Column":6}]},{"RandomW":2219601613,"RandomX":2024304860,"RandomY":404709274,"RandomZ":3238631833,"StartTime":79765.0,"Objects":[{"StartTime":79765.0,"EndTime":79947.0,"Column":3},{"StartTime":79765.0,"EndTime":79947.0,"Column":0}]},{"RandomW":3490718869,"RandomX":2219601613,"RandomY":3210330120,"RandomZ":1566096374,"StartTime":80131.0,"Objects":[{"StartTime":80131.0,"EndTime":80496.0,"Column":5},{"StartTime":80131.0,"EndTime":80496.0,"Column":4}]},{"RandomW":1189469485,"RandomX":1566096374,"RandomY":3490718869,"RandomZ":936182364,"StartTime":80680.0,"Objects":[{"StartTime":80680.0,"EndTime":81045.0,"Column":3}]},{"RandomW":3740948748,"RandomX":3490718869,"RandomY":936182364,"RandomZ":1189469485,"StartTime":81228.0,"Objects":[{"StartTime":81228.0,"EndTime":81228.0,"Column":5},{"StartTime":81228.0,"EndTime":81228.0,"Column":6}]},{"RandomW":3491747463,"RandomX":1189469485,"RandomY":3740948748,"RandomZ":2409626314,"StartTime":81411.0,"Objects":[{"StartTime":81411.0,"EndTime":81776.0,"Column":4}]},{"RandomW":3095098652,"RandomX":3740948748,"RandomY":2409626314,"RandomZ":3491747463,"StartTime":81960.0,"Objects":[{"StartTime":81960.0,"EndTime":81960.0,"Column":3},{"StartTime":81960.0,"EndTime":81960.0,"Column":4}]},{"RandomW":3024447782,"RandomX":2409626314,"RandomY":3491747463,"RandomZ":3095098652,"StartTime":82143.0,"Objects":[{"StartTime":82143.0,"EndTime":82143.0,"Column":2},{"StartTime":82143.0,"EndTime":82143.0,"Column":3}]},{"RandomW":3942236456,"RandomX":3095098652,"RandomY":3024447782,"RandomZ":3296500942,"StartTime":82326.0,"Objects":[{"StartTime":82326.0,"EndTime":82508.0,"Column":5}]},{"RandomW":912304721,"RandomX":3942236456,"RandomY":2303302398,"RandomZ":383442600,"StartTime":82692.0,"Objects":[{"StartTime":82692.0,"EndTime":82874.0,"Column":1},{"StartTime":82692.0,"EndTime":82874.0,"Column":2}]},{"RandomW":2431170151,"RandomX":3622775798,"RandomY":385908797,"RandomZ":604082862,"StartTime":83058.0,"Objects":[{"StartTime":83058.0,"EndTime":83240.0,"Column":4},{"StartTime":83058.0,"EndTime":83240.0,"Column":0}]},{"RandomW":4088921973,"RandomX":1523770388,"RandomY":1345324755,"RandomZ":2436511051,"StartTime":83424.0,"Objects":[{"StartTime":83424.0,"EndTime":83606.0,"Column":2},{"StartTime":83424.0,"EndTime":83606.0,"Column":6}]},{"RandomW":2663434012,"RandomX":3999189199,"RandomY":2928551970,"RandomZ":3800966865,"StartTime":83789.0,"Objects":[{"StartTime":83789.0,"EndTime":83971.0,"Column":5},{"StartTime":83789.0,"EndTime":83971.0,"Column":1}]},{"RandomW":183339481,"RandomX":3405481532,"RandomY":1385906264,"RandomZ":3611020052,"StartTime":84155.0,"Objects":[{"StartTime":84155.0,"EndTime":84337.0,"Column":4},{"StartTime":84155.0,"EndTime":84337.0,"Column":0}]},{"RandomW":472982750,"RandomX":1385906264,"RandomY":3611020052,"RandomZ":183339481,"StartTime":84521.0,"Objects":[{"StartTime":84521.0,"EndTime":84521.0,"Column":4}]},{"RandomW":2485141120,"RandomX":3611020052,"RandomY":183339481,"RandomZ":472982750,"StartTime":84704.0,"Objects":[{"StartTime":84704.0,"EndTime":84704.0,"Column":5}]},{"RandomW":2638881915,"RandomX":183339481,"RandomY":472982750,"RandomZ":2485141120,"StartTime":84887.0,"Objects":[{"StartTime":84887.0,"EndTime":84887.0,"Column":6},{"StartTime":84887.0,"EndTime":84887.0,"Column":0}]},{"RandomW":2991178386,"RandomX":1846348081,"RandomY":4216122958,"RandomZ":938042528,"StartTime":85070.0,"Objects":[{"StartTime":85070.0,"EndTime":85070.0,"Column":5},{"StartTime":85161.0,"EndTime":85161.0,"Column":6},{"StartTime":85252.0,"EndTime":85252.0,"Column":3}]},{"RandomW":2830634920,"RandomX":3020624235,"RandomY":682207034,"RandomZ":1410927339,"StartTime":85436.0,"Objects":[{"StartTime":85436.0,"EndTime":85436.0,"Column":4},{"StartTime":85527.0,"EndTime":85527.0,"Column":2},{"StartTime":85618.0,"EndTime":85618.0,"Column":4}]},{"RandomW":1154798493,"RandomX":682207034,"RandomY":1410927339,"RandomZ":2830634920,"StartTime":85802.0,"Objects":[{"StartTime":85802.0,"EndTime":85802.0,"Column":3}]},{"RandomW":3579754894,"RandomX":1154798493,"RandomY":555826250,"RandomZ":3186828503,"StartTime":85985.0,"Objects":[{"StartTime":85985.0,"EndTime":86167.0,"Column":4}]},{"RandomW":522156379,"RandomX":3186828503,"RandomY":3579754894,"RandomZ":938791043,"StartTime":86350.0,"Objects":[{"StartTime":86350.0,"EndTime":86532.0,"Column":1}]},{"RandomW":2327696617,"RandomX":522156379,"RandomY":1005466611,"RandomZ":459042761,"StartTime":86716.0,"Objects":[{"StartTime":86716.0,"EndTime":86898.0,"Column":0}]},{"RandomW":3698157493,"RandomX":2327696617,"RandomY":1854714180,"RandomZ":615999181,"StartTime":87082.0,"Objects":[{"StartTime":87082.0,"EndTime":87264.0,"Column":2},{"StartTime":87082.0,"EndTime":87264.0,"Column":5}]},{"RandomW":2615638464,"RandomX":3088317005,"RandomY":3005119130,"RandomZ":738255674,"StartTime":87448.0,"Objects":[{"StartTime":87448.0,"EndTime":87448.0,"Column":4},{"StartTime":87448.0,"EndTime":87448.0,"Column":1},{"StartTime":87630.0,"EndTime":87630.0,"Column":2},{"StartTime":87630.0,"EndTime":87630.0,"Column":5},{"StartTime":87812.0,"EndTime":87812.0,"Column":2},{"StartTime":87812.0,"EndTime":87812.0,"Column":5}]},{"RandomW":4236988115,"RandomX":738255674,"RandomY":2615638464,"RandomZ":3154196835,"StartTime":87997.0,"Objects":[{"StartTime":87997.0,"EndTime":88362.0,"Column":6}]},{"RandomW":3260011681,"RandomX":4236988115,"RandomY":3619257163,"RandomZ":1999646981,"StartTime":88546.0,"Objects":[{"StartTime":88546.0,"EndTime":88728.0,"Column":3}]},{"RandomW":1679091693,"RandomX":3619257163,"RandomY":1999646981,"RandomZ":3260011681,"StartTime":88911.0,"Objects":[{"StartTime":88911.0,"EndTime":88911.0,"Column":1},{"StartTime":88911.0,"EndTime":88911.0,"Column":2}]},{"RandomW":4053500035,"RandomX":2020322055,"RandomY":2384790806,"RandomZ":846406319,"StartTime":89277.0,"Objects":[{"StartTime":89277.0,"EndTime":89459.0,"Column":0},{"StartTime":89277.0,"EndTime":89459.0,"Column":6}]},{"RandomW":3656101543,"RandomX":4053500035,"RandomY":3566026276,"RandomZ":1915132950,"StartTime":89643.0,"Objects":[{"StartTime":89643.0,"EndTime":89825.0,"Column":4}]},{"RandomW":3002483376,"RandomX":1234751024,"RandomY":253242681,"RandomZ":2332173547,"StartTime":90009.0,"Objects":[{"StartTime":90009.0,"EndTime":90191.0,"Column":0},{"StartTime":90009.0,"EndTime":90191.0,"Column":2}]},{"RandomW":1769147212,"RandomX":1032909712,"RandomY":4079968510,"RandomZ":1771054860,"StartTime":90375.0,"Objects":[{"StartTime":90375.0,"EndTime":90375.0,"Column":3},{"StartTime":90375.0,"EndTime":90375.0,"Column":6},{"StartTime":90557.0,"EndTime":90557.0,"Column":6},{"StartTime":90557.0,"EndTime":90557.0,"Column":3},{"StartTime":90739.0,"EndTime":90739.0,"Column":5},{"StartTime":90739.0,"EndTime":90739.0,"Column":2}]},{"RandomW":1533402007,"RandomX":1771054860,"RandomY":1769147212,"RandomZ":3552934273,"StartTime":90924.0,"Objects":[{"StartTime":90924.0,"EndTime":91289.0,"Column":4}]},{"RandomW":1123904499,"RandomX":3552934273,"RandomY":1533402007,"RandomZ":3005562800,"StartTime":91472.0,"Objects":[{"StartTime":91472.0,"EndTime":91654.0,"Column":3}]},{"RandomW":3485521641,"RandomX":3005562800,"RandomY":1123904499,"RandomZ":3121355612,"StartTime":91838.0,"Objects":[{"StartTime":91838.0,"EndTime":92203.0,"Column":4}]},{"RandomW":1434626078,"RandomX":1123904499,"RandomY":3121355612,"RandomZ":3485521641,"StartTime":92387.0,"Objects":[{"StartTime":92387.0,"EndTime":92387.0,"Column":4}]},{"RandomW":4013632575,"RandomX":1434626078,"RandomY":4236899246,"RandomZ":646300056,"StartTime":92570.0,"Objects":[{"StartTime":92570.0,"EndTime":92752.0,"Column":2},{"StartTime":92570.0,"EndTime":92752.0,"Column":6}]},{"RandomW":471738692,"RandomX":646300056,"RandomY":4013632575,"RandomZ":2948180894,"StartTime":92936.0,"Objects":[{"StartTime":92936.0,"EndTime":93118.0,"Column":1}]},{"RandomW":1081382077,"RandomX":471738692,"RandomY":346006110,"RandomZ":586362406,"StartTime":93302.0,"Objects":[{"StartTime":93302.0,"EndTime":93302.0,"Column":1},{"StartTime":93302.0,"EndTime":93302.0,"Column":5}]},{"RandomW":1151929163,"RandomX":586362406,"RandomY":1081382077,"RandomZ":2915942910,"StartTime":93485.0,"Objects":[{"StartTime":93485.0,"EndTime":93667.0,"Column":3}]},{"RandomW":3634683246,"RandomX":1151929163,"RandomY":4287668198,"RandomZ":463810005,"StartTime":93850.0,"Objects":[{"StartTime":93850.0,"EndTime":94215.0,"Column":1},{"StartTime":93850.0,"EndTime":94215.0,"Column":4}]},{"RandomW":2941238432,"RandomX":463810005,"RandomY":3634683246,"RandomZ":3562759778,"StartTime":94399.0,"Objects":[{"StartTime":94399.0,"EndTime":94581.0,"Column":2}]},{"RandomW":1661408876,"RandomX":3562759778,"RandomY":2941238432,"RandomZ":2646009625,"StartTime":94765.0,"Objects":[{"StartTime":94765.0,"EndTime":95130.0,"Column":5}]},{"RandomW":3189251976,"RandomX":2646009625,"RandomY":1661408876,"RandomZ":1818231832,"StartTime":95314.0,"Objects":[{"StartTime":95314.0,"EndTime":95314.0,"Column":5},{"StartTime":95314.0,"EndTime":95314.0,"Column":3}]},{"RandomW":2743067846,"RandomX":3189251976,"RandomY":2495392125,"RandomZ":3478354416,"StartTime":95497.0,"Objects":[{"StartTime":95497.0,"EndTime":95497.0,"Column":0},{"StartTime":95497.0,"EndTime":95497.0,"Column":6}]},{"RandomW":2762867836,"RandomX":3722791806,"RandomY":2892228350,"RandomZ":4171994747,"StartTime":95680.0,"Objects":[{"StartTime":95680.0,"EndTime":95680.0,"Column":5},{"StartTime":95680.0,"EndTime":95680.0,"Column":3},{"StartTime":95862.0,"EndTime":95862.0,"Column":2},{"StartTime":95862.0,"EndTime":95862.0,"Column":6},{"StartTime":96044.0,"EndTime":96044.0,"Column":6},{"StartTime":96044.0,"EndTime":96044.0,"Column":4}]},{"RandomW":1153177485,"RandomX":2762867836,"RandomY":1407653164,"RandomZ":3758120376,"StartTime":96228.0,"Objects":[{"StartTime":96228.0,"EndTime":96228.0,"Column":1},{"StartTime":96228.0,"EndTime":96228.0,"Column":5}]},{"RandomW":1153177485,"RandomX":2762867836,"RandomY":1407653164,"RandomZ":3758120376,"StartTime":96411.0,"Objects":[{"StartTime":96411.0,"EndTime":96411.0,"Column":5},{"StartTime":96411.0,"EndTime":96411.0,"Column":1}]},{"RandomW":2430957186,"RandomX":1407653164,"RandomY":3758120376,"RandomZ":1153177485,"StartTime":96777.0,"Objects":[{"StartTime":96777.0,"EndTime":96777.0,"Column":3},{"StartTime":96777.0,"EndTime":96777.0,"Column":4}]},{"RandomW":4223688647,"RandomX":3758120376,"RandomY":1153177485,"RandomZ":2430957186,"StartTime":97143.0,"Objects":[{"StartTime":97143.0,"EndTime":97143.0,"Column":4},{"StartTime":97143.0,"EndTime":97143.0,"Column":5}]},{"RandomW":433008794,"RandomX":1153177485,"RandomY":2430957186,"RandomZ":4223688647,"StartTime":97509.0,"Objects":[{"StartTime":97509.0,"EndTime":97509.0,"Column":5},{"StartTime":97509.0,"EndTime":97509.0,"Column":6}]},{"RandomW":3177925713,"RandomX":2430957186,"RandomY":4223688647,"RandomZ":433008794,"StartTime":97692.0,"Objects":[{"StartTime":97692.0,"EndTime":100619.0,"Column":3}]}]}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162.osu
new file mode 100644
index 000000000000..42669b1516b0
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162.osu
@@ -0,0 +1,297 @@
+osu file format v14
+
+[General]
+StackLeniency: 0.7
+Mode: 0
+
+[Difficulty]
+HPDrainRate:5
+CircleSize:4
+OverallDifficulty:7
+ApproachRate:7.5
+SliderMultiplier:1.4
+SliderTickRate:1
+
+[Events]
+//Background and Video events
+//Break Periods
+//Storyboard Layer 0 (Background)
+//Storyboard Layer 1 (Fail)
+//Storyboard Layer 2 (Pass)
+//Storyboard Layer 3 (Foreground)
+//Storyboard Sound Samples
+
+[TimingPoints]
+1107,365.853658536585,4,2,1,50,1,0
+1107,-166.666666666667,4,2,1,50,0,0
+6960,-111.111111111111,4,2,1,50,0,0
+8424,-100,4,2,1,50,0,0
+48119,-125,4,2,1,50,0,0
+52143,-100,4,2,1,50,0,0
+62570,-100,4,2,1,60,0,1
+85985,-100,4,2,1,50,0,0
+97692,-100,4,2,1,30,0,0
+99155,-100,4,2,1,20,0,0
+100619,-100,4,2,1,5,0,0
+
+[HitObjects]
+38,247,1107,6,0,P|96:269|170:192,1,167.999994873047,2|0,0:0|0:0,0:0:0:0:
+201,128,2570,6,0,L|205:221,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0:
+242,230,3302,2,0,L|234:324,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0:
+205,343,4033,6,0,P|246:296|351:314,1,167.999994873047,2|0,0:0|0:0,0:0:0:0:
+400,368,5497,6,0,L|412:269,1,83.9999974365235,6|0,0:0|0:0,0:0:0:0:
+436,251,6228,2,0,P|425:203|408:153,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0:
+304,200,6960,6,0,P|262:186|234:181,1,62.9999980773926,6|0,0:0|0:0,0:0:0:0:
+202,179,7326,1,8,0:0:0:0:
+276,94,7509,2,0,P|313:92|353:87,1,62.9999980773926,2|0,0:0|0:0,0:0:0:0:
+398,31,7875,1,2,0:0:0:0:
+464,81,8058,2,0,L|450:150,1,62.9999980773926,2|0,0:0|0:0,0:0:0:0:
+449,230,8424,6,0,P|347:206|306:217,1,140,2|8,0:0|0:0,0:0:0:0:
+229,273,8972,2,0,P|225:339|235:361,1,70,2|0,0:0|0:0,0:0:0:0:
+304,313,9338,1,8,0:0:0:0:
+224,190,9521,1,2,0:0:0:0:
+296,45,9887,6,0,P|297:97|288:125,1,70,6|0,0:0|0:0,0:0:0:0:
+224,190,10253,1,8,0:0:0:0:
+167,118,10436,1,8,0:0:0:0:
+76,126,10619,1,8,0:0:0:0:
+39,209,10802,1,8,0:0:0:0:
+93,282,10985,1,10,0:0:0:0:
+184,280,11167,1,10,0:0:0:0:
+102,136,12814,5,2,0:0:0:0:
+102,136,13180,2,0,L|199:130,1,70,8|0,0:0|0:0,0:0:0:0:
+256,167,13546,2,0,L|339:161,1,70,8|2,0:0|0:0,0:0:0:0:
+408,201,13911,2,0,P|454:176|471:143,1,70,8|2,0:0|0:0,0:0:0:0:
+373,54,14277,6,0,L|396:137,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
+305,111,14826,2,0,L|287:274,1,140,0|2,0:0|0:0,0:0:0:0:
+262,337,15375,2,0,L|349:327,1,70,8|2,0:0|0:0,0:0:0:0:
+419,354,15741,1,8,0:0:0:0:
+477,197,16106,6,0,P|423:197|385:209,1,70,8|0,0:0|0:0,0:0:0:0:
+321,170,16472,2,0,P|278:190|253:219,1,70,8|2,0:0|0:0,0:0:0:0:
+171,213,16838,2,0,P|152:259|158:304,1,70,8|2,0:0|0:0,0:0:0:0:
+305,294,17204,6,0,L|224:278,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
+310,202,17753,2,0,L|149:214,1,140,0|2,0:0|0:0,0:0:0:0:
+84,244,18302,2,0,L|92:152,1,70,8|2,0:0|0:0,0:0:0:0:
+47,93,18667,6,0,P|78:53|176:80,1,140,6|8,0:0|0:0,0:0:0:0:
+218,130,19216,1,0,0:0:0:0:
+299,88,19399,2,0,L|387:91,1,70,8|0,0:0|0:0,0:0:0:0:
+458,106,19765,2,0,P|447:139|444:205,1,70,8|0,0:0|0:0,0:0:0:0:
+455,274,20131,5,2,0:0:0:0:
+366,292,20314,2,0,L|353:211,1,70,0|8,0:0|0:0,0:0:0:0:
+277,173,20680,2,0,L|253:342,1,140,0|2,0:0|0:0,0:0:0:0:
+322,376,21228,2,0,P|368:368|416:370,1,70,8|2,0:0|0:0,0:0:0:0:
+500,287,21594,6,0,P|427:273|362:293,2,140,6|8|8,0:0|0:0|0:0,0:0:0:0:
+496,111,22509,1,8,0:0:0:0:
+499,189,22692,2,0,L|418:191,1,70,8|2,0:0|0:0,0:0:0:0:
+344,164,23058,5,6,0:0:0:0:
+344,164,23241,1,12,0:0:0:0:
+261,326,23606,2,0,L|246:178,1,140,8|2,0:0|0:0,0:0:0:0:
+277,100,24155,2,0,P|225:99|196:109,1,70,8|2,0:0|0:0,0:0:0:0:
+165,273,24521,5,6,0:0:0:0:
+83,235,24704,2,0,L|93:81,1,140,0|0,0:0|0:0,0:0:0:0:
+21,37,25253,2,0,L|1:120,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
+110,17,25802,1,0,0:0:0:0:
+172,83,25985,5,2,0:0:0:0:
+236,19,26167,2,0,P|223:70|227:170,1,140,0|0,0:0|0:0,0:0:0:0:
+293,216,26716,2,0,P|316:165|314:134,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
+206,245,27265,1,0,0:0:0:0:
+274,305,27448,5,2,0:0:0:0:
+194,348,27631,2,0,L|363:332,1,140,0|0,0:0|0:0,0:0:0:0:
+424,336,28180,1,2,0:0:0:0:
+431,245,28363,2,0,P|381:252|354:276,2,70,0|8|0,0:0|0:0|0:0,0:0:0:0:
+509,291,28911,6,0,L|496:128,1,140,2|8,0:0|0:0,0:0:0:0:
+504,60,29460,1,0,0:0:0:0:
+417,34,29643,2,0,L|402:183,1,140,2|8,0:0|0:0,0:0:0:0:
+365,262,30192,1,0,0:0:0:0:
+295,202,30375,5,2,0:0:0:0:
+309,112,30558,2,0,P|282:172|196:176,1,140,0|0,0:0|0:0,0:0:0:0:
+148,120,31106,2,0,P|189:99|225:99,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
+129,209,31655,1,0,0:0:0:0:
+63,146,31838,5,2,0:0:0:0:
+16,67,32021,2,0,L|27:220,1,140,0|0,0:0|0:0,0:0:0:0:
+23,297,32570,2,0,P|81:286|111:290,1,70,2|0,0:0|0:0,0:0:0:0:
+173,327,32936,1,8,0:0:0:0:
+338,251,33302,6,0,P|268:254|227:199,1,140,2|8,0:0|0:0,0:0:0:0:
+203,114,33850,2,0,L|185:262,1,140,0|0,0:0|0:0,0:0:0:0:
+244,323,34399,1,8,0:0:0:0:
+334,335,34582,1,0,0:0:0:0:
+419,219,34765,6,0,L|410:304,1,70,2|0,0:0|0:0,0:0:0:0:
+338,251,35131,1,8,0:0:0:0:
+301,111,35314,2,0,L|301:190,1,70,6|0,0:0|0:0,0:0:0:0:
+383,141,35680,1,8,0:0:0:0:
+462,97,35863,2,0,P|427:64|393:54,1,70,2|0,0:0|0:0,0:0:0:0:
+321,23,36228,5,2,0:0:0:0:
+237,60,36411,1,0,0:0:0:0:
+148,38,36594,2,0,P|107:33|56:43,1,70,8|0,0:0|0:0,0:0:0:0:
+86,125,36960,2,0,P|51:125|17:117,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
+175,123,37509,1,0,0:0:0:0:
+129,201,37692,5,2,0:0:0:0:
+198,259,37875,1,0,0:0:0:0:
+205,349,38058,2,0,P|251:330|284:326,1,70,8|0,0:0|0:0,0:0:0:0:
+352,285,38424,2,0,P|361:318|357:353,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
+282,239,38972,1,0,0:0:0:0:
+362,195,39155,5,2,0:0:0:0:
+436,142,39338,2,0,P|398:115|354:112,1,70,0|8,0:0|0:0,0:0:0:0:
+286,92,39704,2,0,L|451:74,1,140,0|0,0:0|0:0,0:0:0:0:
+512,118,40253,2,0,L|494:198,1,70,8|0,0:0|0:0,0:0:0:0:
+430,297,40619,6,0,P|423:236|336:195,1,140,2|8,0:0|0:0,0:0:0:0:
+282,239,41167,1,0,0:0:0:0:
+209,184,41350,2,0,L|222:112,1,70,2|2,0:0|0:0,0:0:0:0:
+177,34,41716,2,0,P|230:26|269:38,1,70,8|0,0:0|0:0,0:0:0:0:
+307,95,42082,5,2,0:0:0:0:
+363,23,42265,2,0,L|359:114,1,70,0|8,0:0|0:0,0:0:0:0:
+360,184,42631,1,0,0:0:0:0:
+450,191,42814,2,0,P|443:145|424:119,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
+393,263,43363,1,0,0:0:0:0:
+304,242,43546,5,2,0:0:0:0:
+241,308,43728,1,0,0:0:0:0:
+167,256,43911,2,0,P|205:228|245:226,1,70,8|0,0:0|0:0,0:0:0:0:
+166,341,44277,2,0,P|118:325|90:289,1,70,2|0,0:0|0:0,0:0:0:0:
+125,177,44643,2,0,P|168:152|201:153,1,70,8|0,0:0|0:0,0:0:0:0:
+276,132,45009,6,0,L|119:105,1,140,2|8,0:0|0:0,0:0:0:0:
+52,74,45558,2,0,L|210:57,1,140,2|0,0:0|0:0,0:0:0:0:
+277,28,46106,1,8,0:0:0:0:
+349,82,46289,1,0,0:0:0:0:
+425,32,46472,6,0,L|451:110,2,70,6|2|8,0:0|0:0|0:0,0:0:0:0:
+349,82,47021,2,0,L|344:235,1,140,2|8,0:0|0:0,0:0:0:0:
+372,308,47570,1,2,0:0:0:0:
+170,324,47936,5,2,0:0:0:0:
+99,286,48119,2,0,L|112:112,1,168,2|2,0:0|0:0,0:0:0:0:
+64,48,48850,2,0,P|125:36|195:111,1,168,2|2,0:0|0:0,0:0:0:0:
+199,189,49582,6,0,L|369:166,1,168,2|2,0:0|0:0,0:0:0:0:
+413,97,50314,2,0,P|390:180|377:274,1,168,2|2,0:0|0:0,0:0:0:0:
+347,339,51046,6,0,P|424:333|463:251,1,168,2|2,0:0|0:0,0:0:0:0:
+473,175,51777,2,0,L|477:105,1,56,2|2,0:0|0:0,0:0:0:0:
+446,24,52143,6,0,P|363:22|308:82,1,140,12|2,0:0|0:0,0:0:0:0:
+282,138,52692,1,8,0:0:0:0:
+193,118,52875,2,0,L|213:281,1,140,2|8,0:0|0:0,0:0:0:0:
+225,347,53424,2,0,P|268:328|286:301,1,70,2|0,0:0|0:0,0:0:0:0:
+304,222,53789,5,2,0:0:0:0:
+385,263,53972,1,0,0:0:0:0:
+462,214,54155,2,0,P|421:185|383:179,1,70,8|0,0:0|0:0,0:0:0:0:
+322,136,54521,2,0,P|360:105|400:93,1,70,2|0,0:0|0:0,0:0:0:0:
+469,107,54887,2,0,L|483:24,1,70,8|0,0:0|0:0,0:0:0:0:
+390,22,55253,6,0,L|223:30,1,140,2|8,0:0|0:0,0:0:0:0:
+180,87,55802,1,0,0:0:0:0:
+230,162,55985,2,0,L|391:154,1,140,2|8,0:0|0:0,0:0:0:0:
+430,223,56533,1,0,0:0:0:0:
+407,311,56716,6,0,P|356:347|285:307,1,140,2|8,0:0|0:0,0:0:0:0:
+236,245,57265,1,0,0:0:0:0:
+145,237,57448,2,0,L|162:316,1,70,2|0,0:0|0:0,0:0:0:0:
+233,360,57814,6,0,P|185:349|142:350,1,70,8|0,0:0|0:0,0:0:0:0:
+11,311,58180,2,0,P|64:302|104:306,1,70,2|0,0:0|0:0,0:0:0:0:
+213,248,58546,2,0,P|162:237|130:237,1,70,8|0,0:0|0:0,0:0:0:0:
+1,194,58911,2,0,P|47:183|74:185,1,70,2|0,0:0|0:0,0:0:0:0:
+234,142,59277,2,0,P|175:129|152:128,1,70,8|0,0:0|0:0,0:0:0:0:
+12,26,59643,6,0,P|66:38|71:140,1,140,2|8,0:0|0:0,0:0:0:0:
+1,194,60192,1,0,0:0:0:0:
+84,230,60375,1,2,0:0:0:0:
+173,216,60558,1,8,0:0:0:0:
+173,216,60649,1,8,0:0:0:0:
+173,216,60741,1,8,0:0:0:0:
+263,213,60924,1,2,0:0:0:0:
+345,174,61106,6,0,P|320:144|286:130,1,70,2|0,0:0|0:0,0:0:0:0:
+200,134,61472,1,8,0:0:0:0:
+249,57,61655,2,0,L|263:12,2,35,12|8|8,0:0|0:0|0:0,0:0:0:0:
+157,64,62021,2,0,L|153:13,2,35,12|8|8,0:0|0:0|0:0,0:0:0:0:
+118,150,62387,1,2,0:0:0:0:
+101,260,62570,6,0,P|207:236|257:243,1,140,2|8,0:0|0:0,0:0:0:0:
+328,304,63119,1,0,0:0:0:0:
+434,156,63302,2,0,P|373:157|329:217,1,140,2|8,0:0|0:0,0:0:0:0:
+408,230,63850,1,2,0:0:0:0:
+483,215,64033,5,6,0:0:0:0:
+508,142,64216,1,0,0:0:0:0:
+482,69,64399,1,8,0:0:0:0:
+413,34,64582,2,0,P|336:30|256:49,1,140,0|2,0:0|0:0,0:0:0:0:
+150,97,65131,2,0,P|190:97|243:107,1,70,8|2,0:0|0:0,0:0:0:0:
+257,168,65497,6,0,L|225:323,1,140,2|8,0:0|0:0,0:0:0:0:
+155,329,66046,1,0,0:0:0:0:
+20,204,66228,2,0,P|92:202|133:271,1,140,8|8,0:0|0:0,0:0:0:0:
+56,274,66777,1,2,0:0:0:0:
+18,125,66960,6,0,L|93:119,1,70,6|0,0:0|0:0,0:0:0:0:
+162,156,67326,1,8,0:0:0:0:
+223,52,67509,2,0,L|227:219,1,140,0|2,0:0|0:0,0:0:0:0:
+266,263,68058,2,0,P|300:229|308:199,1,70,8|2,0:0|0:0,0:0:0:0:
+298,95,68424,6,0,L|458:75,1,140,6|8,0:0|0:0,0:0:0:0:
+512,164,68972,2,0,L|358:154,1,140,0|2,0:0|0:0,0:0:0:0:
+306,209,69521,1,8,0:0:0:0:
+342,334,69704,6,0,P|361:289|369:244,1,70,2|6,0:0|0:0,0:0:0:0:
+250,277,70070,2,0,P|223:228|219:186,1,70,0|8,0:0|0:0,0:0:0:0:
+272,128,70436,1,0,0:0:0:0:
+172,111,70619,2,0,L|343:97,1,140,8|8,0:0|0:0,0:0:0:0:
+385,128,71167,1,2,0:0:0:0:
+494,63,71350,6,0,L|413:54,1,70,6|0,0:0|0:0,0:0:0:0:
+385,128,71716,2,0,L|475:140,1,70,8|0,0:0|0:0,0:0:0:0:
+467,217,72082,2,0,L|386:208,1,70,8|2,0:0|0:0,0:0:0:0:
+358,282,72448,2,0,L|448:294,1,70,8|2,0:0|0:0,0:0:0:0:
+498,339,72814,5,12,0:0:0:0:
+498,339,72997,1,12,0:0:0:0:
+301,343,73363,1,8,0:0:0:0:
+211,173,73728,2,0,L|221:216,2,35,2|2|8,0:0|0:0|0:0,0:0:0:0:
+250,100,74094,1,2,0:0:0:0:
+123,92,74277,6,0,P|129:156|129:236,1,140,2|8,0:0|0:0,0:0:0:0:
+109,321,74826,1,0,0:0:0:0:
+211,173,75009,2,0,P|266:165|333:237,1,140,8|8,0:0|0:0,0:0:0:0:
+341,302,75558,1,2,0:0:0:0:
+418,272,75741,5,6,0:0:0:0:
+484,322,75924,1,0,0:0:0:0:
+407,352,76106,1,8,0:0:0:0:
+341,302,76289,2,0,L|364:147,1,140,0|2,0:0|0:0,0:0:0:0:
+269,60,76838,2,0,P|315:69|349:94,1,70,8|0,0:0|0:0,0:0:0:0:
+269,150,77204,6,0,P|228:160|114:139,1,140,2|8,0:0|0:0,0:0:0:0:
+49,80,77753,1,0,0:0:0:0:
+39,235,77936,2,0,P|103:222|160:277,1,140,8|8,0:0|0:0,0:0:0:0:
+82,297,78485,1,2,0:0:0:0:
+227,326,78667,6,0,L|233:241,1,70,4|0,0:0|0:0,0:0:0:0:
+269,150,79033,1,8,0:0:0:0:
+408,194,79216,2,0,P|359:172|271:187,1,140,0|2,0:0|0:0,0:0:0:0:
+409,281,79765,2,0,P|447:272|478:250,1,70,8|2,0:0|0:0,0:0:0:0:
+497,168,80131,6,0,L|481:332,1,140,6|8,0:0|0:0,0:0:0:0:
+389,365,80680,2,0,L|376:198,1,140,0|2,0:0|0:0,0:0:0:0:
+414,157,81228,1,8,0:0:0:0:
+229,89,81411,6,0,P|304:91|338:167,1,140,2|0,0:0|0:0,0:0:0:0:
+290,222,81960,1,8,0:0:0:0:
+211,214,82143,1,8,0:0:0:0:
+93,155,82326,2,0,P|137:143|172:150,1,70,2|2,0:0|0:0,0:0:0:0:
+235,301,82692,2,0,P|177:296|141:279,1,70,8|2,0:0|0:0,0:0:0:0:
+68,244,83058,6,0,L|72:328,1,70,6|0,0:0|0:0,0:0:0:0:
+166,292,83424,2,0,L|157:372,1,70,8|0,0:0|0:0,0:0:0:0:
+254,227,83789,2,0,L|258:310,1,70,8|2,0:0|0:0,0:0:0:0:
+345,265,84155,2,0,L|336:349,1,70,8|0,0:0|0:0,0:0:0:0:
+331,175,84521,5,2,0:0:0:0:
+416,205,84704,1,2,0:0:0:0:
+481,141,84887,1,8,0:0:0:0:
+431,64,85070,2,0,L|444:26,2,35,8|8|2,0:0|0:0|0:0,0:0:0:0:
+339,79,85436,2,0,L|341:39,2,35,8|8|8,0:0|0:0|0:0,0:0:0:0:
+256,109,85802,1,2,0:0:0:0:
+165,97,85985,6,0,P|167:150|164:187,1,70,2|0,0:0|0:0,0:0:0:0:
+117,244,86350,2,0,P|163:241|204:235,1,70,8|0,0:0|0:0,0:0:0:0:
+229,317,86716,2,0,P|273:305|300:294,1,70,8|2,0:0|0:0,0:0:0:0:
+365,354,87082,2,0,P|404:334|430:310,1,70,8|0,0:0|0:0,0:0:0:0:
+352,230,87448,6,0,L|271:216,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
+378,142,87997,2,0,L|222:144,1,140,0|2,0:0|0:0,0:0:0:0:
+152,112,88546,2,0,L|166:214,1,70,8|2,0:0|0:0,0:0:0:0:
+139,270,88911,5,8,0:0:0:0:
+12,138,89277,2,0,L|29:55,1,70,8|0,0:0|0:0,0:0:0:0:
+91,5,89643,2,0,L|104:97,1,70,8|2,0:0|0:0,0:0:0:0:
+153,149,90009,2,0,L|175:78,1,70,8|0,0:0|0:0,0:0:0:0:
+279,36,90375,6,0,L|357:27,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
+248,122,90924,2,0,L|398:125,1,140,0|2,0:0|0:0,0:0:0:0:
+479,123,91472,2,0,P|468:170|445:195,1,70,8|2,0:0|0:0,0:0:0:0:
+365,204,91838,6,0,P|414:220|409:320,1,140,6|8,0:0|0:0,0:0:0:0:
+354,354,92387,1,0,0:0:0:0:
+262,353,92570,2,0,L|271:273,1,70,8|2,0:0|0:0,0:0:0:0:
+297,196,92936,2,0,P|243:198|216:215,1,70,8|0,0:0|0:0,0:0:0:0:
+172,276,93302,5,6,0:0:0:0:
+137,360,93485,2,0,L|127:265,1,70,0|8,0:0|0:0,0:0:0:0:
+81,212,93850,2,0,P|93:138|118:67,1,140,0|2,0:0|0:0,0:0:0:0:
+170,4,94399,2,0,P|195:37|204:74,1,70,8|2,0:0|0:0,0:0:0:0:
+186,153,94765,6,0,L|340:139,1,140,6|8,0:0|0:0,0:0:0:0:
+408,101,95314,1,2,0:0:0:0:
+443,184,95497,1,6,0:0:0:0:
+369,237,95680,2,0,L|300:224,2,70,8|8|2,0:0|0:0|0:0,0:0:0:0:
+448,282,96228,5,12,0:0:0:0:
+448,282,96411,1,12,0:0:0:0:
+270,320,96777,1,8,0:0:0:0:
+313,143,97143,1,8,0:0:0:0:
+377,314,97509,1,8,0:0:0:0:
+256,192,97692,12,0,100619,0:0:0:0:
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
index ab9f57ecc3e3..a5c18babe2a0 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 @@ public partial class TestSceneBarLine : ManiaSkinnableTestScene
[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 barLines = createBarLines();
-
- foreach (var b in barLines)
- s.Add(b);
+ var maniaPlayfield = new ManiaPlayfield(stageDefinitions);
- return;
- }
-
- for (int i = 0; i < 64; i++)
+ // Must be scheduled so the pool is loaded before we try and retrieve from it.
+ Schedule(() =>
{
- 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 51c2bac6d1ec..7a0abb9e648a 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
@@ -66,7 +66,7 @@ public void TestHoldNoteHeadVisibility()
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 044ce3783224..5f299f419da8 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -200,12 +200,10 @@ public void TestPressAtStartThenReleaseAndImmediatelyRepress()
});
assertHeadJudgement(HitResult.Perfect);
- // judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
- assertComboAtJudgement(1, 1);
+ assertComboAtJudgement(0, 1);
assertTailJudgement(HitResult.Meh);
- assertComboAtJudgement(2, 0);
- // judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
- assertComboAtJudgement(4, 1);
+ assertComboAtJudgement(1, 0);
+ assertComboAtJudgement(3, 1);
}
///
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs
new file mode 100644
index 000000000000..30c0113bffa7
--- /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/TestSceneMaximumScore.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs
index edf866952b30..ee6d9999328d 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs
@@ -54,7 +54,7 @@ public void TestSimultaneousTickAndNote()
AddAssert("all objects perfectly judged",
() => judgementResults.Select(result => result.Type),
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
- AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_030));
+ AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
}
[Test]
@@ -87,7 +87,7 @@ public void TestSimultaneousLongNotes()
AddAssert("all objects perfectly judged",
() => judgementResults.Select(result => result.Type),
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
- AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040));
+ AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
}
private void performTest(List hitObjects, List frames)
diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
index 2a8dc715f92a..341d52afcf68 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
@@ -39,18 +39,18 @@ public TestScenePlayfieldCoveringContainer()
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 fee3ba3e399f..db04142915df 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -91,7 +91,7 @@ private void createNote()
{
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 @@ private void createHoldNote()
{
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 81557c198da0..b5b265792bf0 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
@@ -34,16 +34,21 @@ public partial class TestSceneTimingBasedNoteColouring : OsuTestScene
[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 b991db408c90..eee06acdb8a5 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 28cdf8907ea9..8222e5477da9 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
@@ -22,11 +22,6 @@ public class ManiaBeatmap : Beatmap
///
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 @@ public class ManiaBeatmap : Beatmap
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 ccfe1501bd2b..39ee3d209bd1 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.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 @@ public class ManiaBeatmapConverter : BeatmapConverter
///
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;
- public readonly bool IsForCurrentRuleset;
- private readonly int originalTargetColumns;
+ ///
+ /// Whether the beatmap instantiated with is for the mania ruleset.
+ ///
+ public readonly bool IsForCurrentRuleset;
// 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,51 +69,53 @@ public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
Dual = true;
}
- originalTargetColumns = TargetColumns;
- }
-
- public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
- {
- if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
- return GetColumnCountForNonConvert(difficulty);
-
- double roundedCircleSize = Math.Round(difficulty.CircleSize);
- double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
+ static int getColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
+ {
+ double roundedCircleSize = Math.Round(difficulty.CircleSize);
- int countSliderOrSpinner = difficulty.TotalObjectCount - difficulty.CircleCount;
- float percentSpecialObjects = (float)countSliderOrSpinner / difficulty.TotalObjectCount;
+ if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
+ return (int)Math.Max(1, roundedCircleSize);
- 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;
+ double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
- return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
- }
+ 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;
+ }
- public static int GetColumnCountForNonConvert(IBeatmapDifficultyInfo difficulty)
- {
- double roundedCircleSize = Math.Round(difficulty.CircleSize);
- return (int)Math.Max(1, roundedCircleSize);
+ return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
+ }
}
- public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
-
- protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
+ public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyList? mods = null)
{
- IBeatmapDifficultyInfo difficulty = original.Difficulty;
+ var converter = new ManiaBeatmapConverter(null, difficulty, new ManiaRuleset());
- int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
- Random = new LegacyRandom(seed);
+ if (mods != null)
+ {
+ foreach (var m in mods.OfType())
+ m.ApplyToBeatmapConverter(converter);
+ }
- return base.ConvertBeatmap(original, cancellationToken);
+ return converter.TotalColumns;
}
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
+
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));
@@ -114,10 +133,6 @@ protected override IEnumerable ConvertHitObject(HitObject origin
}
var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap);
-
- if (objects == null)
- yield break;
-
foreach (ManiaHitObject obj in objects)
yield return obj;
}
@@ -151,7 +166,7 @@ private void recordNote(double time, Vector2 position)
/// 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())
{
@@ -170,13 +185,13 @@ private IEnumerable generateSpecific(HitObject original, IBeatma
/// 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;
@@ -194,7 +209,7 @@ private IEnumerable generateConverted(HitObject original, IBeatm
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);
@@ -205,7 +220,7 @@ private IEnumerable generateConverted(HitObject original, IBeatm
{
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;
@@ -230,8 +245,8 @@ private IEnumerable generateConverted(HitObject original, IBeatm
///
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 2265d3d347d4..52bb87ae193b 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 @@ internal class EndTimeObjectPatternGenerator : PatternGenerator
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 27cb6813009f..ad45a3fb21ab 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -23,9 +23,9 @@ internal class HitObjectPatternGenerator : PatternGenerator
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;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs
index 4922915c7db0..6d593a75e73e 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs
@@ -31,8 +31,8 @@ internal class PathObjectPatternGenerator : PatternGenerator
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)
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index 77f93b4ef96c..48b877850198 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 @@ internal abstract class PatternGenerator : Patterns.PatternGenerator
///
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 @@ protected double ConversionDifficulty
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 3d3c35773b51..8d98515fa4ec 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 @@ internal abstract class PatternGenerator
///
/// 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 @@ protected PatternGenerator(HitObject hitObject, ManiaBeatmap beatmap, Pattern pr
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 6bb68790525d..4190e74e5174 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -29,7 +29,7 @@ public class ManiaDifficultyCalculator : DifficultyCalculator
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 ddb4b868a38f..8a1b12726547 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs
@@ -15,7 +15,11 @@ internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator
{
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
{
- return new LegacyScoreAttributes { ComboScore = 1000000 };
+ return new LegacyScoreAttributes
+ {
+ ComboScore = 1000000,
+ MaxCombo = 0 // Max combo is mod-dependent, so any value here is insufficient.
+ };
}
public double GetLegacyScoreMultiplier(IReadOnlyList mods, LegacyBeatmapConversionDifficultyInfo difficulty)
@@ -47,13 +51,8 @@ public double GetLegacyScoreMultiplier(IReadOnlyList mods, LegacyBeatmapCon
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 000000000000..0aa72c28b8f7
--- /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/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index 02ad1655b50a..991b7f476cca 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -5,6 +5,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
@@ -23,7 +24,7 @@ public partial class HoldNotePlacementBlueprint : ManiaPlacementBlueprint HitObject.Duration > 0;
+ protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
public HoldNotePlacementBlueprint()
: base(new HoldNote())
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index 8ec5213d5f72..b8e6aa26a0ea 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!;
+
+ [Resolved]
+ private IEditorChangeHandler? changeHandler { get; set; }
+
+ [Resolved]
+ private EditorBeatmap? editorBeatmap { get; set; }
+
+ [Resolved]
+ private IPositionSnapProvider? positionSnapProvider { get; set; }
- private EditNotePiece head;
- private EditNotePiece tail;
+ private EditHoldNoteEndPiece head = null!;
+ private EditHoldNoteEndPiece tail = null!;
public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold)
@@ -33,8 +42,43 @@ private void load()
{
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 1ae65dd8c0b0..c645ddd98db6 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 @@ public abstract partial class ManiaSelectionBlueprint : HitObjectSelectionBlu
[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 @@ protected ManiaSelectionBlueprint(T hitObject)
RelativeSizeAxes = Axes.None;
}
- protected override void Update()
+ private readonly IBindable directionBindable = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
{
- base.Update();
+ directionBindable.BindTo(scrollingInfo.Direction);
+ }
- var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
+ 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 000000000000..51ead5f423fe
--- /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 000000000000..233c602c215b
--- /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/ManiaBeatmapVerifier.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs
new file mode 100644
index 000000000000..4adabfa4d70a
--- /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 967cdb0e5499..c229039dc340 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -28,7 +28,7 @@ public ManiaHitObjectComposer(Ruleset ruleset)
{
}
- public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
+ public new ManiaPlayfield Playfield => drawableRuleset.Playfield;
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 8fdbada04f64..9ae2112b304e 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 @@ public partial class ManiaSelectionHandler : EditorSelectionHandler
[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 @@ public override bool HandleMovement(MoveSelectionEvent moveEvent)
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 4f983debea86..62b54a7215f1 100644
--- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
@@ -3,20 +3,155 @@
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 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,
+ }
+ },
+ 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.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/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
index 7f8a00bf88ba..8c6efbc72d86 100644
--- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
+++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
@@ -1,9 +1,15 @@
// 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;
@@ -13,9 +19,9 @@ public class ManiaFilterCriteria : IRulesetFilterCriteria
{
private FilterCriteria.OptionalRange keys;
- public bool Matches(BeatmapInfo beatmapInfo)
+ public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
{
- return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo.Difficulty)));
+ return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods));
}
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
@@ -29,5 +35,20 @@ public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
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 0c317e0f8a90..40eb44944cbe 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -65,6 +65,8 @@ public class ManiaRuleset : Ruleset, ILegacyRuleset
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
+ public override IBeatmapVerifier CreateBeatmapVerifier() => new ManiaBeatmapVerifier();
+
public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap)
{
switch (skin)
@@ -247,7 +249,7 @@ public override IEnumerable GetModsFor(ModType type)
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 @@ public override LocalisableString GetVariantName(int variant)
/// 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()
@@ -419,7 +421,10 @@ public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
- public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
+ public override SetupSection CreateEditorDifficultySection() => new ManiaDifficultySection();
+
+ 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 44120e16e64b..046d1c5b3490 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs
@@ -15,10 +15,6 @@ public ManiaSkinComponentLookup(ManiaSkinComponents component)
: 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 050b302bd85a..88d6a198225e 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
@@ -15,6 +15,7 @@ public abstract class ManiaKeyMod : Mod, IApplicableToBeatmapConverter
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 000000000000..eb243bfab789
--- /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/ManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
index a841a8ab3788..bea1a1411012 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
@@ -10,5 +10,10 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
+
+ // For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
+ // make the map harder and is more of a personal preference.
+ // In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
+ public override double ScoreMultiplier => 1;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
index 196514c7b142..54a0b8f36d9f 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 d9de06a81143..189c4b3a5f77 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 f23cb335a5b3..3365b206cf18 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/ManiaModKey1.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
index 31f52610e9ce..7dd0c499da62 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
@@ -11,5 +11,6 @@ public class ManiaModKey1 : ManiaKeyMod
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 67e65b887a73..a6c57d459708 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
@@ -11,5 +11,6 @@ public class ManiaModKey10 : ManiaKeyMod
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 0f8148d25207..0d04395a5253 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
@@ -11,5 +11,6 @@ public class ManiaModKey2 : ManiaKeyMod
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 0f8af7940c62..c83b0979ee8e 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
@@ -11,5 +11,6 @@ public class ManiaModKey3 : ManiaKeyMod
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 f9690b42985b..cc7e270dda83 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/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
index f64f7ae31ad4..7e5e80db6cd7 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
@@ -11,5 +11,10 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModNightcore : ModNightcore, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
+
+ // For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
+ // make the map any harder and is more of a personal preference.
+ // In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
+ public override double ScoreMultiplier => 1;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs
index 2e22e23dbd2e..b02a18c9f473 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs
@@ -1,11 +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.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModPerfect : ModPerfect
{
+ protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
+ {
+ if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
+ return false;
+
+ // Mania allows imperfect "Great" hits without failing.
+ if (result.Judgement.MaxResult == HitResult.Perfect)
+ return result.Type < HitResult.Great;
+
+ return result.Type != result.Judgement.MaxResult;
+ }
+
+ private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
}
}
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 bc76c5cfe958..864ef6c3d623 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs
@@ -6,7 +6,6 @@
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 @@
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 @@ public abstract class ManiaModPlayfieldCover : ModHidden, IApplicableToDrawableR
///
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 @@ public virtual void ApplyToDrawableRuleset(DrawableRuleset drawa
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 3490d50871a1..2b55e81788ad 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -265,7 +265,7 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
if (Tail.AllJudged)
{
if (Tail.IsHit)
- ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ ApplyMaxResult();
else
MissForcefully();
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs
index 1b2efbafdfde..6259033235aa 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs
@@ -25,7 +25,10 @@ internal void TriggerResult(bool hit)
{
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 8498fd36de3a..e98622b8bf21 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -87,7 +87,7 @@ protected override void UpdateHitStateTransforms(ArmedState state)
///
/// 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 c70dfcb76142..f6b92ab4059e 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -13,8 +13,6 @@
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Skinning.Default;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
@@ -40,8 +38,6 @@ public partial class DrawableNote : DrawableManiaHitObject, IKeyBindingHan
private Drawable headPiece;
- private DrawableNotePerfectBonus perfectBonus;
-
public DrawableNote()
: this(null)
{
@@ -93,28 +89,18 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
- {
- perfectBonus.TriggerResult(false);
- ApplyResult(r => r.Type = r.Judgement.MinResult);
- }
+ ApplyMinResult();
return;
}
var result = HitObject.HitWindows.ResultFor(timeOffset);
+
if (result == HitResult.None)
return;
result = GetCappedResult(result);
-
- perfectBonus.TriggerResult(result == HitResult.Perfect);
- ApplyResult(r => r.Type = result);
- }
-
- public override void MissForcefully()
- {
- perfectBonus.TriggerResult(false);
- base.MissForcefully();
+ ApplyResult(result);
}
///
@@ -137,32 +123,6 @@ public virtual void OnReleased(KeyBindingReleaseEvent e)
{
}
- protected override void AddNestedHitObject(DrawableHitObject hitObject)
- {
- switch (hitObject)
- {
- case DrawableNotePerfectBonus bonus:
- AddInternal(perfectBonus = bonus);
- break;
- }
- }
-
- protected override void ClearNestedHitObjects()
- {
- RemoveInternal(perfectBonus, false);
- }
-
- protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
- {
- switch (hitObject)
- {
- case NotePerfectBonus bonus:
- return new DrawableNotePerfectBonus(bonus);
- }
-
- return base.CreateNestedHitObject(hitObject);
- }
-
private void updateSnapColour()
{
if (beatmap == null || HitObject == null) return;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNotePerfectBonus.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNotePerfectBonus.cs
deleted file mode 100644
index 70ddb6029623..000000000000
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNotePerfectBonus.cs
+++ /dev/null
@@ -1,26 +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.Mania.Objects.Drawables
-{
- public partial class DrawableNotePerfectBonus : DrawableManiaHitObject
- {
- public override bool DisplayResult => false;
-
- public DrawableNotePerfectBonus()
- : this(null!)
- {
- }
-
- public DrawableNotePerfectBonus(NotePerfectBonus hitObject)
- : base(hitObject)
- {
- }
-
- ///
- /// Apply a judgement result.
- ///
- /// Whether this tick was reached.
- internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs
index 591413262433..0035960c6333 100644
--- a/osu.Game.Rulesets.Mania/Objects/Note.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Note.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Threading;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
@@ -13,12 +12,5 @@ namespace osu.Game.Rulesets.Mania.Objects
public class Note : ManiaHitObject
{
public override Judgement CreateJudgement() => new ManiaJudgement();
-
- protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
- {
- base.CreateNestedHitObjects(cancellationToken);
-
- AddNested(new NotePerfectBonus { StartTime = StartTime });
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs b/osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs
deleted file mode 100644
index def4c0126848..000000000000
--- a/osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs
+++ /dev/null
@@ -1,20 +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 osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Mania.Judgements;
-using osu.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Mania.Objects
-{
- public class NotePerfectBonus : ManiaHitObject
- {
- public override Judgement CreateJudgement() => new NotePerfectBonusJudgement();
- protected override HitWindows CreateHitWindows() => HitWindows.Empty;
-
- public class NotePerfectBonusJudgement : ManiaJudgement
- {
- public override HitResult MaxResult => HitResult.SmallBonus;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index 7c8afdff1211..dd3208bd89e3 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -87,15 +87,22 @@ private IEnumerable generateActionPoints()
private double calculateReleaseTime(HitObject currentObject, HitObject? nextObject)
{
double endTime = currentObject.GetEndTime();
+ double releaseDelay = RELEASE_DELAY;
- if (currentObject is HoldNote)
- // hold note releases must be timed exactly.
- return endTime;
+ if (currentObject is HoldNote hold)
+ {
+ if (hold.Duration > 0)
+ // hold note releases must be timed exactly.
+ return endTime;
+
+ // Special case for super short hold notes
+ releaseDelay = 1;
+ }
bool canDelayKeyUpFully = nextObject == null ||
- nextObject.StartTime > endTime + RELEASE_DELAY;
+ nextObject.StartTime > endTime + releaseDelay;
- return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.AsNonNull().StartTime - endTime) * 0.9);
+ return endTime + (canDelayKeyUpFully ? releaseDelay : (nextObject.AsNonNull().StartTime - endTime) * 0.9);
}
protected override HitObject? GetNextObject(int currentIndex)
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index c53f3c3e07ee..0444394d8747 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -22,17 +22,41 @@ public ManiaScoreProcessor()
}
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)
{
- return 10000 * comboProgress
- + 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ return 150000 * comboProgress
+ + 850000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion;
}
protected override double GetComboScoreChange(JudgementResult result)
- => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
+ {
+ return getBaseComboScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
+ }
+
+ public override int GetBaseScoreForResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Perfect:
+ return 305;
+ }
+
+ return base.GetBaseScoreForResult(result);
+ }
+
+ private int getBaseComboScoreForResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Perfect:
+ return 300;
+ }
+
+ return GetBaseScoreForResult(result);
+ }
private class JudgementOrderComparer : IComparer
{
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
index 4ce3c50f7c53..0052fd8b78b1 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
@@ -17,8 +17,10 @@
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 @@ public ArgonJudgementPiece(HitResult result)
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
- Y = 160;
+ Y = judgement_y_position;
}
[BackgroundDependencyLoader]
@@ -76,7 +78,7 @@ public virtual void PlayAnimation()
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/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
index ca7f84cb4df5..7f6540e7b514 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
@@ -100,16 +99,9 @@ public ManiaArgonSkinTransformer(ISkin skin, IBeatmap beatmap)
return SkinUtils.As(new Bindable(30));
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
-
- float width;
-
bool isSpecialColumn = stage.IsSpecialColumn(columnIndex);
- // Best effort until we have better mobile support.
- if (RuntimeInfo.IsMobile)
- width = 170 * Math.Min(1, 7f / beatmap.TotalColumns) * (isSpecialColumn ? 1.8f : 1);
- else
- width = 60 * (isSpecialColumn ? 2 : 1);
+ float width = 60 * (isSpecialColumn ? 2 : 1);
return SkinUtils.As(new Bindable(width));
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
index ee274fc45e4b..6de0752671ed 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
@@ -65,11 +65,8 @@ private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObj
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 @@ private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObj
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 @@ private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState stat
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 @@ protected override void Update()
{
base.Update();
+ if (!isHitting.Value)
+ (bodySprite as TextureAnimation)?.GotoFrame(0);
+
if (holdNote.Body.HasHoldBreak)
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
@@ -234,7 +228,7 @@ protected override void Update()
break;
default:
- // this is where things get fucked up.
+ // this is where things get a bit messed up.
// honestly there's three modes to handle here but they seem really pointless?
// let's wait to see if anyone actually uses them in skins.
if (bodySprite != null)
@@ -243,7 +237,9 @@ protected override void Update()
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 1ec218644cda..95b00e32eae2 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
@@ -43,11 +43,8 @@ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
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 48b92a84862d..8f9a2d7e7448 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.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 @@ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
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/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs
index 7e3fb0438cc8..3a69142b3c02 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs
@@ -34,7 +34,9 @@ private void load()
FallbackColumnIndex = "S";
else
{
- int distanceToEdge = Math.Min(Column.Index, (stage.Columns - 1) - Column.Index);
+ // Account for cases like dual-stage (assume that all stages have the same column count for now).
+ int columnInStage = Column.Index % stage.Columns;
+ int distanceToEdge = Math.Min(columnInStage, (stage.Columns - 1) - columnInStage);
FallbackColumnIndex = distanceToEdge % 2 == 0 ? "1" : "2";
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs
index 1a47fe507651..680198c1a60f 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs
@@ -28,13 +28,7 @@ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
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 94892811765b..c05a8f2a2923 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -93,8 +93,7 @@ private void load(GameHost host)
// 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())
@@ -109,7 +108,6 @@ private void load(GameHost host)
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
RegisterPool(10, 50);
- RegisterPool(10, 50);
RegisterPool(10, 50);
RegisterPool(10, 50);
RegisterPool(10, 50);
@@ -182,38 +180,5 @@ public void OnReleased(KeyBindingReleaseEvent e)
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 0bc0bf4caf0f..f44444879743 100644
--- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
+++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
@@ -3,8 +3,6 @@
#nullable disable
-using System.Collections.Generic;
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -25,20 +23,21 @@ public partial class ColumnFlow : CompositeDrawable
///
/// 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,
@@ -46,7 +45,7 @@ public ColumnFlow(StageDefinition stageDefinition)
};
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;
@@ -77,11 +76,12 @@ private void onSkinChanged()
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
?.Value;
- if (width == null)
- // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
- columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
- else
- columns[i].Width = width.Value;
+ bool isSpecialColumn = stageDefinition.IsSpecialColumn(i);
+
+ // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
+ width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
+
+ columns[i].Width = width.Value;
}
}
@@ -90,12 +90,9 @@ private void onSkinChanged()
///
/// The index of the column to set the content of.
/// The content.
- public void SetContentForColumn(int column, TContent content) => columns[column].Child = content;
-
- public new MarginPadding Padding
+ public void SetContentForColumn(int column, TContent content)
{
- get => base.Padding;
- set => base.Padding = value;
+ 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 9169599798d6..ce53862c7630 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.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 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.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
+using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers;
@@ -19,15 +19,19 @@
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
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
{
///
@@ -40,7 +44,7 @@ public partial class DrawableManiaRuleset : DrawableScrollingRuleset
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;
@@ -57,7 +61,9 @@ public partial class DrawableManiaRuleset : DrawableScrollingRuleset mods = null)
+ private ISkinSource currentSkin = null!;
+
+ public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null)
: base(ruleset, beatmap, mods)
{
BarLines = new BarLineGenerator(Beatmap).BarLines;
@@ -67,8 +73,12 @@ public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList())
mod.ApplyToTrack(speedAdjustmentTrack);
@@ -94,6 +104,8 @@ private void load()
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
+
+ KeyBindingInputManager.Add(new ManiaTouchInputArea());
}
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
@@ -104,7 +116,36 @@ protected override void Update()
updateTimeRange();
}
- private void updateTimeRange() => TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
+ 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()
+ {
+ 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;
+ }
///
/// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40.
@@ -121,10 +162,20 @@ protected override void Update()
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 314d19994412..b3420c49f3b8 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.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 @@ public override Quad SkinnableComponentScreenSpaceDrawQuad
}
}
- 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 @@ public ManiaPlayfield(List stageDefinitions)
stages.Add(newStage);
AddNested(newStage);
- firstColumnIndex += newStage.Columns.Count;
+ firstColumnIndex += newStage.Columns.Length;
}
}
@@ -125,9 +133,9 @@ public Column GetColumn(int index)
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 @@ public Column GetColumn(int index)
///
/// 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 @@ private Stage getStageByColumn(int column)
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 000000000000..32e4616a25db
--- /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 92f471e36b71..d8d9705530ae 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.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 @@ public partial class PlayfieldCoveringWrapper : CompositeDrawable
private readonly IBindable scrollDirection = new Bindable();
+ private float currentCoverageHeight;
+
public PlayfieldCoveringWrapper(Drawable content)
{
InternalChild = new BufferedContainer
@@ -94,21 +105,46 @@ private void load(IScrollingInfo scrollingInfo)
scrollDirection.BindValueChanged(onScrollDirectionChanged, true);
}
- private void onScrollDirectionChanged(ValueChangedEvent direction)
- => cover.Rotation = direction.NewValue == ScrollingDirection.Up ? 0 : 180f;
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateCoverSize(true);
+ }
- ///
- /// The relative area that should be completely covered. This does not include the fade.
- ///
- public float Coverage
+ protected override void Update()
{
- set
+ 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
{
- filled.Height = value;
- gradient.Y = -value;
+ 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 direction in which the cover expands.
///
@@ -123,11 +159,13 @@ public enum CoverExpandDirection
///
/// 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 fa9af6d15737..a4a09c9a8274 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 @@ public partial class Stage : ScrollingPlayfield
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 @@ public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction n
InternalChildren = new Drawable[]
{
- judgementPool = new DrawablePool(2),
new Container
{
Anchor = Anchor.TopCentre,
@@ -104,7 +114,7 @@ public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction n
{
RelativeSizeAxes = Axes.Y,
},
- new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
+ new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground))
{
RelativeSizeAxes = Axes.Both
},
@@ -137,11 +147,13 @@ public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction n
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 @@ protected override void Dispose(bool isDisposing)
base.Dispose(isDisposing);
- if (currentSkin != null)
+ if (currentSkin.IsNotNull())
currentSkin.SourceChanged -= onSkinChanged;
}
@@ -180,13 +192,13 @@ protected override void LoadComplete()
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 @@ internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result
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 72f172188eb7..3bca9384506b 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 ed4725dd9410..d0c3484cfdb4 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 e8a46a98286d..b79de6d40bda 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 7d50deb8ba79..cc0233d7fde3 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 61be25b845ae..a68d6e12c04d 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 000000000000..5f49714d9328
--- /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 3d35ab79f740..dfe950c01e0f 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
@@ -68,7 +68,7 @@ public void TestMergeCircleSlider()
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 @@ public void TestMergeSliderSlider()
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 @@ public void TestMergeSliderSlider()
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();
@@ -231,6 +231,137 @@ public void TestSameStartTimeMerge()
(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 623cefff6bcd..b97fe5c5a8b0 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs
@@ -124,6 +124,113 @@ public void TestSelectionIncludingSliderPreservedOnClick()
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 d14e59358775..48aa74c5bf9a 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
@@ -5,9 +5,12 @@
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 +27,22 @@ public void TestGridToggles()
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 +55,120 @@ public void TestDistanceSnapMomentaryToggle()
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),
+ _ => 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 +184,7 @@ private void nextGridSizeIs(int size)
}
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 000000000000..28c1577fcb04
--- /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 82343812832a..9af028fd8cf5 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
@@ -30,23 +30,6 @@ public partial class TestScenePathControlPointVisualiser : OsuManualInputManager
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 +155,36 @@ public void TestCatmullAvailableIffSelectionContainsCatmull()
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);
+ }
+
+ 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 +219,7 @@ private void addContextMenuItemStep(string contextMenuText)
{
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 d7dd30d608f0..30e0dbbf2e6f 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs
@@ -24,14 +24,38 @@ public partial class TestScenePreciseRotation : TestSceneOsuEditor
[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 @@ public void TestHotkeyHandling()
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 7ac34bc6c8c0..bbded55732ed 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
@@ -310,9 +310,9 @@ public void TestSliderDrawingCurve()
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
assertPlaced(true);
- assertLength(760, tolerance: 10);
+ assertLength(808, tolerance: 10);
assertControlPointCount(5);
- assertControlPointType(0, PathType.BSpline(3));
+ assertControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, null);
assertControlPointType(2, null);
assertControlPointType(3, null);
@@ -337,9 +337,9 @@ public void TestSliderDrawingLinear()
assertPlaced(true);
assertLength(600, tolerance: 10);
assertControlPointCount(4);
- assertControlPointType(0, PathType.LINEAR);
- assertControlPointType(1, null);
- assertControlPointType(2, null);
+ assertControlPointType(0, PathType.BSpline(4));
+ assertControlPointType(1, PathType.BSpline(4));
+ assertControlPointType(2, PathType.BSpline(4));
assertControlPointType(3, null);
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
index 6c7733e68aa8..d68cbe6265d1 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
@@ -178,7 +178,7 @@ public void TestSplitRetainsHitsounds()
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 @@ private void moveMouseToControlPoint(int index)
{
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 @@ private void addContextMenuItemStep(string contextMenuText)
{
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 021fdba2255f..52a170b84e99 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 @@ public void TestScalingLinearSlider()
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 88c81c7a3984..7375617aa859 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
@@ -133,7 +133,7 @@ public partial class TestSceneOsuModAlternate : OsuModTestScene
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 a353914cd53e..075fdd88cad6 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 @@ public partial class TestSceneOsuModFlashlight : OsuModTestScene
[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
+ {
+ new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
+ new OsuReplayFrame(990, new Vector2()),
+ new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
+ new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(2001, new Vector2(100)),
+ },
+ Autoplay = false,
+ });
+ }
+
+ [Test]
+ public void TestSliderDoesDimAfterStartTimeIfHitEarly()
+ {
+ bool sliderDimmed = false;
+
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModFlashlight(),
+ PassCondition = () =>
+ {
+ sliderDimmed |=
+ Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType.Flashlight>().Single().FlashlightDim > 0;
+ return Player.GameplayState.HasPassed && sliderDimmed;
+ },
+ Beatmap = new OsuBeatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 1000,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100))
+ })
+ }
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(990, new Vector2(), OsuAction.LeftButton),
+ new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(2001, new Vector2(100)),
+ },
+ Autoplay = false,
+ });
+ }
+
+ [Test]
+ public void TestSliderDoesDimAfterStartTimeIfHitLate()
+ {
+ bool sliderDimmed = false;
+
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModFlashlight(),
+ PassCondition = () =>
+ {
+ sliderDimmed |=
+ Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType.Flashlight>().Single().FlashlightDim > 0;
+ return Player.GameplayState.HasPassed && sliderDimmed;
+ },
+ Beatmap = new OsuBeatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 1000,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100))
+ })
+ }
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(1100, new Vector2(), OsuAction.LeftButton),
+ new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(2001, new Vector2(100)),
+ },
+ Autoplay = false,
+ });
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs
index 9dfa76fc8e64..d3996ebc3be4 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs
@@ -46,7 +46,7 @@ public void TestVisibleDuringBreak()
StartTime = 5000,
}
},
- Breaks = new List
+ Breaks =
{
new BreakPeriod(2000, 4000),
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs
index 26c4133bc487..b01bbbfca139 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs
@@ -1,17 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using NUnit.Framework;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
- public partial class TestSceneOsuModPerfect : ModPerfectTestScene
+ public partial class TestSceneOsuModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
@@ -50,5 +54,30 @@ public void TestSpinner(bool shouldMiss)
CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss);
}
+
+ [Test]
+ public void TestMissSliderTail() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModPerfect(),
+ PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 1000,
+ Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
+ new OsuReplayFrame(1001, new Vector2(256, 192)),
+ }
+ });
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
index 402c680b4675..bd2b205ac84b 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
@@ -132,7 +132,7 @@ public partial class TestSceneOsuModSingleTap : OsuModTestScene
Autoplay = false,
Beatmap = new Beatmap
{
- Breaks = new List
+ Breaks =
{
new BreakPeriod(500, 2000),
},
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs
new file mode 100644
index 000000000000..726b4159774a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+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
+{
+ public partial class TestSceneOsuModStrictTracking : OsuModTestScene
+ {
+ [Test]
+ public void TestSliderInput() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModStrictTracking(),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 1000,
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(0, 100))
+ }
+ }
+ }
+ }
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
+ new OsuReplayFrame(500, new Vector2(200, 0), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(200, 0)),
+ new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
+ new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(1751, new Vector2(0, 100)),
+ },
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs
new file mode 100644
index 000000000000..ea048aaa6e8e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs
@@ -0,0 +1,77 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public partial class TestSceneOsuModSuddenDeath : ModFailConditionTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+
+ public TestSceneOsuModSuddenDeath()
+ : base(new OsuModSuddenDeath())
+ {
+ }
+
+ [Test]
+ public void TestMissTail() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSuddenDeath(),
+ PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 1000,
+ Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
+ new OsuReplayFrame(1001, new Vector2(256, 192)),
+ }
+ });
+
+ [Test]
+ public void TestMissTick() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSuddenDeath(),
+ PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 1000,
+ Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), })
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
+ new OsuReplayFrame(1001, new Vector2(256, 192)),
+ }
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs
index cd51ccd75134..8c814317700e 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs
@@ -30,7 +30,7 @@ public partial class TestSceneOsuModTouchDevice : RateAdjustedBeatmapTestScene
private SessionStatics statics { get; set; } = null!;
private ScoreAccessibleSoloPlayer currentPlayer = null!;
- private readonly ManualClock manualClock = new ManualClock { Rate = 0 };
+ private readonly ManualClock manualClock = new ManualClock { Rate = 1 };
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio);
@@ -51,7 +51,6 @@ public override void SetUpSteps()
public void TestUserAlreadyHasTouchDeviceActive()
{
loadPlayer();
- // it is presumed that a previous screen (i.e. song select) will set this up
AddStep("set up touchscreen user", () =>
{
currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray();
@@ -69,6 +68,14 @@ public void TestUserAlreadyHasTouchDeviceActive()
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf());
}
+ [Test]
+ public void TestTouchActivePriorToPlayerLoad()
+ {
+ AddStep("set touch input active", () => statics.SetValue(Static.TouchInputActive, true));
+ loadPlayer();
+ AddUntilStep("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf());
+ }
+
[Test]
public void TestTouchDuringBreak()
{
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
new file mode 100644
index 000000000000..cf93e0ce7bd7
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.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 NUnit.Framework;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class OsuHealthProcessorTest
+ {
+ private static readonly object[][] test_cases =
+ [
+ // hitobject, starting HP, fail expected after miss
+ [new HitCircle(), 0.01, true],
+ [new SliderHeadCircle(), 0.01, true],
+ [new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true],
+ [new SliderTick(), 0.01, true],
+ [new SliderRepeat(new Slider()), 0.01, true],
+ [new SliderTailCircle(new Slider()), 0, true],
+ [new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true],
+ [new Slider(), 0, true],
+ [new Slider { ClassicSliderBehaviour = true }, 0.01, true],
+ [new SpinnerTick(), 0, false],
+ [new SpinnerBonusTick(), 0, false],
+ [new Spinner(), 0.01, true],
+ ];
+
+ [TestCaseSource(nameof(test_cases))]
+ public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected)
+ {
+ var healthProcessor = new OsuHealthProcessor(0);
+ healthProcessor.ApplyBeatmap(new OsuBeatmap
+ {
+ HitObjects = { hitObject }
+ });
+ healthProcessor.Health.Value = startingHealth;
+
+ var result = new OsuJudgementResult(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(OsuHitObject hitObject, double startingHealth, bool _)
+ {
+ var healthProcessor = new OsuHealthProcessor(0);
+ healthProcessor.ApplyBeatmap(new OsuBeatmap
+ {
+ HitObjects = { hitObject }
+ });
+ healthProcessor.Health.Value = startingHealth;
+
+ var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
+ result.Type = result.Judgement.MaxResult;
+ healthProcessor.ApplyResult(result);
+
+ Assert.That(healthProcessor.HasFailed, Is.False);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs
index d78c32aa6a0a..77ef4627cbc1 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs
@@ -31,7 +31,7 @@ private static Slider createTestSlider()
new PathControlPoint(new Vector2(-128, 0), PathType.LINEAR) // absolute position: (0, 128)
}
},
- RepeatCount = 1
+ RepeatCount = 2
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return slider;
@@ -45,7 +45,9 @@ public void TestReflectSliderHorizontallyAlongPlayfield()
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider);
Assert.That(slider.Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
- Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X, 128)));
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
{
new Vector2(),
@@ -62,7 +64,9 @@ public void TestReflectSliderVerticallyAlongPlayfield()
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider);
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
- Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
+ Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
{
new Vector2(),
@@ -79,7 +83,9 @@ public void TestFlipSliderInPlaceHorizontally()
OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, 128)));
- Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(256, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(128, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(256, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(256, 128)));
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
{
new Vector2(),
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs
new file mode 100644
index 000000000000..aa903205c85d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class OsuRateAdjustedDisplayDifficultyTest
+ {
+ private static IEnumerable difficultyValuesToTest()
+ {
+ for (float i = 0; i <= 10; i += 0.5f)
+ yield return i;
+ }
+
+ [TestCaseSource(nameof(difficultyValuesToTest))]
+ public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
+ {
+ var ruleset = new OsuRuleset();
+ var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
+
+ var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
+
+ Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
+ }
+
+ [TestCaseSource(nameof(difficultyValuesToTest))]
+ public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty)
+ {
+ var ruleset = new OsuRuleset();
+ var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
+
+ var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
+
+ Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
+ }
+
+ [Test]
+ public void TestRateBelowOne()
+ {
+ var ruleset = new OsuRuleset();
+ var difficulty = new BeatmapDifficulty();
+
+ var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
+
+ Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
+ Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
+ }
+
+ [Test]
+ public void TestRateAboveOne()
+ {
+ var ruleset = new OsuRuleset();
+ var difficulty = new BeatmapDifficulty();
+
+ var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
+
+ Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
+ Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs
index f99518997b20..5524af206107 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs
@@ -54,7 +54,7 @@ public partial class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = new Vector2(130)
+ Size = new Vector2(300)
}
};
});
@@ -85,6 +85,30 @@ public void TestManualPlacement()
AddStep("return user input", () => InputManager.UseParentInput = true);
}
+ [Test]
+ public void TestAllPoints()
+ {
+ AddStep("add points", () =>
+ {
+ float minX = object1.DrawPosition.X - object1.DrawSize.X / 2;
+ float maxX = object1.DrawPosition.X + object1.DrawSize.X / 2;
+
+ float minY = object1.DrawPosition.Y - object1.DrawSize.Y / 2;
+ float maxY = object1.DrawPosition.Y + object1.DrawSize.Y / 2;
+
+ for (int i = 0; i < 10; i++)
+ {
+ for (float x = minX; x <= maxX; x += 0.5f)
+ {
+ for (float y = minY; y <= maxY; y += 0.5f)
+ {
+ accuracyHeatmap.AddPoint(object2.Position, object1.Position, new Vector2(x, y), RNG.NextSingle(10, 500));
+ }
+ }
+ }
+ });
+ }
+
protected override bool OnMouseDown(MouseDownEvent e)
{
accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index 421a32b9eb44..4db66fde4b31 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@@ -13,6 +14,7 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
using osu.Framework.Testing.Input;
using osu.Game.Audio;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
@@ -47,7 +49,7 @@ public void TestLegacySmoothCursorTrail()
{
createTest(() =>
{
- var skinContainer = new LegacySkinContainer(renderer, false);
+ var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
@@ -61,7 +63,7 @@ public void TestLegacyDisjointCursorTrail()
{
createTest(() =>
{
- var skinContainer = new LegacySkinContainer(renderer, true);
+ var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
@@ -70,6 +72,22 @@ public void TestLegacyDisjointCursorTrail()
});
}
+ [Test]
+ public void TestLegacyDisjointCursorTrailViaNoCursor()
+ {
+ createTest(() =>
+ {
+ var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false, provideCursor: false);
+ var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
+
+ skinContainer.Child = legacyCursorTrail;
+
+ return skinContainer;
+ });
+
+ AddAssert("trail is disjoint", () => this.ChildrenOfType().Single().DisjointTrail, () => Is.True);
+ }
+
private void createTest(Func createContent) => AddStep("create trail", () =>
{
Clear();
@@ -86,12 +104,14 @@ public void TestLegacyDisjointCursorTrail()
private partial class LegacySkinContainer : Container, ISkinSource
{
private readonly IRenderer renderer;
- private readonly bool disjoint;
+ private readonly bool provideMiddle;
+ private readonly bool provideCursor;
- public LegacySkinContainer(IRenderer renderer, bool disjoint)
+ public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true)
{
this.renderer = renderer;
- this.disjoint = disjoint;
+ this.provideMiddle = provideMiddle;
+ this.provideCursor = provideCursor;
RelativeSizeAxes = Axes.Both;
}
@@ -102,15 +122,14 @@ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wra
{
switch (componentName)
{
- case "cursortrail":
- var tex = new Texture(renderer.WhitePixel);
+ case "cursor":
+ return provideCursor ? new Texture(renderer.WhitePixel) : null;
- if (disjoint)
- tex.ScaleAdjust = 1 / 25f;
- return tex;
+ case "cursortrail":
+ return new Texture(renderer.WhitePixel);
case "cursormiddle":
- return disjoint ? null : renderer.WhitePixel;
+ return provideMiddle ? null : renderer.WhitePixel;
}
return null;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index 874130233ac1..a239f671af02 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -25,16 +25,16 @@ public partial class TestSceneDrawableJudgement : OsuSkinnableTestScene
[Resolved]
private OsuConfigManager config { get; set; } = null!;
- private readonly List> pools;
+ private readonly List> pools = new List>();
- public TestSceneDrawableJudgement()
+ [TestCaseSource(nameof(validResults))]
+ public void Test(HitResult result)
{
- pools = new List>();
-
- foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType