diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 985fc09df3..5a3eadf607 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -9,7 +9,7 @@
]
},
"jetbrains.resharper.globaltools": {
- "version": "2020.3.2",
+ "version": "2022.1.0-eap10",
"commands": [
"jb"
]
@@ -27,7 +27,7 @@
]
},
"ppy.localisationanalyser.tools": {
- "version": "2021.1210.0",
+ "version": "2022.417.0",
"commands": [
"localisation"
]
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 0000000000..8be6479043
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,2 @@
+# Normalize all the line endings
+32a74f95a5c80a0ed18e693f13a47522099df5c3
diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml
new file mode 100644
index 0000000000..91ca622f55
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-issue.yml
@@ -0,0 +1,80 @@
+name: Bug report
+description: Report a very clearly broken issue.
+body:
+ - type: markdown
+ attributes:
+ value: |
+ # osu! bug report
+
+ Important to note that your issue may have already been reported before. Please check:
+ - Pinned issues, at the top of https://github.com/ppy/osu/issues.
+ - 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.
+
+ - type: dropdown
+ attributes:
+ label: Type
+ options:
+ - Crash to desktop
+ - Game behaviour
+ - Performance
+ - Cosmetic
+ - Other
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Bug description
+ description: How did you find the bug? Any additional details that might help?
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Screenshots or videos
+ description: Add screenshots or videos that show the bug here.
+ placeholder: Drag and drop the screenshots/videos into this box.
+ validations:
+ required: false
+ - 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.
+ validations:
+ required: true
+ - type: markdown
+ attributes:
+ 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.
+
+ ### 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
+
+ The default places to find the logs on desktop platforms are as follows:
+ - `%AppData%/osu/logs` *on Windows*
+ - `~/.local/share/osu/logs` *on Linux & macOS*
+
+ If you have selected a custom location for the game files, you can find the `logs` folder there.
+
+ ### Mobile platforms
+
+ The places to find the logs on mobile platforms are as follows:
+ - *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
+ placeholder: Drag and drop the log files into this box.
+ validations:
+ required: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3c52802cf6..f2066f27de 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,6 +2,60 @@ on: [push, pull_request]
name: Continuous Integration
jobs:
+ inspect-code:
+ name: Code Quality
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ # 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@v1
+ with:
+ dotnet-version: "3.1.x"
+
+ - name: Install .NET 6.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "6.0.x"
+
+ - name: Restore Tools
+ run: dotnet tool restore
+
+ - name: Restore Packages
+ run: dotnet restore
+
+ - name: Restore inspectcode cache
+ uses: actions/cache@v3
+ with:
+ path: ${{ github.workspace }}/inspectcode
+ key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }}
+
+ - name: CodeFileSanity
+ run: |
+ # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
+ # FIXME: Suppress warnings from templates project
+ exit_code=0
+ while read -r line; do
+ if [[ ! -z "$line" ]]; then
+ echo "::error::$line"
+ exit_code=1
+ fi
+ done <<< $(dotnet codefilesanity)
+ exit $exit_code
+
+ # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
+ # - name: .NET Format (Dry Run)
+ # run: dotnet format --dry-run --check
+
+ - name: InspectCode
+ run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+
+ - name: NVika
+ run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
+
test:
name: Test
runs-on: ${{matrix.os.fullname}}
@@ -20,10 +74,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- - name: Install .NET 5.0.x
+ - name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
- dotnet-version: "5.0.x"
+ dotnet-version: "6.0.x"
# FIXME: libavformat is not included in Ubuntu. Let's fix that.
# https://github.com/ppy/osu-framework/issues/4349
@@ -65,10 +119,10 @@ jobs:
run: |
$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2
- - name: Install .NET 5.0.x
+ - name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
- dotnet-version: "5.0.x"
+ dotnet-version: "6.0.x"
# Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
# cannot accept .sln(f) files as arguments.
@@ -84,61 +138,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- - name: Install .NET 5.0.x
+ - name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
- dotnet-version: "5.0.x"
+ dotnet-version: "6.0.x"
# Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
# cannot accept .sln(f) files as arguments.
# Build just the main game for now.
- name: Build
- run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
-
- inspect-code:
- name: Code Quality
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
-
- # FIXME: Tools won't run in .NET 5.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@v1
- with:
- dotnet-version: "3.1.x"
-
- - name: Install .NET 5.0.x
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: "5.0.x"
-
- - name: Restore Tools
- run: dotnet tool restore
-
- - name: Restore Packages
- run: dotnet restore
-
- - name: CodeFileSanity
- run: |
- # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
- # FIXME: Suppress warnings from templates project
- exit_code=0
- while read -r line; do
- if [[ ! -z "$line" ]]; then
- echo "::error::$line"
- exit_code=1
- fi
- done <<< $(dotnet codefilesanity)
- exit $exit_code
-
- # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
- # - name: .NET Format (Dry Run)
- # run: dotnet format --dry-run --check
-
- - name: InspectCode
- run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
-
- - name: NVika
- run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
+ run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/misc.xml b/.idea/.idea.osu.Desktop/.idea/misc.xml
index 1d8c84d0af..4e1d56f4dd 100644
--- a/.idea/.idea.osu.Desktop/.idea/misc.xml
+++ b/.idea/.idea.osu.Desktop/.idea/misc.xml
@@ -1,5 +1,10 @@
+
+
+
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
index 498a710df9..d500c595c0 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 657b885df1..6da760dead 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 847dcf822c..741679707a 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 5dc1168e35..104b1266ca 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 ab4ce5a9cc..f58f9d4ae2 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 61331944a8..0f2c390328 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 9a00f58c3b..898aec880c 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 4ae656b6d8..dae6e032b1 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 e58c602962..519107b5e3 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/.run/osu! (Second Client).run.xml b/.run/osu! (Second Client).run.xml
index 599b4b986b..9a471df902 100644
--- a/.run/osu! (Second Client).run.xml
+++ b/.run/osu! (Second Client).run.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,9 +12,9 @@
-
+
-
\ No newline at end of file
+
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000000..5b7a98f4ba
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "ms-dotnettools.csharp"
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 1b590008cd..d93fddf42d 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Debug/net5.0/osu!.dll"
+ "${workspaceRoot}/osu.Desktop/bin/Debug/net6.0/osu!.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
@@ -19,7 +19,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Release/net5.0/osu!.dll"
+ "${workspaceRoot}/osu.Desktop/bin/Release/net6.0/osu!.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Release)",
@@ -31,7 +31,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tests/bin/Debug/net5.0/osu.Game.Tests.dll"
+ "${workspaceRoot}/osu.Game.Tests/bin/Debug/net6.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/net5.0/osu.Game.Tests.dll"
+ "${workspaceRoot}/osu.Game.Tests/bin/Release/net6.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/net5.0/osu!.dll",
+ "${workspaceRoot}/osu.Desktop/bin/Debug/net6.0/osu!.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
@@ -68,7 +68,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Desktop/bin/Release/net5.0/osu!.dll",
+ "${workspaceRoot}/osu.Desktop/bin/Release/net6.0/osu!.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
@@ -81,7 +81,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net5.0/osu.Game.Tournament.Tests.dll",
+ "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net6.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/net5.0/osu.Game.Tournament.Tests.dll",
+ "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net6.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/net5.0/osu.Game.Benchmarks.dll",
+ "program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/net6.0/osu.Game.Benchmarks.dll",
"args": [
"--filter",
"*"
diff --git a/Directory.Build.props b/Directory.Build.props
index 894ea25c8b..709545bf1d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,7 +18,6 @@
-
$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset
@@ -27,19 +26,6 @@
true
$(NoWarn);CS1591
-
-
- $(NoWarn);NU1701;CA9998
-
false
ppy Pty Ltd
@@ -48,7 +34,7 @@
https://github.com/ppy/osu
Automated release.
ppy Pty Ltd
- Copyright (c) 2021 ppy Pty Ltd
+ Copyright (c) 2022 ppy Pty Ltd
osu game
diff --git a/Gemfile.lock b/Gemfile.lock
index 1010027af9..ddab497657 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -8,17 +8,17 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
- aws-partitions (1.553.0)
- aws-sdk-core (3.126.0)
+ aws-partitions (1.570.0)
+ aws-sdk-core (3.130.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.54.0)
- aws-sdk-core (~> 3, >= 3.126.0)
+ aws-sdk-kms (1.55.0)
+ aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.112.0)
- aws-sdk-core (~> 3, >= 3.126.0)
+ aws-sdk-s3 (1.113.0)
+ aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
@@ -36,8 +36,8 @@ GEM
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.3)
- excon (0.91.0)
- faraday (1.9.3)
+ excon (0.92.1)
+ faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
- fastlane (2.204.2)
+ fastlane (2.205.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -130,10 +130,10 @@ GEM
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.5.0)
- faraday (>= 0.17.3, < 2.0)
+ google-cloud-env (1.6.0)
+ faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
- google-cloud-storage (1.36.0)
+ google-cloud-storage (1.36.1)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
@@ -141,8 +141,8 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
- googleauth (1.1.0)
- faraday (>= 0.17.3, < 2.0)
+ googleauth (1.1.2)
+ faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
@@ -152,7 +152,7 @@ GEM
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
- jmespath (1.5.0)
+ jmespath (1.6.1)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
@@ -182,9 +182,9 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
- signet (0.16.0)
+ signet (0.16.1)
addressable (~> 2.8)
- faraday (>= 0.17.3, < 2.0)
+ faraday (>= 0.17.5, < 3.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
@@ -205,7 +205,7 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.8)
+ unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
diff --git a/InspectCode.ps1 b/InspectCode.ps1
index 8316f48ff3..df0d73ea43 100644
--- a/InspectCode.ps1
+++ b/InspectCode.ps1
@@ -5,7 +5,7 @@ dotnet tool restore
# - cmd: dotnet format --dry-run --check
dotnet CodeFileSanity
-dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
exit $LASTEXITCODE
diff --git a/InspectCode.sh b/InspectCode.sh
index cf2bc18175..65b55e0da0 100755
--- a/InspectCode.sh
+++ b/InspectCode.sh
@@ -2,5 +2,5 @@
dotnet tool restore
dotnet CodeFileSanity
-dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
diff --git a/LICENCE b/LICENCE
index b5962ad3b2..d3e7537cef 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,4 +1,4 @@
-Copyright (c) 2021 ppy Pty Ltd .
+Copyright (c) 2022 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 b1dfcab416..dba0b2670d 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:**
-| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.15+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
+| [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 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
@@ -48,9 +48,9 @@ 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 5.0 SDK](https://dotnet.microsoft.com/download) installed.
+- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
-- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
+- 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/).
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
### Downloading the source code
@@ -72,7 +72,7 @@ git pull
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
-- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations.
+- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will allow access to template run configurations.
You can also build and run *osu!* from the command-line with a single command:
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 fd03878699..b433819346 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/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
+ "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
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 3c6aaa39ca..cb922c5a58 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
@@ -20,7 +20,7 @@
WinExe
- net5.0
+ net6.0
osu.Game.Rulesets.EmptyFreeform.Tests
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
index fae3784f5e..312d3d5e9a 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.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.Game.Beatmaps;
@@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty();
}
}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs
index d4496a24fd..d3ef3f6e56 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs
@@ -3,22 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.EmptyFreeform.Replays;
using osu.Game.Rulesets.Mods;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.EmptyFreeform.Mods
{
public class EmptyFreeformModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo
- {
- User = new APIUser { Username = "sample" },
- },
- Replay = new EmptyFreeformAutoGenerator(beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new EmptyFreeformAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}
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 bd9db14259..d60bc2571d 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/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
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 0719dd30df..5ecd9cc675 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
@@ -20,7 +20,7 @@
WinExe
- net5.0
+ net6.0
osu.Game.Rulesets.Pippidon.Tests
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
index 6e1fe42ee2..f57b874ff3 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
@@ -3,22 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Replays;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo
- {
- User = new APIUser { Username = "sample" },
- },
- Replay = new PippidonAutoGenerator(beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
index ca64636076..f6addab279 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.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.Game.Beatmaps;
@@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty();
}
}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs
deleted file mode 100644
index 1c4fe698c2..0000000000
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Scoring/PippidonScoreProcessor.cs
+++ /dev/null
@@ -1,11 +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.Scoring;
-
-namespace osu.Game.Rulesets.Pippidon.Scoring
-{
- public class PippidonScoreProcessor : ScoreProcessor
- {
- }
-}
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 24e4873ed6..f1f37f6363 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/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll"
+ "${workspaceRoot}/bin/Release/net6.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/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index d0db43cc81..33ad0ac4f7 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
@@ -20,7 +20,7 @@
WinExe
- net5.0
+ net6.0
osu.Game.Rulesets.EmptyScrolling.Tests
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
index 63a8b48b3c..a4dc1762d5 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.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.Game.Beatmaps;
@@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty();
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs
index c5bacb522f..5cf40c30cd 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs
@@ -1,24 +1,16 @@
// 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.Beatmaps;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.EmptyScrolling.Replays;
-using osu.Game.Scoring;
using System.Collections.Generic;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.EmptyScrolling.Replays;
+using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.EmptyScrolling.Mods
{
public class EmptyScrollingModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo
- {
- User = new APIUser { Username = "sample" },
- },
- Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new EmptyScrollingAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}
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 bd9db14259..d60bc2571d 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/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll"
+ "${workspaceRoot}/bin/Release/net6.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/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 0719dd30df..5ecd9cc675 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
@@ -20,7 +20,7 @@
WinExe
- net5.0
+ net6.0
osu.Game.Rulesets.Pippidon.Tests
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
index 6e1fe42ee2..f57b874ff3 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
@@ -3,22 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Replays;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo
- {
- User = new APIUser { Username = "sample" },
- },
- Replay = new PippidonAutoGenerator(beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
index ca64636076..f6addab279 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.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.Game.Beatmaps;
@@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty();
}
}
diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj
index 4624d3d771..b8c3ad373a 100644
--- a/Templates/osu.Game.Templates.csproj
+++ b/Templates/osu.Game.Templates.csproj
@@ -8,7 +8,7 @@
https://github.com/ppy/osu/blob/master/Templates
https://github.com/ppy/osu
Automated release.
- Copyright (c) 2021 ppy Pty Ltd
+ Copyright (c) 2022 ppy Pty Ltd
Templates to use when creating a ruleset for consumption in osu!.
dotnet-new;templates;osu
netstandard2.1
diff --git a/osu.Android.props b/osu.Android.props
index 1a2859c851..8d79eb94a8 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,11 +51,11 @@
-
-
+
+
-
+
diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
index 3642f70a56..d87b25a4c7 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
@@ -108,10 +109,7 @@ namespace osu.Desktop
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
// update ruleset
- int onlineID = ruleset.Value.OnlineID;
- bool isLegacyRuleset = onlineID >= 0 && onlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID;
-
- presence.Assets.SmallImageKey = isLegacyRuleset ? $"mode_{onlineID}" : "mode_custom";
+ presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
presence.Assets.SmallImageText = ruleset.Value.Name;
client.SetPresence(presence);
diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
index 97a4c57bf0..10761bc315 100644
--- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
+++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
@@ -77,10 +77,9 @@ namespace osu.Desktop.LegacyIpc
case LegacyIpcDifficultyCalculationRequest req:
try
{
- var ruleset = getLegacyRulesetFromID(req.RulesetId);
-
+ WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile);
+ var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
- WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset);
return new LegacyIpcDifficultyCalculationResponse
{
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index cd3fb7eb61..be8159a7cc 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -96,6 +97,8 @@ namespace osu.Desktop
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
+ Debug.Assert(OperatingSystem.IsWindows());
+
return new SquirrelUpdateManager();
default:
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index b944068e78..e317a44bc3 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using osu.Desktop.LegacyIpc;
@@ -12,6 +13,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.IPC;
using osu.Game.Tournament;
+using Squirrel;
namespace osu.Desktop
{
@@ -24,6 +26,10 @@ namespace osu.Desktop
[STAThread]
public static void Main(string[] args)
{
+ // run Squirrel first, as the app may exit after these run
+ if (OperatingSystem.IsWindows())
+ setupSquirrel();
+
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
@@ -104,6 +110,23 @@ namespace osu.Desktop
}
}
+ [SupportedOSPlatform("windows")]
+ private static void setupSquirrel()
+ {
+ SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) =>
+ {
+ tools.CreateShortcutForThisExe();
+ tools.CreateUninstallerRegistryEntry();
+ }, onAppUninstall: (version, tools) =>
+ {
+ tools.RemoveShortcutForThisExe();
+ tools.RemoveUninstallerRegistryEntry();
+ }, onEveryRun: (version, tools, firstRun) =>
+ {
+ tools.SetProcessAppUserModelId();
+ });
+ }
+
private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
///
diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
index 8f3ad853dc..ba37a14442 100644
--- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
+++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
@@ -19,7 +19,7 @@ namespace osu.Desktop.Security
public class ElevatedPrivilegesChecker : Component
{
[Resolved]
- private NotificationOverlay notifications { get; set; }
+ private INotificationOverlay notifications { get; set; }
private bool elevated;
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 7b60bc03e4..c09cce1235 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -16,14 +17,15 @@ using osu.Game.Overlays.Notifications;
using osuTK;
using osuTK.Graphics;
using Squirrel;
-using LogLevel = Splat.LogLevel;
+using Squirrel.SimpleSplat;
namespace osu.Desktop.Updater
{
+ [SupportedOSPlatform("windows")]
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{
private UpdateManager updateManager;
- private NotificationOverlay notificationOverlay;
+ private INotificationOverlay notificationOverlay;
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
@@ -34,12 +36,14 @@ namespace osu.Desktop.Updater
///
private bool updatePending;
- [BackgroundDependencyLoader]
- private void load(NotificationOverlay notification)
- {
- notificationOverlay = notification;
+ private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
- Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
+ [BackgroundDependencyLoader]
+ private void load(INotificationOverlay notifications)
+ {
+ notificationOverlay = notifications;
+
+ SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
}
protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
@@ -49,9 +53,11 @@ namespace osu.Desktop.Updater
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
+ const string github_token = null; // TODO: populate.
+
try
{
- updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false);
+ updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
@@ -201,11 +207,11 @@ namespace osu.Desktop.Updater
}
}
- private class SquirrelLogger : Splat.ILogger, IDisposable
+ private class SquirrelLogger : ILogger, IDisposable
{
- public LogLevel Level { get; set; } = LogLevel.Info;
+ public Squirrel.SimpleSplat.LogLevel Level { get; set; } = Squirrel.SimpleSplat.LogLevel.Info;
- public void Write(string message, LogLevel logLevel)
+ public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel)
{
if (logLevel < Level)
return;
diff --git a/osu.Desktop/app.manifest b/osu.Desktop/app.manifest
index 2e9127bf44..a11cee132c 100644
--- a/osu.Desktop/app.manifest
+++ b/osu.Desktop/app.manifest
@@ -1,6 +1,7 @@
+ 1
@@ -17,4 +18,4 @@
true
-
\ No newline at end of file
+
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 89b9ffb94b..a4f309c6ac 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -1,6 +1,6 @@
- net5.0
+ net6.0
WinExe
true
A free-to-win rhythm game. Rhythm is just a *click* away!
@@ -24,13 +24,14 @@
-
+
-
-
-
-
-
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index 1757fd7c73..dc1ec17e2c 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -11,7 +11,7 @@
false
A free-to-win rhythm game. Rhythm is just a *click* away!
testing
- Copyright (c) 2021 ppy Pty Ltd
+ Copyright (c) 2022 ppy Pty Ltd
en-AU
diff --git a/osu.Game.Benchmarks/BenchmarkRealmReads.cs b/osu.Game.Benchmarks/BenchmarkRealmReads.cs
index bf9467700c..615e2e964d 100644
--- a/osu.Game.Benchmarks/BenchmarkRealmReads.cs
+++ b/osu.Game.Benchmarks/BenchmarkRealmReads.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Benchmarks
storage = new TemporaryNativeStorage("realm-benchmark");
storage.DeleteDirectory(string.Empty);
- realm = new RealmAccess(storage, "client");
+ realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME);
realm.Run(r =>
{
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 57b914bee6..434c0e0367 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
Exe
false
diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json
index 9aaaf418c2..201343a036 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/net5.0/osu.Game.Rulesets.Catch.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Catch.Tests.dll"
+ "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index 7e8d567fbe..48d46636df 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
- [TestCase(4.0505463516206195d, "diffcalc-test")]
- public void Test(double expected, string name)
- => base.Test(expected, name);
+ [TestCase(4.0505463516206195d, 127, "diffcalc-test")]
+ public void Test(double expectedStarRating, int expectedMaxCombo, string name)
+ => base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(5.1696411260785498d, "diffcalc-test")]
- public void TestClockRateAdjusted(double expected, string name)
- => Test(expected, name, new CatchModDoubleTime());
+ [TestCase(5.1696411260785498d, 127, "diffcalc-test")]
+ public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap);
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
index e70def7f8b..bb3a724b91 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public TestLegacySkin(SkinInfo skin, IResourceStore storage)
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
- : base(skin, storage, null, "skin.ini")
+ : base(skin, null, storage)
{
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
index 064a84cb98..b720ab1e97 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[SetUp]
public void SetUp() => Schedule(() =>
{
- scoreProcessor = new ScoreProcessor();
+ scoreProcessor = new ScoreProcessor(new CatchRuleset());
SetContents(_ => new CatchComboDisplay
{
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 13f2e25f05..fc6d900567 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
@@ -9,9 +9,9 @@
WinExe
- net5.0
+ net6.0
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 70d11c42e5..80b9436b2c 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -19,7 +19,6 @@ using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Catch.Edit;
@@ -182,7 +181,7 @@ namespace osu.Game.Rulesets.Catch
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
- public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
+ public override PerformanceCalculator CreatePerformanceCalculator() => new CatchPerformanceCalculator();
public int LegacyID => 2;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
index 39a58d336d..8e069d7d16 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
@@ -9,6 +9,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyAttributes : DifficultyAttributes
{
+ ///
+ /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ ///
+ /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
+ ///
[JsonProperty("approach_rate")]
public double ApproachRate { get; set; }
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index 8cdbe500f0..b30b85be2d 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -13,33 +13,29 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchPerformanceCalculator : PerformanceCalculator
{
- protected new CatchDifficultyAttributes Attributes => (CatchDifficultyAttributes)base.Attributes;
-
- private Mod[] mods;
-
private int fruitsHit;
private int ticksHit;
private int tinyTicksHit;
private int tinyTicksMissed;
private int misses;
- public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
- : base(ruleset, attributes, score)
+ public CatchPerformanceCalculator()
+ : base(new CatchRuleset())
{
}
- public override PerformanceAttributes Calculate()
+ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{
- mods = Score.Mods;
+ 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);
+ 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);
// We are heavily relying on aim in catch the beat
- double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
+ double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo
int numTotalHits = totalComboHits();
@@ -52,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= Math.Pow(0.97, misses);
// Combo scaling
- if (Attributes.MaxCombo > 0)
- value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
+ if (catchAttributes.MaxCombo > 0)
+ value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
- double approachRate = Attributes.ApproachRate;
+ double approachRate = catchAttributes.ApproachRate;
double approachRateFactor = 1.0;
if (approachRate > 9.0)
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
@@ -66,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= approachRateFactor;
- if (mods.Any(m => m is ModHidden))
+ if (score.Mods.Any(m => m is ModHidden))
{
// Hiddens gives almost nothing on max approach rate, and more the lower it is
if (approachRate <= 10.0)
@@ -75,12 +71,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11
}
- if (mods.Any(m => m is ModFlashlight))
+ if (score.Mods.Any(m => m is ModFlashlight))
value *= 1.35 * lengthBonus;
value *= Math.Pow(accuracy(), 5.5);
- if (mods.Any(m => m is ModNoFail))
+ if (score.Mods.Any(m => m is ModNoFail))
value *= 0.90;
return new CatchPerformanceAttributes
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
index 11fffb31de..50e48101d3 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
@@ -3,19 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } },
- Replay = new CatchAutoGenerator(beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
index 6d2286b957..7eda6b37d3 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
@@ -3,20 +3,15 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModCinema : ModCinema
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } },
- Replay = new CatchAutoGenerator(beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
}
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index 2cc05826b4..51b1ccaaba 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -7,5 +7,11 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreProcessor : ScoreProcessor
{
+ public CatchScoreProcessor()
+ : base(new CatchRuleset())
+ {
+ }
+
+ protected override double ClassicScoreMultiplier => 28;
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json
index e3d7956e85..f6a067a831 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/net5.0/osu.Game.Rulesets.Mania.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Mania.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Mania.Tests.dll"
+ "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Mania.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
index 6ec49d7634..715614a201 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
- [TestCase(2.3449735700206298d, "diffcalc-test")]
- public void Test(double expected, string name)
- => base.Test(expected, name);
+ [TestCase(2.3449735700206298d, 151, "diffcalc-test")]
+ public void Test(double expectedStarRating, int expectedMaxCombo, string name)
+ => base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(2.7879104989252959d, "diffcalc-test")]
- public void TestClockRateAdjusted(double expected, string name)
- => Test(expected, name, new ManiaModDoubleTime());
+ [TestCase(2.7879104989252959d, 151, "diffcalc-test")]
+ public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap);
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 d51a6da4f9..ddad2adfea 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
@@ -9,9 +9,9 @@
WinExe
- net5.0
+ net6.0
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
index 979a04ddf8..5b7a460079 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
@@ -9,9 +9,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaDifficultyAttributes : DifficultyAttributes
{
+ ///
+ /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ ///
+ /// Rate-adjusting mods do not affect the hit window at all in osu-stable.
+ ///
[JsonProperty("great_hit_window")]
public double GreatHitWindow { get; set; }
+ ///
+ /// The score multiplier applied via score-reducing mods.
+ ///
[JsonProperty("score_multiplier")]
public double ScoreMultiplier { get; set; }
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index 1f82eb7ccd..b17aa7fc4d 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -48,7 +48,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods,
- GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
+ // In osu-stable mania, rate-adjustment mods don't affect the hit window.
+ // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
+ GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
};
@@ -108,7 +110,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
}
}
- private int getHitWindow300(Mod[] mods)
+ private double getHitWindow300(Mod[] mods)
{
if (isForCurrentRuleset)
{
@@ -121,19 +123,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return applyModAdjustments(47, mods);
- static int applyModAdjustments(double value, Mod[] mods)
+ static double applyModAdjustments(double value, Mod[] mods)
{
if (mods.Any(m => m is ManiaModHardRock))
value /= 1.4;
else if (mods.Any(m => m is ManiaModEasy))
value *= 1.4;
- if (mods.Any(m => m is ManiaModDoubleTime))
- value *= 1.5;
- else if (mods.Any(m => m is ManiaModHalfTime))
- value *= 0.75;
-
- return (int)value;
+ return value;
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index 8a8c41bb8a..b347cc9ae2 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -13,10 +13,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaPerformanceCalculator : PerformanceCalculator
{
- protected new ManiaDifficultyAttributes Attributes => (ManiaDifficultyAttributes)base.Attributes;
-
- private Mod[] mods;
-
// Score after being scaled by non-difficulty-increasing mods
private double scaledScore;
@@ -27,42 +23,40 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private int countMeh;
private int countMiss;
- public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
- : base(ruleset, attributes, score)
+ public ManiaPerformanceCalculator()
+ : base(new ManiaRuleset())
{
}
- public override PerformanceAttributes Calculate()
+ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{
- mods = Score.Mods;
- scaledScore = Score.TotalScore;
- countPerfect = Score.Statistics.GetValueOrDefault(HitResult.Perfect);
- countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
- countGood = Score.Statistics.GetValueOrDefault(HitResult.Good);
- countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
- countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
- countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
+ var maniaAttributes = (ManiaDifficultyAttributes)attributes;
- IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease);
+ scaledScore = score.TotalScore;
+ countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect);
+ countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
+ countGood = score.Statistics.GetValueOrDefault(HitResult.Good);
+ countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
+ countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
+ countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
- double scoreMultiplier = 1.0;
- foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m)))
- scoreMultiplier *= m.ScoreMultiplier;
-
- // Scale score up, so it's comparable to other keymods
- scaledScore *= 1.0 / scoreMultiplier;
+ if (maniaAttributes.ScoreMultiplier > 0)
+ {
+ // Scale score up, so it's comparable to other keymods
+ scaledScore *= 1.0 / maniaAttributes.ScoreMultiplier;
+ }
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed.
double multiplier = 0.8;
- if (mods.Any(m => m is ModNoFail))
+ if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.9;
- if (mods.Any(m => m is ModEasy))
+ if (score.Mods.Any(m => m is ModEasy))
multiplier *= 0.5;
- double difficultyValue = computeDifficultyValue();
- double accValue = computeAccuracyValue(difficultyValue);
+ double difficultyValue = computeDifficultyValue(maniaAttributes);
+ double accValue = computeAccuracyValue(difficultyValue, maniaAttributes);
double totalValue =
Math.Pow(
Math.Pow(difficultyValue, 1.1) +
@@ -78,9 +72,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
};
}
- private double computeDifficultyValue()
+ private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
{
- double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
+ double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
@@ -100,14 +94,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return difficultyValue;
}
- private double computeAccuracyValue(double difficultyValue)
+ private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes)
{
- if (Attributes.GreatHitWindow <= 0)
+ if (attributes.GreatHitWindow <= 0)
return 0;
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
- double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667)
+ double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667)
* difficultyValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 180b9ef71b..bd6a67bf67 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
- public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score);
+ public override PerformanceCalculator CreatePerformanceCalculator() => new ManiaPerformanceCalculator();
public const string SHORT_NAME = "mania";
@@ -258,6 +258,7 @@ namespace osu.Game.Rulesets.Mania
{
new MultiMod(new ModWindUp(), new ModWindDown()),
new ManiaModMuted(),
+ new ModAdaptiveSpeed()
};
default:
@@ -394,6 +395,7 @@ namespace osu.Game.Rulesets.Mania
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
+ new AverageHitError(score.HitEvents),
new UnstableRate(score.HitEvents)
}), true)
}
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index 36fa336d0c..bd3b8c3b10 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -45,10 +45,5 @@ namespace osu.Game.Rulesets.Mania
}
};
}
-
- private class TimeSlider : OsuSliderBar
- {
- public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
index 1504c868d0..d444c9b634 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
@@ -3,20 +3,15 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } },
- Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedUser { Username = "osu!topus" });
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
index 4f1276946b..f0db742eac 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
@@ -3,21 +3,16 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModCinema : ModCinema
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } },
- Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedUser { Username = "osu!topus" });
}
}
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 48b377c794..02d62a090b 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -7,8 +7,15 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor
{
+ public ManiaScoreProcessor()
+ : base(new ManiaRuleset())
+ {
+ }
+
protected override double DefaultAccuracyPortion => 0.99;
protected override double DefaultComboPortion => 0.01;
+
+ protected override double ClassicScoreMultiplier => 16;
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
index 952fc7ddd6..fdacc75c92 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
@@ -98,8 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
float rightLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1;
bool hasLeftLine = leftLineWidth > 0;
- bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
- || isLastColumn;
+ bool hasRightLine = (rightLineWidth > 0 && skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value >= 2.4m) || isLastColumn;
Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White;
Color4 backgroundColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, columnIndex)?.Value ?? Color4.Black;
diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json
index 01a5985464..61be25b845 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/net5.0/osu.Game.Rulesets.Osu.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Osu.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Osu.Tests.dll"
+ "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Osu.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
index 559d612037..70a9c03e65 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
@@ -7,6 +7,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
@@ -72,7 +73,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
- AddStep("change beat divisor", () => beatDivisor.Value = 3);
+ AddStep("change beat divisor", () =>
+ {
+ beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
+ beatDivisor.Value = 3;
+ });
convertToStream();
diff --git a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs
new file mode 100644
index 0000000000..d8c10b814d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs
@@ -0,0 +1,108 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Osu.Skinning.Legacy;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [HeadlessTest]
+ public class LegacyMainCirclePieceTest : OsuTestScene
+ {
+ private static readonly object?[][] texture_priority_cases =
+ {
+ // default priority lookup
+ new object?[]
+ {
+ // available textures
+ new[] { @"hitcircle", @"hitcircleoverlay" },
+ // priority lookup prefix
+ null,
+ // expected circle and overlay
+ @"hitcircle", @"hitcircleoverlay",
+ },
+ // custom priority lookup
+ new object?[]
+ {
+ new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle", @"sliderstartcircleoverlay" },
+ @"sliderstartcircle",
+ @"sliderstartcircle", @"sliderstartcircleoverlay",
+ },
+ // when no sprites are available for the specified prefix, fall back to "hitcircle"/"hitcircleoverlay".
+ new object?[]
+ {
+ new[] { @"hitcircle", @"hitcircleoverlay" },
+ @"sliderstartcircle",
+ @"hitcircle", @"hitcircleoverlay",
+ },
+ // when a circle is available for the specified prefix but no overlay exists, no overlay is displayed.
+ new object?[]
+ {
+ new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle" },
+ @"sliderstartcircle",
+ @"sliderstartcircle", null
+ },
+ // when no circle is available for the specified prefix but an overlay exists, the overlay is ignored.
+ new object?[]
+ {
+ new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircleoverlay" },
+ @"sliderstartcircle",
+ @"hitcircle", @"hitcircleoverlay",
+ }
+ };
+
+ [TestCaseSource(nameof(texture_priority_cases))]
+ public void TestTexturePriorities(string[] textureFilenames, string priorityLookup, string? expectedCircle, string? expectedOverlay)
+ {
+ TestLegacyMainCirclePiece piece = null!;
+
+ AddStep("load circle piece", () =>
+ {
+ var skin = new Mock();
+
+ // shouldn't be required as GetTexture(string) calls GetTexture(string, WrapMode, WrapMode) by default,
+ // but moq doesn't handle that well, therefore explicitly requiring to use `CallBase`:
+ // https://github.com/moq/moq4/issues/972
+ skin.Setup(s => s.GetTexture(It.IsAny())).CallBase();
+
+ skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny(), It.IsAny()))
+ .Returns((string componentName, WrapMode _, WrapMode __) => new Texture(1, 1) { AssetName = componentName });
+
+ Child = new DependencyProvidingContainer
+ {
+ CachedDependencies = new (Type, object)[] { (typeof(ISkinSource), skin.Object) },
+ Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
+ };
+
+ var sprites = this.ChildrenOfType().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray();
+ Debug.Assert(sprites.Length <= 2);
+ });
+
+ AddAssert("check circle sprite", () => piece.CircleSprite?.Texture?.AssetName == expectedCircle);
+ AddAssert("check overlay sprite", () => piece.OverlaySprite?.Texture?.AssetName == expectedOverlay);
+ }
+
+ private class TestLegacyMainCirclePiece : LegacyMainCirclePiece
+ {
+ public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
+ public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
+
+ public TestLegacyMainCirclePiece(string? priorityLookupPrefix)
+ : base(priorityLookupPrefix, false)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs
similarity index 71%
rename from osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs
rename to osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs
index b8310bc4e7..9b49e60363 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs
@@ -6,18 +6,18 @@ using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
- public class TestSceneOsuModAimAssist : OsuModTestScene
+ public class TestSceneOsuModMagnetised : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
- public void TestAimAssist(float strength)
+ public void TestMagnetised(float strength)
{
CreateModTest(new ModTestData
{
- Mod = new OsuModAimAssist
+ Mod = new OsuModMagnetised
{
- AssistStrength = { Value = strength },
+ AttractionStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
index 24e69703a6..a8953c1a6f 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
@@ -8,12 +8,15 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
@@ -23,13 +26,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
protected override bool AllowFail => true;
[Test]
- public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData
+ public void TestSpinnerAutoCompleted()
{
- Mod = new OsuModSpunOut(),
- Autoplay = false,
- Beatmap = singleSpinnerBeatmap,
- PassCondition = () => Player.ChildrenOfType().SingleOrDefault()?.Progress >= 1
- });
+ DrawableSpinner spinner = null;
+ JudgementResult lastResult = null;
+
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSpunOut(),
+ Autoplay = false,
+ Beatmap = singleSpinnerBeatmap,
+ PassCondition = () =>
+ {
+ // Bind to the first spinner's results for further tracking.
+ if (spinner == null)
+ {
+ // We only care about the first spinner we encounter for this test.
+ var nextSpinner = Player.ChildrenOfType().SingleOrDefault();
+
+ if (nextSpinner == null)
+ return false;
+
+ lastResult = null;
+
+ spinner = nextSpinner;
+ spinner.OnNewResult += (o, result) => lastResult = result;
+ }
+
+ return lastResult?.Type == HitResult.Great;
+ }
+ });
+ }
[TestCase(null)]
[TestCase(typeof(OsuModDoubleTime))]
@@ -48,7 +75,57 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
PassCondition = () =>
{
var counter = Player.ChildrenOfType().SingleOrDefault();
- return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
+ var spinner = Player.ChildrenOfType().FirstOrDefault();
+
+ if (counter == null || spinner == null)
+ return false;
+
+ // ignore cases where the spinner hasn't started as these lead to false-positives
+ if (Precision.AlmostEquals(counter.Result.Value, 0, 1))
+ return false;
+
+ float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration);
+
+ return Precision.AlmostEquals(counter.Result.Value, rotationSpeed * 1000 * 60, 1);
+ }
+ });
+ }
+
+ [Test]
+ public void TestSpinnerGetsNoBonusScore()
+ {
+ DrawableSpinner spinner = null;
+ List results = new List();
+
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSpunOut(),
+ Autoplay = false,
+ Beatmap = singleSpinnerBeatmap,
+ PassCondition = () =>
+ {
+ // Bind to the first spinner's results for further tracking.
+ if (spinner == null)
+ {
+ // We only care about the first spinner we encounter for this test.
+ var nextSpinner = Player.ChildrenOfType().SingleOrDefault();
+
+ if (nextSpinner == null)
+ return false;
+
+ spinner = nextSpinner;
+ spinner.OnNewResult += (o, result) => results.Add(result);
+
+ results.Clear();
+ }
+
+ // we should only be checking the bonus/progress after the spinner has fully completed.
+ if (results.OfType().All(r => r.TimeCompleted == null))
+ return false;
+
+ return
+ results.Any(r => r.Type == HitResult.SmallBonus)
+ && results.All(r => r.Type != HitResult.LargeBonus);
}
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index b7984e6995..df577ea8d3 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,15 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.6972307565739273d, "diffcalc-test")]
- [TestCase(1.4484754139145539d, "zero-length-sliders")]
- public void Test(double expected, string name)
- => base.Test(expected, name);
+ [TestCase(6.6972307565739273d, 206, "diffcalc-test")]
+ [TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
+ public void Test(double expectedStarRating, int expectedMaxCombo, string name)
+ => base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(8.9382559208689809d, "diffcalc-test")]
- [TestCase(1.7548875851757628d, "zero-length-sliders")]
- public void TestClockRateAdjusted(double expected, string name)
- => Test(expected, name, new OsuModDoubleTime());
+ [TestCase(8.9382559208689809d, 206, "diffcalc-test")]
+ [TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
+ public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
+
+ [TestCase(6.6972307218715166d, 239, "diffcalc-test")]
+ [TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
+ public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
index 840d871b7b..a9325f98f7 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
@@ -13,7 +12,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK;
@@ -67,11 +65,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestAutoMod : OsuModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
- Replay = new MissingAutoGenerator(beatmap, mods).Generate()
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new MissingAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
private class MissingAutoGenerator : OsuAutoGeneratorBase
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index fea2e408f6..4ce29ab5c7 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -3,16 +3,16 @@
-
+
WinExe
- net5.0
+ net6.0
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
index 2d3cc3c103..a5282877ee 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
+using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
@@ -20,13 +21,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
new BeatmapStatistic
{
- Name = @"Circle Count",
+ Name = BeatmapsetsStrings.ShowStatsCountCircles,
Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
},
new BeatmapStatistic
{
- Name = @"Slider Count",
+ Name = BeatmapsetsStrings.ShowStatsCountSliders,
Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
},
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index 126a9b0183..3deed4ea3d 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -12,30 +12,68 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuDifficultyAttributes : DifficultyAttributes
{
+ ///
+ /// The difficulty corresponding to the aim skill.
+ ///
[JsonProperty("aim_difficulty")]
public double AimDifficulty { get; set; }
+ ///
+ /// The difficulty corresponding to the speed skill.
+ ///
[JsonProperty("speed_difficulty")]
public double SpeedDifficulty { get; set; }
+ ///
+ /// The difficulty corresponding to the flashlight skill.
+ ///
[JsonProperty("flashlight_difficulty")]
public double FlashlightDifficulty { get; set; }
+ ///
+ /// Describes how much of is contributed to by hitcircles or sliders.
+ /// A value closer to 1.0 indicates most of is contributed by hitcircles.
+ /// A value closer to 0.0 indicates most of is contributed by sliders.
+ ///
[JsonProperty("slider_factor")]
public double SliderFactor { get; set; }
+ ///
+ /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ ///
+ /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
+ ///
[JsonProperty("approach_rate")]
public double ApproachRate { get; set; }
+ ///
+ /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ ///
+ /// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing.
+ ///
[JsonProperty("overall_difficulty")]
public double OverallDifficulty { get; set; }
+ ///
+ /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
+ ///
public double DrainRate { get; set; }
+ ///
+ /// The number of hitcircles in the beatmap.
+ ///
public int HitCircleCount { get; set; }
+ ///
+ /// The number of sliders in the beatmap.
+ ///
public int SliderCount { get; set; }
+ ///
+ /// The number of spinners in the beatmap.
+ ///
public int SpinnerCount { get; set; }
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index c5b1baaad1..df6fd19d36 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -61,10 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
-
- int maxCombo = beatmap.HitObjects.Count;
- // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
- maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1);
+ int maxCombo = beatmap.GetMaxCombo();
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 604ab73454..a93a1641a1 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -14,10 +13,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceCalculator : PerformanceCalculator
{
- public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
-
- private Mod[] mods;
-
private double accuracy;
private int scoreMaxCombo;
private int countGreat;
@@ -27,31 +22,32 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double effectiveMissCount;
- public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
- : base(ruleset, attributes, score)
+ public OsuPerformanceCalculator()
+ : base(new OsuRuleset())
{
}
- public override PerformanceAttributes Calculate()
+ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{
- mods = Score.Mods;
- accuracy = Score.Accuracy;
- scoreMaxCombo = Score.MaxCombo;
- countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
- countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
- countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
- countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
- effectiveMissCount = calculateEffectiveMissCount();
+ var osuAttributes = (OsuDifficultyAttributes)attributes;
+
+ accuracy = score.Accuracy;
+ scoreMaxCombo = score.MaxCombo;
+ countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
+ countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
+ countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
+ countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
+ effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
- if (mods.Any(m => m is OsuModNoFail))
+ if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
- if (mods.Any(m => m is OsuModSpunOut) && totalHits > 0)
- multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
+ if (score.Mods.Any(m => m is OsuModSpunOut) && totalHits > 0)
+ multiplier *= 1.0 - Math.Pow((double)osuAttributes.SpinnerCount / totalHits, 0.85);
- if (mods.Any(h => h is OsuModRelax))
+ if (score.Mods.Any(h => h is OsuModRelax))
{
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits);
@@ -59,10 +55,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
multiplier *= 0.6;
}
- double aimValue = computeAimValue();
- double speedValue = computeSpeedValue();
- double accuracyValue = computeAccuracyValue();
- double flashlightValue = computeFlashlightValue();
+ double aimValue = computeAimValue(score, osuAttributes);
+ double speedValue = computeSpeedValue(score, osuAttributes);
+ double accuracyValue = computeAccuracyValue(score, osuAttributes);
+ double flashlightValue = computeFlashlightValue(score, osuAttributes);
double totalValue =
Math.Pow(
Math.Pow(aimValue, 1.1) +
@@ -82,11 +78,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
};
}
- private double computeAimValue()
+ private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- double rawAim = Attributes.AimDifficulty;
+ double rawAim = attributes.AimDifficulty;
- if (mods.Any(m => m is OsuModTouchDevice))
+ if (score.Mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
@@ -99,44 +95,44 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (effectiveMissCount > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
- aimValue *= getComboScalingFactor();
+ aimValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0;
- if (Attributes.ApproachRate > 10.33)
- approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33);
- else if (Attributes.ApproachRate < 8.0)
- approachRateFactor = 0.1 * (8.0 - Attributes.ApproachRate);
+ if (attributes.ApproachRate > 10.33)
+ approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
+ else if (attributes.ApproachRate < 8.0)
+ approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate);
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
- if (mods.Any(m => m is OsuModBlinds))
- aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
- else if (mods.Any(h => h is OsuModHidden))
+ if (score.Mods.Any(m => m is OsuModBlinds))
+ aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
+ else if (score.Mods.Any(h => h is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
- aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
+ aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
- double estimateDifficultSliders = Attributes.SliderCount * 0.15;
+ double estimateDifficultSliders = attributes.SliderCount * 0.15;
- if (Attributes.SliderCount > 0)
+ if (attributes.SliderCount > 0)
{
- double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, Attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
- double sliderNerfFactor = (1 - Attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + Attributes.SliderFactor;
+ double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
+ double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
aimValue *= sliderNerfFactor;
}
aimValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy.
- aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
+ aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
return aimValue;
}
- private double computeSpeedValue()
+ private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
+ double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@@ -146,27 +142,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (effectiveMissCount > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
- speedValue *= getComboScalingFactor();
+ speedValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0;
- if (Attributes.ApproachRate > 10.33)
- approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33);
+ if (attributes.ApproachRate > 10.33)
+ approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
- if (mods.Any(m => m is OsuModBlinds))
+ if (score.Mods.Any(m => m is OsuModBlinds))
{
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
speedValue *= 1.12;
}
- else if (mods.Any(m => m is OsuModHidden))
+ else if (score.Mods.Any(m => m is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
- speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
+ speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
// Scale the speed value with accuracy and OD.
- speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
+ speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@@ -174,14 +170,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return speedValue;
}
- private double computeAccuracyValue()
+ private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- if (mods.Any(h => h is OsuModRelax))
+ if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage;
- int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
+ int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
@@ -194,43 +190,43 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
- double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
+ double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
- if (mods.Any(m => m is OsuModBlinds))
+ if (score.Mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14;
- else if (mods.Any(m => m is OsuModHidden))
+ else if (score.Mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08;
- if (mods.Any(m => m is OsuModFlashlight))
+ if (score.Mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02;
return accuracyValue;
}
- private double computeFlashlightValue()
+ private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- if (!mods.Any(h => h is OsuModFlashlight))
+ if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;
- double rawFlashlight = Attributes.FlashlightDifficulty;
+ double rawFlashlight = attributes.FlashlightDifficulty;
- if (mods.Any(m => m is OsuModTouchDevice))
+ if (score.Mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8);
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
- if (mods.Any(h => h is OsuModHidden))
+ if (score.Mods.Any(h => h is OsuModHidden))
flashlightValue *= 1.3;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
- flashlightValue *= getComboScalingFactor();
+ flashlightValue *= getComboScalingFactor(attributes);
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
@@ -239,19 +235,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that.
- flashlightValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
+ flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
return flashlightValue;
}
- private double calculateEffectiveMissCount()
+ private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
{
// Guess the number of misses + slider breaks from combo
double comboBasedMissCount = 0.0;
- if (Attributes.SliderCount > 0)
+ if (attributes.SliderCount > 0)
{
- double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount;
+ double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
if (scoreMaxCombo < fullComboThreshold)
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
}
@@ -262,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return Math.Max(countMiss, comboBasedMissCount);
}
- private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
+ private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 983964d639..aaf455e95f 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
public bool PerformFail() => false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index 2668013321..b31ef5d2fd 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
@@ -5,21 +5,16 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Replays;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
- Replay = new OsuAutoGenerator(beatmap, mods).Generate()
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index ad4c5dfd5d..7567c96b50 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1.12;
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
+
private DrawableOsuBlinds blinds;
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
index ff31cfcd18..5b42772358 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
@@ -5,22 +5,17 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
- Replay = new OsuAutoGenerator(beatmap, mods).Generate()
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
index e04a30d06c..f46573c494 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Configuration;
@@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset
{
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) };
+
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index 38c84be295..44d72fae61 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
@@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModFlashlight : ModFlashlight, IApplicableToDrawableHitObject
{
public override double ScoreMultiplier => 1.12;
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
private const double default_follow_delay = 120;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
similarity index 81%
rename from osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs
rename to osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index ed4b139e00..ca6e9cfb1d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -16,20 +16,20 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
- internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset
+ internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset
{
- public override string Name => "Aim Assist";
- public override string Acronym => "AA";
- public override IconUsage? Icon => FontAwesome.Solid.MousePointer;
+ public override string Name => "Magnetised";
+ public override string Acronym => "MG";
+ public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun;
- public override string Description => "No need to chase the circle – the circle chases you!";
+ public override string Description => "No need to chase the circles – your cursor is a magnet!";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
private IFrameStableClock gameplayClock;
- [SettingSource("Assist strength", "How much this mod will assist you.", 0)]
- public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f)
+ [SettingSource("Attraction strength", "How strong the pull is.", 0)]
+ public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
{
Precision = 0.05f,
MinValue = 0.05f,
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void easeTo(DrawableHitObject hitObject, Vector2 destination)
{
- double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value);
+ double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
index 778447e444..70c075276f 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
- public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
+ public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 9b9ebcad04..fea9246035 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -1,19 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System;
-using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
-using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -26,281 +24,40 @@ namespace osu.Game.Rulesets.Osu.Mods
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
- ///
- /// Number of previous hitobjects to be shifted together when another object is being moved.
- ///
- private const int preceding_hitobjects_to_shift = 10;
-
- private Random rng;
+ private Random? rng;
public void ApplyToBeatmap(IBeatmap beatmap)
{
if (!(beatmap is OsuBeatmap osuBeatmap))
return;
- var hitObjects = osuBeatmap.HitObjects;
-
Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value);
- RandomObjectInfo previous = null;
+ var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects);
float rateOfChangeMultiplier = 0;
- for (int i = 0; i < hitObjects.Count; i++)
+ foreach (var positionInfo in positionInfos)
{
- var hitObject = hitObjects[i];
-
- var current = new RandomObjectInfo(hitObject);
-
// rateOfChangeMultiplier only changes every 5 iterations in a combo
// to prevent shaky-line-shaped streams
- if (hitObject.IndexInCurrentCombo % 5 == 0)
+ if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
- if (hitObject is Spinner)
+ if (positionInfo == positionInfos.First())
{
- previous = null;
- continue;
+ positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
+ positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
}
-
- applyRandomisation(rateOfChangeMultiplier, previous, current);
-
- // Move hit objects back into the playfield if they are outside of it
- Vector2 shift = Vector2.Zero;
-
- switch (hitObject)
+ else
{
- case HitCircle circle:
- shift = clampHitCircleToPlayfield(circle, current);
- break;
-
- case Slider slider:
- shift = clampSliderToPlayfield(slider, current);
- break;
+ positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f));
}
-
- if (shift != Vector2.Zero)
- {
- var toBeShifted = new List();
-
- for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
- {
- // only shift hit circles
- if (!(hitObjects[j] is HitCircle)) break;
-
- toBeShifted.Add(hitObjects[j]);
- }
-
- if (toBeShifted.Count > 0)
- applyDecreasingShift(toBeShifted, shift);
- }
-
- previous = current;
- }
- }
-
- ///
- /// Returns the final position of the hit object
- ///
- /// Final position of the hit object
- private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo previous, RandomObjectInfo current)
- {
- if (previous == null)
- {
- var playfieldSize = OsuPlayfield.BASE_SIZE;
-
- current.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
- current.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y);
-
- return;
}
- float distanceToPrev = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
-
- // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
- // is proportional to the distance between the last and the current hit object
- // to allow jumps and prevent too sharp turns during streams.
-
- // Allow maximum jump angle when jump distance is more than half of playfield diagonal length
- double randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, distanceToPrev / (playfield_diagonal * 0.5f));
-
- current.AngleRad = (float)randomAngleRad + previous.AngleRad;
- if (current.AngleRad < 0)
- current.AngleRad += 2 * (float)Math.PI;
-
- var posRelativeToPrev = new Vector2(
- distanceToPrev * (float)Math.Cos(current.AngleRad),
- distanceToPrev * (float)Math.Sin(current.AngleRad)
- );
-
- posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev);
-
- current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
-
- current.PositionRandomised = previous.EndPositionRandomised + posRelativeToPrev;
- }
-
- ///
- /// Move the randomised position of a hit circle so that it fits inside the playfield.
- ///
- /// The deviation from the original randomised position in order to fit within the playfield.
- private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo)
- {
- var previousPosition = objectInfo.PositionRandomised;
- objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding(
- objectInfo.PositionRandomised,
- (float)circle.Radius
- );
-
- circle.Position = objectInfo.PositionRandomised;
-
- return objectInfo.PositionRandomised - previousPosition;
- }
-
- ///
- /// Moves the and all necessary nested s into the if they aren't already.
- ///
- /// The deviation from the original randomised position in order to fit within the playfield.
- private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo)
- {
- var possibleMovementBounds = calculatePossibleMovementBounds(slider);
-
- var previousPosition = objectInfo.PositionRandomised;
-
- // Clamp slider position to the placement area
- // If the slider is larger than the playfield, force it to stay at the original position
- float newX = possibleMovementBounds.Width < 0
- ? objectInfo.PositionOriginal.X
- : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
-
- float newY = possibleMovementBounds.Height < 0
- ? objectInfo.PositionOriginal.Y
- : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
-
- slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY);
- objectInfo.EndPositionRandomised = slider.EndPosition;
-
- shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal);
-
- return objectInfo.PositionRandomised - previousPosition;
- }
-
- ///
- /// Decreasingly shift a list of s by a specified amount.
- /// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
- ///
- /// The list of hit objects to be shifted.
- /// The amount to be shifted.
- private void applyDecreasingShift(IList hitObjects, Vector2 shift)
- {
- for (int i = 0; i < hitObjects.Count; i++)
- {
- var hitObject = hitObjects[i];
- // The first object is shifted by a vector slightly smaller than shift
- // The last object is shifted by a vector slightly larger than zero
- Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
-
- hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
- }
- }
-
- ///
- /// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates)
- /// such that the entire slider is inside the playfield.
- ///
- ///
- /// If the slider is larger than the playfield, the returned may have negative width/height.
- ///
- private RectangleF calculatePossibleMovementBounds(Slider slider)
- {
- var pathPositions = new List();
- slider.Path.GetPathToProgress(pathPositions, 0, 1);
-
- float minX = float.PositiveInfinity;
- float maxX = float.NegativeInfinity;
-
- float minY = float.PositiveInfinity;
- float maxY = float.NegativeInfinity;
-
- // Compute the bounding box of the slider.
- foreach (var pos in pathPositions)
- {
- minX = MathF.Min(minX, pos.X);
- maxX = MathF.Max(maxX, pos.X);
-
- minY = MathF.Min(minY, pos.Y);
- maxY = MathF.Max(maxY, pos.Y);
- }
-
- // Take the circle radius into account.
- float radius = (float)slider.Radius;
-
- minX -= radius;
- minY -= radius;
-
- maxX += radius;
- maxY += radius;
-
- // Given the bounding box of the slider (via min/max X/Y),
- // the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
- // and the amount that it can move to the right is WIDTH - maxX.
- // Same calculation applies for the Y axis.
- float left = -minX;
- float right = OsuPlayfield.BASE_SIZE.X - maxX;
- float top = -minY;
- float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
-
- return new RectangleF(left, top, right - left, bottom - top);
- }
-
- ///
- /// Shifts all nested s and s by the specified shift.
- ///
- /// whose nested s and s should be shifted
- /// The the 's nested s and s should be shifted by
- private void shiftNestedObjects(Slider slider, Vector2 shift)
- {
- foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
- {
- if (!(hitObject is OsuHitObject osuHitObject))
- continue;
-
- osuHitObject.Position += shift;
- }
- }
-
- ///
- /// Clamp a position to playfield, keeping a specified distance from the edges.
- ///
- /// The position to be clamped.
- /// The minimum distance allowed from playfield edges.
- /// The clamped position.
- private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
- {
- return new Vector2(
- Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
- Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
- );
- }
-
- private class RandomObjectInfo
- {
- public float AngleRad { get; set; }
-
- public Vector2 PositionOriginal { get; }
- public Vector2 PositionRandomised { get; set; }
-
- public Vector2 EndPositionOriginal { get; }
- public Vector2 EndPositionRandomised { get; set; }
-
- public RandomObjectInfo(OsuHitObject hitObject)
- {
- PositionRandomised = PositionOriginal = hitObject.Position;
- EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
- AngleRad = 0;
- }
+ osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 1bf63ef6d4..6b81efdca6 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
- public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray();
///
/// How early before a hitobject's start time to trigger a hit.
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index 098c639949..9be0dc748a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -45,7 +45,11 @@ namespace osu.Game.Rulesets.Osu.Mods
// for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time.
// for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here.
double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate;
- spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f));
+
+ // multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact,
+ // some spinners may not complete due to very minor decimal loss during calculation
+ float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration);
+ spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
new file mode 100644
index 0000000000..ee325db66a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
@@ -0,0 +1,148 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using System.Threading;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModStrictTracking : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset
+ {
+ public override string Name => @"Strict Tracking";
+ public override string Acronym => @"ST";
+ public override IconUsage? Icon => FontAwesome.Solid.PenFancy;
+ public override ModType Type => ModType.DifficultyIncrease;
+ public override string Description => @"Follow circles just got serious...";
+ public override double ScoreMultiplier => 1.0;
+ public override Type[] IncompatibleMods => new[] { typeof(ModClassic) };
+
+ public void ApplyToDrawableHitObject(DrawableHitObject drawable)
+ {
+ if (drawable is DrawableSlider slider)
+ {
+ slider.Tracking.ValueChanged += e =>
+ {
+ if (e.NewValue || slider.Judged) return;
+
+ var tail = slider.NestedHitObjects.OfType().First();
+
+ if (!tail.Judged)
+ tail.MissForcefully();
+ };
+ }
+ }
+
+ public void ApplyToBeatmap(IBeatmap beatmap)
+ {
+ var osuBeatmap = (OsuBeatmap)beatmap;
+
+ if (osuBeatmap.HitObjects.Count == 0) return;
+
+ var hitObjects = osuBeatmap.HitObjects.Select(ho =>
+ {
+ if (ho is Slider slider)
+ {
+ var newSlider = new StrictTrackingSlider(slider);
+ return newSlider;
+ }
+
+ return ho;
+ }).ToList();
+
+ osuBeatmap.HitObjects = hitObjects;
+ }
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ drawableRuleset.Playfield.RegisterPool(10, 100);
+ }
+
+ private class StrictTrackingSliderTailCircle : SliderTailCircle
+ {
+ public StrictTrackingSliderTailCircle(Slider slider)
+ : base(slider)
+ {
+ }
+
+ public override Judgement CreateJudgement() => new OsuJudgement();
+ }
+
+ private class StrictTrackingDrawableSliderTail : DrawableSliderTail
+ {
+ public override bool DisplayResult => true;
+ }
+
+ private class StrictTrackingSlider : Slider
+ {
+ public StrictTrackingSlider(Slider original)
+ {
+ StartTime = original.StartTime;
+ Samples = original.Samples;
+ Path = original.Path;
+ NodeSamples = original.NodeSamples;
+ RepeatCount = original.RepeatCount;
+ Position = original.Position;
+ NewCombo = original.NewCombo;
+ ComboOffset = original.ComboOffset;
+ LegacyLastTickOffset = original.LegacyLastTickOffset;
+ TickDistanceMultiplier = original.TickDistanceMultiplier;
+ }
+
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
+
+ foreach (var e in sliderEvents)
+ {
+ switch (e.Type)
+ {
+ case SliderEventType.Head:
+ AddNested(HeadCircle = new SliderHeadCircle
+ {
+ StartTime = e.Time,
+ Position = Position,
+ StackHeight = StackHeight,
+ });
+ break;
+
+ case SliderEventType.LegacyLastTick:
+ AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
+ {
+ RepeatIndex = e.SpanIndex,
+ StartTime = e.Time,
+ Position = EndPosition,
+ StackHeight = StackHeight
+ });
+ break;
+
+ case SliderEventType.Repeat:
+ AddNested(new SliderRepeat(this)
+ {
+ RepeatIndex = e.SpanIndex,
+ StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
+ Position = Position + Path.PositionAt(e.PathProgress),
+ StackHeight = StackHeight,
+ Scale = Scale,
+ });
+ break;
+ }
+ }
+
+ UpdateNestedSamples();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index 28c3b069b6..45ce4d555a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) };
private float theta;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index 40a05400ea..693a5bee0b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 628d95dff4..fa2d2ba38c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -6,10 +6,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
-using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@@ -21,10 +21,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public readonly IBindable ScaleBindable = new BindableFloat();
public readonly IBindable IndexInCurrentComboBindable = new Bindable();
- // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects.
+ // Must be set to update IsHovered as it's used in relax mod to detect osu hit objects.
public override bool HandlePositionalInput => true;
- protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
+ protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
///
/// Whether this can be hit, given a time value.
@@ -89,6 +89,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
+ private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent.ScreenSpaceDrawQuad.AABBFloat;
+
+ ///
+ /// Calculates the position of the given relative to the playfield area.
+ ///
+ /// The drawable to calculate its relative position.
+ protected float CalculateDrawableRelativePosition(Drawable drawable) => (drawable.ScreenSpaceDrawQuad.Centre.X - parentScreenSpaceRectangle.X) / parentScreenSpaceRectangle.Width;
+
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 3acec4498d..c48ab998ba 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -2,23 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
-using osuTK;
-using osu.Framework.Graphics;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
-using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
-using osuTK.Graphics;
using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -126,18 +124,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
-
- var slidingSamples = new List();
-
- var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
- if (normalSample != null)
- slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide"));
-
- var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
- if (whistleSample != null)
- slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle"));
-
- slidingSample.Samples = slidingSamples.ToArray();
+ slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
}
public override void StopAllSamples()
@@ -220,7 +207,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position
- slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X);
+ slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index ec1387eb54..64964ed396 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -74,6 +74,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
+ protected override void LoadSamples()
+ {
+ // Tail models don't actually get samples, as the playback is handled by DrawableSlider.
+ // This override is only here for visibility in explaining this weird flow.
+ }
+
+ public override void PlaySamples()
+ {
+ // Tail models don't actually get samples, as the playback is handled by DrawableSlider.
+ // This override is only here for visibility in explaining this weird flow.
+ }
+
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index c6db02ee02..a904658a4c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -121,15 +121,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.LoadSamples();
- var firstSample = HitObject.Samples.FirstOrDefault();
-
- if (firstSample != null)
- {
- var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin");
-
- spinningSample.Samples = new ISampleInfo[] { clone };
- spinningSample.Frequency.Value = spinning_sample_initial_frequency;
- }
+ spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
+ spinningSample.Frequency.Value = spinning_sample_initial_frequency;
}
private void updateSpinningSample(ValueChangedEvent tracking)
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 5c1c3fd253..a698311bf7 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -29,6 +29,23 @@ namespace osu.Game.Rulesets.Osu.Objects
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
+ public override IList AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray();
+
+ public IList CreateSlidingSamples()
+ {
+ var slidingSamples = new List();
+
+ var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
+ if (normalSample != null)
+ slidingSamples.Add(normalSample.With("sliderslide"));
+
+ var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
+ if (whistleSample != null)
+ slidingSamples.Add(whistleSample.With("sliderwhistle"));
+
+ return slidingSamples;
+ }
+
private readonly Cached endPositionCache = new Cached();
public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1);
@@ -137,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider()
{
- SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples();
+ SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions();
}
@@ -210,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects
}
}
- updateNestedSamples();
+ UpdateNestedSamples();
}
private void updateNestedPositions()
@@ -224,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects
TailCircle.Position = EndPosition;
}
- private void updateNestedSamples()
+ protected void UpdateNestedSamples()
{
var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 1eddfb7fef..ddee4d3ebd 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -1,7 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@@ -73,5 +77,20 @@ namespace osu.Game.Rulesets.Osu.Objects
public override Judgement CreateJudgement() => new OsuJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+ public override IList AuxiliarySamples => CreateSpinningSamples();
+
+ public HitSampleInfo[] CreateSpinningSamples()
+ {
+ var referenceSample = Samples.FirstOrDefault();
+
+ if (referenceSample == null)
+ return Array.Empty();
+
+ return new[]
+ {
+ SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin")
+ };
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index ad00a025a1..207e7a4ab0 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
+ new OsuModStrictTracking()
};
case ModType.Conversion:
@@ -194,7 +195,8 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
- new OsuModAimAssist(),
+ new OsuModMagnetised(),
+ new ModAdaptiveSpeed()
};
case ModType.System:
@@ -212,7 +214,7 @@ namespace osu.Game.Rulesets.Osu
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(RulesetInfo, beatmap);
- public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score);
+ public override PerformanceCalculator CreatePerformanceCalculator() => new OsuPerformanceCalculator();
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
@@ -314,6 +316,7 @@ namespace osu.Game.Rulesets.Osu
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
+ new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
}
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index 44118227d9..ab0c0850dc 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -11,6 +11,13 @@ namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuScoreProcessor : ScoreProcessor
{
+ public OsuScoreProcessor()
+ : base(new OsuRuleset())
+ {
+ }
+
+ protected override double ClassicScoreMultiplier => 36;
+
protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/KiaiFlash.cs b/osu.Game.Rulesets.Osu/Skinning/Default/KiaiFlash.cs
index d49b1713f6..506f679836 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/KiaiFlash.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/KiaiFlash.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 osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
@@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Child
.FadeTo(flash_opacity, EarlyActivationMilliseconds, Easing.OutQuint)
.Then()
- .FadeOut(timingPoint.BeatLength - fade_length, Easing.OutSine);
+ .FadeOut(Math.Max(fade_length, timingPoint.BeatLength - fade_length), Easing.OutSine);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index c6007885be..391147648f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
@@ -3,10 +3,10 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
@@ -16,63 +16,61 @@ using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
+#nullable enable
+
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class LegacyMainCirclePiece : CompositeDrawable
{
public override bool RemoveCompletedTransforms => false;
- private readonly string priorityLookup;
+ ///
+ /// A prioritised prefix to perform texture lookups with.
+ ///
+ private readonly string? priorityLookupPrefix;
+
private readonly bool hasNumber;
- public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
+ protected Drawable CircleSprite = null!;
+ protected Drawable OverlaySprite = null!;
+
+ protected Container OverlayLayer { get; private set; } = null!;
+
+ private SkinnableSpriteText hitCircleText = null!;
+
+ private readonly Bindable accentColour = new Bindable();
+ private readonly IBindable indexInCurrentCombo = new Bindable();
+
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? drawableObject { get; set; }
+
+ [Resolved]
+ private ISkinSource skin { get; set; } = null!;
+
+ public LegacyMainCirclePiece(string? priorityLookupPrefix = null, bool hasNumber = true)
{
- this.priorityLookup = priorityLookup;
+ this.priorityLookupPrefix = priorityLookupPrefix;
this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
- private Drawable hitCircleSprite;
-
- protected Container OverlayLayer { get; private set; }
-
- private Drawable hitCircleOverlay;
- private SkinnableSpriteText hitCircleText;
-
- private readonly Bindable accentColour = new Bindable();
- private readonly IBindable indexInCurrentCombo = new Bindable();
-
- [Resolved]
- private DrawableHitObject drawableObject { get; set; }
-
- [Resolved]
- private ISkinSource skin { get; set; }
-
[BackgroundDependencyLoader]
private void load()
{
- var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
+ var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
- bool allowFallback = false;
-
- // attempt lookup using priority specification
- Texture baseTexture = getTextureWithFallback(string.Empty);
-
- // if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
- if (baseTexture == null)
- {
- allowFallback = true;
- baseTexture = getTextureWithFallback(string.Empty);
- }
+ // if a base texture for the specified prefix exists, continue using it for subsequent lookups.
+ // otherwise fall back to the default prefix "hitcircle".
+ string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle";
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
- // the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
- // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
+ // the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
+ // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
InternalChildren = new[]
{
- hitCircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = baseTexture })
+ CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -81,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Child = hitCircleOverlay = new KiaiFlashingDrawable(() => getAnimationWithFallback(@"overlay", 1000 / 2d))
+ Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -105,39 +103,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
if (overlayAboveNumber)
- OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue);
+ OverlayLayer.ChangeChildDepth(OverlaySprite, float.MinValue);
- accentColour.BindTo(drawableObject.AccentColour);
- indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
-
- Texture getTextureWithFallback(string name)
+ if (drawableOsuObject != null)
{
- Texture tex = null;
-
- if (!string.IsNullOrEmpty(priorityLookup))
- {
- tex = skin.GetTexture($"{priorityLookup}{name}");
-
- if (!allowFallback)
- return tex;
- }
-
- return tex ?? skin.GetTexture($"hitcircle{name}");
- }
-
- Drawable getAnimationWithFallback(string name, double frameLength)
- {
- Drawable animation = null;
-
- if (!string.IsNullOrEmpty(priorityLookup))
- {
- animation = skin.GetAnimation($"{priorityLookup}{name}", true, true, frameLength: frameLength);
-
- if (!allowFallback)
- return animation;
- }
-
- return animation ?? skin.GetAnimation($"hitcircle{name}", true, true, frameLength: frameLength);
+ accentColour.BindTo(drawableOsuObject.AccentColour);
+ indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
}
}
@@ -145,28 +116,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
base.LoadComplete();
- accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
+ accentColour.BindValueChanged(colour => CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
- drawableObject.ApplyCustomUpdateState += updateStateTransforms;
- updateStateTransforms(drawableObject, drawableObject.State.Value);
+ if (drawableObject != null)
+ {
+ drawableObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableObject, drawableObject.State.Value);
+ }
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
const double legacy_fade_duration = 240;
- using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ using (BeginAbsoluteSequence(drawableObject.AsNonNull().HitStateUpdateTime))
{
switch (state)
{
case ArmedState.Hit:
- hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
- hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ CircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
+ CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
- hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
- hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ OverlaySprite.FadeOut(legacy_fade_duration, Easing.Out);
+ OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber)
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index ff9f6f0e07..900ad6f6d3 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (osuComponent.Component)
{
case OsuSkinComponents.FollowPoint:
- return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false);
+ return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false);
case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
index 97a4b14a62..da73c2addb 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Utils
{
- public static class OsuHitObjectGenerationUtils
+ public static partial class OsuHitObjectGenerationUtils
{
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
new file mode 100644
index 0000000000..d1bc3b45df
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -0,0 +1,340 @@
+// 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.Graphics.Primitives;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osuTK;
+
+#nullable enable
+
+namespace osu.Game.Rulesets.Osu.Utils
+{
+ public static partial class OsuHitObjectGenerationUtils
+ {
+ ///
+ /// Number of previous hitobjects to be shifted together when an object is being moved.
+ ///
+ private const int preceding_hitobjects_to_shift = 10;
+
+ private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
+
+ ///
+ /// Generate a list of s containing information for how the given list of
+ /// s are positioned.
+ ///
+ /// A list of s to process.
+ /// A list of s describing how each hit object is positioned relative to the previous one.
+ public static List GeneratePositionInfos(IEnumerable hitObjects)
+ {
+ var positionInfos = new List();
+ Vector2 previousPosition = playfield_centre;
+ float previousAngle = 0;
+
+ foreach (OsuHitObject hitObject in hitObjects)
+ {
+ Vector2 relativePosition = hitObject.Position - previousPosition;
+ float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ float relativeAngle = absoluteAngle - previousAngle;
+
+ positionInfos.Add(new ObjectPositionInfo(hitObject)
+ {
+ RelativeAngle = relativeAngle,
+ DistanceFromPrevious = relativePosition.Length
+ });
+
+ previousPosition = hitObject.EndPosition;
+ previousAngle = absoluteAngle;
+ }
+
+ return positionInfos;
+ }
+
+ ///
+ /// Reposition the hit objects according to the information in .
+ ///
+ /// Position information for each hit object.
+ /// The repositioned hit objects.
+ public static List RepositionHitObjects(IEnumerable objectPositionInfos)
+ {
+ List workingObjects = objectPositionInfos.Select(o => new WorkingObject(o)).ToList();
+ WorkingObject? previous = null;
+
+ for (int i = 0; i < workingObjects.Count; i++)
+ {
+ var current = workingObjects[i];
+ var hitObject = current.HitObject;
+
+ if (hitObject is Spinner)
+ {
+ previous = null;
+ continue;
+ }
+
+ computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null);
+
+ // Move hit objects back into the playfield if they are outside of it
+ Vector2 shift = Vector2.Zero;
+
+ switch (hitObject)
+ {
+ case HitCircle _:
+ shift = clampHitCircleToPlayfield(current);
+ break;
+
+ case Slider _:
+ shift = clampSliderToPlayfield(current);
+ break;
+ }
+
+ if (shift != Vector2.Zero)
+ {
+ var toBeShifted = new List();
+
+ for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
+ {
+ // only shift hit circles
+ if (!(workingObjects[j].HitObject is HitCircle)) break;
+
+ toBeShifted.Add(workingObjects[j].HitObject);
+ }
+
+ if (toBeShifted.Count > 0)
+ applyDecreasingShift(toBeShifted, shift);
+ }
+
+ previous = current;
+ }
+
+ return workingObjects.Select(p => p.HitObject).ToList();
+ }
+
+ ///
+ /// Compute the modified position of a hit object while attempting to keep it inside the playfield.
+ ///
+ /// The representing the hit object to have the modified position computed for.
+ /// The representing the hit object immediately preceding the current one.
+ /// The representing the hit object immediately preceding the one.
+ private static void computeModifiedPosition(WorkingObject current, WorkingObject? previous, WorkingObject? beforePrevious)
+ {
+ float previousAbsoluteAngle = 0f;
+
+ if (previous != null)
+ {
+ Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
+ Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
+ previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ }
+
+ float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
+
+ var posRelativeToPrev = new Vector2(
+ current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
+ current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
+ );
+
+ Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
+
+ posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
+
+ current.PositionModified = lastEndPosition + posRelativeToPrev;
+ }
+
+ ///
+ /// Move the modified position of a so that it fits inside the playfield.
+ ///
+ /// The deviation from the original modified position in order to fit within the playfield.
+ private static Vector2 clampHitCircleToPlayfield(WorkingObject workingObject)
+ {
+ var previousPosition = workingObject.PositionModified;
+ workingObject.EndPositionModified = workingObject.PositionModified = clampToPlayfieldWithPadding(
+ workingObject.PositionModified,
+ (float)workingObject.HitObject.Radius
+ );
+
+ workingObject.HitObject.Position = workingObject.PositionModified;
+
+ return workingObject.PositionModified - previousPosition;
+ }
+
+ ///
+ /// Moves the and all necessary nested s into the if they aren't already.
+ ///
+ /// The deviation from the original modified position in order to fit within the playfield.
+ private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
+ {
+ var slider = (Slider)workingObject.HitObject;
+ var possibleMovementBounds = calculatePossibleMovementBounds(slider);
+
+ var previousPosition = workingObject.PositionModified;
+
+ // Clamp slider position to the placement area
+ // If the slider is larger than the playfield, force it to stay at the original position
+ float newX = possibleMovementBounds.Width < 0
+ ? workingObject.PositionOriginal.X
+ : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
+
+ float newY = possibleMovementBounds.Height < 0
+ ? workingObject.PositionOriginal.Y
+ : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
+
+ slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
+ workingObject.EndPositionModified = slider.EndPosition;
+
+ shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
+
+ return workingObject.PositionModified - previousPosition;
+ }
+
+ ///
+ /// Decreasingly shift a list of s by a specified amount.
+ /// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
+ ///
+ /// The list of hit objects to be shifted.
+ /// The amount to be shifted.
+ private static void applyDecreasingShift(IList hitObjects, Vector2 shift)
+ {
+ for (int i = 0; i < hitObjects.Count; i++)
+ {
+ var hitObject = hitObjects[i];
+ // The first object is shifted by a vector slightly smaller than shift
+ // The last object is shifted by a vector slightly larger than zero
+ Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
+
+ hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
+ }
+ }
+
+ ///
+ /// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates)
+ /// such that the entire slider is inside the playfield.
+ ///
+ ///
+ /// If the slider is larger than the playfield, the returned may have negative width/height.
+ ///
+ private static RectangleF calculatePossibleMovementBounds(Slider slider)
+ {
+ var pathPositions = new List();
+ slider.Path.GetPathToProgress(pathPositions, 0, 1);
+
+ float minX = float.PositiveInfinity;
+ float maxX = float.NegativeInfinity;
+
+ float minY = float.PositiveInfinity;
+ float maxY = float.NegativeInfinity;
+
+ // Compute the bounding box of the slider.
+ foreach (var pos in pathPositions)
+ {
+ minX = MathF.Min(minX, pos.X);
+ maxX = MathF.Max(maxX, pos.X);
+
+ minY = MathF.Min(minY, pos.Y);
+ maxY = MathF.Max(maxY, pos.Y);
+ }
+
+ // Take the circle radius into account.
+ float radius = (float)slider.Radius;
+
+ minX -= radius;
+ minY -= radius;
+
+ maxX += radius;
+ maxY += radius;
+
+ // Given the bounding box of the slider (via min/max X/Y),
+ // the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
+ // and the amount that it can move to the right is WIDTH - maxX.
+ // Same calculation applies for the Y axis.
+ float left = -minX;
+ float right = OsuPlayfield.BASE_SIZE.X - maxX;
+ float top = -minY;
+ float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
+
+ return new RectangleF(left, top, right - left, bottom - top);
+ }
+
+ ///
+ /// Shifts all nested s and s by the specified shift.
+ ///
+ /// whose nested s and s should be shifted
+ /// The the 's nested s and s should be shifted by
+ private static void shiftNestedObjects(Slider slider, Vector2 shift)
+ {
+ foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
+ {
+ if (!(hitObject is OsuHitObject osuHitObject))
+ continue;
+
+ osuHitObject.Position += shift;
+ }
+ }
+
+ ///
+ /// Clamp a position to playfield, keeping a specified distance from the edges.
+ ///
+ /// The position to be clamped.
+ /// The minimum distance allowed from playfield edges.
+ /// The clamped position.
+ private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
+ {
+ return new Vector2(
+ Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
+ Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
+ );
+ }
+
+ public class ObjectPositionInfo
+ {
+ ///
+ /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
+ ///
+ ///
+ /// of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
+ ///
+ ///
+ /// If is 0, the player's cursor doesn't need to change its direction of movement when passing
+ /// the previous object to reach this one.
+ ///
+ public float RelativeAngle { get; set; }
+
+ ///
+ /// The jump distance from the previous hit object to this one.
+ ///
+ ///
+ /// of the first hit object in a beatmap is relative to the playfield center.
+ ///
+ public float DistanceFromPrevious { get; set; }
+
+ ///
+ /// The hit object associated with this .
+ ///
+ public OsuHitObject HitObject { get; }
+
+ public ObjectPositionInfo(OsuHitObject hitObject)
+ {
+ HitObject = hitObject;
+ }
+ }
+
+ private class WorkingObject
+ {
+ public Vector2 PositionOriginal { get; }
+ public Vector2 PositionModified { get; set; }
+ public Vector2 EndPositionModified { get; set; }
+
+ public ObjectPositionInfo PositionInfo { get; }
+ public OsuHitObject HitObject => PositionInfo.HitObject;
+
+ public WorkingObject(ObjectPositionInfo positionInfo)
+ {
+ PositionInfo = positionInfo;
+ PositionModified = PositionOriginal = HitObject.Position;
+ EndPositionModified = HitObject.EndPosition;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json
index 779ebba9ae..56ec7d8d9c 100644
--- a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json
+++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Taiko.Tests.dll"
+ "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Taiko.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
@@ -20,7 +20,7 @@
"request": "launch",
"program": "dotnet",
"args": [
- "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Taiko.Tests.dll"
+ "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Taiko.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index 2b1cbc580e..226da7df09 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(2.2420075288523802d, "diffcalc-test")]
- [TestCase(2.2420075288523802d, "diffcalc-test-strong")]
- public void Test(double expected, string name)
- => base.Test(expected, name);
+ [TestCase(2.2420075288523802d, 200, "diffcalc-test")]
+ [TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
+ public void Test(double expectedStarRating, int expectedMaxCombo, string name)
+ => base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(3.134084469440479d, "diffcalc-test")]
- [TestCase(3.134084469440479d, "diffcalc-test-strong")]
- public void TestClockRateAdjusted(double expected, string name)
- => Test(expected, name, new TaikoModDoubleTime());
+ [TestCase(3.134084469440479d, 200, "diffcalc-test")]
+ [TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
+ public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap);
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
index 63854e7ead..5c7e3954e8 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
@@ -28,9 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
// flying hits all land in one common scrolling container (and stay there for rewind purposes),
// so we need to manually get the latest one.
- flyingHit = this.ChildrenOfType()
- .OrderByDescending(h => h.HitObject.StartTime)
- .FirstOrDefault();
+ flyingHit = this.ChildrenOfType().MaxBy(h => h.HitObject.StartTime);
});
AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType);
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index ad3713e047..a6b8eb8651 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -9,9 +9,9 @@
WinExe
- net5.0
+ net6.0
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index 31f5a6f570..3dc5438072 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -9,18 +9,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyAttributes : DifficultyAttributes
{
+ ///
+ /// The difficulty corresponding to the stamina skill.
+ ///
[JsonProperty("stamina_difficulty")]
public double StaminaDifficulty { get; set; }
+ ///
+ /// The difficulty corresponding to the rhythm skill.
+ ///
[JsonProperty("rhythm_difficulty")]
public double RhythmDifficulty { get; set; }
+ ///
+ /// The difficulty corresponding to the colour skill.
+ ///
[JsonProperty("colour_difficulty")]
public double ColourDifficulty { get; set; }
+ ///
+ /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ ///
+ /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
+ ///
[JsonProperty("approach_rate")]
public double ApproachRate { get; set; }
+ ///
+ /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ ///
+ /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
+ ///
[JsonProperty("great_hit_window")]
public double GreatHitWindow { get; set; }
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index bcd55f8fae..a8122551ff 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -14,37 +14,35 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoPerformanceCalculator : PerformanceCalculator
{
- protected new TaikoDifficultyAttributes Attributes => (TaikoDifficultyAttributes)base.Attributes;
-
- private Mod[] mods;
private int countGreat;
private int countOk;
private int countMeh;
private int countMiss;
- public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
- : base(ruleset, attributes, score)
+ public TaikoPerformanceCalculator()
+ : base(new TaikoRuleset())
{
}
- public override PerformanceAttributes Calculate()
+ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{
- mods = Score.Mods;
- countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
- countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
- countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
- countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
+ var taikoAttributes = (TaikoDifficultyAttributes)attributes;
+
+ countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
+ countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
+ countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
+ countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
- if (mods.Any(m => m is ModNoFail))
+ if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.90;
- if (mods.Any(m => m is ModHidden))
+ if (score.Mods.Any(m => m is ModHidden))
multiplier *= 1.10;
- double difficultyValue = computeDifficultyValue();
- double accuracyValue = computeAccuracyValue();
+ double difficultyValue = computeDifficultyValue(score, taikoAttributes);
+ double accuracyValue = computeAccuracyValue(score, taikoAttributes);
double totalValue =
Math.Pow(
Math.Pow(difficultyValue, 1.1) +
@@ -59,30 +57,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
};
}
- private double computeDifficultyValue()
+ private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{
- double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
+ double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus;
difficultyValue *= Math.Pow(0.985, countMiss);
- if (mods.Any(m => m is ModHidden))
+ if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025;
- if (mods.Any(m => m is ModFlashlight))
+ if (score.Mods.Any(m => m is ModFlashlight))
difficultyValue *= 1.05 * lengthBonus;
- return difficultyValue * Score.Accuracy;
+ return difficultyValue * score.Accuracy;
}
- private double computeAccuracyValue()
+ private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{
- if (Attributes.GreatHitWindow <= 0)
+ if (attributes.GreatHitWindow <= 0)
return 0;
- double accValue = Math.Pow(150.0 / Attributes.GreatHitWindow, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0;
+ double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0;
// Bonus for many objects - it's harder to keep good accuracy up for longer
return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
index 5832ae3dc1..4b74b4991e 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
@@ -3,19 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Replays;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo { User = new APIUser { Username = "mekkadosu!" } },
- Replay = new TaikoAutoGenerator(beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs
index f76e04a069..fee0cb2744 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs
@@ -3,20 +3,15 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModCinema : ModCinema
{
- public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
- {
- ScoreInfo = new ScoreInfo { User = new APIUser { Username = "mekkadosu!" } },
- Replay = new TaikoAutoGenerator(beatmap).Generate(),
- };
+ public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
+ => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index 6520517039..5a6f57bc36 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
+ drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
}
public void Update(Playfield playfield)
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
index 9540e35780..99a064d35f 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
get
{
- string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N1}";
+ string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N2}";
return string.Join(", ", new[]
{
diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
index f047c03f4b..1a1fde1990 100644
--- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
///
/// Default size of a drawable taiko hit object.
///
- public const float DEFAULT_SIZE = 0.45f;
+ public const float DEFAULT_SIZE = 0.475f;
public override Judgement CreateJudgement() => new TaikoJudgement();
diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
index 6c17573b50..6e0f6a3109 100644
--- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
///
/// Scale multiplier for a strong drawable taiko hit object.
///
- public const float STRONG_SCALE = 1.4f;
+ public const float STRONG_SCALE = 1 / 0.65f;
///
/// Default size of a strong drawable taiko hit object.
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
index 1829ea2513..bacc22714e 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
@@ -7,8 +7,15 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
internal class TaikoScoreProcessor : ScoreProcessor
{
+ public TaikoScoreProcessor()
+ : base(new TaikoRuleset())
+ {
+ }
+
protected override double DefaultAccuracyPortion => 0.75;
protected override double DefaultComboPortion => 0.25;
+
+ protected override double ClassicScoreMultiplier => 22;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
index a106c4f629..f2452ad88c 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
@@ -11,6 +11,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Default
@@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
///
public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
{
- public const float SYMBOL_SIZE = 0.45f;
+ public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_SIZE;
public const float SYMBOL_BORDER = 8;
+
private const double pre_beat_transition_time = 80;
private Color4 accentColour;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
index 9feb2054da..c4657fcc49 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("approachcircle"),
- Scale = new Vector2(0.73f),
+ Scale = new Vector2(0.83f),
Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("taikobigcircle"),
- Scale = new Vector2(0.7f),
+ Scale = new Vector2(0.8f),
Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index e56aabaf9d..615fbf093f 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -151,6 +151,7 @@ namespace osu.Game.Rulesets.Taiko
{
new MultiMod(new ModWindUp(), new ModWindDown()),
new TaikoModMuted(),
+ new ModAdaptiveSpeed()
};
default:
@@ -170,7 +171,7 @@ namespace osu.Game.Rulesets.Taiko
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap);
- public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score);
+ public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator();
public int LegacyID => 1;
@@ -237,6 +238,7 @@ namespace osu.Game.Rulesets.Taiko
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
+ new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 824b95639b..2efc4176f5 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
+ public readonly BindableBool LockPlayfieldAspect = new BindableBool(true);
+
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
protected override bool UserScrollSpeedAdjustment => false;
@@ -70,7 +72,10 @@ namespace osu.Game.Rulesets.Taiko.UI
return ControlPoints[result];
}
- public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer();
+ public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
+ {
+ LockPlayfieldAspect = { BindTarget = LockPlayfieldAspect }
+ };
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index d650cab729..504b10e9bc 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.UI
///
/// Default height of a when inside a .
///
- public const float DEFAULT_HEIGHT = 212;
+ public const float DEFAULT_HEIGHT = 200;
private Container hitExplosionContainer;
private Container kiaiExplosionContainer;
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 1041456020..9cf530e903 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI;
-using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -13,16 +13,22 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
+ public readonly IBindable LockPlayfieldAspect = new BindableBool(true);
+
protected override void Update()
{
base.Update();
- float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
- Size = new Vector2(1, default_relative_height * aspectAdjust);
+ float height = default_relative_height;
+
+ if (LockPlayfieldAspect.Value)
+ height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
+
+ Height = height;
// Position the taiko playfield exactly one playfield from the top of the screen.
RelativePositionAxes = Axes.Y;
- Y = Size.Y;
+ Y = height;
}
}
}
diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
index b45a3249ff..afafec6b1f 100644
--- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
+++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
@@ -79,7 +79,7 @@
-
+
5.0.0
diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
index 97df9b2cd5..05b3cad6da 100644
--- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
+++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
@@ -48,7 +48,7 @@
-
+
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index d19b3c71f1..0d436c1ef7 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(IResourceStore storage, string fileName)
- : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName)
+ : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName)
{
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
index 2ba8c51a10..1474f2d277 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
@@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Formats;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
@@ -64,6 +65,62 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [TestCase(3, true)]
+ [TestCase(6, false)]
+ [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
+ public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
+ {
+ const double first_frame_time = 48;
+ const double second_frame_time = 65;
+
+ var decoder = new TestLegacyScoreDecoder(beatmapVersion);
+
+ using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
+ {
+ var score = decoder.Parse(resourceStream);
+
+ Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
+ Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
+ }
+ }
+
+ [TestCase(3)]
+ [TestCase(6)]
+ [TestCase(LegacyBeatmapDecoder.LATEST_VERSION)]
+ public void TestLegacyBeatmapReplayOffsetsEncodeDecode(int beatmapVersion)
+ {
+ const double first_frame_time = 2000;
+ const double second_frame_time = 3000;
+
+ var ruleset = new OsuRuleset().RulesetInfo;
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ var beatmap = new TestBeatmap(ruleset)
+ {
+ BeatmapInfo =
+ {
+ BeatmapVersion = beatmapVersion
+ }
+ };
+
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(first_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
+ new OsuReplayFrame(second_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
+ }
+ }
+ };
+
+ var decodedAfterEncode = encodeThenDecode(beatmapVersion, score, beatmap);
+
+ Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time));
+ Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
+ }
+
[Test]
public void TestCultureInvariance()
{
@@ -86,15 +143,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// rather than the classic ASCII U+002D HYPHEN-MINUS.
CultureInfo.CurrentCulture = new CultureInfo("se");
- var encodeStream = new MemoryStream();
-
- var encoder = new LegacyScoreEncoder(score, beatmap);
- encoder.Encode(encodeStream);
-
- var decodeStream = new MemoryStream(encodeStream.GetBuffer());
-
- var decoder = new TestLegacyScoreDecoder();
- var decodedAfterEncode = decoder.Parse(decodeStream);
+ var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
@@ -110,6 +159,20 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
+ private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
+ {
+ var encodeStream = new MemoryStream();
+
+ var encoder = new LegacyScoreEncoder(score, beatmap);
+ encoder.Encode(encodeStream);
+
+ var decodeStream = new MemoryStream(encodeStream.GetBuffer());
+
+ var decoder = new TestLegacyScoreDecoder(beatmapVersion);
+ var decodedAfterEncode = decoder.Parse(decodeStream);
+ return decodedAfterEncode;
+ }
+
[TearDown]
public void TearDown()
{
@@ -118,6 +181,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacyScoreDecoder : LegacyScoreDecoder
{
+ private readonly int beatmapVersion;
+
private static readonly Dictionary rulesets = new Ruleset[]
{
new OsuRuleset(),
@@ -126,6 +191,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
new ManiaRuleset()
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
+ public TestLegacyScoreDecoder(int beatmapVersion = LegacyBeatmapDecoder.LATEST_VERSION)
+ {
+ this.beatmapVersion = beatmapVersion;
+ }
+
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
@@ -134,7 +204,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo,
- Difficulty = new BeatmapDifficulty()
+ Difficulty = new BeatmapDifficulty(),
+ BeatmapVersion = beatmapVersion,
}
});
}
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index 8def8005f1..cea4d510c1 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -409,26 +409,26 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(2, result.Links.Count);
- Assert.AreEqual("osu://chan/#english", result.Links[0].Url);
- Assert.AreEqual("osu://chan/#japanese", result.Links[1].Url);
+ Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#english", result.Links[0].Url);
+ Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#japanese", result.Links[1].Url);
}
[Test]
public void TestOsuProtocol()
{
- Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a custom protocol osu://chan/#english." });
+ Message result = MessageFormatter.FormatMessage(new Message { Content = $"This is a custom protocol {OsuGameBase.OSU_PROTOCOL}chan/#english." });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
- Assert.AreEqual("osu://chan/#english", result.Links[0].Url);
+ Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#english", result.Links[0].Url);
Assert.AreEqual(26, result.Links[0].Index);
Assert.AreEqual(19, result.Links[0].Length);
- result = MessageFormatter.FormatMessage(new Message { Content = "This is a [custom protocol](osu://chan/#english)." });
+ result = MessageFormatter.FormatMessage(new Message { Content = $"This is a [custom protocol]({OsuGameBase.OSU_PROTOCOL}chan/#english)." });
Assert.AreEqual("This is a custom protocol.", result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
- Assert.AreEqual("osu://chan/#english", result.Links[0].Url);
+ Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#english", result.Links[0].Url);
Assert.AreEqual("#english", result.Links[0].Argument);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(15, result.Links[0].Length);
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index 2c7d0211a0..f9c13a8169 100644
--- a/osu.Game.Tests/Database/BeatmapImporterTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using (var importer = new BeatmapModelManager(realm, storage))
- using (new RulesetStore(realm, storage))
+ using (new RealmRulesetStore(realm, storage))
{
Live? beatmapSet;
@@ -85,7 +85,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using (var importer = new BeatmapModelManager(realm, storage))
- using (new RulesetStore(realm, storage))
+ using (new RealmRulesetStore(realm, storage))
{
Live? beatmapSet;
@@ -142,12 +142,15 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using (var importer = new BeatmapModelManager(realm, storage))
- using (new RulesetStore(realm, storage))
+ using (new RealmRulesetStore(realm, storage))
{
Live? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
+ {
imported = await importer.Import(reader);
+ EnsureLoaded(realm.Realm);
+ }
Assert.AreEqual(1, realm.Realm.All().Count());
@@ -171,7 +174,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
await LoadOszIntoStore(importer, realm.Realm);
});
@@ -183,7 +186,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -201,7 +204,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -215,7 +218,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? tempPath = TestResources.GetTestBeatmapForImport();
@@ -245,7 +248,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
@@ -265,7 +268,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -314,7 +317,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -366,7 +369,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -414,7 +417,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -463,7 +466,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -496,7 +499,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var progressNotification = new ImportProgressNotification();
@@ -510,6 +513,8 @@ namespace osu.Game.Tests.Database
new ImportTask(zipStream, string.Empty)
);
+ realm.Run(r => r.Refresh());
+
checkBeatmapSetCount(realm.Realm, 0);
checkBeatmapCount(realm.Realm, 0);
@@ -532,7 +537,7 @@ namespace osu.Game.Tests.Database
};
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -565,6 +570,8 @@ namespace osu.Game.Tests.Database
{
}
+ EnsureLoaded(realm.Realm);
+
checkBeatmapSetCount(realm.Realm, 1);
checkBeatmapCount(realm.Realm, 12);
@@ -582,7 +589,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -590,6 +597,8 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.DeletePending);
+ var originalAddedDate = imported.DateAdded;
+
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
@@ -597,6 +606,7 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending);
+ Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
});
}
@@ -606,7 +616,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapModelManager(realmFactory, storage);
- using var store = new RulesetStore(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Realm);
@@ -638,7 +648,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new NonOptimisedBeatmapImporter(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -646,6 +656,8 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.DeletePending);
+ var originalAddedDate = imported.DateAdded;
+
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
@@ -653,6 +665,7 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending);
+ Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
});
}
@@ -662,7 +675,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -688,7 +701,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealm((realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
var metadata = new BeatmapMetadata
{
@@ -720,6 +733,8 @@ namespace osu.Game.Tests.Database
var imported = importer.Import(toImport);
+ realm.Run(r => r.Refresh());
+
Assert.NotNull(imported);
Debug.Assert(imported != null);
@@ -734,7 +749,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
using (File.OpenRead(temp))
@@ -751,7 +766,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -787,7 +802,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -829,7 +844,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -880,11 +895,13 @@ namespace osu.Game.Tests.Database
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapModelManager(realm, storage);
- using var store = new RulesetStore(realm, storage);
+ using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
await importer.Import(temp);
+ EnsureLoaded(realm.Realm);
+
// Update via the beatmap, not the beatmap info, to ensure correct linking
BeatmapSetInfo setToUpdate = realm.Realm.All().First();
diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
index d62ce3b585..d99bcc092d 100644
--- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
+++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
@@ -1,23 +1,129 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions;
using osu.Game.Beatmaps;
+using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using Realms;
-#nullable enable
-
namespace osu.Game.Tests.Database
{
[TestFixture]
public class RealmSubscriptionRegistrationTests : RealmTest
{
+ [Test]
+ public void TestSubscriptionCollectionAndPropertyChanges()
+ {
+ int collectionChanges = 0;
+ int propertyChanges = 0;
+
+ ChangeSet? lastChanges = null;
+
+ RunTestWithRealm((realm, _) =>
+ {
+ var registration = realm.RegisterForNotifications(r => r.All(), onChanged);
+
+ realm.Run(r => r.Refresh());
+
+ realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
+ realm.Run(r => r.Refresh());
+
+ Assert.That(collectionChanges, Is.EqualTo(1));
+ Assert.That(propertyChanges, Is.EqualTo(0));
+ Assert.That(lastChanges?.InsertedIndices, Has.One.Items);
+ Assert.That(lastChanges?.ModifiedIndices, Is.Empty);
+ Assert.That(lastChanges?.NewModifiedIndices, Is.Empty);
+
+ realm.Write(r => r.All().First().Beatmaps.First().CountdownOffset = 5);
+ realm.Run(r => r.Refresh());
+
+ Assert.That(collectionChanges, Is.EqualTo(1));
+ Assert.That(propertyChanges, Is.EqualTo(1));
+ Assert.That(lastChanges?.InsertedIndices, Is.Empty);
+ Assert.That(lastChanges?.ModifiedIndices, Has.One.Items);
+ Assert.That(lastChanges?.NewModifiedIndices, Has.One.Items);
+
+ registration.Dispose();
+ });
+
+ void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error)
+ {
+ lastChanges = changes;
+
+ if (changes == null)
+ return;
+
+ if (changes.HasCollectionChanges())
+ {
+ Interlocked.Increment(ref collectionChanges);
+ }
+ else
+ {
+ Interlocked.Increment(ref propertyChanges);
+ }
+ }
+ }
+
+ [Test]
+ public void TestSubscriptionWithAsyncWrite()
+ {
+ ChangeSet? lastChanges = null;
+
+ RunTestWithRealm((realm, _) =>
+ {
+ var registration = realm.RegisterForNotifications(r => r.All(), onChanged);
+
+ realm.Run(r => r.Refresh());
+
+ // Without forcing the write onto its own thread, realm will internally run the operation synchronously, which can cause a deadlock with `WaitSafely`.
+ Task.Run(async () =>
+ {
+ await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
+ }).WaitSafely();
+
+ realm.Run(r => r.Refresh());
+
+ Assert.That(lastChanges?.InsertedIndices, Has.One.Items);
+
+ registration.Dispose();
+ });
+
+ void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => lastChanges = changes;
+ }
+
+ [Test]
+ public void TestPropertyChangedSubscription()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ bool? receivedValue = null;
+
+ realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
+
+ using (realm.SubscribeToPropertyChanged(r => r.All().First(), setInfo => setInfo.Protected, val => receivedValue = val))
+ {
+ Assert.That(receivedValue, Is.False);
+
+ realm.Write(r => r.All().First().Protected = true);
+
+ realm.Run(r => r.Refresh());
+
+ Assert.That(receivedValue, Is.True);
+ }
+ });
+ }
+
[Test]
public void TestSubscriptionWithContextLoss()
{
@@ -134,5 +240,41 @@ namespace osu.Game.Tests.Database
Assert.That(beatmapSetInfo, Is.Null);
});
}
+
+ [Test]
+ public void TestPropertyChangedSubscriptionWithContextLoss()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ bool? receivedValue = null;
+
+ realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
+
+ var subscription = realm.SubscribeToPropertyChanged(
+ r => r.All().First(),
+ setInfo => setInfo.Protected,
+ val => receivedValue = val);
+
+ Assert.That(receivedValue, Is.Not.Null);
+ receivedValue = null;
+
+ using (realm.BlockAllOperations())
+ {
+ }
+
+ // re-registration after context restore.
+ realm.Run(r => r.Refresh());
+ Assert.That(receivedValue, Is.Not.Null);
+
+ subscription.Dispose();
+ receivedValue = null;
+
+ using (realm.BlockAllOperations())
+ Assert.That(receivedValue, Is.Null);
+
+ realm.Run(r => r.Refresh());
+ Assert.That(receivedValue, Is.Null);
+ });
+ }
}
}
diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs
index 838759c991..16072888b9 100644
--- a/osu.Game.Tests/Database/RealmTest.cs
+++ b/osu.Game.Tests/Database/RealmTest.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Database
// ReSharper disable once AccessToDisposedClosure
var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller));
- using (var realm = new RealmAccess(testStorage, "client"))
+ using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}");
testAction(realm, testStorage);
@@ -62,7 +62,7 @@ namespace osu.Game.Tests.Database
{
var testStorage = storage.GetStorageForDirectory(caller);
- using (var realm = new RealmAccess(testStorage, "client"))
+ using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}");
await testAction(realm, testStorage);
diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs
index 7544142b70..f48b5cba11 100644
--- a/osu.Game.Tests/Database/RulesetStoreTests.cs
+++ b/osu.Game.Tests/Database/RulesetStoreTests.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realm, storage) =>
{
- var rulesets = new RulesetStore(realm, storage);
+ var rulesets = new RealmRulesetStore(realm, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, realm.Realm.All().Count());
@@ -26,8 +26,8 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realm, storage) =>
{
- var rulesets = new RulesetStore(realm, storage);
- var rulesets2 = new RulesetStore(realm, storage);
+ var rulesets = new RealmRulesetStore(realm, storage);
+ var rulesets2 = new RealmRulesetStore(realm, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, rulesets2.AvailableRulesets.Count());
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realm, storage) =>
{
- var rulesets = new RulesetStore(realm, storage);
+ var rulesets = new RealmRulesetStore(realm, storage);
Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged);
Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged);
diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
index 77b402ad3c..5c04ac88a7 100644
--- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
@@ -26,6 +26,12 @@ namespace osu.Game.Tests.Gameplay
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
}
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("reset audio offset", () => localConfig.SetValue(OsuSetting.AudioOffset, 0.0));
+ }
+
[Test]
public void TestStartThenElapsedTime()
{
@@ -36,7 +42,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
- Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
+ Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
});
AddStep("start clock", () => gameplayClockContainer.Start());
@@ -53,7 +59,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
- Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
+ Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
});
AddStep("start clock", () => gameplayClockContainer.Start());
@@ -73,26 +79,29 @@ namespace osu.Game.Tests.Gameplay
public void TestSeekPerformsInGameplayTime(
[Values(1.0, 0.5, 2.0)] double clockRate,
[Values(0.0, 200.0, -200.0)] double userOffset,
- [Values(false, true)] bool whileStopped)
+ [Values(false, true)] bool whileStopped,
+ [Values(false, true)] bool setAudioOffsetBeforeConstruction)
{
ClockBackedTestWorkingBeatmap working = null;
GameplayClockContainer gameplayClockContainer = null;
+ if (setAudioOffsetBeforeConstruction)
+ AddStep($"preset audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
+
AddStep("create container", () =>
{
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
working.LoadTrack();
- Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
+ Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
- if (whileStopped)
- gameplayClockContainer.Stop();
-
- gameplayClockContainer.Reset();
+ gameplayClockContainer.Reset(startClock: !whileStopped);
});
AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate)));
- AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
+
+ if (!setAudioOffsetBeforeConstruction)
+ AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
index 70ba868de6..9c307341bd 100644
--- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Gameplay
{
var beatmap = new Beatmap { HitObjects = { new HitObject() } };
- var scoreProcessor = new ScoreProcessor();
+ var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a miss judgement
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Gameplay
{
var beatmap = new Beatmap { HitObjects = { new HitObject() } };
- var scoreProcessor = new ScoreProcessor();
+ var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a judgement
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Gameplay
{
var beatmap = new Beatmap { HitObjects = { new HitCircle() } };
- var scoreProcessor = new ScoreProcessor();
+ var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 88862ea28b..e0a497cf24 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -1,29 +1,23 @@
// 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.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
-using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
-using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
@@ -94,7 +88,7 @@ namespace osu.Game.Tests.Gameplay
[Test]
public void TestSampleHasLifetimeEndWithInitialClockTime()
{
- GameplayClockContainer gameplayContainer = null;
+ MasterGameplayClockContainer gameplayContainer = null;
DrawableStoryboardSample sample = null;
AddStep("create container", () =>
@@ -102,8 +96,11 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
- Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true)
+ const double start_time = 1000;
+
+ Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time)
{
+ StartTime = start_time,
IsPaused = { Value = true },
Child = new FrameStabilityContainer
{
@@ -118,59 +115,6 @@ namespace osu.Game.Tests.Gameplay
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);
}
- [TestCase(typeof(OsuModDoubleTime), 1.5)]
- [TestCase(typeof(OsuModHalfTime), 0.75)]
- [TestCase(typeof(ModWindUp), 1.5)]
- [TestCase(typeof(ModWindDown), 0.75)]
- [TestCase(typeof(OsuModDoubleTime), 2)]
- [TestCase(typeof(OsuModHalfTime), 0.5)]
- [TestCase(typeof(ModWindUp), 2)]
- [TestCase(typeof(ModWindDown), 0.5)]
- public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate)
- {
- GameplayClockContainer gameplayContainer = null;
- StoryboardSampleInfo sampleInfo = null;
- TestDrawableStoryboardSample sample = null;
-
- Mod testedMod = Activator.CreateInstance(expectedMod) as Mod;
-
- switch (testedMod)
- {
- case ModRateAdjust m:
- m.SpeedChange.Value = expectedRate;
- break;
-
- case ModTimeRamp m:
- m.FinalRate.Value = m.InitialRate.Value = expectedRate;
- break;
- }
-
- AddStep("setup storyboard sample", () =>
- {
- Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, this);
- SelectedMods.Value = new[] { testedMod };
-
- var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
-
- Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
- {
- Child = beatmapSkinSourceContainer
- });
-
- beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(sampleInfo = new StoryboardSampleInfo("test-sample", 1, 1))
- {
- Clock = gameplayContainer.GameplayClock
- });
- });
-
- AddStep("start", () => gameplayContainer.Start());
-
- AddAssert("sample playback rate matches mod rates", () =>
- testedMod != null && Precision.AlmostEquals(
- sample.ChildrenOfType().First().AggregateFrequency.Value,
- ((IApplicableToRate)testedMod).ApplyToRate(sampleInfo.StartTime)));
- }
-
[Test]
public void TestSamplePlaybackWithBeatmapHitsoundsOff()
{
@@ -207,7 +151,7 @@ namespace osu.Game.Tests.Gameplay
private class TestSkin : LegacySkin
{
public TestSkin(string resourceName, IStorageResourceProvider resources)
- : base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini")
+ : base(DefaultLegacySkin.CreateInfo(), resources, new TestResourceStore(resourceName))
{
}
}
diff --git a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
new file mode 100644
index 0000000000..312b939315
--- /dev/null
+++ b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.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;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
+using osu.Game.Utils;
+
+namespace osu.Game.Tests.Mods
+{
+ [TestFixture]
+ public class MultiModIncompatibilityTest
+ {
+ ///
+ /// Ensures that all mods grouped into s, as declared by the default rulesets, are pairwise incompatible with each other.
+ ///
+ [TestCase(typeof(OsuRuleset))]
+ [TestCase(typeof(TaikoRuleset))]
+ [TestCase(typeof(CatchRuleset))]
+ [TestCase(typeof(ManiaRuleset))]
+ public void TestAllMultiModsFromRulesetAreIncompatible(Type rulesetType)
+ {
+ var ruleset = (Ruleset)Activator.CreateInstance(rulesetType);
+ Assert.That(ruleset, Is.Not.Null);
+
+ var allMultiMods = getMultiMods(ruleset);
+
+ Assert.Multiple(() =>
+ {
+ foreach (var multiMod in allMultiMods)
+ {
+ int modCount = multiMod.Mods.Length;
+
+ for (int i = 0; i < modCount; ++i)
+ {
+ // indexing from i + 1 ensures that only pairs of different mods are checked, and are checked only once
+ // (indexing from 0 would check each pair twice, and also check each mod against itself).
+ for (int j = i + 1; j < modCount; ++j)
+ {
+ var firstMod = multiMod.Mods[i];
+ var secondMod = multiMod.Mods[j];
+
+ Assert.That(
+ ModUtils.CheckCompatibleSet(new[] { firstMod, secondMod }), Is.False,
+ $"{firstMod.Name} ({firstMod.Acronym}) and {secondMod.Name} ({secondMod.Acronym}) should be incompatible.");
+ }
+ }
+ }
+ });
+ }
+
+ ///
+ /// This local helper is used rather than , because the aforementioned method flattens multi mods.
+ /// >
+ private static IEnumerable getMultiMods(Ruleset ruleset)
+ => Enum.GetValues(typeof(ModType)).Cast().SelectMany(ruleset.GetModsFor).OfType();
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index 834930a05e..fd5691a9f4 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -143,14 +143,14 @@ namespace osu.Game.Tests.NonVisual
Assert.That(osuStorage, Is.Not.Null);
// In the following tests, realm files are ignored as
- // - in the case of checking the source, interacting with the pipe files (client.realm.note) may
+ // - in the case of checking the source, interacting with the pipe files (.realm.note) may
// lead to unexpected behaviour.
// - in the case of checking the destination, the files may have already been recreated by the game
// as part of the standard migration flow.
foreach (string file in osuStorage.IgnoreFiles)
{
- if (!file.Contains("realm", StringComparison.Ordinal))
+ if (!file.Contains(".realm", StringComparison.Ordinal))
{
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False, () => $"{file} exists in destination when it was expected to be ignored");
@@ -159,7 +159,7 @@ namespace osu.Game.Tests.NonVisual
foreach (string dir in osuStorage.IgnoreDirectories)
{
- if (!dir.Contains("realm", StringComparison.Ordinal))
+ if (!dir.Contains(".realm", StringComparison.Ordinal))
{
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.Exists(dir), Is.False, () => $"{dir} exists in destination when it was expected to be ignored");
@@ -188,19 +188,17 @@ namespace osu.Game.Tests.NonVisual
{
var osu = LoadOsuIntoHost(host);
- const string database_filename = "client.realm";
-
Assert.DoesNotThrow(() => osu.Migrate(customPath));
- Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
+ Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
- Assert.That(File.Exists(Path.Combine(customPath2, database_filename)));
+ Assert.That(File.Exists(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME)));
// some files may have been left behind for whatever reason, but that's not what we're testing here.
cleanupPath(customPath);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
- Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
+ Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
}
finally
{
@@ -233,6 +231,46 @@ namespace osu.Game.Tests.NonVisual
}
}
+ [Test]
+ public void TestMigrationFailsOnExistingData()
+ {
+ string customPath = prepareCustomPath();
+ string customPath2 = prepareCustomPath();
+
+ using (var host = new CustomTestHeadlessGameHost())
+ {
+ try
+ {
+ var osu = LoadOsuIntoHost(host);
+
+ var storage = osu.Dependencies.Get();
+ var osuStorage = storage as OsuStorage;
+
+ string originalDirectory = storage.GetFullPath(".");
+
+ Assert.DoesNotThrow(() => osu.Migrate(customPath));
+ Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
+
+ Directory.CreateDirectory(customPath2);
+ File.Copy(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME), Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME));
+
+ // Fails because file already exists.
+ Assert.Throws(() => osu.Migrate(customPath2));
+
+ osuStorage?.ChangeDataPath(customPath2);
+
+ Assert.That(osuStorage?.CustomStoragePath, Is.EqualTo(customPath2));
+ Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath2}"));
+ }
+ finally
+ {
+ host.Exit();
+ cleanupPath(customPath);
+ cleanupPath(customPath2);
+ }
+ }
+ }
+
[Test]
public void TestMigrationToNestedTargetFails()
{
diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
index 0c49a18c8f..4adb7002a0 100644
--- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -21,8 +21,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
{
var user = new APIUser { Id = 33 };
- AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
- AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
+ AddRepeatStep("add user multiple times", () => MultiplayerClient.AddUser(user), 3);
+ AddAssert("room has 2 users", () => MultiplayerClient.Room?.Users.Count == 2);
}
[Test]
@@ -30,11 +30,11 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
{
var user = new APIUser { Id = 44 };
- AddStep("add user", () => Client.AddUser(user));
- AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
+ AddStep("add user", () => MultiplayerClient.AddUser(user));
+ AddAssert("room has 2 users", () => MultiplayerClient.Room?.Users.Count == 2);
- AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
- AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
+ AddRepeatStep("remove user multiple times", () => MultiplayerClient.RemoveUser(user), 3);
+ AddAssert("room has 1 user", () => MultiplayerClient.Room?.Users.Count == 1);
}
[Test]
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
{
int id = 2000;
- AddRepeatStep("add some users", () => Client.AddUser(new APIUser { Id = id++ }), 5);
+ AddRepeatStep("add some users", () => MultiplayerClient.AddUser(new APIUser { Id = id++ }), 5);
checkPlayingUserCount(0);
changeState(3, MultiplayerUserState.WaitingForLoad);
@@ -57,17 +57,17 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
changeState(6, MultiplayerUserState.WaitingForLoad);
checkPlayingUserCount(6);
- AddStep("another user left", () => Client.RemoveUser((Client.Room?.Users.Last().User).AsNonNull()));
+ AddStep("another user left", () => MultiplayerClient.RemoveUser((MultiplayerClient.Room?.Users.Last().User).AsNonNull()));
checkPlayingUserCount(5);
- AddStep("leave room", () => Client.LeaveRoom());
+ AddStep("leave room", () => MultiplayerClient.LeaveRoom());
checkPlayingUserCount(0);
}
[Test]
public void TestPlayingUsersUpdatedOnJoin()
{
- AddStep("leave room", () => Client.LeaveRoom());
+ AddStep("leave room", () => MultiplayerClient.LeaveRoom());
AddUntilStep("wait for room part", () => !RoomJoined);
AddStep("create room initially in gameplay", () =>
@@ -76,7 +76,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
newRoom.CopyFrom(SelectedRoom.Value);
newRoom.RoomID.Value = null;
- Client.RoomSetupAction = room =>
+ MultiplayerClient.RoomSetupAction = room =>
{
room.State = MultiplayerRoomState.Playing;
room.Users.Add(new MultiplayerRoomUser(PLAYER_1_ID)
@@ -94,15 +94,15 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
}
private void checkPlayingUserCount(int expectedCount)
- => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => Client.CurrentMatchPlayingUserIds.Count == expectedCount);
+ => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count == expectedCount);
private void changeState(int userCount, MultiplayerUserState state)
=> AddStep($"{"user".ToQuantity(userCount)} in {state}", () =>
{
for (int i = 0; i < userCount; ++i)
{
- int userId = Client.Room?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!");
- Client.ChangeUserState(userId, state);
+ int userId = MultiplayerClient.Room?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!");
+ MultiplayerClient.ChangeUserState(userId, state);
}
});
}
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
index 69e66942ab..76c49edf78 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
@@ -1,12 +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;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using NUnit.Framework;
-using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
+using osu.Game.Database;
+using osu.Game.IO;
using osu.Game.Skinning;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
namespace osu.Game.Tests.NonVisual.Skinning
{
@@ -60,6 +69,34 @@ namespace osu.Game.Tests.NonVisual.Skinning
"Gameplay/osu/followpoint",
"followpoint", 1
},
+ new object[]
+ {
+ // Looking up a filename with extension specified should work.
+ new[] { "followpoint.png" },
+ "followpoint.png",
+ "followpoint.png", 1
+ },
+ new object[]
+ {
+ // Looking up a filename with extension specified should also work with @2x sprites.
+ new[] { "followpoint@2x.png" },
+ "followpoint.png",
+ "followpoint@2x.png", 2
+ },
+ new object[]
+ {
+ // Looking up a path with extension specified should work.
+ new[] { "Gameplay/osu/followpoint.png" },
+ "Gameplay/osu/followpoint.png",
+ "Gameplay/osu/followpoint.png", 1
+ },
+ new object[]
+ {
+ // Looking up a path with extension specified should also work with @2x sprites.
+ new[] { "Gameplay/osu/followpoint@2x.png" },
+ "Gameplay/osu/followpoint.png",
+ "Gameplay/osu/followpoint@2x.png", 2
+ },
};
[TestCaseSource(nameof(fallbackTestCases))]
@@ -71,7 +108,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
var texture = legacySkin.GetTexture(requestedComponent);
Assert.IsNotNull(texture);
- Assert.AreEqual(textureStore.Textures[expectedTexture], texture);
+ Assert.AreEqual(textureStore.Textures[expectedTexture].Width, texture.Width);
Assert.AreEqual(expectedScale, texture.ScaleAdjust);
}
@@ -88,23 +125,50 @@ namespace osu.Game.Tests.NonVisual.Skinning
private class TestLegacySkin : LegacySkin
{
- public TestLegacySkin(TextureStore textureStore)
- : base(new SkinInfo(), null, null, string.Empty)
+ public TestLegacySkin(IResourceStore textureStore)
+ : base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty)
{
- Textures = textureStore;
+ }
+
+ private class TestResourceProvider : IStorageResourceProvider
+ {
+ private readonly IResourceStore textureStore;
+
+ public TestResourceProvider(IResourceStore textureStore)
+ {
+ this.textureStore = textureStore;
+ }
+
+ public AudioManager AudioManager => null;
+ public IResourceStore Files => null;
+ public IResourceStore Resources => null;
+ public RealmAccess RealmAccess => null;
+ public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => textureStore;
}
}
- private class TestTextureStore : TextureStore
+ private class TestTextureStore : IResourceStore
{
- public readonly Dictionary Textures;
+ public readonly Dictionary Textures;
public TestTextureStore(params string[] fileNames)
{
- Textures = fileNames.ToDictionary(fileName => fileName, fileName => new Texture(1, 1));
+ // use an incrementing width to allow assertion matching on correct textures as they turn from uploads into actual textures.
+ int width = 1;
+ Textures = fileNames.ToDictionary(fileName => fileName, fileName => new TextureUpload(new Image(width, width++)));
}
- public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => Textures.GetValueOrDefault(name);
+ public TextureUpload Get(string name) => Textures.GetValueOrDefault(name);
+
+ public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => Task.FromResult(Get(name));
+
+ public Stream GetStream(string name) => throw new NotImplementedException();
+
+ public IEnumerable GetAvailableResources() => throw new NotImplementedException();
+
+ public void Dispose()
+ {
+ }
}
}
}
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index 1b7a7656b5..0622514783 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -24,6 +24,21 @@ namespace osu.Game.Tests.Online
[TestFixture]
public class TestAPIModJsonSerialization
{
+ [Test]
+ public void TestUnknownMod()
+ {
+ var apiMod = new APIMod { Acronym = "WNG" };
+
+ var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod));
+
+ var converted = deserialized?.ToMod(new TestRuleset());
+
+ Assert.NotNull(converted);
+ Assert.That(converted, Is.TypeOf(typeof(UnknownMod)));
+ Assert.That(converted.Type, Is.EqualTo(ModType.System));
+ Assert.That(converted.Acronym, Is.EqualTo("WNG??"));
+ }
+
[Test]
public void TestAcronymIsPreserved()
{
@@ -121,6 +136,17 @@ namespace osu.Game.Tests.Online
Assert.That((deserialised?.Mods[0])?.Settings["speed_change"], Is.EqualTo(2));
}
+ [Test]
+ public void TestAPIModDetachedFromSource()
+ {
+ var mod = new OsuModDoubleTime { SpeedChange = { Value = 1.01 } };
+ var apiMod = new APIMod(mod);
+
+ mod.SpeedChange.Value = 1.5;
+
+ Assert.That(apiMod.Settings["speed_change"], Is.EqualTo(1.01d));
+ }
+
private class TestRuleset : Ruleset
{
public override IEnumerable GetModsFor(ModType type) => new Mod[]
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 343fc7e6e0..db988a544d 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
@@ -12,6 +13,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Testing;
@@ -21,6 +23,8 @@ using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
@@ -45,7 +49,7 @@ namespace osu.Game.Tests.Online
[BackgroundDependencyLoader]
private void load(AudioManager audio, GameHost host)
{
- Dependencies.Cache(rulesets = new RulesetStore(Realm));
+ Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API));
}
@@ -53,6 +57,25 @@ namespace osu.Game.Tests.Online
[SetUp]
public void SetUp() => Schedule(() =>
{
+ ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case GetBeatmapsRequest beatmapsReq:
+ var beatmap = CreateAPIBeatmap();
+ beatmap.OnlineID = testBeatmapInfo.OnlineID;
+ beatmap.OnlineBeatmapSetID = testBeatmapSet.OnlineID;
+ beatmap.Checksum = testBeatmapInfo.MD5Hash;
+ beatmap.BeatmapSet!.OnlineID = testBeatmapSet.OnlineID;
+
+ beatmapsReq.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = new List { beatmap } });
+ return true;
+
+ default:
+ return false;
+ }
+ };
+
beatmaps.AllowImport = new TaskCompletionSource();
testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
@@ -63,22 +86,39 @@ namespace osu.Game.Tests.Online
Realm.Write(r => r.RemoveAll());
Realm.Write(r => r.RemoveAll());
- selectedItem.Value = new PlaylistItem
+ selectedItem.Value = new PlaylistItem(testBeatmapInfo)
{
- Beatmap = { Value = testBeatmapInfo },
- Ruleset = { Value = testBeatmapInfo.Ruleset },
+ RulesetID = testBeatmapInfo.Ruleset.OnlineID,
};
- Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
- {
- SelectedItem = { BindTarget = selectedItem, }
- };
+ recreateChildren();
});
+ private void recreateChildren()
+ {
+ var beatmapLookupCache = new BeatmapLookupCache();
+
+ Child = new DependencyProvidingContainer
+ {
+ CachedDependencies = new[]
+ {
+ (typeof(BeatmapLookupCache), (object)beatmapLookupCache)
+ },
+ Children = new Drawable[]
+ {
+ beatmapLookupCache,
+ availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
+ {
+ SelectedItem = { BindTarget = selectedItem, }
+ }
+ }
+ };
+ }
+
[Test]
public void TestBeatmapDownloadingFlow()
{
- AddAssert("ensure beatmap unavailable", () => !beatmaps.IsAvailableLocally(testBeatmapSet));
+ AddUntilStep("ensure beatmap unavailable", () => !beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded);
AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet));
@@ -92,7 +132,7 @@ namespace osu.Game.Tests.Online
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddUntilStep("wait for import", () => beatmaps.CurrentImport != null);
- AddAssert("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
+ AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable);
}
@@ -123,10 +163,7 @@ namespace osu.Game.Tests.Online
});
addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded);
- AddStep("recreate tracker", () => Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
- {
- SelectedItem = { BindTarget = selectedItem }
- });
+ AddStep("recreate tracker", recreateChildren);
addAvailabilityCheckStep("state not downloaded as well", BeatmapAvailability.NotDownloaded);
AddStep("reimport original beatmap", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely());
@@ -167,7 +204,8 @@ namespace osu.Game.Tests.Online
public Live CurrentImport { get; private set; }
- public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null)
+ public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources,
+ GameHost host = null, WorkingBeatmap defaultBeatmap = null)
: base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap)
{
}
diff --git a/osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs b/osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs
new file mode 100644
index 0000000000..662660bce4
--- /dev/null
+++ b/osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+using NUnit.Framework;
+using osu.Game.IO.Serialization;
+using osu.Game.Online.Solo;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Online
+{
+ ///
+ /// Basic testing to ensure our attribute-based naming is correctly working.
+ ///
+ [TestFixture]
+ public class TestSubmittableScoreJsonSerialization
+ {
+ [Test]
+ public void TestScoreSerialisationViaExtensionMethod()
+ {
+ var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
+
+ string serialised = score.Serialize();
+
+ Assert.That(serialised, Contains.Substring("large_tick_hit"));
+ Assert.That(serialised, Contains.Substring("\"rank\": \"S\""));
+ }
+
+ [Test]
+ public void TestScoreSerialisationWithoutSettings()
+ {
+ var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
+
+ string serialised = JsonConvert.SerializeObject(score);
+
+ Assert.That(serialised, Contains.Substring("large_tick_hit"));
+ Assert.That(serialised, Contains.Substring("\"rank\":\"S\""));
+ }
+ }
+}
diff --git a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs
index d33081662d..9e7ea02101 100644
--- a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs
+++ b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs
@@ -3,6 +3,7 @@
using System;
using NUnit.Framework;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
namespace osu.Game.Tests.OnlinePlay
@@ -29,9 +30,9 @@ namespace osu.Game.Tests.OnlinePlay
{
var items = new[]
{
- new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 },
- new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 },
- new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, PlaylistOrder = 1 },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, PlaylistOrder = 2 },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1003 }) { ID = 3, PlaylistOrder = 3 },
};
Assert.Multiple(() =>
@@ -47,9 +48,9 @@ namespace osu.Game.Tests.OnlinePlay
{
var items = new[]
{
- new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 },
- new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 },
- new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, PlaylistOrder = 2 },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, PlaylistOrder = 1 },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1003 }) { ID = 3, PlaylistOrder = 3 },
};
Assert.Multiple(() =>
@@ -65,9 +66,9 @@ namespace osu.Game.Tests.OnlinePlay
{
var items = new[]
{
- new PlaylistItem { ID = 1, BeatmapID = 1001, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) },
- new PlaylistItem { ID = 2, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) },
- new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1003 }) { ID = 3, PlaylistOrder = 3 },
};
Assert.Multiple(() =>
@@ -83,9 +84,9 @@ namespace osu.Game.Tests.OnlinePlay
{
var items = new[]
{
- new PlaylistItem { ID = 1, BeatmapID = 1001, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) },
- new PlaylistItem { ID = 2, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) },
- new PlaylistItem { ID = 3, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 57, 0, TimeSpan.Zero) },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) },
+ new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 3, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 57, 0, TimeSpan.Zero) },
};
Assert.Multiple(() =>
diff --git a/osu.Game.Tests/Resources/client.db b/osu.Game.Tests/Resources/client.db
new file mode 100644
index 0000000000..079d5af3b7
Binary files /dev/null and b/osu.Game.Tests/Resources/client.db differ
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
index f0d9ece06f..7ecd509193 100644
--- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -6,11 +6,15 @@ using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Rulesets.Scoring
@@ -23,7 +27,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[SetUp]
public void SetUp()
{
- scoreProcessor = new ScoreProcessor();
+ scoreProcessor = new ScoreProcessor(new TestRuleset());
beatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = new List
@@ -36,9 +40,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)]
[TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)]
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
- [TestCase(ScoringMode.Classic, HitResult.Meh, 41)]
- [TestCase(ScoringMode.Classic, HitResult.Ok, 46)]
- [TestCase(ScoringMode.Classic, HitResult.Great, 72)]
+ [TestCase(ScoringMode.Classic, HitResult.Meh, 20)]
+ [TestCase(ScoringMode.Classic, HitResult.Ok, 23)]
+ [TestCase(ScoringMode.Classic, HitResult.Great, 36)]
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
scoreProcessor.Mode.Value = scoringMode;
@@ -86,17 +90,17 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
- [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 68)]
- [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 81)]
- [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 109)]
- [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 149)]
- [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 149)]
- [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 9)]
- [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 15)]
+ [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 86)]
+ [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 104)]
+ [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 140)]
+ [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 190)]
+ [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 190)]
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 18)]
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 31)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
- [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 149)]
- [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 18)]
- [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 18)]
+ [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 12)]
+ [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 36)]
+ [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)]
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
{
var minResult = new TestJudgement(hitResult).MinResult;
@@ -128,8 +132,8 @@ namespace osu.Game.Tests.Rulesets.Scoring
///
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
- [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 69)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
- [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 60)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 34)]
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 30)]
public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
IEnumerable hitObjects = Enumerable
@@ -300,7 +304,26 @@ namespace osu.Game.Tests.Rulesets.Scoring
HitObjects = { new TestHitObject(result) }
});
- Assert.That(scoreProcessor.GetImmediateScore(ScoringMode.Standardised, result.AffectsCombo() ? 1 : 0, statistic), Is.EqualTo(expectedScore).Within(0.5d));
+ Assert.That(scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, new ScoreInfo
+ {
+ Ruleset = new TestRuleset().RulesetInfo,
+ MaxCombo = result.AffectsCombo() ? 1 : 0,
+ Statistics = statistic
+ }), Is.EqualTo(expectedScore).Within(0.5d));
+ }
+
+ private class TestRuleset : Ruleset
+ {
+ public override IEnumerable GetModsFor(ModType type) => throw new System.NotImplementedException();
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException();
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new System.NotImplementedException();
+
+ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new System.NotImplementedException();
+
+ public override string Description => string.Empty;
+ public override string ShortName => string.Empty;
}
private class TestJudgement : Judgement
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index 9b0facd625..dde8715764 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Game.Database;
+using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Skinning;
@@ -110,6 +111,27 @@ namespace osu.Game.Tests.Skins.IO
assertImportedOnce(import1, import2);
});
+ [Test]
+ public Task TestImportExportedSkinFilename() => runSkinTest(async osu =>
+ {
+ MemoryStream exportStream = new MemoryStream();
+
+ var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "custom.osk"));
+ assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu);
+
+ import1.PerformRead(s =>
+ {
+ new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream);
+ });
+
+ string exportFilename = import1.GetDisplayString();
+
+ var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(exportStream, $"{exportFilename}.osk"));
+ assertCorrectMetadata(import2, "name 1 [custom]", "author 1", osu);
+
+ assertImportedOnce(import1, import2);
+ });
+
[Test]
public Task TestSameMetadataNameSameFolderName() => runSkinTest(async osu =>
{
diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs
index 71544e94f3..0c1981b35d 100644
--- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs
+++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Skins
public class BeatmapSkinSource : LegacyBeatmapSkin
{
public BeatmapSkinSource()
- : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
+ : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
{
}
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index 870d6d8f57..d3cacaa88c 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins
public class BeatmapSkinSource : LegacyBeatmapSkin
{
public BeatmapSkinSource()
- : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
+ : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
{
}
}
diff --git a/osu.Game.Tests/Utils/NamingUtilsTest.cs b/osu.Game.Tests/Utils/NamingUtilsTest.cs
new file mode 100644
index 0000000000..62e688db90
--- /dev/null
+++ b/osu.Game.Tests/Utils/NamingUtilsTest.cs
@@ -0,0 +1,132 @@
+// 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.Utils;
+
+namespace osu.Game.Tests.Utils
+{
+ [TestFixture]
+ public class NamingUtilsTest
+ {
+ [Test]
+ public void TestEmptySet()
+ {
+ string nextBestName = NamingUtils.GetNextBestName(Enumerable.Empty(), "New Difficulty");
+
+ Assert.AreEqual("New Difficulty", nextBestName);
+ }
+
+ [Test]
+ public void TestNotTaken()
+ {
+ string[] existingNames =
+ {
+ "Something",
+ "Entirely",
+ "Different"
+ };
+
+ string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty");
+
+ Assert.AreEqual("New Difficulty", nextBestName);
+ }
+
+ [Test]
+ public void TestNotTakenButClose()
+ {
+ string[] existingNames =
+ {
+ "New Difficulty(1)",
+ "New Difficulty (abcd)",
+ "New Difficulty but not really"
+ };
+
+ string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty");
+
+ Assert.AreEqual("New Difficulty", nextBestName);
+ }
+
+ [Test]
+ public void TestAlreadyTaken()
+ {
+ string[] existingNames =
+ {
+ "New Difficulty"
+ };
+
+ string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty");
+
+ Assert.AreEqual("New Difficulty (1)", nextBestName);
+ }
+
+ [Test]
+ public void TestAlreadyTakenWithDifferentCase()
+ {
+ string[] existingNames =
+ {
+ "new difficulty"
+ };
+
+ string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty");
+
+ Assert.AreEqual("New Difficulty (1)", nextBestName);
+ }
+
+ [Test]
+ public void TestAlreadyTakenWithBrackets()
+ {
+ string[] existingNames =
+ {
+ "new difficulty (copy)"
+ };
+
+ string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty (copy)");
+
+ Assert.AreEqual("New Difficulty (copy) (1)", nextBestName);
+ }
+
+ [Test]
+ public void TestMultipleAlreadyTaken()
+ {
+ string[] existingNames =
+ {
+ "New Difficulty",
+ "New difficulty (1)",
+ "new Difficulty (2)",
+ "New DIFFICULTY (3)"
+ };
+
+ string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty");
+
+ Assert.AreEqual("New Difficulty (4)", nextBestName);
+ }
+
+ [Test]
+ public void TestEvenMoreAlreadyTaken()
+ {
+ string[] existingNames = Enumerable.Range(1, 30).Select(i => $"New Difficulty ({i})").Append("New Difficulty").ToArray();
+
+ string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty");
+
+ Assert.AreEqual("New Difficulty (31)", nextBestName);
+ }
+
+ [Test]
+ public void TestMultipleAlreadyTakenWithGaps()
+ {
+ string[] existingNames =
+ {
+ "New Difficulty",
+ "New Difficulty (1)",
+ "New Difficulty (4)",
+ "New Difficulty (9)"
+ };
+
+ string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty");
+
+ Assert.AreEqual("New Difficulty (2)", nextBestName);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 40e7c0a844..f7140537ee 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Background
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
- Dependencies.Cache(rulesets = new RulesetStore(Realm));
+ Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage));
Dependencies.Cache(Realm);
@@ -359,9 +359,9 @@ namespace osu.Game.Tests.Visual.Background
protected override BackgroundScreen CreateBackground() =>
new FadeAccessibleBackground(Beatmap.Value);
- public override void OnEntering(IScreen last)
+ public override void OnEntering(ScreenTransitionEvent e)
{
- base.OnEntering(last);
+ base.OnEntering(e);
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
}
diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
index d4c13059da..51ca55f37f 100644
--- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
+++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Collections
[BackgroundDependencyLoader]
private void load(GameHost host)
{
- Dependencies.Cache(rulesets = new RulesetStore(Realm));
+ Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Collections
});
Dependencies.Cache(manager);
- Dependencies.Cache(dialogOverlay);
+ Dependencies.CacheAs(dialogOverlay);
}
[SetUp]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
index ed7bb9e301..6a0950c6dd 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
@@ -20,30 +22,31 @@ namespace osu.Game.Tests.Visual.Editing
private BeatDivisorControl beatDivisorControl;
private BindableBeatDivisor bindableBeatDivisor;
- private SliderBar tickSliderBar;
- private EquilateralTriangle tickMarkerHead;
+ private SliderBar tickSliderBar => beatDivisorControl.ChildrenOfType>().Single();
+ private EquilateralTriangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single();
[SetUp]
public void SetUp() => Schedule(() =>
{
- Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16))
+ Child = new PopoverContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(90, 90)
+ RelativeSizeAxes = Axes.Both,
+ Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16))
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(90, 90)
+ }
};
-
- tickSliderBar = beatDivisorControl.ChildrenOfType>().Single();
- tickMarkerHead = tickSliderBar.ChildrenOfType().Single();
});
[Test]
public void TestBindableBeatDivisor()
{
- AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 4);
+ AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 2);
AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4);
- AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 3);
- AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 12);
+ AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 1);
+ AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 8);
}
[Test]
@@ -79,5 +82,115 @@ namespace osu.Game.Tests.Visual.Editing
sliderDrawQuad.Centre.Y
);
}
+
+ [Test]
+ public void TestBeatChevronNavigation()
+ {
+ switchBeatSnap(1);
+ assertBeatSnap(1);
+
+ switchBeatSnap(3);
+ assertBeatSnap(8);
+
+ switchBeatSnap(-1);
+ assertBeatSnap(4);
+
+ switchBeatSnap(-3);
+ assertBeatSnap(16);
+ }
+
+ [Test]
+ public void TestBeatPresetNavigation()
+ {
+ assertPreset(BeatDivisorType.Common);
+
+ switchPresets(1);
+ assertPreset(BeatDivisorType.Triplets);
+
+ switchPresets(1);
+ assertPreset(BeatDivisorType.Common);
+
+ switchPresets(-1);
+ assertPreset(BeatDivisorType.Triplets);
+
+ switchPresets(-1);
+ assertPreset(BeatDivisorType.Common);
+
+ setDivisorViaInput(3);
+ assertPreset(BeatDivisorType.Triplets);
+
+ setDivisorViaInput(8);
+ assertPreset(BeatDivisorType.Common);
+
+ setDivisorViaInput(15);
+ assertPreset(BeatDivisorType.Custom, 15);
+
+ switchBeatSnap(-1);
+ assertBeatSnap(5);
+
+ switchBeatSnap(-1);
+ assertBeatSnap(3);
+
+ setDivisorViaInput(5);
+ assertPreset(BeatDivisorType.Custom, 15);
+
+ switchPresets(1);
+ assertPreset(BeatDivisorType.Common);
+
+ switchPresets(-1);
+ assertPreset(BeatDivisorType.Triplets);
+ }
+
+ private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () =>
+ {
+ int chevronIndex = direction > 0 ? 1 : 0;
+ var chevronButton = beatDivisorControl.ChildrenOfType().ElementAt(chevronIndex);
+ InputManager.MoveMouseTo(chevronButton);
+ InputManager.Click(MouseButton.Left);
+ }, Math.Abs(direction));
+
+ private void assertBeatSnap(int expected) => AddAssert($"beat snap is {expected}",
+ () => bindableBeatDivisor.Value == expected);
+
+ private void switchPresets(int direction) => AddRepeatStep($"move presets {(direction > 0 ? "forward" : "backward")}", () =>
+ {
+ int chevronIndex = direction > 0 ? 3 : 2;
+ var chevronButton = beatDivisorControl.ChildrenOfType().ElementAt(chevronIndex);
+ InputManager.MoveMouseTo(chevronButton);
+ InputManager.Click(MouseButton.Left);
+ }, Math.Abs(direction));
+
+ private void assertPreset(BeatDivisorType type, int? maxDivisor = null)
+ {
+ AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type == type);
+
+ if (type == BeatDivisorType.Custom)
+ {
+ Debug.Assert(maxDivisor != null);
+ AddAssert($"max divisor is {maxDivisor}", () => bindableBeatDivisor.ValidDivisors.Value.Presets.Max() == maxDivisor.Value);
+ }
+ }
+
+ private void setDivisorViaInput(int divisor)
+ {
+ AddStep("open divisor input popover", () =>
+ {
+ var button = beatDivisorControl.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ BeatDivisorControl.CustomDivisorPopover popover = null;
+ AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().SingleOrDefault()) != null && popover.IsLoaded);
+ AddStep($"set divisor to {divisor}", () =>
+ {
+ var textBox = popover.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(textBox);
+ InputManager.Click(MouseButton.Left);
+ textBox.Text = divisor.ToString();
+ InputManager.Key(Key.Enter);
+ });
+ AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any());
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
index d100fba8d6..30c8539d85 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
@@ -1,44 +1,71 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
+using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene
{
- [Cached(typeof(EditorBeatmap))]
- [Cached(typeof(IBeatSnapProvider))]
- private readonly EditorBeatmap editorBeatmap =
- new EditorBeatmap(new OsuBeatmap
- {
- BeatmapInfo =
- {
- Ruleset = new OsuRuleset().RulesetInfo
- }
- });
+ private EditorBeatmap editorBeatmap;
[Cached]
private EditorClipboard clipboard = new EditorClipboard();
- protected override void LoadComplete()
+ [SetUpSteps]
+ public void SetUpSteps()
{
- base.LoadComplete();
-
- Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
-
- Child = new ComposeScreen
+ AddStep("setup compose screen", () =>
{
- State = { Value = Visibility.Visible },
- };
+ var beatmap = new OsuBeatmap
+ {
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
+ };
+
+ editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null));
+
+ Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
+
+ Child = new DependencyProvidingContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(EditorBeatmap), editorBeatmap),
+ (typeof(IBeatSnapProvider), editorBeatmap),
+ },
+ Child = new ComposeScreen { State = { Value = Visibility.Visible } },
+ };
+ });
+
+ AddUntilStep("wait for composer", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+ }
+
+ ///
+ /// Ensures that the skin of the edited beatmap is properly wrapped in a .
+ ///
+ [Test]
+ public void TestLegacyBeatmapSkinHasTransformer()
+ {
+ AddAssert("legacy beatmap skin has transformer", () =>
+ {
+ var sources = this.ChildrenOfType().First().AllSources;
+ return sources.OfType().Count(t => t.Skin == editorBeatmap.BeatmapSkin.AsNonNull().Skin) == 1;
+ });
}
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index a14c9aded3..b109234fec 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -6,15 +6,25 @@ using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Database;
+using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Storyboards;
using osu.Game.Tests.Resources;
+using osuTK;
+using osuTK.Input;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
@@ -55,13 +65,19 @@ namespace osu.Game.Tests.Visual.Editing
EditorBeatmap editorBeatmap = null;
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
- AddStep("exit without save", () =>
+
+ AddStep("exit without save", () => Editor.Exit());
+ AddStep("hold to confirm", () =>
{
- Editor.Exit();
- DialogOverlay.CurrentDialog.PerformOkAction();
+ var confirmButton = DialogOverlay.CurrentDialog.ChildrenOfType().First();
+
+ InputManager.MoveMouseTo(confirmButton);
+ InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
+ AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
+
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == true);
}
@@ -92,12 +108,27 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
- public void TestCreateNewDifficulty()
+ public void TestCreateNewDifficulty([Values] bool sameRuleset)
{
string firstDifficultyName = Guid.NewGuid().ToString();
string secondDifficultyName = Guid.NewGuid().ToString();
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
+ AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
+ {
+ new HitCircle
+ {
+ Position = new Vector2(0),
+ StartTime = 0
+ },
+ new HitCircle
+ {
+ Position = OsuPlayfield.BASE_SIZE,
+ StartTime = 1000
+ }
+ }));
+
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () =>
{
@@ -111,13 +142,27 @@ namespace osu.Game.Tests.Visual.Editing
});
AddAssert("can save again", () => Editor.Save());
- AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
+ AddStep("create new difficulty", () => Editor.CreateNewDifficulty(sameRuleset ? new OsuRuleset().RulesetInfo : new CatchRuleset().RulesetInfo));
+
+ if (sameRuleset)
+ {
+ AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
+ AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction());
+ }
+
AddUntilStep("wait for created", () =>
{
string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != firstDifficultyName;
});
+ AddAssert("created difficulty has timing point", () =>
+ {
+ var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
+ return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
+ });
+ AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0);
+
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName);
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () =>
@@ -133,11 +178,111 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
- public void TestCreateNewBeatmapFailsWithBlankNamedDifficulties()
+ public void TestCopyDifficulty()
+ {
+ string originalDifficultyName = Guid.NewGuid().ToString();
+ string copyDifficultyName = $"{originalDifficultyName} (copy)";
+
+ AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = originalDifficultyName);
+ AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
+ {
+ new HitCircle
+ {
+ Position = new Vector2(0),
+ StartTime = 0
+ },
+ new HitCircle
+ {
+ Position = OsuPlayfield.BASE_SIZE,
+ StartTime = 1000
+ }
+ }));
+ AddStep("set approach rate", () => EditorBeatmap.Difficulty.ApproachRate = 4);
+ AddStep("set combo colours", () =>
+ {
+ var beatmapSkin = EditorBeatmap.BeatmapSkin.AsNonNull();
+ beatmapSkin.ComboColours.Clear();
+ beatmapSkin.ComboColours.AddRange(new[]
+ {
+ new Colour4(255, 0, 0, 255),
+ new Colour4(0, 0, 255, 255)
+ });
+ });
+ AddStep("set status & online ID", () =>
+ {
+ EditorBeatmap.BeatmapInfo.OnlineID = 123456;
+ EditorBeatmap.BeatmapInfo.Status = BeatmapOnlineStatus.WIP;
+ });
+
+ AddStep("save beatmap", () => Editor.Save());
+ AddAssert("new beatmap persisted", () =>
+ {
+ var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == originalDifficultyName);
+ var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
+
+ return beatmap != null
+ && beatmap.DifficultyName == originalDifficultyName
+ && set != null
+ && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID);
+ });
+ AddAssert("can save again", () => Editor.Save());
+
+ AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
+
+ AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
+ AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick());
+
+ AddUntilStep("wait for created", () =>
+ {
+ string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
+ return difficultyName != null && difficultyName != originalDifficultyName;
+ });
+
+ AddAssert("created difficulty has copy suffix in name", () => EditorBeatmap.BeatmapInfo.DifficultyName == copyDifficultyName);
+ AddAssert("created difficulty has timing point", () =>
+ {
+ var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
+ return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
+ });
+ AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2);
+ AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4);
+ AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2);
+
+ AddAssert("status not copied", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None);
+ AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1);
+
+ AddStep("save beatmap", () => Editor.Save());
+
+ BeatmapInfo refetchedBeatmap = null;
+ Live refetchedBeatmapSet = null;
+
+ AddStep("refetch from database", () =>
+ {
+ refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == copyDifficultyName);
+ refetchedBeatmapSet = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
+ });
+
+ AddAssert("new beatmap persisted", () =>
+ {
+ return refetchedBeatmap != null
+ && refetchedBeatmap.DifficultyName == copyDifficultyName
+ && refetchedBeatmapSet != null
+ && refetchedBeatmapSet.PerformRead(s =>
+ s.Beatmaps.Count == 2
+ && s.Beatmaps.Any(b => b.DifficultyName == originalDifficultyName)
+ && s.Beatmaps.Any(b => b.DifficultyName == copyDifficultyName));
+ });
+ AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2));
+ }
+
+ [Test]
+ public void TestCreateMultipleNewDifficultiesSucceeds()
{
Guid setId = Guid.Empty;
AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID);
+ AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = "New Difficulty");
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () =>
{
@@ -146,15 +291,24 @@ namespace osu.Game.Tests.Visual.Editing
});
AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
- AddAssert("beatmap set unchanged", () =>
+ AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
+ AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction());
+
+ AddUntilStep("wait for created", () =>
+ {
+ string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
+ return difficultyName != null && difficultyName != "New Difficulty";
+ });
+ AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)");
+ AddAssert("new difficulty persisted", () =>
{
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
- return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
+ return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2);
});
}
[Test]
- public void TestCreateNewBeatmapFailsWithSameNamedDifficulties()
+ public void TestSavingBeatmapFailsWithSameNamedDifficulties([Values] bool sameRuleset)
{
Guid setId = Guid.Empty;
const string duplicate_difficulty_name = "duplicate";
@@ -168,7 +322,14 @@ namespace osu.Game.Tests.Visual.Editing
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
});
- AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
+ AddStep("create new difficulty", () => Editor.CreateNewDifficulty(sameRuleset ? new OsuRuleset().RulesetInfo : new CatchRuleset().RulesetInfo));
+
+ if (sameRuleset)
+ {
+ AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
+ AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction());
+ }
+
AddUntilStep("wait for created", () =>
{
string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index adaa24d542..e75c7f25a3 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -17,6 +17,13 @@ namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorSaving : EditorSavingTestScene
{
+ [Test]
+ public void TestCantExitWithoutSaving()
+ {
+ AddRepeatStep("Exit", () => InputManager.Key(Key.Escape), 10);
+ AddAssert("Editor is still active screen", () => Game.ScreenStack.CurrentScreen is Editor);
+ }
+
[Test]
public void TestMetadata()
{
@@ -49,6 +56,8 @@ namespace osu.Game.Tests.Visual.Editing
double originalTimelineZoom = 0;
double changedTimelineZoom = 0;
+ AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+
AddStep("Set beat divisor", () => Editor.Dependencies.Get().Value = 16);
AddStep("Set timeline zoom", () =>
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs
index 98d8a41674..2efd125f81 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs
@@ -4,11 +4,9 @@
using System;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
-using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Tests.Visual.Editing
{
@@ -22,7 +20,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("switch between all screens at once", () =>
{
foreach (var screen in Enum.GetValues(typeof(EditorScreenMode)).Cast())
- Editor.ChildrenOfType().Single().Mode.Value = screen;
+ Editor.Mode.Value = screen;
});
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index fdc3916c47..346a88a2d5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -5,12 +5,14 @@ using System.ComponentModel;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking;
+using osu.Game.Users.Drawables;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -39,11 +41,18 @@ namespace osu.Game.Tests.Visual.Gameplay
seekToBreak(1);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
- AddUntilStep("results displayed", () => getResultsScreen() != null);
+
+ AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0);
+ AddUntilStep("avatar displayed", () => getAvatar() != null);
+ AddAssert("avatar not clickable", () => getAvatar().ChildrenOfType().First().Action == null);
+
+ ClickableAvatar getAvatar() => getResultsScreen()
+ .ChildrenOfType().FirstOrDefault();
+
ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen;
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
new file mode 100644
index 0000000000..8ca49837da
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
@@ -0,0 +1,102 @@
+// 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.Testing;
+using osu.Game.Overlays.Settings;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play.PlayerSettings;
+using osu.Game.Tests.Visual.Ranking;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneBeatmapOffsetControl : OsuTestScene
+ {
+ private BeatmapOffsetControl offsetControl;
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("Create control", () =>
+ {
+ Child = new PlayerSettingsGroup("Some settings")
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ offsetControl = new BeatmapOffsetControl()
+ }
+ };
+ });
+ }
+
+ [Test]
+ public void TestTooShortToDisplay()
+ {
+ AddStep("Set short reference score", () =>
+ {
+ offsetControl.ReferenceScore.Value = new ScoreInfo
+ {
+ HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2)
+ };
+ });
+
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestCalibrationFromZero()
+ {
+ const double average_error = -4.5;
+
+ AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0);
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ AddStep("Set reference score", () =>
+ {
+ offsetControl.ReferenceScore.Value = new ScoreInfo
+ {
+ HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error)
+ };
+ });
+
+ AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any());
+ AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick());
+ AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error);
+
+ AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value);
+ AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ }
+
+ ///
+ /// When a beatmap offset was already set, the calibration should take it into account.
+ ///
+ [Test]
+ public void TestCalibrationFromNonZero()
+ {
+ const double average_error = -4.5;
+ const double initial_offset = -2;
+
+ AddStep("Set offset non-neutral", () => offsetControl.Current.Value = initial_offset);
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ AddStep("Set reference score", () =>
+ {
+ offsetControl.ReferenceScore.Value = new ScoreInfo
+ {
+ HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error)
+ };
+ });
+
+ AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any());
+ AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick());
+ AddAssert("Offset is adjusted", () => offsetControl.Current.Value == initial_offset - average_error);
+
+ AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value);
+ AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
index c5f56cae9e..53364b6d89 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
@@ -18,9 +18,11 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osu.Game.Storyboards;
+using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -32,17 +34,23 @@ namespace osu.Game.Tests.Visual.Gameplay
private SkinManager skinManager { get; set; }
[Cached]
- private ScoreProcessor scoreProcessor = new ScoreProcessor();
+ private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
+ [Cached]
+ private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
+
+ [Cached]
+ private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
+
protected override bool HasCustomSteps => true;
[Test]
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
- CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
+ CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs
index 52bedc328d..b2f4fa2738 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs
@@ -1,14 +1,16 @@
// 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.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
-using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables;
using osuTK;
@@ -22,6 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private Storyboard storyboard { get; set; } = new Storyboard();
+ private IEnumerable sprites => this.ChildrenOfType();
+
[Test]
public void TestSkinSpriteDisallowedByDefault()
{
@@ -31,7 +35,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
- assertSpritesFromSkin(false);
+ AddAssert("sprite didn't find texture", () =>
+ sprites.All(sprite => sprite.ChildrenOfType().All(s => s.Texture == null)));
}
[Test]
@@ -41,9 +46,57 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
- AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.Centre, Vector2.Zero)));
+ AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
- assertSpritesFromSkin(true);
+ // Only checking for at least one sprite that succeeded, as not all skins in this test provide the hitcircleoverlay texture.
+ AddAssert("sprite found texture", () =>
+ sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Texture != null)));
+
+ AddAssert("skinnable sprite has correct size", () =>
+ sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Size == new Vector2(128))));
+ }
+
+ [Test]
+ public void TestFlippedSprite()
+ {
+ const string lookup_name = "hitcircleoverlay";
+
+ AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
+ AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
+ AddStep("flip sprites", () => sprites.ForEach(s =>
+ {
+ s.FlipH = true;
+ s.FlipV = true;
+ }));
+ AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight));
+ }
+
+ [Test]
+ public void TestNegativeScale()
+ {
+ const string lookup_name = "hitcircleoverlay";
+
+ AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
+ AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
+ AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1)));
+ AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight));
+ }
+
+ [Test]
+ public void TestNegativeScaleWithFlippedSprite()
+ {
+ const string lookup_name = "hitcircleoverlay";
+
+ AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
+ AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
+ AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1)));
+ AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight));
+ AddStep("flip sprites", () => sprites.ForEach(s =>
+ {
+ s.FlipH = true;
+ s.FlipV = true;
+ }));
+ AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft));
}
private DrawableStoryboardSprite createSprite(string lookupName, Anchor origin, Vector2 initialPosition)
@@ -54,10 +107,5 @@ namespace osu.Game.Tests.Visual.Gameplay
s.LifetimeStart = double.MinValue;
s.LifetimeEnd = double.MaxValue;
});
-
- private void assertSpritesFromSkin(bool fromSkin) =>
- AddAssert($"sprites are {(fromSkin ? "from skin" : "from storyboard")}",
- () => this.ChildrenOfType()
- .All(sprite => sprite.ChildrenOfType().Any() == fromSkin));
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
index 744227c55e..83d7d769df 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
@@ -56,10 +56,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private double lastFrequency = double.MaxValue;
- protected override void Update()
+ protected override void UpdateAfterChildren()
{
- base.Update();
+ base.UpdateAfterChildren();
+ // This must be done in UpdateAfterChildren to allow the gameplay clock to have updated before checking values.
double freq = Beatmap.Value.Track.AggregateFrequency.Value;
FrequencyIncreased |= freq > lastFrequency;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
index 6430c29dfa..79d7bb366d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
AddAssert("total number of results == 1", () =>
{
- var score = new ScoreInfo();
+ var score = new ScoreInfo { Ruleset = Ruleset.Value };
((FailPlayer)Player).ScoreProcessor.PopulateScore(score);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
index 6b3fc304e0..ae2bc60fc6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("get variables", () =>
{
sampleDisabler = Player;
- slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).FirstOrDefault();
+ slider = Player.ChildrenOfType().MinBy(s => s.HitObject.StartTime);
samples = slider?.ChildrenOfType().ToArray();
return slider != null;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index 4b54cd3510..2d12645811 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -8,11 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
+using osu.Game.Tests.Beatmaps;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -24,11 +27,17 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay;
[Cached]
- private ScoreProcessor scoreProcessor = new ScoreProcessor();
+ private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
+ [Cached]
+ private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
+
+ [Cached]
+ private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
+
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
index c1260f0231..7febb54010 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
@@ -48,7 +48,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create display", () => recreateDisplay(new OsuHitWindows(), 5));
- AddRepeatStep("New random judgement", () => newJudgement(), 40);
+ AddRepeatStep("New random judgement", () =>
+ {
+ double offset = RNG.Next(-150, 150);
+ newJudgement(offset, drawableRuleset.HitWindows.ResultFor(offset));
+ }, 400);
AddRepeatStep("New max negative", () => newJudgement(-drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
@@ -273,6 +277,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestScoreProcessor : ScoreProcessor
{
+ public TestScoreProcessor()
+ : base(new OsuRuleset())
+ {
+ }
+
public void Reset() => base.Reset(false);
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
index e03c8d7561..b90bd93002 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
@@ -1,11 +1,10 @@
// Copyright (c) ppy Pty Ltd