diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml
index 91ca622f55..ff6d869e72 100644
--- a/.github/ISSUE_TEMPLATE/bug-issue.yml
+++ b/.github/ISSUE_TEMPLATE/bug-issue.yml
@@ -58,7 +58,8 @@ body:
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*
+ - `~/.local/share/osu/logs` *on Linux*
+ - `~/Library/Application Support/osu/logs` *on macOS*
If you have selected a custom location for the game files, you can find the `logs` folder there.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 56b3ebe87b..213c5082ab 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ jobs:
run: dotnet tool restore
- name: Restore Packages
- run: dotnet restore
+ run: dotnet restore osu.Desktop.slnf
- name: Restore inspectcode cache
uses: actions/cache@v3
@@ -113,27 +113,36 @@ jobs:
with:
dotnet-version: "6.0.x"
- - name: Setup MSBuild
- uses: microsoft/setup-msbuild@v1
+ - name: Install .NET workloads
+ run: dotnet workload install maui-android
- - name: Build
- run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
+ - name: Compile
+ run: dotnet build -c Debug osu.Android.slnf
build-only-ios:
name: Build only (iOS)
- runs-on: macos-latest
+ # change to macos-latest once GitHub finishes migrating all repositories to macOS 12.
+ runs-on: macos-12
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
+ # see https://github.com/actions/runner-images/issues/6771#issuecomment-1354713617
+ # remove once all workflow VMs use Xcode 14.1
+ - name: Set Xcode Version
+ shell: bash
+ run: |
+ sudo xcode-select -s "/Applications/Xcode_14.1.app"
+ echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.1.app" >> $GITHUB_ENV
+
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
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: Install .NET Workloads
+ run: dotnet workload install maui-ios
+
- name: Build
- run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
+ run: dotnet build -c Debug osu.iOS
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index bfc9620174..99e39f6f56 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -28,7 +28,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Annotate CI run with test results
- uses: dorny/test-reporter@v1.4.2
+ uses: dorny/test-reporter@v1.6.0
with:
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ae2bdd2e82..c72a3b442e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,7 +23,8 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
* the in-game logs, which are located at:
* `%AppData%/osu/logs` (on Windows),
- * `~/.local/share/osu/logs` (on Linux and macOS),
+ * `~/.local/share/osu/logs` (on Linux),
+ * `~/Library/Application Support/osu/logs` (on macOS),
* `Android/data/sh.ppy.osulazer/files/logs` (on Android),
* on iOS they can be obtained by connecting your device to your desktop and [copying the `logs` directory from the app's own document storage using iTunes](https://support.apple.com/en-us/HT201301#copy-to-computer),
* your system specifications (including the operating system and platform you are playing on),
diff --git a/README.md b/README.md
index 75d61dad4d..0de82eba75 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,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+ ([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) |
+| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
| ------------- | ------------- | ------------- | ------------- | ------------- |
- 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.
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
index 092a013614..d09e7647e0 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
osu.Game.Rulesets.EmptyFreeform
Library
AnyCPU
@@ -12,4 +12,4 @@
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
index a3607343c9..9c8867f4ef 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
osu.Game.Rulesets.Pippidon
Library
AnyCPU
@@ -12,4 +12,4 @@
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
index 2ea52429ab..5bf3884f53 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
osu.Game.Rulesets.EmptyScrolling
Library
AnyCPU
@@ -12,4 +12,4 @@
-
\ No newline at end of file
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
index a3607343c9..9c8867f4ef 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
osu.Game.Rulesets.Pippidon
Library
AnyCPU
@@ -12,4 +12,4 @@
-
\ No newline at end of file
+
diff --git a/appveyor.yml b/appveyor.yml
index 5be73f9875..ed48a997e8 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,6 +1,6 @@
clone_depth: 1
version: '{branch}-{build}'
-image: Visual Studio 2019
+image: Visual Studio 2022
cache:
- '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
@@ -11,6 +11,8 @@ dotnet_csproj:
before_build:
- cmd: dotnet --info # Useful when version mismatch between CI and local
+ - cmd: dotnet workload install maui-android # Change to `dotnet workload restore` once there's no old projects
+ - cmd: dotnet workload install maui-ios # Change to `dotnet workload restore` once there's no old projects
- cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
build:
diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml
index adf98848bc..175c8d0f1b 100644
--- a/appveyor_deploy.yml
+++ b/appveyor_deploy.yml
@@ -1,6 +1,6 @@
clone_depth: 1
version: '{build}'
-image: Visual Studio 2019
+image: Visual Studio 2022
test: off
skip_non_tags: true
configuration: Release
@@ -83,4 +83,4 @@ artifacts:
deploy:
- provider: Environment
- name: nuget
\ No newline at end of file
+ name: nuget
diff --git a/osu.Android.props b/osu.Android.props
index 75828147a5..c6cf7812d1 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -1,61 +1,20 @@
- 8.0
- bin\$(Configuration)
- 4
- 2.0
- false
- false
- Library
- 512
- Off
- True
- Xamarin.Android.Net.AndroidClientHandler
- v10.0
- false
- true
- armeabi-v7a;x86;arm64-v8a
- true
- cjk,mideast,other,rare,west
- SdkOnly
- prompt
-
-
- True
- portable
- False
- DEBUG;TRACE
- false
- true
- false
-
-
- false
- None
- True
- false
- False
+ 21.0
+ android-x86;android-arm;android-arm64
+ apk
+ CJK;Mideast;Rare;West;Other;
+ Xamarin.Android.Net.AndroidMessageHandler
+
+ true
true
-
- osu.licenseheader
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ true
+
diff --git a/osu.Android/Properties/AndroidManifest.xml b/osu.Android/AndroidManifest.xml
similarity index 71%
rename from osu.Android/Properties/AndroidManifest.xml
rename to osu.Android/AndroidManifest.xml
index 165a64a424..bc2f49b1a9 100644
--- a/osu.Android/Properties/AndroidManifest.xml
+++ b/osu.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index be40db7508..ca3d628447 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
@@ -74,11 +75,23 @@ namespace osu.Android
Debug.Assert(Resources?.DisplayMetrics != null);
Point displaySize = new Point();
+#pragma warning disable 618 // GetSize is deprecated
WindowManager.DefaultDisplay.GetSize(displaySize);
+#pragma warning restore 618
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
bool isTablet = smallestWidthDp >= 600f;
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
+
+ // Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
+ // The assembly files are not available as files either after native AOT.
+ // Manually load them so that they can be loaded by RulesetStore.loadFromAppDomain.
+ // REMEMBER to fully uninstall previous version every time when investigating this!
+ // Don't forget osu.Game.Tests.Android too.
+ Assembly.Load("osu.Game.Rulesets.Osu");
+ Assembly.Load("osu.Game.Rulesets.Taiko");
+ Assembly.Load("osu.Game.Rulesets.Catch");
+ Assembly.Load("osu.Game.Rulesets.Mania");
}
protected override void OnNewIntent(Intent intent) => handleIntent(intent);
@@ -127,7 +140,7 @@ namespace osu.Android
cursor.MoveToFirst();
- int filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
+ int filenameColumn = cursor.GetColumnIndex(IOpenableColumns.DisplayName);
string filename = cursor.GetString(filenameColumn);
// SharpCompress requires archive streams to be seekable, which the stream opened by
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 1c6f41a7ec..0227d2aec2 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -5,7 +5,7 @@
using System;
using Android.App;
-using Android.OS;
+using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Input.Handlers;
@@ -14,7 +14,6 @@ using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Updater;
using osu.Game.Utils;
-using Xamarin.Essentials;
namespace osu.Android
{
@@ -48,7 +47,7 @@ namespace osu.Android
// https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
string versionName = string.Empty;
- if (Build.VERSION.SdkInt >= BuildVersionCodes.P)
+ if (OperatingSystem.IsAndroidVersionAtLeast(28))
{
versionName = packageInfo.LongVersionCode.ToString();
// ensure we only read the trailing portion of long (the part we are interested in).
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 004cc8c39c..1507bfaa29 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -1,73 +1,22 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Android
osu.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
- false
-
-
- cjk;mideast;other;rare;west
- d8
- r8
-
-
- None
- cjk;mideast;other;rare;west
- true
+ true
+
+ false
+ 0.0.0
+ 1
+ $(Version)
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
- {58f6c80c-1253-4a0e-a465-b8c85ebeadf3}
- osu.Game.Rulesets.Catch
-
-
- {48f4582b-7687-4621-9cbe-5c24197cb536}
- osu.Game.Rulesets.Mania
-
-
- {c92a607b-1fdd-4954-9f92-03ff547d9080}
- osu.Game.Rulesets.Osu
-
-
- {f167e17a-7de6-4af5-b920-a5112296c695}
- osu.Game.Rulesets.Taiko
-
-
- {2a66dd92-adb1-4994-89e2-c94e04acda0d}
- osu.Game
-
-
-
-
-
-
-
- 5.0.0
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
index f8c3fcd894..bf7c0bfeca 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
index 94fdba4a3e..4ee3219442 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
@@ -1,49 +1,24 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Rulesets.Catch.Tests
osu.Game.Rulesets.Catch.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
- None
- cjk;mideast;other;rare;west
- true
-
-
-
-
-
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
+
+
+ %(RecursiveDir)%(Filename)%(Extension)
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {58f6c80c-1253-4a0e-a465-b8c85ebeadf3}
- osu.Game.Rulesets.Catch
-
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
+
+
-
-
- 5.0.0
-
-
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
index 71d943ece1..1fcb0aa427 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Rulesets.Catch.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
index 16a2b99997..5ace6c07f5 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
index be6044bbd0..acf12bb0ac 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj
@@ -1,35 +1,19 @@
-
-
+
- Debug
- iPhoneSimulator
- {4004C7B7-1A62-43F1-9DF2-52450FA67E70}
Exe
+ net6.0-ios
+ 13.4
osu.Game.Rulesets.Catch.Tests
osu.Game.Rulesets.Catch.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}
- osu.Game.Rulesets.Catch
-
+
+
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
index 50b928d509..c48bf7adc9 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs
@@ -18,6 +18,36 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+ [Test]
+ public void TestAlwaysHidden()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new CatchModNoScope
+ {
+ HiddenComboCount = { Value = 0 },
+ },
+ Autoplay = true,
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Fruit
+ {
+ X = CatchPlayfield.CENTER_X * 0.5f,
+ StartTime = 1000,
+ },
+ new Fruit
+ {
+ X = CatchPlayfield.CENTER_X * 1.5f,
+ StartTime = 2000,
+ }
+ }
+ }
+ });
+ }
+
[Test]
public void TestVisibleDuringBreak()
{
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
index 8472b995e8..5835ccaf78 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
@@ -4,8 +4,10 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@@ -55,6 +57,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
}
});
+ [Test]
+ public void TestGameCursorHidden()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new CatchModRelax(),
+ Autoplay = false,
+ PassCondition = () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ return this.ChildrenOfType().Single().State.Value == Visibility.Hidden;
+ }
+ });
+ }
+
private bool passCondition()
{
var playfield = this.ChildrenOfType().Single();
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs
index e5cdcf706c..43918bda57 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs
@@ -121,9 +121,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return new SnapResult(originPosition, StartTime);
}
- return enumerateSnappingCandidates(time)
- .OrderBy(pos => Vector2.DistanceSquared(screenSpacePosition, pos.ScreenSpacePosition))
- .FirstOrDefault();
+ return enumerateSnappingCandidates(time).MinBy(pos => Vector2.DistanceSquared(screenSpacePosition, pos.ScreenSpacePosition));
}
private IEnumerable enumerateSnappingCandidates(double time)
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
index 19b4a39f97..ddeea51ecb 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
@@ -26,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.Mods
var catchPlayfield = (CatchPlayfield)playfield;
bool shouldAlwaysShowCatcher = IsBreakTime.Value;
float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha;
+
+ // AlwaysPresent required for catcher to still act on input when fully hidden.
+ catchPlayfield.CatcherArea.AlwaysPresent = true;
catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs
deleted file mode 100644
index 82d10e500d..0000000000
--- a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs
+++ /dev/null
@@ -1,193 +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 System;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Utils;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Scoring;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Rulesets.Catch.Skinning.Argon
-{
- public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
- {
- protected readonly HitResult Result;
-
- protected SpriteText JudgementText { get; private set; } = null!;
-
- private RingExplosion? ringExplosion;
-
- [Resolved]
- private OsuColour colours { get; set; } = null!;
-
- public ArgonJudgementPiece(HitResult result)
- {
- Result = result;
- Origin = Anchor.Centre;
- Y = 160;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- AutoSizeAxes = Axes.Both;
-
- InternalChildren = new Drawable[]
- {
- JudgementText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Text = Result.GetDescription().ToUpperInvariant(),
- Colour = colours.ForHitResult(Result),
- Blending = BlendingParameters.Additive,
- Spacing = new Vector2(10, 0),
- Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
- },
- };
-
- if (Result.IsHit())
- {
- AddInternal(ringExplosion = new RingExplosion(Result)
- {
- Colour = colours.ForHitResult(Result),
- });
- }
- }
-
- ///
- /// Plays the default animation for this judgement piece.
- ///
- ///
- /// The base implementation only handles fade (for all result types) and misses.
- /// Individual rulesets are recommended to implement their appropriate hit animations.
- ///
- public virtual void PlayAnimation()
- {
- switch (Result)
- {
- default:
- JudgementText
- .ScaleTo(Vector2.One)
- .ScaleTo(new Vector2(1.4f), 1800, Easing.OutQuint);
- break;
-
- case HitResult.Miss:
- this.ScaleTo(1.6f);
- this.ScaleTo(1, 100, Easing.In);
-
- this.MoveTo(Vector2.Zero);
- this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
-
- this.RotateTo(0);
- this.RotateTo(40, 800, Easing.InQuint);
- break;
- }
-
- this.FadeOutFromOne(800);
-
- ringExplosion?.PlayAnimation();
- }
-
- public Drawable? GetAboveHitObjectsProxiedContent() => null;
-
- private partial class RingExplosion : CompositeDrawable
- {
- private readonly float travel = 52;
-
- public RingExplosion(HitResult result)
- {
- const float thickness = 4;
-
- const float small_size = 9;
- const float large_size = 14;
-
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
-
- Blending = BlendingParameters.Additive;
-
- int countSmall = 0;
- int countLarge = 0;
-
- switch (result)
- {
- case HitResult.Meh:
- countSmall = 3;
- travel *= 0.3f;
- break;
-
- case HitResult.Ok:
- case HitResult.Good:
- countSmall = 4;
- travel *= 0.6f;
- break;
-
- case HitResult.Great:
- case HitResult.Perfect:
- countSmall = 4;
- countLarge = 4;
- break;
- }
-
- for (int i = 0; i < countSmall; i++)
- AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
-
- for (int i = 0; i < countLarge; i++)
- AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
- }
-
- public void PlayAnimation()
- {
- foreach (var c in InternalChildren)
- {
- const float start_position_ratio = 0.3f;
-
- float direction = RNG.NextSingle(0, 360);
- float distance = RNG.NextSingle(travel / 2, travel);
-
- c.MoveTo(new Vector2(
- MathF.Cos(direction) * distance * start_position_ratio,
- MathF.Sin(direction) * distance * start_position_ratio
- ));
-
- c.MoveTo(new Vector2(
- MathF.Cos(direction) * distance,
- MathF.Sin(direction) * distance
- ), 600, Easing.OutQuint);
- }
-
- this.FadeOutFromOne(1000, Easing.OutQuint);
- }
-
- public partial class RingPiece : CircularContainer
- {
- public RingPiece(float thickness = 9)
- {
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
-
- Masking = true;
- BorderThickness = thickness;
- BorderColour = Color4.White;
-
- Child = new Box
- {
- AlwaysPresent = true,
- Alpha = 0,
- RelativeSizeAxes = Axes.Both
- };
- }
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
index b36d7f11cb..f6b2c52498 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -32,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
- foreach (var state in Enum.GetValues(typeof(CatcherAnimationState)).Cast())
+ foreach (var state in Enum.GetValues())
{
AddInternal(drawables[state] = getDrawableFor(state).With(d =>
{
diff --git a/osu.Game.Rulesets.Catch/UI/CatchCursorContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchCursorContainer.cs
new file mode 100644
index 0000000000..4ae61ef8c7
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatchCursorContainer.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public partial class CatchCursorContainer : GameplayCursorContainer
+ {
+ // Just hide the cursor.
+ // The main goal here is to show that we have a cursor so the game never shows the global one.
+ protected override Drawable CreateCursor() => Empty();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 4df297565e..6167ee53f6 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -49,6 +50,8 @@ namespace osu.Game.Rulesets.Catch.UI
this.difficulty = difficulty;
}
+ protected override GameplayCursorContainer CreateCursor() => new CatchCursorContainer();
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs b/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
index d23913136d..10e43cf74a 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
@@ -11,7 +11,6 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
-using osuTK.Input;
namespace osu.Game.Rulesets.Catch.UI
{
@@ -106,41 +105,17 @@ namespace osu.Game.Rulesets.Catch.UI
return false;
}
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- return updateAction(e.Button, getTouchCatchActionFromInput(e.ScreenSpaceMousePosition));
- }
-
protected override bool OnTouchDown(TouchDownEvent e)
{
return updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
}
- protected override bool OnMouseMove(MouseMoveEvent e)
- {
- Show();
-
- TouchCatchAction? action = getTouchCatchActionFromInput(e.ScreenSpaceMousePosition);
-
- // multiple mouse buttons may be pressed and handling the same action.
- foreach (MouseButton button in e.PressedButtons)
- updateAction(button, action);
-
- return false;
- }
-
protected override void OnTouchMove(TouchMoveEvent e)
{
updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
base.OnTouchMove(e);
}
- protected override void OnMouseUp(MouseUpEvent e)
- {
- updateAction(e.Button, null);
- base.OnMouseUp(e);
- }
-
protected override void OnTouchUp(TouchUpEvent e)
{
updateAction(e.Touch.Source, null);
diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
index e2f95ca177..ecce7c1b3f 100644
--- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
+++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
catch the fruit. to the beat.
@@ -15,4 +15,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
index de7935b2ef..4a1545a423 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
index 9674186039..25335754d2 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
@@ -1,49 +1,24 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {531F1092-DB27-445D-AA33-2A77C7187C99}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Rulesets.Mania.Tests
osu.Game.Rulesets.Mania.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
- None
- cjk;mideast;other;rare;west
- true
-
-
-
-
-
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
+
+
+ %(RecursiveDir)%(Filename)%(Extension)
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {48f4582b-7687-4621-9cbe-5c24197cb536}
- osu.Game.Rulesets.Mania
-
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
+
+
-
-
- 5.0.0
-
-
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
index 2d1015387a..a508198f7f 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Rulesets.Mania.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
index 82d1c8ea24..ff5dde856e 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
index 88ad484bc1..51e07dd6c1 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj
@@ -1,35 +1,19 @@
-
-
+
- Debug
- iPhoneSimulator
- {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}
Exe
+ net6.0-ios
+ 13.4
osu.Game.Rulesets.Mania.Tests
osu.Game.Rulesets.Mania.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {48F4582B-7687-4621-9CBE-5C24197CB536}
- osu.Game.Rulesets.Mania
-
+
+
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index 653c75baac..aca555552f 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -90,6 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public override bool CursorInPlacementArea => false;
public TestHitObjectComposer(Playfield playfield)
+ : base(new ManiaRuleset())
{
Playfield = playfield;
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index 308238d87a..77f93b4ef9 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
@@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(hitObject, beatmap, previousPattern)
{
- if (random == null) throw new ArgumentNullException(nameof(random));
- if (originalBeatmap == null) throw new ArgumentNullException(nameof(originalBeatmap));
+ ArgumentNullException.ThrowIfNull(random);
+ ArgumentNullException.ThrowIfNull(originalBeatmap);
Random = random;
OriginalBeatmap = originalBeatmap;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
index b2e89c3410..931673f337 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
@@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
protected PatternGenerator(HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern)
{
- if (hitObject == null) throw new ArgumentNullException(nameof(hitObject));
- if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
- if (previousPattern == null) throw new ArgumentNullException(nameof(previousPattern));
+ ArgumentNullException.ThrowIfNull(hitObject);
+ ArgumentNullException.ThrowIfNull(beatmap);
+ ArgumentNullException.ThrowIfNull(previousPattern);
HitObject = hitObject;
Beatmap = beatmap;
diff --git a/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
index 1a67117c03..4d93826240 100644
--- a/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
+++ b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
@@ -22,8 +22,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
public static void Sort(T[] keys, IComparer comparer)
{
- if (keys == null)
- throw new ArgumentNullException(nameof(keys));
+ ArgumentNullException.ThrowIfNull(keys);
if (keys.Length == 0)
return;
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
index 2dbf475c7e..4ce3c50f7c 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -18,20 +17,18 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
- public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
- protected readonly HitResult Result;
-
- protected SpriteText JudgementText { get; private set; } = null!;
-
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
+ : base(result)
{
- Result = result;
+ AutoSizeAxes = Axes.Both;
+
Origin = Anchor.Centre;
Y = 160;
}
@@ -39,22 +36,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
[BackgroundDependencyLoader]
private void load()
{
- AutoSizeAxes = Axes.Both;
-
- InternalChildren = new Drawable[]
- {
- JudgementText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Text = Result.GetDescription().ToUpperInvariant(),
- Colour = colours.ForHitResult(Result),
- Blending = BlendingParameters.Additive,
- Spacing = new Vector2(10, 0),
- Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
- },
- };
-
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@@ -64,6 +45,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
}
}
+ protected override SpriteText CreateJudgementText() =>
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(10, 0),
+ Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
+ };
+
///
/// Plays the default animation for this judgement piece.
///
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
index eb7f63fbe2..057b7eb0d9 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
@@ -27,6 +27,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
switch (lookup)
{
case GameplaySkinComponentLookup resultComponent:
+ // This should eventually be moved to a skin setting, when supported.
+ if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
+ return Drawable.Empty();
+
return new ArgonJudgementPiece(resultComponent.Component);
case ManiaSkinComponentLookup maniaComponent:
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 6a31fb3fda..6ca830a82f 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -134,6 +134,9 @@ namespace osu.Game.Rulesets.Mania.UI
protected override void Dispose(bool isDisposing)
{
+ // must happen before children are disposed in base call to prevent illegal accesses to the hit explosion pool.
+ NewResult -= OnNewResult;
+
base.Dispose(isDisposing);
if (skin != null)
@@ -206,18 +209,6 @@ namespace osu.Game.Rulesets.Mania.UI
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
}
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- keyBindingContainer?.TriggerPressed(column.Action.Value);
- return base.OnMouseDown(e);
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- keyBindingContainer?.TriggerReleased(column.Action.Value);
- base.OnMouseUp(e);
- }
-
protected override bool OnTouchDown(TouchDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 01e9926ad7..e3ebadc836 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Mania.UI
public ManiaPlayfield(List stageDefinitions)
{
- if (stageDefinitions == null)
- throw new ArgumentNullException(nameof(stageDefinitions));
+ ArgumentNullException.ThrowIfNull(stageDefinitions);
if (stageDefinitions.Count <= 0)
throw new ArgumentException("Can't have zero or fewer stages.");
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index fc38a96a35..c1d3e85bf1 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -156,6 +156,9 @@ namespace osu.Game.Rulesets.Mania.UI
protected override void Dispose(bool isDisposing)
{
+ // must happen before children are disposed in base call to prevent illegal accesses to the judgement pool.
+ NewResult -= OnNewResult;
+
base.Dispose(isDisposing);
if (currentSkin != null)
diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
index 4f6840f9ca..72f172188e 100644
--- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
+++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
smash the keys. to the beat.
@@ -15,4 +15,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
index 3ce17ccc27..45d27dda70 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
index f4b673f10b..e8a46a9828 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
@@ -1,49 +1,27 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {90CAB706-39CB-4B93-9629-3218A6FF8E9B}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Rulesets.Osu.Tests
osu.Game.Rulesets.Osu.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
- None
- cjk;mideast;other;rare;west
- true
-
-
-
-
-
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
+
+
+ %(RecursiveDir)%(Filename)%(Extension)
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {c92a607b-1fdd-4954-9f92-03ff547d9080}
- osu.Game.Rulesets.Osu
-
-
- {2a66dd92-adb1-4994-89e2-c94e04acda0d}
- osu.Game
-
+
+
-
- 5.0.0
-
+
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
index ad23f3ee33..6ef29fa68e 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Rulesets.Osu.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
index a88b74695c..1e33f2ff16 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
index 545abcec6c..7d50deb8ba 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj
@@ -1,35 +1,20 @@
-
-
+
- Debug
- iPhoneSimulator
- {6653CA6F-DB06-4604-A3FD-762E25C2AF96}
+ Exe
+ net6.0-ios
+ 13.4
Exe
osu.Game.Rulesets.Osu.Tests
osu.Game.Rulesets.Osu.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {C92A607B-1FDD-4954-9F92-03FF547D9080}
- osu.Game.Rulesets.Osu
-
+
+
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs
new file mode 100644
index 0000000000..daa914cac2
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs
@@ -0,0 +1,91 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+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.Tests
+{
+ [TestFixture]
+ public class OsuHitObjectGenerationUtilsTest
+ {
+ private static Slider createTestSlider()
+ {
+ var slider = new Slider
+ {
+ Position = new Vector2(128, 128),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(new Vector2(), PathType.Linear),
+ new PathControlPoint(new Vector2(-64, -128), PathType.Linear), // absolute position: (64, 0)
+ new PathControlPoint(new Vector2(-128, 0), PathType.Linear) // absolute position: (0, 128)
+ }
+ },
+ RepeatCount = 1
+ };
+ slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ return slider;
+ }
+
+ [Test]
+ public void TestReflectSliderHorizontallyAlongPlayfield()
+ {
+ var slider = createTestSlider();
+
+ OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider);
+
+ Assert.That(slider.Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128)));
+ Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
+ {
+ new Vector2(),
+ new Vector2(64, -128),
+ new Vector2(128, 0)
+ }));
+ }
+
+ [Test]
+ public void TestReflectSliderVerticallyAlongPlayfield()
+ {
+ var slider = createTestSlider();
+
+ OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider);
+
+ Assert.That(slider.Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
+ Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
+ {
+ new Vector2(),
+ new Vector2(-64, 128),
+ new Vector2(-128, 0)
+ }));
+ }
+
+ [Test]
+ public void TestFlipSliderInPlaceHorizontally()
+ {
+ var slider = createTestSlider();
+
+ OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
+
+ Assert.That(slider.Position, Is.EqualTo(new Vector2(128, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(256, 128)));
+ Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
+ {
+ new Vector2(),
+ new Vector2(64, -128),
+ new Vector2(128, 0)
+ }));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index 32d0cc8939..1e9f931b74 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -160,9 +160,9 @@ namespace osu.Game.Rulesets.Osu.Tests
static bool assertSamples(HitObject hitObject) => hitObject.Samples.All(s => s.Name != HitSampleInfo.HIT_CLAP && s.Name != HitSampleInfo.HIT_WHISTLE);
}
- private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
+ private Drawable testSimpleBig(int repeats = 0) => createSlider(repeats: repeats);
- private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
+ private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(repeats: repeats, stackHeight: 10);
private Drawable testDistanceOverflow(int repeats = 0)
{
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index 3a175888d9..66d99e0bf1 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -133,6 +133,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(e.NewItems != null);
+
// If inserting in the path (not appending),
// update indices of existing connections after insert location
if (e.NewStartingIndex < Pieces.Count)
@@ -164,6 +166,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(e.OldItems != null);
+
foreach (var point in e.OldItems.Cast())
{
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
index e412c47c09..73ee5df9dc 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
@@ -4,6 +4,8 @@
#nullable disable
using System;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
@@ -22,6 +24,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
private bool isPlacingEnd;
+ [Resolved(CanBeNull = true)]
+ [CanBeNull]
+ private IBeatSnapProvider beatSnapProvider { get; set; }
+
public SpinnerPlacementBlueprint()
: base(new Spinner { Position = OsuPlayfield.BASE_SIZE / 2 })
{
@@ -33,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
base.Update();
if (isPlacingEnd)
- HitObject.EndTime = Math.Max(HitObject.StartTime, EditorClock.CurrentTime);
+ updateEndTimeFromCurrent();
piece.UpdateFrom(HitObject);
}
@@ -45,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
if (e.Button != MouseButton.Right)
return false;
- HitObject.EndTime = EditorClock.CurrentTime;
+ updateEndTimeFromCurrent();
EndPlacement(true);
}
else
@@ -61,5 +67,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return true;
}
+
+ private void updateEndTimeFromCurrent()
+ {
+ HitObject.EndTime = beatSnapProvider == null
+ ? Math.Max(HitObject.StartTime, EditorClock.CurrentTime)
+ : Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime));
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
index 5430929143..19d4a1bf83 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
var osuObject = (OsuHitObject)hitObject;
- OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
+ OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
index 0a54d58718..6d01808fb5 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
@@ -27,16 +27,16 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (Reflection.Value)
{
case MirrorType.Horizontal:
- OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
+ OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(osuObject);
break;
case MirrorType.Vertical:
- OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
+ OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
break;
case MirrorType.Both:
- OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
- OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
+ OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(osuObject);
+ OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 1621bb50b1..307d731fd4 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -65,6 +65,11 @@ namespace osu.Game.Rulesets.Osu.Mods
flowDirection = !flowDirection;
}
+ if (positionInfos[i].HitObject is Slider slider && random.NextDouble() < 0.5)
+ {
+ OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
+ }
+
if (i == 0)
{
positionInfos[i].DistanceFromPrevious = (float)(random.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
index 55c20eebe9..77cf340b95 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
@@ -17,7 +17,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
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;
@@ -196,8 +195,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private IEnumerable generateBeats(IBeatmap beatmap, IReadOnlyCollection originalHitObjects)
{
- double startTime = originalHitObjects.First().StartTime;
- double endTime = originalHitObjects.Last().GetEndTime();
+ double startTime = beatmap.HitObjects.First().StartTime;
+ double endTime = beatmap.GetLastObjectTime();
var beats = beatmap.ControlPointInfo.TimingPoints
// Ignore timing points after endTime
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
index 1cb3208c30..74e16f7e0b 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
@@ -82,10 +82,10 @@ namespace osu.Game.Rulesets.Osu.Replays
private class ReplayFrameComparer : IComparer
{
- public int Compare(ReplayFrame f1, ReplayFrame f2)
+ public int Compare(ReplayFrame? f1, ReplayFrame? f2)
{
- if (f1 == null) throw new ArgumentNullException(nameof(f1));
- if (f2 == null) throw new ArgumentNullException(nameof(f2));
+ ArgumentNullException.ThrowIfNull(f1);
+ ArgumentNullException.ThrowIfNull(f2);
return f1.Time.CompareTo(f2.Time);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
index 95c75164aa..fca3e70236 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
@@ -1,35 +1,64 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public partial class ArgonFollowCircle : FollowCircle
{
+ private readonly CircularContainer circleContainer;
+ private readonly Box circleFill;
+
+ private readonly IBindable accentColour = new Bindable();
+
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? parentObject { get; set; }
+
public ArgonFollowCircle()
{
- InternalChild = new CircularContainer
+ InternalChild = circleContainer = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 4,
- BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
Blending = BlendingParameters.Additive,
- Child = new Box
+ Child = circleFill = new Box
{
- Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Alpha = 0.3f,
}
};
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ if (parentObject != null)
+ accentColour.BindTo(parentObject.AccentColour);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ accentColour.BindValueChanged(colour =>
+ {
+ circleContainer.BorderColour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
+ circleFill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
+ }, true);
+ }
+
protected override void OnSliderPress()
{
const float duration = 300f;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
index f5f410210b..6f55d93eff 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -17,42 +16,24 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
- public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
- protected readonly HitResult Result;
-
- protected SpriteText JudgementText { get; private set; } = null!;
-
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
+ : base(result)
{
- Result = result;
+ AutoSizeAxes = Axes.Both;
+
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
- AutoSizeAxes = Axes.Both;
-
- InternalChildren = new Drawable[]
- {
- JudgementText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Text = Result.GetDescription().ToUpperInvariant(),
- Colour = colours.ForHitResult(Result),
- Blending = BlendingParameters.Additive,
- Spacing = new Vector2(5, 0),
- Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
- },
- };
-
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@@ -62,6 +43,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
}
}
+ protected override SpriteText CreateJudgementText() =>
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(5, 0),
+ Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
+ };
+
///
/// Plays the default animation for this judgement piece.
///
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
index 48b43f359d..d6ce793c7e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -21,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
+ private readonly IBindable accentColour = new Bindable();
+
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
@@ -37,7 +41,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
fill = new Box
{
- Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -53,10 +56,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
};
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ if (parentObject != null)
+ accentColour.BindTo(parentObject.AccentColour);
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
+ accentColour.BindValueChanged(colour =>
+ {
+ fill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
+ }, true);
+
if (parentObject != null)
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
index 86194d2c43..f98a47097d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
@@ -19,6 +19,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
switch (lookup)
{
case GameplaySkinComponentLookup resultComponent:
+ // This should eventually be moved to a skin setting, when supported.
+ if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
+ return Drawable.Empty();
+
return new ArgonJudgementPiece(resultComponent.Component);
case OsuSkinComponentLookup osuComponent:
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 122330d09b..ed02284a4b 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.UI
HitPolicy = new StartTimeOrderedHitPolicy();
var hitWindows = new OsuHitWindows();
- foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
+ foreach (var result in Enum.GetValues().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
AddRangeInternal(poolDictionary.Values);
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
index 3a8b3f67d0..aa4cd0af14 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -112,44 +112,46 @@ namespace osu.Game.Rulesets.Osu.Utils
/// Reflects the position of the in the playfield horizontally.
///
/// The object to reflect.
- public static void ReflectHorizontally(OsuHitObject osuObject)
+ public static void ReflectHorizontallyAlongPlayfield(OsuHitObject osuObject)
{
osuObject.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - osuObject.X, osuObject.Position.Y);
- if (!(osuObject is Slider slider))
+ if (osuObject is not Slider slider)
return;
- // No need to update the head and tail circles, since slider handles that when the new slider path is set
- slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
- slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
+ void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - nested.Position.X, nested.Position.Y);
+ static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
- var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
- foreach (var point in controlPoints)
- point.Position = new Vector2(-point.Position.X, point.Position.Y);
-
- slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
+ modifySlider(slider, reflectNestedObject, reflectControlPoint);
}
///
/// Reflects the position of the in the playfield vertically.
///
/// The object to reflect.
- public static void ReflectVertically(OsuHitObject osuObject)
+ public static void ReflectVerticallyAlongPlayfield(OsuHitObject osuObject)
{
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
- if (!(osuObject is Slider slider))
+ if (osuObject is not Slider slider)
return;
- // No need to update the head and tail circles, since slider handles that when the new slider path is set
- slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
- slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
+ void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(nested.Position.X, OsuPlayfield.BASE_SIZE.Y - nested.Position.Y);
+ static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
- var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
- foreach (var point in controlPoints)
- point.Position = new Vector2(point.Position.X, -point.Position.Y);
+ modifySlider(slider, reflectNestedObject, reflectControlPoint);
+ }
- slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
+ ///
+ /// Flips the position of the around its start position horizontally.
+ ///
+ /// The slider to be flipped.
+ public static void FlipSliderInPlaceHorizontally(Slider slider)
+ {
+ void flipNestedObject(OsuHitObject nested) => nested.Position = new Vector2(slider.X - (nested.X - slider.X), nested.Y);
+ static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
+
+ modifySlider(slider, flipNestedObject, flipControlPoint);
}
///
@@ -160,14 +162,20 @@ namespace osu.Game.Rulesets.Osu.Utils
public static void RotateSlider(Slider slider, float rotation)
{
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
+ void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
+ modifySlider(slider, rotateNestedObject, rotateControlPoint);
+ }
+
+ private static void modifySlider(Slider slider, Action modifyNestedObject, Action modifyControlPoint)
+ {
// No need to update the head and tail circles, since slider handles that when the new slider path is set
- slider.NestedHitObjects.OfType().ForEach(rotateNestedObject);
- slider.NestedHitObjects.OfType().ForEach(rotateNestedObject);
+ slider.NestedHitObjects.OfType().ForEach(modifyNestedObject);
+ slider.NestedHitObjects.OfType().ForEach(modifyNestedObject);
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints)
- point.Position = rotateVector(point.Position, rotation);
+ modifyControlPoint(point);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
index 98f1e69bd1..bf776fe5dd 100644
--- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
+++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
click the circles. to the beat.
@@ -15,4 +15,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
index d9de0fde4e..452b9683ec 100644
--- a/osu.Game.Rulesets.Taiko.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
index 4d4dabebe6..a639326ebd 100644
--- a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
@@ -1,49 +1,24 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {3701A0A1-8476-42C6-B5C4-D24129B4A484}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Rulesets.Taiko.Tests
osu.Game.Rulesets.Taiko.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
- None
- cjk;mideast;other;rare;west
- true
-
-
-
-
-
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
+
+
+ %(RecursiveDir)%(Filename)%(Extension)
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {f167e17a-7de6-4af5-b920-a5112296c695}
- osu.Game.Rulesets.Taiko
-
-
- {2a66dd92-adb1-4994-89e2-c94e04acda0d}
- osu.Game
-
+
+
-
-
- 5.0.0
-
-
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs b/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs
index 1ebbd61a94..0e3a953728 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Rulesets.Taiko.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
index 9628475b3e..76cb3c0db0 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj b/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj
index 8ee640cd99..e648a11299 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj
@@ -1,35 +1,19 @@
-
-
+
- Debug
- iPhoneSimulator
- {7E408809-66AC-49D1-AF4D-98834F9B979A}
Exe
+ net6.0-ios
+ 13.4
osu.Game.Rulesets.Taiko.Tests
osu.Game.Rulesets.Taiko.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {F167E17A-7DE6-4AF5-B920-A5112296C695}
- osu.Game.Rulesets.Taiko
-
+
+
-
-
\ No newline at end of file
+
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoKiaiGlow.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoKiaiGlow.cs
new file mode 100644
index 0000000000..a5e2eb0dbb
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoKiaiGlow.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Taiko.Skinning.Legacy;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ public partial class TestSceneTaikoKiaiGlow : TaikoSkinnableTestScene
+ {
+ [Test]
+ public void TestKiaiGlow()
+ {
+ AddStep("Create kiai glow", () => SetContents(_ => new LegacyKiaiGlow()));
+ AddToggleStep("Toggle kiai mode", setUpBeatmap);
+ }
+
+ private void setUpBeatmap(bool withKiai)
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+
+ if (withKiai)
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ Beatmap.Value.Track.Start();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index f7fdd447d6..3b3b3e606c 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
- drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
+ drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
index 6756001089..bbd62ff85b 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -18,20 +17,16 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
- public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
- protected readonly HitResult Result;
-
- protected SpriteText JudgementText { get; private set; } = null!;
-
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
+ : base(result)
{
- Result = result;
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Both;
}
@@ -39,21 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
[BackgroundDependencyLoader]
private void load()
{
- InternalChildren = new Drawable[]
- {
- JudgementText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Text = Result.GetDescription().ToUpperInvariant(),
- Colour = colours.ForHitResult(Result),
- Blending = BlendingParameters.Additive,
- Spacing = new Vector2(10, 0),
- RelativePositionAxes = Axes.Both,
- Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular),
- },
- };
-
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@@ -64,6 +44,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
}
}
+ protected override SpriteText CreateJudgementText() =>
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(10, 0),
+ RelativePositionAxes = Axes.Both,
+ Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular),
+ };
+
///
/// Plays the default animation for this judgement piece.
///
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs
index a5d091a1c8..780018af4e 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs
@@ -19,6 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
switch (component)
{
case GameplaySkinComponentLookup resultComponent:
+ // This should eventually be moved to a skin setting, when supported.
+ if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
+ return Drawable.Empty();
+
return new ArgonJudgementPiece(resultComponent.Component);
case TaikoSkinComponentLookup taikoComponent:
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs
new file mode 100644
index 0000000000..623243e9e1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.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 osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
+{
+ internal partial class LegacyKiaiGlow : BeatSyncedContainer
+ {
+ private bool isKiaiActive;
+
+ private Sprite sprite = null!;
+
+ [BackgroundDependencyLoader(true)]
+ private void load(ISkinSource skin, HealthProcessor? healthProcessor)
+ {
+ Child = sprite = new Sprite
+ {
+ Texture = skin.GetTexture("taiko-glow"),
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Alpha = 0,
+ Scale = new Vector2(0.7f),
+ Colour = new Colour4(255, 228, 0, 255),
+ };
+
+ if (healthProcessor != null)
+ healthProcessor.NewJudgement += onNewJudgement;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (isKiaiActive)
+ sprite.Alpha = (float)Math.Min(1, sprite.Alpha + Math.Abs(Clock.ElapsedFrameTime) / 100f);
+ else
+ sprite.Alpha = (float)Math.Max(0, sprite.Alpha - Math.Abs(Clock.ElapsedFrameTime) / 600f);
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
+ {
+ isKiaiActive = effectPoint.KiaiMode;
+ }
+
+ private void onNewJudgement(JudgementResult result)
+ {
+ if (!result.IsHit || !isKiaiActive)
+ return;
+
+ sprite.ScaleTo(0.85f).Then()
+ .ScaleTo(0.7f, 80, Easing.OutQuad);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs
index 86175d3bca..85870d0fd6 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.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.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
private Sprite kiai = null!;
- private bool kiaiDisplayed;
+ private bool isKiaiActive;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
@@ -41,17 +42,19 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
};
}
+ protected override void Update()
+ {
+ base.Update();
+
+ if (isKiaiActive)
+ kiai.Alpha = (float)Math.Min(1, kiai.Alpha + Math.Abs(Clock.ElapsedFrameTime) / 200f);
+ else
+ kiai.Alpha = (float)Math.Max(0, kiai.Alpha - Math.Abs(Clock.ElapsedFrameTime) / 200f);
+ }
+
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
- base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
-
- if (effectPoint.KiaiMode != kiaiDisplayed)
- {
- kiaiDisplayed = effectPoint.KiaiMode;
-
- kiai.ClearTransforms();
- kiai.FadeTo(kiaiDisplayed ? 1 : 0, 200);
- }
+ isKiaiActive = effectPoint.KiaiMode;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index 7bf99306f0..d61f9ac35d 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -129,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.Mascot:
return new DrawableTaikoMascot();
+ case TaikoSkinComponents.KiaiGlow:
+ if (GetTexture("taiko-glow") != null)
+ return new LegacyKiaiGlow();
+
+ return null;
+
default:
throw new UnsupportedSkinComponentException(lookup);
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index bf48898dd2..b8e3313e1b 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -21,5 +21,6 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionKiai,
Scroller,
Mascot,
+ KiaiGlow
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 32a83d87b8..146daa8c27 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
- public readonly BindableBool LockPlayfieldAspect = new BindableBool(true);
+ public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
- LockPlayfieldAspect = { BindTarget = LockPlayfieldAspect }
+ LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs
index ab8c0a484e..0232c10d65 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs
@@ -107,24 +107,6 @@ namespace osu.Game.Rulesets.Taiko.UI
return false;
}
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- if (!validMouse(e))
- return false;
-
- handleDown(e.Button, e.ScreenSpaceMousePosition);
- return true;
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- if (!validMouse(e))
- return;
-
- handleUp(e.Button);
- base.OnMouseUp(e);
- }
-
protected override bool OnTouchDown(TouchDownEvent e)
{
handleDown(e.Touch.Source, e.ScreenSpaceTouchDownPosition);
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 6ce0be5868..9f9debe7d7 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -112,6 +112,10 @@ namespace osu.Game.Rulesets.Taiko.UI
FillMode = FillMode.Fit,
Children = new[]
{
+ new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.KiaiGlow), _ => Empty())
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
hitExplosionContainer = new Container
{
RelativeSizeAxes = Axes.Both,
@@ -186,7 +190,7 @@ namespace osu.Game.Rulesets.Taiko.UI
var hitWindows = new TaikoHitWindows();
- foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => hitWindows.IsHitResultAllowed(r)))
+ foreach (var result in Enum.GetValues().Where(r => hitWindows.IsHitResultAllowed(r)))
{
judgementPools.Add(result, new DrawablePool(15));
explosionPools.Add(result, new HitExplosionPool(result));
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 79c5c36e08..42732d90e4 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -13,7 +13,7 @@ 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);
+ public readonly IBindable LockPlayfieldMaxAspect = new BindableBool(true);
protected override void Update()
{
@@ -21,7 +21,12 @@ namespace osu.Game.Rulesets.Taiko.UI
float height = default_relative_height;
- if (LockPlayfieldAspect.Value)
+ // Players coming from stable expect to be able to change the aspect ratio regardless of the window size.
+ // We originally wanted to limit this more, but there was considerable pushback from the community.
+ //
+ // As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
+ // This is still a bit weird, because readability changes with window size, but it is what it is.
+ if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Height = height;
diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj
index b752c13d18..f0e1cb8e8f 100644
--- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj
+++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
bash the drum. to the beat.
diff --git a/osu.Game.Tests.Android/Properties/AndroidManifest.xml b/osu.Game.Tests.Android/AndroidManifest.xml
similarity index 98%
rename from osu.Game.Tests.Android/Properties/AndroidManifest.xml
rename to osu.Game.Tests.Android/AndroidManifest.xml
index 4a63f0c357..f25b2e5328 100644
--- a/osu.Game.Tests.Android/Properties/AndroidManifest.xml
+++ b/osu.Game.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Tests.Android/MainActivity.cs b/osu.Game.Tests.Android/MainActivity.cs
index 6c4f9bac58..bdb947fbb4 100644
--- a/osu.Game.Tests.Android/MainActivity.cs
+++ b/osu.Game.Tests.Android/MainActivity.cs
@@ -3,7 +3,9 @@
#nullable disable
+using System.Reflection;
using Android.App;
+using Android.OS;
using osu.Framework.Android;
namespace osu.Game.Tests.Android
@@ -12,5 +14,16 @@ namespace osu.Game.Tests.Android
public class MainActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
+
+ protected override void OnCreate(Bundle savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+
+ // See the comment in OsuGameActivity
+ Assembly.Load("osu.Game.Rulesets.Osu");
+ Assembly.Load("osu.Game.Rulesets.Taiko");
+ Assembly.Load("osu.Game.Rulesets.Catch");
+ Assembly.Load("osu.Game.Rulesets.Mania");
+ }
}
}
diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
index afafec6b1f..b745d91980 100644
--- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
+++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
@@ -1,88 +1,34 @@
-
-
+
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}
- {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {122416d6-6b49-4ee2-a1e8-b825f31c79fe}
+ net6.0-android
+ Exe
osu.Game.Tests
osu.Game.Tests.Android
- Properties\AndroidManifest.xml
- armeabi-v7a;x86;arm64-v8a
-
-
-
-
-
-
$(NoWarn);CA2007
-
- None
- cjk;mideast;other;rare;west
- true
-
-
+
%(RecursiveDir)%(Filename)%(Extension)
-
-
+
+
%(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
+ Android\%(RecursiveDir)%(Filename)%(Extension)
+
-
- {58f6c80c-1253-4a0e-a465-b8c85ebeadf3}
- osu.Game.Rulesets.Catch
-
-
- {48f4582b-7687-4621-9cbe-5c24197cb536}
- osu.Game.Rulesets.Mania
-
-
- {c92a607b-1fdd-4954-9f92-03ff547d9080}
- osu.Game.Rulesets.Osu
-
-
- {f167e17a-7de6-4af5-b920-a5112296c695}
- osu.Game.Rulesets.Taiko
-
-
- {2a66dd92-adb1-4994-89e2-c94e04acda0d}
- osu.Game
-
+
+
+
+
+
-
- 5.0.0
-
-
diff --git a/osu.Game.Tests.iOS/Application.cs b/osu.Game.Tests.iOS/Application.cs
index cf36fea139..4678be4fb8 100644
--- a/osu.Game.Tests.iOS/Application.cs
+++ b/osu.Game.Tests.iOS/Application.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Framework.iOS;
using UIKit;
namespace osu.Game.Tests.iOS
@@ -12,7 +11,7 @@ namespace osu.Game.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
+ UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist
index 31e2b3f257..ac661f6263 100644
--- a/osu.Game.Tests.iOS/Info.plist
+++ b/osu.Game.Tests.iOS/Info.plist
@@ -13,7 +13,7 @@
LSRequiresIPhoneOS
MinimumOSVersion
- 10.0
+ 13.4
UIDeviceFamily
1
diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
index 05b3cad6da..79771fcd50 100644
--- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
+++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
@@ -1,54 +1,26 @@
-
-
+
- Debug
- iPhoneSimulator
Exe
- {65FF8E19-6934-469B-B690-23C6D6E56A17}
+ net6.0-ios
+ 13.4
osu.Game.Tests
osu.Game.Tests.iOS
-
-
-
- Linker.xml
-
-
-
%(RecursiveDir)%(Filename)%(Extension)
-
- $(NoWarn);CA2007
-
-
- {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}
- osu.Game
-
-
- {C92A607B-1FDD-4954-9F92-03FF547D9080}
- osu.Game.Rulesets.Osu
-
-
- {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}
- osu.Game.Rulesets.Catch
-
-
- {48F4582B-7687-4621-9CBE-5C24197CB536}
- osu.Game.Rulesets.Mania
-
-
- {F167E17A-7DE6-4AF5-B920-A5112296C695}
- osu.Game.Rulesets.Taiko
-
+
+
+
+
+
-
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index c6bdd25e8b..5787bd6066 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -314,6 +314,24 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestGetLastObjectTime()
+ {
+ var decoder = new LegacyBeatmapDecoder();
+
+ using (var resStream = TestResources.OpenResource("mania-last-object-not-latest.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+
+ Assert.That(beatmap.HitObjects.Last().StartTime, Is.EqualTo(2494));
+ Assert.That(beatmap.HitObjects.Last().GetEndTime(), Is.EqualTo(2494));
+
+ Assert.That(beatmap.HitObjects.Max(h => h.GetEndTime()), Is.EqualTo(2582));
+ Assert.That(beatmap.GetLastObjectTime(), Is.EqualTo(2582));
+ }
+ }
+
[Test]
public void TestDecodeBeatmapComboOffsetsOsu()
{
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
index cd6e5e7919..93cda34ef7 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
@@ -16,7 +16,9 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
@@ -179,6 +181,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
+ [Test]
+ public void TestSoloScoreData()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ scoreInfo.Mods = new Mod[]
+ {
+ new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
+ };
+
+ var beatmap = new TestBeatmap(ruleset);
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
+ }
+ }
+ };
+
+ var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(decodedAfterEncode.ScoreInfo.Statistics, Is.EqualTo(scoreInfo.Statistics));
+ Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
+ Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
+ });
+ }
+
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index ebfa9bd8b7..3c35dc311f 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -26,6 +26,16 @@ namespace osu.Game.Tests.Chat
MessageFormatter.WebsiteRootUrl = originalWebsiteRootUrl;
}
+ [Test]
+ public void TestUnsupportedProtocolLink()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a gopher://really-old-protocol we don't support." });
+
+ Assert.AreEqual(result.Content, result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("gopher://really-old-protocol", result.Links[0].Url);
+ }
+
[Test]
public void TestBareLink()
{
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index 56964aa8b2..446eb72b04 100644
--- a/osu.Game.Tests/Database/BeatmapImporterTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -564,7 +564,7 @@ namespace osu.Game.Tests.Database
var imported = await importer.Import(
progressNotification,
- new ImportTask(zipStream, string.Empty)
+ new[] { new ImportTask(zipStream, string.Empty) }
);
realm.Run(r => r.Refresh());
@@ -1052,7 +1052,7 @@ namespace osu.Game.Tests.Database
{
string? temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
- var importedSet = await importer.Import(new ImportTask(temp), batchImport);
+ var importedSet = await importer.Import(new ImportTask(temp), new ImportParameters { Batch = batchImport });
Assert.NotNull(importedSet);
Debug.Assert(importedSet != null);
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
index 62863524fe..04fc4cafbd 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
@@ -137,6 +138,31 @@ namespace osu.Game.Tests.Gameplay
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
}
+ [Test]
+ public void TestResultSetBeforeLoadComplete()
+ {
+ TestDrawableHitObject dho = null;
+ HitObjectLifetimeEntry lifetimeEntry = null;
+ AddStep("Create lifetime entry", () =>
+ {
+ var hitObject = new HitObject { StartTime = Time.Current };
+ lifetimeEntry = new HitObjectLifetimeEntry(hitObject)
+ {
+ Result = new JudgementResult(hitObject, hitObject.CreateJudgement())
+ {
+ Type = HitResult.Great
+ }
+ };
+ });
+ AddStep("Create DHO and apply entry", () =>
+ {
+ dho = new TestDrawableHitObject();
+ dho.Apply(lifetimeEntry);
+ Child = dho;
+ });
+ AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit));
+ }
+
private partial class TestDrawableHitObject : DrawableHitObject
{
public const double INITIAL_LIFETIME_OFFSET = 100;
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
index 3ce7aa72d9..90c7688443 100644
--- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements;
@@ -139,6 +140,29 @@ namespace osu.Game.Tests.Gameplay
Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1));
}
+ [Test]
+ public void TestAccuracyModes()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = Enumerable.Range(0, 4).Select(_ => new TestHitObject(HitResult.Great)).ToList()
+ };
+
+ var scoreProcessor = new ScoreProcessor(new OsuRuleset());
+ scoreProcessor.ApplyBeatmap(beatmap);
+
+ Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
+ Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo(0));
+ Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo(1));
+
+ scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
+ scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.Great });
+
+ Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo((double)(100 + 300) / (2 * 300)).Within(Precision.DOUBLE_EPSILON));
+ Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo((double)(100 + 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
+ Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo((double)(100 + 3 * 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
+ }
+
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }
diff --git a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
index b8a3828a64..ca5240a39d 100644
--- a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
+++ b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
@@ -60,6 +60,6 @@ namespace osu.Game.Tests.Mods
/// 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();
+ => Enum.GetValues().SelectMany(ruleset.GetModsFor).OfType();
}
}
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index e1d8e08c5e..585fd516bd 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -226,12 +226,12 @@ namespace osu.Game.Tests.Online
this.testBeatmapManager = testBeatmapManager;
}
- public override Live ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
+ public override Live ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default)
{
if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
throw new TimeoutException("Timeout waiting for import to be allowed.");
- return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
+ return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, parameters, cancellationToken));
}
}
}
diff --git a/osu.Game.Tests/Resources/mania-last-object-not-latest.osu b/osu.Game.Tests/Resources/mania-last-object-not-latest.osu
new file mode 100644
index 0000000000..51893383d8
--- /dev/null
+++ b/osu.Game.Tests/Resources/mania-last-object-not-latest.osu
@@ -0,0 +1,39 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+StackLeniency: 0.7
+Mode: 3
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.59999990463257
+SliderTickRate:2
+
+[TimingPoints]
+24,352.941176470588,4,1,1,100,1,0
+6376,-50,4,1,1,100,0,0
+
+[HitObjects]
+51,192,24,1,0,0:0:0:0:
+153,192,200,1,0,0:0:0:0:
+358,192,376,1,0,0:0:0:0:
+460,192,553,1,0,0:0:0:0:
+460,192,729,128,0,1435:0:0:0:0:
+358,192,906,128,0,1612:0:0:0:0:
+256,192,1082,128,0,1788:0:0:0:0:
+153,192,1259,128,0,1965:0:0:0:0:
+51,192,1435,128,0,2141:0:0:0:0:
+51,192,2318,1,12,0:0:0:0:
+153,192,2318,1,4,0:0:0:0:
+256,192,2318,1,6,0:0:0:0:
+358,192,2318,1,14,0:0:0:0:
+460,192,2318,1,0,0:0:0:0:
+51,192,2494,128,0,2582:0:0:0:0:
+153,192,2494,128,14,2582:0:0:0:0:
+256,192,2494,128,6,2582:0:0:0:0:
+358,192,2494,128,4,2582:0:0:0:0:
+460,192,2494,1,12,0:0:0:0:0:
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
index e9e94aa897..826c610f56 100644
--- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
@@ -355,6 +356,28 @@ namespace osu.Game.Tests.Rulesets.Scoring
}
#pragma warning restore CS0618
+ [Test]
+ public void TestAccuracyWhenNearPerfect()
+ {
+ const int count_judgements = 1000;
+ const int count_misses = 1;
+
+ double actual = new TestScoreProcessor().ComputeAccuracy(new ScoreInfo
+ {
+ Statistics = new Dictionary
+ {
+ { HitResult.Great, count_judgements - count_misses },
+ { HitResult.Miss, count_misses }
+ }
+ });
+
+ const double expected = (count_judgements - count_misses) / (double)count_judgements;
+
+ Assert.That(actual, Is.Not.EqualTo(0.0));
+ Assert.That(actual, Is.Not.EqualTo(1.0));
+ Assert.That(actual, Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON));
+ }
+
private class TestRuleset : Ruleset
{
public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index 5c20f46787..0bd40e9962 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -360,7 +360,7 @@ namespace osu.Game.Tests.Skins.IO
private async Task> loadSkinIntoOsu(OsuGameBase osu, ImportTask import, bool batchImport = false)
{
var skinManager = osu.Dependencies.Get();
- return await skinManager.Import(import, batchImport);
+ return await skinManager.Import(import, new ImportParameters { Batch = batchImport });
}
}
}
diff --git a/osu.Game.Tests/Utils/NamingUtilsTest.cs b/osu.Game.Tests/Utils/NamingUtilsTest.cs
index 62e688db90..1f7e06f996 100644
--- a/osu.Game.Tests/Utils/NamingUtilsTest.cs
+++ b/osu.Game.Tests/Utils/NamingUtilsTest.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Tests.Utils
public class NamingUtilsTest
{
[Test]
- public void TestEmptySet()
+ public void TestNextBestNameEmptySet()
{
string nextBestName = NamingUtils.GetNextBestName(Enumerable.Empty(), "New Difficulty");
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
- public void TestNotTaken()
+ public void TestNextBestNameNotTaken()
{
string[] existingNames =
{
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
- public void TestNotTakenButClose()
+ public void TestNextBestNameNotTakenButClose()
{
string[] existingNames =
{
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
- public void TestAlreadyTaken()
+ public void TestNextBestNameAlreadyTaken()
{
string[] existingNames =
{
@@ -62,7 +62,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
- public void TestAlreadyTakenWithDifferentCase()
+ public void TestNextBestNameAlreadyTakenWithDifferentCase()
{
string[] existingNames =
{
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
- public void TestAlreadyTakenWithBrackets()
+ public void TestNextBestNameAlreadyTakenWithBrackets()
{
string[] existingNames =
{
@@ -88,7 +88,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
- public void TestMultipleAlreadyTaken()
+ public void TestNextBestNameMultipleAlreadyTaken()
{
string[] existingNames =
{
@@ -104,7 +104,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
- public void TestEvenMoreAlreadyTaken()
+ public void TestNextBestNameEvenMoreAlreadyTaken()
{
string[] existingNames = Enumerable.Range(1, 30).Select(i => $"New Difficulty ({i})").Append("New Difficulty").ToArray();
@@ -114,7 +114,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
- public void TestMultipleAlreadyTakenWithGaps()
+ public void TestNextBestNameMultipleAlreadyTakenWithGaps()
{
string[] existingNames =
{
@@ -128,5 +128,153 @@ namespace osu.Game.Tests.Utils
Assert.AreEqual("New Difficulty (2)", nextBestName);
}
+
+ [Test]
+ public void TestNextBestFilenameEmptySet()
+ {
+ string nextBestFilename = NamingUtils.GetNextBestFilename(Enumerable.Empty(), "test_file.osr");
+
+ Assert.AreEqual("test_file.osr", nextBestFilename);
+ }
+
+ [Test]
+ public void TestNextBestFilenameNotTaken()
+ {
+ string[] existingFiles =
+ {
+ "this file exists.zip",
+ "that file exists.too",
+ "three.4",
+ };
+
+ string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "test_file.osr");
+
+ Assert.AreEqual("test_file.osr", nextBestFilename);
+ }
+
+ [Test]
+ public void TestNextBestFilenameNotTakenButClose()
+ {
+ string[] existingFiles =
+ {
+ "replay_file(1).osr",
+ "replay_file (not a number).zip",
+ "replay_file (1 <- now THAT is a number right here).lol",
+ };
+
+ string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
+
+ Assert.AreEqual("replay_file.osr", nextBestFilename);
+ }
+
+ [Test]
+ public void TestNextBestFilenameAlreadyTaken()
+ {
+ string[] existingFiles =
+ {
+ "replay_file.osr",
+ };
+
+ string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
+
+ Assert.AreEqual("replay_file (1).osr", nextBestFilename);
+ }
+
+ [Test]
+ public void TestNextBestFilenameAlreadyTakenDifferentCase()
+ {
+ string[] existingFiles =
+ {
+ "replay_file.osr",
+ "RePlAy_FiLe (1).OsR",
+ "REPLAY_FILE (2).OSR",
+ };
+
+ string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
+ Assert.AreEqual("replay_file (3).osr", nextBestFilename);
+ }
+
+ [Test]
+ public void TestNextBestFilenameAlreadyTakenWithBrackets()
+ {
+ string[] existingFiles =
+ {
+ "replay_file.osr",
+ "replay_file (copy).osr",
+ };
+
+ string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
+ Assert.AreEqual("replay_file (1).osr", nextBestFilename);
+
+ nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file (copy).osr");
+ Assert.AreEqual("replay_file (copy) (1).osr", nextBestFilename);
+ }
+
+ [Test]
+ public void TestNextBestFilenameMultipleAlreadyTaken()
+ {
+ string[] existingFiles =
+ {
+ "replay_file.osr",
+ "replay_file (1).osr",
+ "replay_file (2).osr",
+ "replay_file (3).osr",
+ };
+
+ string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
+
+ Assert.AreEqual("replay_file (4).osr", nextBestFilename);
+ }
+
+ [Test]
+ public void TestNextBestFilenameMultipleAlreadyTakenWithGaps()
+ {
+ string[] existingFiles =
+ {
+ "replay_file.osr",
+ "replay_file (1).osr",
+ "replay_file (2).osr",
+ "replay_file (4).osr",
+ "replay_file (5).osr",
+ };
+
+ string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
+
+ Assert.AreEqual("replay_file (3).osr", nextBestFilename);
+ }
+
+ [Test]
+ public void TestNextBestFilenameNoExtensions()
+ {
+ string[] existingFiles =
+ {
+ "those",
+ "are definitely",
+ "files",
+ };
+
+ string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "surely");
+ Assert.AreEqual("surely", nextBestFilename);
+
+ nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "those");
+ Assert.AreEqual("those (1)", nextBestFilename);
+ }
+
+ [Test]
+ public void TestNextBestFilenameDifferentExtensions()
+ {
+ string[] existingFiles =
+ {
+ "replay_file.osr",
+ "replay_file (1).osr",
+ "replay_file.txt",
+ };
+
+ string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
+ Assert.AreEqual("replay_file (2).osr", nextBestFilename);
+
+ nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.txt");
+ Assert.AreEqual("replay_file (1).txt", nextBestFilename);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
index ccd2feef9c..f255dd08a8 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
@@ -6,6 +6,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
@@ -21,7 +22,13 @@ namespace osu.Game.Tests.Visual.Editing
public TestSceneEditorSummaryTimeline()
{
- editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
+ var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
+
+ beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = 100 });
+ beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
+ beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 };
+
+ editorBeatmap = new EditorBeatmap(beatmap);
}
protected override void LoadComplete()
diff --git a/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs b/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs
new file mode 100644
index 0000000000..3319788c8a
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public partial class TestScenePreviewTime : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ [Test]
+ public void TestSceneSetPreviewTimingPoint()
+ {
+ AddStep("seek to 1000", () => EditorClock.Seek(1000));
+ AddAssert("time is 1000", () => EditorClock.CurrentTime == 1000);
+ AddStep("set current time as preview point", () => Editor.SetPreviewPointToCurrentTime());
+ AddAssert("preview time is 1000", () => EditorBeatmap.PreviewTime.Value == 1000);
+ }
+
+ [Test]
+ public void TestScenePreviewTimeline()
+ {
+ AddStep("set preview time to -1", () => EditorBeatmap.PreviewTime.Value = -1);
+ AddAssert("preview time line should not show", () => !Editor.ChildrenOfType().Single().Children.Any());
+ AddStep("set preview time to 1000", () => EditorBeatmap.PreviewTime.Value = 1000);
+ AddAssert("preview time line should show", () => Editor.ChildrenOfType().Single().Children.Single().Alpha == 1);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
index 6bc2922253..a141e4d431 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneZoomableScrollContainer : OsuManualInputManagerTestScene
{
- private ZoomableScrollContainer scrollContainer;
+ private TestZoomableScrollContainer scrollContainer;
private Drawable innerBox;
[SetUpSteps]
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(30)
},
- scrollContainer = new ZoomableScrollContainer(1, 60, 1)
+ scrollContainer = new TestZoomableScrollContainer(1, 60, 1)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -93,6 +93,14 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
}
+ [Test]
+ public void TestWidthUpdatesOnSecondZoomSetup()
+ {
+ AddAssert("Inner container width = 1x", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
+ AddStep("reload zoom", () => scrollContainer.SetupZoom(10, 10, 60));
+ AddAssert("Inner container width = 10x", () => innerBox.DrawWidth == scrollContainer.DrawWidth * 10);
+ }
+
[Test]
public void TestZoom0()
{
@@ -190,5 +198,15 @@ namespace osu.Game.Tests.Visual.Editing
private Quad scrollQuad => scrollContainer.ScreenSpaceDrawQuad;
private Quad boxQuad => innerBox.ScreenSpaceDrawQuad;
+
+ private partial class TestZoomableScrollContainer : ZoomableScrollContainer
+ {
+ public TestZoomableScrollContainer(int minimum, float maximum, float initial)
+ : base(minimum, maximum, initial)
+ {
+ }
+
+ public new void SetupZoom(float initial, float minimum, float maximum) => base.SetupZoom(initial, minimum, maximum);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
index 2fbdfbc198..d5031bc606 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTestAPI(true);
- createPlayerTest(false, createRuleset: () => new OsuRuleset
+ createPlayerTest(createRuleset: () => new OsuRuleset
{
RulesetInfo =
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
index 8f1eb98c79..ffd034e4d2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
@@ -261,7 +261,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestFinalFramesPurgedBeforeEndingPlay()
{
- AddStep("begin playing", () => spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), new Score()));
+ AddStep("begin playing", () => spectatorClient.BeginPlaying(0, TestGameplayState.Create(new OsuRuleset()), new Score()));
AddStep("send frames and finish play", () =>
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index 1ad1da0994..794860b9ec 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
};
- spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), recordingScore);
+ spectatorClient.BeginPlaying(0, TestGameplayState.Create(new OsuRuleset()), recordingScore);
spectatorClient.OnNewFrames += onNewFrames;
});
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index 0bc42b06dd..aef6f9ade0 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -201,6 +201,23 @@ namespace osu.Game.Tests.Visual.Menus
AddAssert("volume not changed", () => Audio.Volume.Value == 0.5);
}
+ [Test]
+ public void TestRulesetSelectorOverflow()
+ {
+ AddStep("set toolbar width", () =>
+ {
+ toolbar.RelativeSizeAxes = Axes.None;
+ toolbar.Width = 400;
+ });
+ AddStep("move mouse over news toggle button", () =>
+ {
+ var button = toolbar.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(button);
+ });
+ AddAssert("no ruleset toggle buttons hovered", () => !toolbar.ChildrenOfType().Any(button => button.IsHovered));
+ AddUntilStep("toolbar gradient visible", () => toolbar.ChildrenOfType().Single().Children.All(d => d.Alpha > 0));
+ }
+
public partial class TestToolbar : Toolbar
{
public new Bindable OverlayActivationMode => base.OverlayActivationMode as Bindable;
diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
index fa7d2c04f4..649c662e41 100644
--- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
@@ -117,11 +117,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
BeatmapID = 0,
RulesetID = 0,
Mods = user.Mods,
- MaximumScoringValues = new ScoringValues
+ MaximumStatistics = new Dictionary
{
- BaseScore = 10000,
- MaxCombo = 1000,
- CountBasicHitObjects = 1000
+ { HitResult.Perfect, 100 }
}
};
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
index 91e9ce5ea2..ae27db0dd1 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -46,10 +47,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
}
- [Test]
- public void TestNextItemSelectedAfterDeletion()
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestNextItemSelectedAfterDeletion(bool allowSelection)
{
- createPlaylist();
+ createPlaylist(p =>
+ {
+ p.AllowSelection = allowSelection;
+ });
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
@@ -57,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
- AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+ AddAssert("item 0 is " + (allowSelection ? "selected" : "not selected"), () => playlist.SelectedItem.Value == (allowSelection ? playlist.Items[0] : null));
}
[Test]
@@ -117,7 +122,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset);
});
- private void createPlaylist()
+ private void createPlaylist(Action setupPlaylist = null)
{
AddStep("create playlist", () =>
{
@@ -154,6 +159,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
}
+
+ setupPlaylist?.Invoke(playlist);
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index d8fda5b21f..ddb01b90ce 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -19,6 +19,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
+using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
@@ -515,6 +516,28 @@ namespace osu.Game.Tests.Visual.Navigation
AddWaitStep("wait two frames", 2);
}
+ [Test]
+ public void TestFeaturedArtistDisclaimerDialog()
+ {
+ BeatmapListingOverlay getBeatmapListingOverlay() => Game.ChildrenOfType().FirstOrDefault();
+
+ AddStep("Wait for notifications to load", () => Game.SearchBeatmapSet(string.Empty));
+ AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null);
+
+ AddUntilStep("Wait for beatmap overlay to load", () => getBeatmapListingOverlay()?.State.Value == Visibility.Visible);
+ AddAssert("featured artist filter is on", () => getBeatmapListingOverlay().ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+ AddStep("toggle featured artist filter",
+ () => getBeatmapListingOverlay().ChildrenOfType>().First(i => i.Value == SearchGeneral.FeaturedArtists).TriggerClick());
+
+ AddAssert("disclaimer dialog is shown", () => Game.ChildrenOfType().Single().CurrentDialog != null);
+ AddAssert("featured artist filter is still on", () => getBeatmapListingOverlay().ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+
+ AddStep("confirm", () => InputManager.Key(Key.Enter));
+ AddAssert("dialog dismissed", () => Game.ChildrenOfType().Single().CurrentDialog == null);
+
+ AddUntilStep("featured artist filter is off", () => !getBeatmapListingOverlay().ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+ }
+
[Test]
public void TestMainOverlaysClosesNotificationOverlay()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index c64343b47b..5e49cb633e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
@@ -80,6 +80,15 @@ namespace osu.Game.Tests.Visual.Online
AddStep("reset size", () => localConfig.SetValue(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal));
}
+ [Test]
+ public void TestFeaturedArtistFilter()
+ {
+ AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
+ AddAssert("featured artist filter is on", () => overlay.ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+ AddStep("toggle featured artist filter", () => overlay.ChildrenOfType>().First(i => i.Value == SearchGeneral.FeaturedArtists).TriggerClick());
+ AddAssert("featured artist filter is off", () => !overlay.ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists));
+ }
+
[Test]
public void TestHideViaBack()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 3335f69dbb..7d978b9726 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -54,6 +54,8 @@ namespace osu.Game.Tests.Visual.Online
{
overlay.ShowBeatmapSet(new APIBeatmapSet
{
+ Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Future genre" },
+ Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "Future language" },
OnlineID = 1235,
Title = @"an awesome beatmap",
Artist = @"naru narusegawa",
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 8cc4eabcd7..a8369dd6d9 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -530,6 +530,52 @@ namespace osu.Game.Tests.Visual.Online
});
}
+ [Test]
+ public void TestTextBoxSavePerChannel()
+ {
+ var testPMChannel = new Channel(testUser);
+
+ AddStep("show overlay", () => chatOverlay.Show());
+ joinTestChannel(0);
+ joinChannel(testPMChannel);
+
+ AddAssert("listing is visible", () => listingIsVisible);
+ AddStep("search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
+ AddAssert("'number 2' saved to selector", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "number 2");
+
+ AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
+ AddAssert("text box cleared on normal channel", () => chatOverlayTextBox.Text == string.Empty);
+ AddAssert("nothing saved on normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
+ AddStep("type '727'", () => chatOverlayTextBox.Text = "727");
+ AddAssert("'727' saved to normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "727");
+
+ AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
+ AddAssert("text box cleared on PM channel", () => chatOverlayTextBox.Text == string.Empty);
+ AddAssert("nothing saved on PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
+ AddStep("type 'hello'", () => chatOverlayTextBox.Text = "hello");
+ AddAssert("'hello' saved to PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "hello");
+
+ AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
+ AddAssert("text box contains '727'", () => chatOverlayTextBox.Text == "727");
+
+ AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
+ AddAssert("text box contains 'hello'", () => chatOverlayTextBox.Text == "hello");
+ AddStep("click close button", () =>
+ {
+ ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType().Single();
+ clickDrawable(closeButton);
+ });
+
+ AddAssert("listing is visible", () => listingIsVisible);
+ AddAssert("text box contains 'channel 2'", () => chatOverlayTextBox.Text == "number 2");
+ AddUntilStep("only channel 2 visible", () =>
+ {
+ IEnumerable listingItems = chatOverlay.ChildrenOfType()
+ .Where(item => item.IsPresent);
+ return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
+ });
+ }
+
private void joinTestChannel(int i)
{
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
index 7981e212d4..d925141510 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
@@ -77,14 +77,14 @@ namespace osu.Game.Tests.Visual.Online
{
var comments = this.ChildrenOfType();
var ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
- return ourComment != null && ourComment.ChildrenOfType().Any(x => x.Text == "Delete");
+ return ourComment != null && ourComment.ChildrenOfType().Any(x => x.Text == "delete");
});
AddAssert("Second doesn't", () =>
{
var comments = this.ChildrenOfType();
var ourComment = comments.Single(x => x.Comment.Id == 2);
- return ourComment.ChildrenOfType().All(x => x.Text != "Delete");
+ return ourComment.ChildrenOfType().All(x => x.Text != "delete");
});
}
@@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("It has delete button", () =>
{
- var btn = ourComment.ChildrenOfType().Single(x => x.Text == "Delete");
+ var btn = ourComment.ChildrenOfType().Single(x => x.Text == "delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
@@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("It has delete button", () =>
{
- var btn = ourComment.ChildrenOfType().Single(x => x.Text == "Delete");
+ var btn = ourComment.ChildrenOfType().Single(x => x.Text == "delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
@@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Online
if (request is not CommentDeleteRequest req)
return false;
- req.TriggerFailure(new Exception());
+ req.TriggerFailure(new InvalidOperationException());
delete = true;
return false;
};
@@ -245,7 +245,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("Click the button", () =>
{
- var btn = targetComment.ChildrenOfType().Single(x => x.Text == "Report");
+ var btn = targetComment.ChildrenOfType().Single(x => x.Text == "report");
InputManager.MoveMouseTo(btn);
InputManager.Click(MouseButton.Left);
});
diff --git a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs
index d884c0cf14..28f0e6ff9c 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
index e753632474..84497245db 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Overlays.Profile.Sections.Kudosu;
using System.Collections.Generic;
using System;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs
index e81b7a2ac8..a4d8238fa3 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Rulesets.Catch;
@@ -24,7 +22,7 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneProfileRulesetSelector()
{
ProfileRulesetSelector selector;
- var user = new Bindable();
+ var user = new Bindable();
Child = selector = new ProfileRulesetSelector
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
new file mode 100644
index 0000000000..e62e53bd02
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
@@ -0,0 +1,302 @@
+// 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.Framework.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Models;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Solo;
+using osu.Game.Online.Spectator;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [HeadlessTest]
+ public partial class TestSceneSoloStatisticsWatcher : OsuTestScene
+ {
+ protected override bool UseOnlineAPI => false;
+
+ private SoloStatisticsWatcher watcher = null!;
+
+ [Resolved]
+ private SpectatorClient spectatorClient { get; set; } = null!;
+
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
+ private Action? handleGetUsersRequest;
+ private Action? handleGetUserRequest;
+
+ private IDisposable? subscription;
+
+ private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("clear server-side stats", () => serverSideStatistics.Clear());
+ AddStep("set up request handling", () =>
+ {
+ handleGetUserRequest = null;
+ handleGetUsersRequest = null;
+
+ dummyAPI.HandleRequest = request =>
+ {
+ switch (request)
+ {
+ case GetUsersRequest getUsersRequest:
+ if (handleGetUsersRequest != null)
+ {
+ handleGetUsersRequest?.Invoke(getUsersRequest);
+ }
+ else
+ {
+ int userId = getUsersRequest.UserIds.Single();
+ var response = new GetUsersResponse
+ {
+ Users = new List
+ {
+ new APIUser
+ {
+ Id = userId,
+ RulesetsStatistics = new Dictionary
+ {
+ ["osu"] = tryGetStatistics(userId, "osu"),
+ ["taiko"] = tryGetStatistics(userId, "taiko"),
+ ["fruits"] = tryGetStatistics(userId, "fruits"),
+ ["mania"] = tryGetStatistics(userId, "mania"),
+ }
+ }
+ }
+ };
+ getUsersRequest.TriggerSuccess(response);
+ }
+
+ return true;
+
+ case GetUserRequest getUserRequest:
+ if (handleGetUserRequest != null)
+ {
+ handleGetUserRequest.Invoke(getUserRequest);
+ }
+ else
+ {
+ int userId = int.Parse(getUserRequest.Lookup);
+ string rulesetName = getUserRequest.Ruleset.ShortName;
+ var response = new APIUser
+ {
+ Id = userId,
+ Statistics = tryGetStatistics(userId, rulesetName)
+ };
+ getUserRequest.TriggerSuccess(response);
+ }
+
+ return true;
+
+ default:
+ return false;
+ }
+ };
+ });
+
+ AddStep("create watcher", () =>
+ {
+ Child = watcher = new SoloStatisticsWatcher();
+ });
+ }
+
+ private UserStatistics tryGetStatistics(int userId, string rulesetName)
+ => serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics();
+
+ [Test]
+ public void TestStatisticsUpdateFiredAfterRegistrationAddedAndScoreProcessed()
+ {
+ int userId = getUserId();
+ long scoreId = getScoreId();
+ setUpUser(userId);
+
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddUntilStep("update received", () => update != null);
+ AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
+ AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(5_000_000));
+ }
+
+ [Test]
+ public void TestStatisticsUpdateFiredAfterScoreProcessedAndRegistrationAdded()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ // note ordering - in this test processing completes *before* the registration is added.
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddUntilStep("update received", () => update != null);
+ AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
+ AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(5_000_000));
+ }
+
+ [Test]
+ public void TestStatisticsUpdateNotFiredIfUserLoggedOut()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("log out user", () => dummyAPI.Logout());
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddWaitStep("wait a bit", 5);
+ AddAssert("update not received", () => update == null);
+
+ AddStep("log in user", () => dummyAPI.Login("user", "password"));
+ }
+
+ [Test]
+ public void TestStatisticsUpdateNotFiredIfAnotherUserLoggedIn()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("change user", () => dummyAPI.LocalUser.Value = new APIUser { Id = getUserId() });
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddWaitStep("wait a bit", 5);
+ AddAssert("update not received", () => update == null);
+ }
+
+ [Test]
+ public void TestStatisticsUpdateNotFiredIfScoreIdDoesNotMatch()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("signal another score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, getScoreId()));
+ AddWaitStep("wait a bit", 5);
+ AddAssert("update not received", () => update == null);
+ }
+
+ // the behaviour exercised in this test may not be final, it is mostly assumed for simplicity.
+ // in the long run we may want each score's update to be entirely isolated from others, rather than have prior unobserved updates merge into the latest.
+ [Test]
+ public void TestIgnoredScoreUpdateIsMergedIntoNextOne()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long firstScoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, firstScoreId));
+
+ long secondScoreId = getScoreId();
+
+ feignScoreProcessing(userId, ruleset, 6_000_000);
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
+ AddUntilStep("update received", () => update != null);
+ AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
+ AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000));
+ }
+
+ [Test]
+ public void TestStatisticsUpdateNotFiredAfterSubscriptionDisposal()
+ {
+ int userId = getUserId();
+ setUpUser(userId);
+
+ long scoreId = getScoreId();
+ var ruleset = new OsuRuleset().RulesetInfo;
+
+ SoloStatisticsUpdate? update = null;
+ registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
+ AddStep("unsubscribe", () => subscription!.Dispose());
+
+ feignScoreProcessing(userId, ruleset, 5_000_000);
+
+ AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
+ AddWaitStep("wait a bit", 5);
+ AddAssert("update not received", () => update == null);
+ }
+
+ private int nextUserId = 2000;
+ private long nextScoreId = 50000;
+
+ private int getUserId() => ++nextUserId;
+ private long getScoreId() => ++nextScoreId;
+
+ private void setUpUser(int userId)
+ {
+ AddStep("fetch initial stats", () =>
+ {
+ serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 };
+ serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 };
+ serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 };
+ serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 };
+
+ dummyAPI.LocalUser.Value = new APIUser { Id = userId };
+ });
+ }
+
+ private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action onUpdateReady) =>
+ AddStep("register for updates", () => subscription = watcher.RegisterForStatisticsUpdateAfter(
+ new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
+ {
+ Ruleset = rulesetInfo,
+ OnlineID = scoreId
+ },
+ onUpdateReady));
+
+ private void feignScoreProcessing(int userId, RulesetInfo rulesetInfo, long newTotalScore)
+ => AddStep("feign score processing", () => serverSideStatistics[(userId, rulesetInfo.ShortName)] = new UserStatistics { TotalScore = newTotalScore });
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
index 7d2ac90939..d7f79d3e30 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
@@ -50,6 +50,8 @@ namespace osu.Game.Tests.Visual.Online
private ChannelManager channelManager;
private TestStandAloneChatDisplay chatDisplay;
+ private TestStandAloneChatDisplay chatWithTextBox;
+ private TestStandAloneChatDisplay chatWithTextBox2;
private int messageIdSequence;
private Channel testChannel;
@@ -73,7 +75,12 @@ namespace osu.Game.Tests.Visual.Online
messageIdSequence = 0;
channelManager.CurrentChannel.Value = testChannel = new Channel();
- Children = new[]
+ reinitialiseDrawableDisplay();
+ });
+
+ private void reinitialiseDrawableDisplay()
+ {
+ Children = new Drawable[]
{
chatDisplay = new TestStandAloneChatDisplay
{
@@ -83,22 +90,38 @@ namespace osu.Game.Tests.Visual.Online
Size = new Vector2(400, 80),
Channel = { Value = testChannel },
},
- new TestStandAloneChatDisplay(true)
+ new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
Margin = new MarginPadding(20),
- Size = new Vector2(400, 150),
- Channel = { Value = testChannel },
+ Children = new[]
+ {
+ chatWithTextBox = new TestStandAloneChatDisplay(true)
+ {
+ Margin = new MarginPadding(20),
+ Size = new Vector2(400, 150),
+ Channel = { Value = testChannel },
+ },
+ chatWithTextBox2 = new TestStandAloneChatDisplay(true)
+ {
+ Margin = new MarginPadding(20),
+ Size = new Vector2(400, 150),
+ Channel = { Value = testChannel },
+ },
+ }
}
};
- });
+ }
[Test]
public void TestSystemMessageOrdering()
{
var standardMessage = new Message(messageIdSequence++)
{
+ Timestamp = DateTimeOffset.Now,
Sender = admin,
Content = "I am a wang!"
};
@@ -106,14 +129,45 @@ namespace osu.Game.Tests.Visual.Online
var infoMessage1 = new InfoMessage($"the system is calling {messageIdSequence++}");
var infoMessage2 = new InfoMessage($"the system is calling {messageIdSequence++}");
+ var standardMessage2 = new Message(messageIdSequence++)
+ {
+ Timestamp = DateTimeOffset.Now,
+ Sender = admin,
+ Content = "I am a wang!"
+ };
+
AddStep("message from admin", () => testChannel.AddNewMessages(standardMessage));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage1));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage2));
+ AddStep("message from admin", () => testChannel.AddNewMessages(standardMessage2));
- AddAssert("message order is correct", () => testChannel.Messages.Count == 3
- && testChannel.Messages[0] == standardMessage
- && testChannel.Messages[1] == infoMessage1
- && testChannel.Messages[2] == infoMessage2);
+ AddAssert("count is correct", () => testChannel.Messages.Count, () => Is.EqualTo(4));
+
+ AddAssert("message order is correct", () => testChannel.Messages, () => Is.EqualTo(new[]
+ {
+ standardMessage,
+ infoMessage1,
+ infoMessage2,
+ standardMessage2
+ }));
+
+ AddAssert("displayed order is correct", () => chatDisplay.DrawableChannel.ChildrenOfType().Select(c => c.Message), () => Is.EqualTo(new[]
+ {
+ standardMessage,
+ infoMessage1,
+ infoMessage2,
+ standardMessage2
+ }));
+
+ AddStep("reinit drawable channel", reinitialiseDrawableDisplay);
+
+ AddAssert("displayed order is still correct", () => chatDisplay.DrawableChannel.ChildrenOfType().Select(c => c.Message), () => Is.EqualTo(new[]
+ {
+ standardMessage,
+ infoMessage1,
+ infoMessage2,
+ standardMessage2
+ }));
}
[Test]
@@ -314,6 +368,13 @@ namespace osu.Game.Tests.Visual.Online
checkScrolledToBottom();
}
+ [Test]
+ public void TestTextBoxSync()
+ {
+ AddStep("type 'hello' to text box 1", () => chatWithTextBox.ChildrenOfType().Single().Text = "hello");
+ AddAssert("text box 2 contains 'hello'", () => chatWithTextBox2.ChildrenOfType().Single().Text == "hello");
+ }
+
private void fillChat(int count = 10)
{
AddStep("fill chat", () =>
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
index d93bf059dd..454242270d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs
index 75743d788a..bfd6372e6f 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using NUnit.Framework;
@@ -20,7 +18,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
- private ProfileHeader header;
+ private ProfileHeader header = null!;
[SetUpSteps]
public void SetUpSteps()
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index 02d01b4a46..35c7e7bf28 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using NUnit.Framework;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs
index fcefb31716..921738d331 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
@@ -14,7 +12,7 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
{
- private PreviousUsernames container;
+ private PreviousUsernames container = null!;
[SetUp]
public void SetUp() => Schedule(() =>
@@ -50,7 +48,7 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("Is hidden", () => container.Alpha == 0);
}
- private static readonly APIUser[] users =
+ private static readonly APIUser?[] users =
{
new APIUser { Id = 1, PreviousUsernames = new[] { "username1" } },
new APIUser { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } },
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs
index f8432118d4..9d0ea80533 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
index 6f0ef10911..5249e8694d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs b/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs
index ef3a677efc..fdbd8a4325 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
index b486f800c6..b353123649 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
@@ -16,13 +16,17 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers.Markdown;
+using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki.Markdown;
+using osu.Game.Users.Drawables;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
- public partial class TestSceneWikiMarkdownContainer : OsuTestScene
+ public partial class TestSceneWikiMarkdownContainer : OsuManualInputManagerTestScene
{
+ private OverlayScrollContainer scrollContainer;
private TestMarkdownContainer markdownContainer;
[Cached]
@@ -38,15 +42,25 @@ namespace osu.Game.Tests.Visual.Online
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
- new BasicScrollContainer
+ scrollContainer = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
- Child = markdownContainer = new TestMarkdownContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- }
+ }
+ };
+
+ scrollContainer.Child = new DependencyProvidingContainer
+ {
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(OverlayScrollContainer), scrollContainer)
+ },
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = markdownContainer = new TestMarkdownContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
}
};
});
@@ -197,6 +211,63 @@ Line after image";
markdownContainer.CurrentPath = @"https://dev.ppy.sh";
markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::";
});
+ AddAssert("Two flags visible", () => markdownContainer.ChildrenOfType().Count(), () => Is.EqualTo(2));
+ }
+
+ [Test]
+ public void TestHeadingWithIdAttribute()
+ {
+ AddStep("Add heading with ID", () =>
+ {
+ markdownContainer.Text = "# This is a heading with an ID {#this-is-the-id}";
+ });
+ AddAssert("ID not visible", () => markdownContainer.ChildrenOfType().All(spriteText => spriteText.Text != "{#this-is-the-id}"));
+ }
+
+ [Test]
+ public void TestFootnotes()
+ {
+ AddStep("set content", () => markdownContainer.Text = @"This text has a footnote[^test].
+
+Here's some more text[^test2] with another footnote!
+
+# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam efficitur laoreet posuere. Ut accumsan tortor in ipsum tincidunt ultrices. Suspendisse a malesuada tellus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce a sagittis nibh. In et velit sit amet mauris aliquet consectetur quis vehicula lorem. Etiam sit amet tellus ac velit ornare maximus. Donec quis metus eget libero ullamcorper imperdiet id vitae arcu. Vivamus iaculis rhoncus purus malesuada mollis. Vestibulum dictum at nisi sed tincidunt. Suspendisse finibus, ipsum ut dapibus commodo, leo eros porttitor sapien, non scelerisque nisi ligula sed ex. Pellentesque magna orci, hendrerit eu iaculis sit amet, ullamcorper in urna. Vivamus dictum mauris orci, nec facilisis dolor fringilla eu. Sed at porttitor nisi, at venenatis urna. Ut at orci vitae libero semper ullamcorper eu ut risus. Mauris hendrerit varius enim, ut varius nisi feugiat mattis.
+
+## In at eros urna. Sed ipsum lorem, tempor sit amet purus in, vehicula pellentesque leo. Fusce volutpat pellentesque velit sit amet porttitor. Nulla eget erat ex. Praesent eu lacinia est, quis vehicula lacus. Donec consequat ultrices neque, at finibus quam efficitur vel. Vestibulum molestie nisl sit amet metus semper, at vestibulum massa rhoncus. Quisque imperdiet suscipit augue, et dignissim odio eleifend ut.
+
+Aliquam sed vestibulum mauris, ut lobortis elit. Sed quis lacinia erat. Nam ultricies, risus non pellentesque sollicitudin, mauris dolor tincidunt neque, ac porta ipsum dui quis libero. Integer eget velit neque. Vestibulum venenatis mauris vitae rutrum vestibulum. Maecenas suscipit eu purus eu tempus. Nam dui nisl, bibendum condimentum mollis et, gravida vel dui. Sed et eros rutrum, facilisis sapien eu, mattis ligula. Fusce finibus pulvinar dolor quis consequat.
+
+Donec ipsum felis, feugiat vel fermentum at, commodo eu sapien. Suspendisse nec enim vitae felis laoreet laoreet. Phasellus purus quam, fermentum a pharetra vel, tempor et urna. Integer vitae quam diam. Aliquam tincidunt tortor a iaculis convallis. Suspendisse potenti. Cras quis risus quam. Nullam tincidunt in lorem posuere sagittis.
+
+Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed urna tristique, laoreet pharetra nulla. Vivamus maximus turpis purus, eu viverra dolor sodales porttitor. Praesent bibendum sapien purus, sed ultricies dolor iaculis sed. Fusce congue hendrerit malesuada. Nulla nulla est, auctor ac fringilla sed, ornare a lorem. Donec quis velit imperdiet, imperdiet sem non, pellentesque sapien. Maecenas in orci id ipsum placerat facilisis non sed nisi. Duis dictum lorem sodales odio dictum eleifend. Vestibulum bibendum euismod quam, eget pharetra orci facilisis sed. Vivamus at diam non ipsum consequat tristique. Pellentesque gravida dignissim pellentesque. Donec ullamcorper lacinia orci, id consequat purus faucibus quis. Phasellus metus nunc, iaculis a interdum vel, congue sed erat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam eros libero, hendrerit luctus nulla vitae, luctus maximus nunc.
+
+[^test]: This is a **footnote**.
+[^test2]: This is another footnote [with a link](https://google.com/)!");
+ AddStep("shrink scroll height", () => scrollContainer.Height = 0.5f);
+
+ AddStep("press second footnote link", () =>
+ {
+ InputManager.MoveMouseTo(markdownContainer.ChildrenOfType().ElementAt(1));
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("second footnote scrolled into view", () =>
+ {
+ var footnote = markdownContainer.ChildrenOfType().ElementAt(1);
+ return scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.TopLeft)
+ && scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.BottomRight);
+ });
+
+ AddStep("press first footnote backlink", () =>
+ {
+ InputManager.MoveMouseTo(markdownContainer.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("first footnote link scrolled into view", () =>
+ {
+ var footnote = markdownContainer.ChildrenOfType().First();
+ return scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.TopLeft)
+ && scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.BottomRight);
+ });
}
private partial class TestMarkdownContainer : WikiMarkdownContainer
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
index 620fd710e3..b0e4303ca4 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
@@ -4,8 +4,11 @@
#nullable disable
using System;
+using System.Linq;
using System.Net;
using NUnit.Framework;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -29,6 +32,15 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Show main page", () => wiki.Show());
}
+ [Test]
+ public void TestCancellationDoesntShowError()
+ {
+ AddStep("Show main page", () => wiki.Show());
+ AddStep("Show another page", () => wiki.ShowPage("Article_styling_criteria/Formatting"));
+
+ AddUntilStep("Current path is not error", () => wiki.CurrentPath != "error");
+ }
+
[Test]
public void TestArticlePage()
{
@@ -56,7 +68,9 @@ namespace osu.Game.Tests.Visual.Online
public void TestErrorPage()
{
setUpWikiResponse(responseArticlePage);
- AddStep("Show Error Page", () => wiki.ShowPage("Error"));
+ AddStep("Show nonexistent page", () => wiki.ShowPage("This_page_will_error_out"));
+ AddUntilStep("Wait for error page", () => wiki.CurrentPath == "error");
+ AddUntilStep("Error message correct", () => wiki.ChildrenOfType().Any(text => text.Text == "\"This_page_will_error_out\"."));
}
private void setUpWikiResponse(APIWikiPage r, string redirectionPath = null)
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs
new file mode 100644
index 0000000000..355a572f95
--- /dev/null
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs
@@ -0,0 +1,117 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Game.Online.Solo;
+using osu.Game.Scoring;
+using osu.Game.Screens.Ranking.Statistics.User;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Ranking
+{
+ public partial class TestSceneOverallRanking : OsuTestScene
+ {
+ private OverallRanking overallRanking = null!;
+
+ [Test]
+ public void TestUpdatePending()
+ {
+ createDisplay();
+ }
+
+ [Test]
+ public void TestAllIncreased()
+ {
+ createDisplay();
+ displayUpdate(
+ new UserStatistics
+ {
+ GlobalRank = 12_345,
+ Accuracy = 98.99,
+ MaxCombo = 2_322,
+ RankedScore = 23_123_543_456,
+ TotalScore = 123_123_543_456,
+ PP = 5_072
+ },
+ new UserStatistics
+ {
+ GlobalRank = 1_234,
+ Accuracy = 99.07,
+ MaxCombo = 2_352,
+ RankedScore = 23_124_231_435,
+ TotalScore = 123_124_231_435,
+ PP = 5_434
+ });
+ }
+
+ [Test]
+ public void TestAllDecreased()
+ {
+ createDisplay();
+ displayUpdate(
+ new UserStatistics
+ {
+ GlobalRank = 1_234,
+ Accuracy = 99.07,
+ MaxCombo = 2_352,
+ RankedScore = 23_124_231_435,
+ TotalScore = 123_124_231_435,
+ PP = 5_434
+ },
+ new UserStatistics
+ {
+ GlobalRank = 12_345,
+ Accuracy = 98.99,
+ MaxCombo = 2_322,
+ RankedScore = 23_123_543_456,
+ TotalScore = 123_123_543_456,
+ PP = 5_072
+ });
+ }
+
+ [Test]
+ public void TestNoChanges()
+ {
+ var statistics = new UserStatistics
+ {
+ GlobalRank = 12_345,
+ Accuracy = 98.99,
+ MaxCombo = 2_322,
+ RankedScore = 23_123_543_456,
+ TotalScore = 123_123_543_456,
+ PP = 5_072
+ };
+
+ createDisplay();
+ displayUpdate(statistics, statistics);
+ }
+
+ [Test]
+ public void TestNotRanked()
+ {
+ var statistics = new UserStatistics
+ {
+ GlobalRank = null,
+ Accuracy = 98.99,
+ MaxCombo = 2_322,
+ RankedScore = 23_123_543_456,
+ TotalScore = 123_123_543_456,
+ PP = null
+ };
+
+ createDisplay();
+ displayUpdate(statistics, statistics);
+ }
+
+ private void createDisplay() => AddStep("create display", () => Child = overallRanking = new OverallRanking
+ {
+ Width = 400,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ });
+
+ private void displayUpdate(UserStatistics before, UserStatistics after) =>
+ AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new SoloStatisticsUpdate(new ScoreInfo(), before, after));
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
index e47b7e25a8..11d55bc0bd 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
@@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.SongSelect
if (testRequest.Progress >= 0.5f)
{
- testRequest.TriggerFailure(new Exception());
+ testRequest.TriggerFailure(new InvalidOperationException());
return true;
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonsInput.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonsInput.cs
new file mode 100644
index 0000000000..985f613b63
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonsInput.cs
@@ -0,0 +1,129 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Overlays.Settings;
+using NUnit.Framework;
+using osuTK;
+using osu.Game.Overlays;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Graphics.UserInterface;
+using osu.Framework.Allocation;
+using osu.Game.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public partial class TestSceneButtonsInput : OsuManualInputManagerTestScene
+ {
+ private const int width = 500;
+
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
+ private readonly SettingsButton settingsButton;
+ private readonly OsuClickableContainer clickableContainer;
+ private readonly RoundedButton roundedButton;
+ private readonly ShearedButton shearedButton;
+
+ public TestSceneButtonsInput()
+ {
+ Add(new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ Width = 500,
+ Spacing = new Vector2(0, 5),
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ clickableContainer = new OsuClickableContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 40,
+ Enabled = { Value = true },
+ Masking = true,
+ CornerRadius = 20,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Red
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "Rounded clickable container"
+ }
+ }
+ },
+ settingsButton = new SettingsButton
+ {
+ Enabled = { Value = true },
+ Text = "Settings button"
+ },
+ roundedButton = new RoundedButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Enabled = { Value = true },
+ Text = "Rounded button"
+ },
+ shearedButton = new ShearedButton(width)
+ {
+ Text = "Sheared button",
+ LighterColour = Colour4.FromHex("#FFFFFF"),
+ DarkerColour = Colour4.FromHex("#FFCC22"),
+ TextColour = Colour4.Black,
+ Height = 40,
+ Enabled = { Value = true },
+ Padding = new MarginPadding(0)
+ }
+ }
+ });
+ }
+
+ [Test]
+ public void TestSettingsButtonInput()
+ {
+ AddStep("Move cursor to button", () => InputManager.MoveMouseTo(settingsButton));
+ AddAssert("Button is hovered", () => settingsButton.IsHovered);
+ AddStep("Move cursor to padded area", () => InputManager.MoveMouseTo(settingsButton.ScreenSpaceDrawQuad.TopLeft + new Vector2(SettingsPanel.CONTENT_MARGINS / 2f, 10)));
+ AddAssert("Cursor within a button", () => settingsButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
+ AddAssert("Button is not hovered", () => !settingsButton.IsHovered);
+ }
+
+ [Test]
+ public void TestRoundedButtonInput()
+ {
+ AddStep("Move cursor to button", () => InputManager.MoveMouseTo(roundedButton));
+ AddAssert("Button is hovered", () => roundedButton.IsHovered);
+ AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(roundedButton.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
+ AddAssert("Cursor within a button", () => roundedButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
+ AddAssert("Button is not hovered", () => !roundedButton.IsHovered);
+ }
+
+ [Test]
+ public void TestShearedButtonInput()
+ {
+ AddStep("Move cursor to button", () => InputManager.MoveMouseTo(shearedButton));
+ AddAssert("Button is hovered", () => shearedButton.IsHovered);
+ AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(shearedButton.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
+ AddAssert("Cursor within a button", () => shearedButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
+ AddAssert("Button is not hovered", () => !shearedButton.IsHovered);
+ }
+
+ [Test]
+ public void TestRoundedClickableContainerInput()
+ {
+ AddStep("Move cursor to button", () => InputManager.MoveMouseTo(clickableContainer));
+ AddAssert("Button is hovered", () => clickableContainer.IsHovered);
+ AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(clickableContainer.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
+ AddAssert("Cursor within a button", () => clickableContainer.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
+ AddAssert("Button is not hovered", () => !clickableContainer.IsHovered);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
index 0d02a72d87..f45f5b9f59 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
@@ -126,6 +126,21 @@ namespace osu.Game.Tests.Visual.UserInterface
checkBindableAtValue("Circle Size", 9);
}
+ [Test]
+ public void TestExtendedLimitsRetainedAfterBoundCopyCreation()
+ {
+ setExtendedLimits(true);
+ setSliderValue("Circle Size", 11);
+
+ checkSliderAtValue("Circle Size", 11);
+ checkBindableAtValue("Circle Size", 11);
+
+ AddStep("create bound copy", () => _ = modDifficultyAdjust.CircleSize.GetBoundCopy());
+
+ checkSliderAtValue("Circle Size", 11);
+ checkBindableAtValue("Circle Size", 11);
+ }
+
[Test]
public void TestResetToDefault()
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs
deleted file mode 100644
index 41e5d47093..0000000000
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using NUnit.Framework;
-using osu.Framework.Graphics;
-using osu.Game.Graphics.UserInterface;
-using osuTK;
-
-namespace osu.Game.Tests.Visual.UserInterface
-{
- public partial class TestSceneOsuButton : OsuTestScene
- {
- [Test]
- public void TestToggleEnabled()
- {
- OsuButton button = null;
-
- AddStep("add button", () => Child = button = new OsuButton
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(200),
- Text = "Button"
- });
-
- AddToggleStep("toggle enabled", toggle =>
- {
- for (int i = 0; i < 6; i++)
- button.Action = toggle ? () => { } : null;
- });
- }
-
- [Test]
- public void TestInitiallyDisabled()
- {
- AddStep("add button", () => Child = new OsuButton
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(200),
- Text = "Button"
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
index b4b45da133..c51095f360 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Game.Overlays.Profile.Sections;
using osu.Framework.Testing;
@@ -18,7 +16,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
- private ProfileSubsectionHeader header;
+ private ProfileSubsectionHeader header = null!;
[Test]
public void TestHiddenCounter()
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
index 71b98ed9af..f96d2feba8 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
@@ -55,6 +55,16 @@ namespace osu.Game.Tests.Visual.UserInterface
};
});
+ [Test]
+ public void TestDisplay()
+ {
+ AddRepeatStep("toggle expanded state", () =>
+ {
+ InputManager.MoveMouseTo(group.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ }, 5);
+ }
+
[Test]
public void TestClickExpandButtonMultipleTimes()
{
diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index 52769321a9..1157b50377 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Tournament.Components
public TournamentBeatmapPanel(TournamentBeatmap beatmap, string mod = null)
{
- if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
+ ArgumentNullException.ThrowIfNull(beatmap);
Beatmap = beatmap;
this.mod = mod;
diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs
index f940571ffe..7babb3ea5a 100644
--- a/osu.Game.Tournament/IPC/FileBasedIPC.cs
+++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs
@@ -127,7 +127,7 @@ namespace osu.Game.Tournament.IPC
using (var stream = IPCStorage.GetStream(file_ipc_state_filename))
using (var sr = new StreamReader(stream))
{
- State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine().AsNonNull());
+ State.Value = Enum.Parse(sr.ReadLine().AsNonNull());
}
}
catch (Exception)
@@ -245,8 +245,10 @@ namespace osu.Game.Tournament.IPC
{
string stableInstallPath;
+#pragma warning disable CA1416
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
+#pragma warning restore CA1416
if (ipcFileExistsInDirectory(stableInstallPath))
return stableInstallPath;
diff --git a/osu.Game.Tournament/SaveChangesOverlay.cs b/osu.Game.Tournament/SaveChangesOverlay.cs
index a81f11cbe1..1bc576bc63 100644
--- a/osu.Game.Tournament/SaveChangesOverlay.cs
+++ b/osu.Game.Tournament/SaveChangesOverlay.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Tournament
private async Task checkForChanges()
{
- string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder());
+ string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder()).ConfigureAwait(false);
// If a save hasn't been triggered by the user yet, populate the initial value
lastSerialisedLadder ??= serialisedLadder;
diff --git a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs
index c230607343..74afb42c1a 100644
--- a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
continue;
// ReSharper disable once PossibleNullReferenceException
- string[] split = line.Split(':');
+ string[] split = line.Split(':', StringSplitOptions.TrimEntries);
if (split.Length < 2)
{
@@ -55,9 +55,9 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
teams.Add(new TournamentTeam
{
- FullName = { Value = split[1].Trim(), },
- Acronym = { Value = split.Length >= 3 ? split[2].Trim() : null, },
- FlagName = { Value = split[0].Trim() }
+ FullName = { Value = split[1], },
+ Acronym = { Value = split.Length >= 3 ? split[2] : null, },
+ FlagName = { Value = split[0] }
});
}
}
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index 988f0a02f0..c9d897ca11 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -44,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Editors
{
var countries = new List();
- foreach (var country in Enum.GetValues(typeof(CountryCode)).Cast().Skip(1))
+ foreach (var country in Enum.GetValues().Skip(1))
{
countries.Add(new TournamentTeam
{
@@ -54,8 +53,6 @@ namespace osu.Game.Tournament.Screens.Editors
});
}
- Debug.Assert(countries != null);
-
foreach (var c in countries)
Storage.Add(c);
}
diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
index 8f0d1de0cb..0fb6c1367b 100644
--- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -102,10 +103,14 @@ namespace osu.Game.Tournament.Screens.Editors
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(args.NewItems != null);
+
args.NewItems.Cast().ForEach(i => flow.Add(CreateDrawable(i)));
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(args.OldItems != null);
+
args.OldItems.Cast().ForEach(i => flow.RemoveAll(d => d.Model == i, true));
break;
}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
index 603a7830c7..5aea551c00 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -20,8 +21,6 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
{
public partial class LadderEditorSettings : PlayerSettingsGroup
{
- private const int padding = 10;
-
private SettingsDropdown roundDropdown;
private PlayerCheckbox losersCheckbox;
private DateTextBox dateTimeBox;
@@ -103,10 +102,14 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(args.NewItems != null);
+
args.NewItems.Cast().ForEach(add);
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(args.OldItems != null);
+
args.OldItems.Cast().ForEach(i => Control.RemoveDropdownItem(i));
break;
}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
index c90cdb7775..f7a42e4f50 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -25,10 +26,14 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(args.NewItems != null);
+
args.NewItems.Cast().ForEach(add);
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(args.OldItems != null);
+
args.OldItems.Cast().ForEach(i => Control.RemoveDropdownItem(i));
break;
}
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
index 595f08ed36..176c06c0e5 100644
--- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
+++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Caching;
@@ -81,11 +82,15 @@ namespace osu.Game.Tournament.Screens.Ladder
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(args.NewItems != null);
+
foreach (var p in args.NewItems.Cast())
addMatch(p);
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(args.OldItems != null);
+
foreach (var p in args.OldItems.Cast())
{
foreach (var d in MatchesContainer.Where(d => d.Match == p))
@@ -153,7 +158,7 @@ namespace osu.Game.Tournament.Screens.Ladder
foreach (var round in LadderInfo.Rounds)
{
- var topMatch = MatchesContainer.Where(p => !p.Match.Losers.Value && p.Match.Round.Value == round).OrderBy(p => p.Y).FirstOrDefault();
+ var topMatch = MatchesContainer.Where(p => !p.Match.Losers.Value && p.Match.Round.Value == round).MinBy(p => p.Y);
if (topMatch == null) continue;
@@ -167,7 +172,7 @@ namespace osu.Game.Tournament.Screens.Ladder
foreach (var round in LadderInfo.Rounds)
{
- var topMatch = MatchesContainer.Where(p => p.Match.Losers.Value && p.Match.Round.Value == round).OrderBy(p => p.Y).FirstOrDefault();
+ var topMatch = MatchesContainer.Where(p => p.Match.Losers.Value && p.Match.Round.Value == round).MinBy(p => p.Y);
if (topMatch == null) continue;
diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj
index b049542bb0..ab67e490cd 100644
--- a/osu.Game.Tournament/osu.Game.Tournament.csproj
+++ b/osu.Game.Tournament/osu.Game.Tournament.csproj
@@ -1,6 +1,6 @@
- netstandard2.1
+ net6.0
Library
true
tools for tournaments.
@@ -11,4 +11,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs
index ea226ab650..2c63c16274 100644
--- a/osu.Game/Audio/PreviewTrack.cs
+++ b/osu.Game/Audio/PreviewTrack.cs
@@ -109,6 +109,8 @@ namespace osu.Game.Audio
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+
+ Stop();
Track?.Dispose();
}
}
diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs
index 19c78f34b2..0191f96825 100644
--- a/osu.Game/Audio/SampleInfo.cs
+++ b/osu.Game/Audio/SampleInfo.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Audio
public bool Equals(SampleInfo? other)
=> other != null && sampleNames.SequenceEqual(other.sampleNames);
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
=> obj is SampleInfo other && Equals(other);
}
}
diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs
index 2d02fb6200..416d655cc3 100644
--- a/osu.Game/Beatmaps/Beatmap.cs
+++ b/osu.Game/Beatmaps/Beatmap.cs
@@ -81,9 +81,14 @@ namespace osu.Game.Beatmaps
public double GetMostCommonBeatLength()
{
+ double lastTime;
+
// The last playable time in the beatmap - the last timing point extends to this time.
// Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context.
- double lastTime = HitObjects.LastOrDefault()?.GetEndTime() ?? ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0;
+ if (!HitObjects.Any())
+ lastTime = ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0;
+ else
+ lastTime = this.GetLastObjectTime();
var mostCommon =
// Construct a set of (beatLength, duration) tuples for each individual timing point.
diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index bcb1d7f961..4752a88199 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps
public override async Task?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
{
- var imported = await Import(notification, importTask);
+ var imported = await Import(notification, new[] { importTask }).ConfigureAwait(true);
if (!imported.Any())
return null;
@@ -203,10 +203,10 @@ namespace osu.Game.Beatmaps
}
}
- protected override void PostImport(BeatmapSetInfo model, Realm realm, bool batchImport)
+ protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
{
- base.PostImport(model, realm, batchImport);
- ProcessBeatmap?.Invoke((model, batchImport));
+ base.PostImport(model, realm, parameters);
+ ProcessBeatmap?.Invoke((model, parameters.Batch));
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 965cc43815..f0533f27be 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -456,15 +456,15 @@ namespace osu.Game.Beatmaps
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
- public Task Import(params ImportTask[] tasks) => beatmapImporter.Import(tasks);
+ public Task Import(ImportTask[] tasks, ImportParameters parameters = default) => beatmapImporter.Import(tasks, parameters);
- public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) => beatmapImporter.Import(notification, tasks);
+ public Task>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => beatmapImporter.Import(notification, tasks, parameters);
- public Task?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) =>
- beatmapImporter.Import(task, batchImport, cancellationToken);
+ public Task?> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
+ beatmapImporter.Import(task, parameters, cancellationToken);
public Live? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) =>
- beatmapImporter.ImportModel(item, archive, false, cancellationToken);
+ beatmapImporter.ImportModel(item, archive, default, cancellationToken);
public IEnumerable HandledExtensions => beatmapImporter.HandledExtensions;
diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineNomination.cs b/osu.Game/Beatmaps/BeatmapSetOnlineNomination.cs
new file mode 100644
index 0000000000..f6de414628
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapSetOnlineNomination.cs
@@ -0,0 +1,22 @@
+// 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;
+
+namespace osu.Game.Beatmaps
+{
+ public struct BeatmapSetOnlineNomination
+ {
+ [JsonProperty(@"beatmapset_id")]
+ public int BeatmapsetId { get; set; }
+
+ [JsonProperty(@"reset")]
+ public bool Reset { get; set; }
+
+ [JsonProperty(@"rulesets")]
+ public string[]? Rulesets { get; set; }
+
+ [JsonProperty(@"user_id")]
+ public int UserId { get; set; }
+ }
+}
diff --git a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs
index 2fd1d06b7b..71d40b1a48 100644
--- a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs
+++ b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs
@@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps
{
try
{
- await cacheDownloadRequest.PerformAsync();
+ await cacheDownloadRequest.PerformAsync().ConfigureAwait(false);
}
catch
{
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 0a09e6e7e6..f46e4af332 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time;
- public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
+ public int CompareTo(ControlPoint? other) => Time.CompareTo(other?.Time);
public virtual Color4 GetRepresentingColour(OsuColour colours) => colours.Yellow;
@@ -32,7 +32,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public ControlPoint DeepClone()
{
- var copy = (ControlPoint)Activator.CreateInstance(GetType());
+ var copy = (ControlPoint)Activator.CreateInstance(GetType())!;
copy.CopyFrom(this);
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs
index db479f0e5b..1f34f3777d 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Beatmaps.ControlPoints
Time = time;
}
- public int CompareTo(ControlPointGroup other) => Time.CompareTo(other.Time);
+ public int CompareTo(ControlPointGroup? other) => Time.CompareTo(other?.Time);
public void Add(ControlPoint point)
{
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index 422e306450..29b7191ecf 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -70,14 +70,14 @@ namespace osu.Game.Beatmaps.ControlPoints
///
[JsonIgnore]
public double BPMMaximum =>
- 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
+ 60000 / (TimingPoints.MinBy(c => c.BeatLength) ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Finds the minimum BPM represented by any timing control point.
///
[JsonIgnore]
public double BPMMinimum =>
- 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
+ 60000 / (TimingPoints.MaxBy(c => c.BeatLength) ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Remove all s and return to a pristine state.
@@ -211,8 +211,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public static T BinarySearch(IReadOnlyList list, double time)
where T : class, IControlPoint
{
- if (list == null)
- throw new ArgumentNullException(nameof(list));
+ ArgumentNullException.ThrowIfNull(list);
if (list.Count == 0)
return null;
@@ -300,7 +299,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public ControlPointInfo DeepClone()
{
- var controlPointInfo = (ControlPointInfo)Activator.CreateInstance(GetType());
+ var controlPointInfo = (ControlPointInfo)Activator.CreateInstance(GetType())!;
foreach (var point in AllControlPoints)
controlPointInfo.Add(point.Time, point.DeepClone());
diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs
index 7a23b32c84..ec00756fd9 100644
--- a/osu.Game/Beatmaps/DifficultyRecommender.cs
+++ b/osu.Game/Beatmaps/DifficultyRecommender.cs
@@ -51,11 +51,11 @@ namespace osu.Game.Beatmaps
if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation))
continue;
- BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r)).OrderBy(b =>
+ BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b =>
{
double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
- }).FirstOrDefault();
+ });
if (beatmapInfo != null)
return beatmapInfo;
@@ -90,7 +90,7 @@ namespace osu.Game.Beatmaps
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
- .Where(r => !r.Equals(ruleset.Value.ShortName))
+ .Where(r => !r.Equals(ruleset.Value.ShortName, StringComparison.Ordinal))
.Prepend(ruleset.Value.ShortName);
}
}
diff --git a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
index d31a7ae2fe..767504fcb1 100644
--- a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
+++ b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
@@ -15,8 +15,7 @@ namespace osu.Game.Beatmaps.Drawables
public BeatmapBackgroundSprite(IWorkingBeatmap working)
{
- if (working == null)
- throw new ArgumentNullException(nameof(working));
+ ArgumentNullException.ThrowIfNull(working);
this.working = working;
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
index 00f9a6b3d5..94b2956b4e 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
@@ -5,16 +5,19 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
+using osu.Game.Localisation;
namespace osu.Game.Beatmaps.Drawables.Cards
{
- public abstract partial class BeatmapCard : OsuClickableContainer
+ public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu
{
public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10;
@@ -96,5 +99,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
throw new ArgumentOutOfRangeException(nameof(size), size, @"Unsupported card size");
}
}
+
+ public MenuItem[] ContextMenuItems => new MenuItem[]
+ {
+ new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, Action),
+ };
}
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
index d4cbe6ddd0..7deb5f768c 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
@@ -138,9 +138,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards
// This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
this.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
- background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
- dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
- borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ if (Expanded.Value)
+ {
+ background.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ dropdownContent.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ borderContainer.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ }
+ else
+ {
+ background.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
+ dropdownContent.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
+ borderContainer.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
+ }
content.TweenEdgeEffectTo(new EdgeEffectParameters
{
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs
index 781133aac7..c99d1f0c76 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs
@@ -11,7 +11,7 @@ using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
-using osu.Game.Screens.Ranking.Expanded.Accuracy;
+using osu.Framework.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private readonly UpdateableOnlineBeatmapSetCover cover;
private readonly Container foreground;
private readonly PlayButton playButton;
- private readonly SmoothCircularProgress progress;
+ private readonly CircularProgress progress;
private readonly Container content;
protected override Container Content => content;
@@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
RelativeSizeAxes = Axes.Both
},
- progress = new SmoothCircularProgress
+ progress = new CircularProgress
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs
index e4ffc1d553..fc7c14e734 100644
--- a/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs
+++ b/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs
@@ -18,8 +18,7 @@ namespace osu.Game.Beatmaps.Drawables
public OnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover)
{
- if (set == null)
- throw new ArgumentNullException(nameof(set));
+ ArgumentNullException.ThrowIfNull(set);
this.set = set;
this.type = type;
diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs
index ca1bcc97fd..4f0f11d053 100644
--- a/osu.Game/Beatmaps/Formats/Decoder.cs
+++ b/osu.Game/Beatmaps/Formats/Decoder.cs
@@ -57,8 +57,7 @@ namespace osu.Game.Beatmaps.Formats
public static Decoder GetDecoder(LineBufferedReader stream)
where T : new()
{
- if (stream == null)
- throw new ArgumentNullException(nameof(stream));
+ ArgumentNullException.ThrowIfNull(stream);
if (!decoders.TryGetValue(typeof(T), out var typedDecoders))
throw new IOException(@"Unknown decoder type");
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 5f0a2a0824..9c710b690e 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -160,7 +160,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"SampleSet":
- defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value);
+ defaultSampleBank = Enum.Parse(pair.Value);
break;
case @"SampleVolume":
@@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"Countdown":
- beatmap.BeatmapInfo.Countdown = (CountdownType)Enum.Parse(typeof(CountdownType), pair.Value);
+ beatmap.BeatmapInfo.Countdown = Enum.Parse(pair.Value);
break;
case @"CountdownOffset":
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 03c63ff4f2..7e058d755e 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps.Formats
{
var comboColour = colours[i];
- writer.Write(FormattableString.Invariant($"Combo{i}: "));
+ writer.Write(FormattableString.Invariant($"Combo{1 + i}: "));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},"));
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index a4e15f790a..704756e3dd 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -132,13 +132,7 @@ namespace osu.Game.Beatmaps.Formats
protected KeyValuePair SplitKeyVal(string line, char separator = ':', bool shouldTrim = true)
{
- string[] split = line.Split(separator, 2);
-
- if (shouldTrim)
- {
- for (int i = 0; i < split.Length; i++)
- split[i] = split[i].Trim();
- }
+ string[] split = line.Split(separator, 2, shouldTrim ? StringSplitOptions.TrimEntries : StringSplitOptions.None);
return new KeyValuePair
(
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index 2b4f377ab6..44dbb3cc9f 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
@@ -301,11 +301,11 @@ namespace osu.Game.Beatmaps.Formats
}
}
- private string parseLayer(string value) => Enum.Parse(typeof(LegacyStoryLayer), value).ToString();
+ private string parseLayer(string value) => Enum.Parse(value).ToString();
private Anchor parseOrigin(string value)
{
- var origin = (LegacyOrigins)Enum.Parse(typeof(LegacyOrigins), value);
+ var origin = Enum.Parse(value);
switch (origin)
{
@@ -343,8 +343,8 @@ namespace osu.Game.Beatmaps.Formats
private AnimationLoopType parseAnimationLoopType(string value)
{
- var parsed = (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), value);
- return Enum.IsDefined(typeof(AnimationLoopType), parsed) ? parsed : AnimationLoopType.LoopForever;
+ var parsed = Enum.Parse(value);
+ return Enum.IsDefined(parsed) ? parsed : AnimationLoopType.LoopForever;
}
private void handleVariables(string line)
diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs
index 0e892b6581..f6771f7adf 100644
--- a/osu.Game/Beatmaps/IBeatmap.cs
+++ b/osu.Game/Beatmaps/IBeatmap.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
@@ -102,5 +103,16 @@ namespace osu.Game.Beatmaps
addCombo(nested, ref combo);
}
}
+
+ ///
+ /// Find the absolute end time of the latest in a beatmap. Will throw if beatmap contains no objects.
+ ///
+ ///
+ /// This correctly accounts for rulesets which have concurrent hitobjects which may have durations, causing the .Last() object
+ /// to not necessarily have the latest end time.
+ ///
+ /// It's not super efficient so calls should be kept to a minimum.
+ ///
+ public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime());
}
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 393c4ba892..ab790617bb 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
// TODO: remove once the fallback lookup is not required (and access via `working.BeatmapInfo.Metadata` directly).
public BeatmapMetadata Metadata => BeatmapInfo.Metadata;
- public Waveform Waveform => waveform.Value;
-
public Storyboard Storyboard => storyboard.Value;
public Texture Background => GetBackground(); // Texture uses ref counting, so we want to return a new instance every usage.
@@ -48,10 +46,11 @@ namespace osu.Game.Beatmaps
private readonly object beatmapFetchLock = new object();
- private readonly Lazy waveform;
private readonly Lazy storyboard;
private readonly Lazy skin;
+
private Track track; // track is not Lazy as we allow transferring and loading multiple times.
+ private Waveform waveform; // waveform is also not Lazy as the track may change.
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
{
@@ -60,7 +59,6 @@ namespace osu.Game.Beatmaps
BeatmapInfo = beatmapInfo;
BeatmapSetInfo = beatmapInfo.BeatmapSet ?? new BeatmapSetInfo();
- waveform = new Lazy(GetWaveform);
storyboard = new Lazy(GetStoryboard);
skin = new Lazy(GetSkin);
}
@@ -108,7 +106,16 @@ namespace osu.Game.Beatmaps
public virtual bool TrackLoaded => track != null;
- public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
+ public Track LoadTrack()
+ {
+ track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
+
+ // the track may have changed, recycle the current waveform.
+ waveform?.Dispose();
+ waveform = null;
+
+ return track;
+ }
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
{
@@ -171,6 +178,12 @@ namespace osu.Game.Beatmaps
#endregion
+ #region Waveform
+
+ public Waveform Waveform => waveform ??= GetWaveform();
+
+ #endregion
+
#region Beatmap
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index adb5f8c433..e6f96330e7 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -141,6 +141,9 @@ namespace osu.Game.Beatmaps
try
{
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
+
+ // TODO: check validity of file
+
var stream = GetStream(fileStorePath);
if (stream == null)
diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs
index 12a30a0c84..276563e163 100644
--- a/osu.Game/Configuration/SessionStatics.cs
+++ b/osu.Game/Configuration/SessionStatics.cs
@@ -19,6 +19,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LoginOverlayDisplayed, false);
SetDefault(Static.MutedAudioNotificationShownOnce, false);
SetDefault(Static.LowBatteryNotificationShownOnce, false);
+ SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.SeasonalBackgrounds, null);
}
@@ -42,6 +43,7 @@ namespace osu.Game.Configuration
LoginOverlayDisplayed,
MutedAudioNotificationShownOnce,
LowBatteryNotificationShownOnce,
+ FeaturedArtistDisclaimerShownOnce,
///
/// Info about seasonal backgrounds available fetched from API - see .
@@ -53,6 +55,6 @@ namespace osu.Game.Configuration
/// The last playback time in milliseconds of a hover sample (from ).
/// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like .
///
- LastHoverSoundPlaybackTime
+ LastHoverSoundPlaybackTime,
}
}
diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 043bba3134..1e425c88a6 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -91,15 +91,15 @@ namespace osu.Game.Configuration
OrderPosition = orderPosition;
}
- public int CompareTo(SettingSourceAttribute other)
+ public int CompareTo(SettingSourceAttribute? other)
{
- if (OrderPosition == other.OrderPosition)
+ if (OrderPosition == other?.OrderPosition)
return 0;
// unordered items come last (are greater than any ordered items).
if (OrderPosition == null)
return 1;
- if (other.OrderPosition == null)
+ if (other?.OrderPosition == null)
return -1;
// ordered items are sorted by the order value.
@@ -113,7 +113,7 @@ namespace osu.Game.Configuration
{
foreach (var (attr, property) in obj.GetOrderedSettingsSourceProperties())
{
- object value = property.GetValue(obj);
+ object value = property.GetValue(obj)!;
if (attr.SettingControlType != null)
{
@@ -121,7 +121,7 @@ namespace osu.Game.Configuration
if (controlType.EnumerateBaseTypes().All(t => !t.IsGenericType || t.GetGenericTypeDefinition() != typeof(SettingsItem<>)))
throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} had an unsupported custom control type ({controlType.ReadableName()})");
- var control = (Drawable)Activator.CreateInstance(controlType);
+ var control = (Drawable)Activator.CreateInstance(controlType)!;
controlType.GetProperty(nameof(SettingsItem
/// The model prepared for import.
/// The current realm context.
- /// Whether the import was part of a batch.
- protected virtual void PostImport(TModel model, Realm realm, bool batchImport)
+ /// Parameters to further configure the import process.
+ protected virtual void PostImport(TModel model, Realm realm, ImportParameters parameters)
{
}
diff --git a/osu.Game/Database/RealmFileStore.cs b/osu.Game/Database/RealmFileStore.cs
index 036b15ea17..f75d3be725 100644
--- a/osu.Game/Database/RealmFileStore.cs
+++ b/osu.Game/Database/RealmFileStore.cs
@@ -10,6 +10,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Extensions;
+using osu.Game.IO;
using osu.Game.Models;
using Realms;
@@ -41,7 +42,8 @@ namespace osu.Game.Database
/// The file data stream.
/// The realm instance to add to. Should already be in a transaction.
/// Whether the should immediately be added to the underlying realm. If false is provided here, the instance must be manually added.
- public RealmFile Add(Stream data, Realm realm, bool addToRealm = true)
+ /// Whether this import should use hard links rather than file copy operations if available.
+ public RealmFile Add(Stream data, Realm realm, bool addToRealm = true, bool preferHardLinks = false)
{
string hash = data.ComputeSHA2Hash();
@@ -50,7 +52,7 @@ namespace osu.Game.Database
var file = existing ?? new RealmFile { Hash = hash };
if (!checkFileExistsAndMatchesHash(file))
- copyToStore(file, data);
+ copyToStore(file, data, preferHardLinks);
if (addToRealm && !file.IsManaged)
realm.Add(file);
@@ -58,8 +60,15 @@ namespace osu.Game.Database
return file;
}
- private void copyToStore(RealmFile file, Stream data)
+ private void copyToStore(RealmFile file, Stream data, bool preferHardLinks)
{
+ if (data is FileStream fs && preferHardLinks)
+ {
+ // attempt to do a fast hard link rather than copy.
+ if (HardLinkHelper.TryCreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name))
+ return;
+ }
+
data.Seek(0, SeekOrigin.Begin);
using (var output = Storage.CreateFileSafely(file.GetStoragePath()))
diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs
index 35f2d61437..65b9e46764 100644
--- a/osu.Game/Extensions/DrawableExtensions.cs
+++ b/osu.Game/Extensions/DrawableExtensions.cs
@@ -66,10 +66,10 @@ namespace osu.Game.Extensions
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
- if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
+ if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
continue;
- skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue);
+ skinnable.CopyAdjustedSetting(((IBindable)property.GetValue(component)!), settingValue);
}
}
diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs
index b4a0c02e35..43abb59042 100644
--- a/osu.Game/Extensions/TaskExtensions.cs
+++ b/osu.Game/Extensions/TaskExtensions.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Extensions
if (cancellationToken.IsCancellationRequested)
{
- tcs.SetCanceled();
+ tcs.SetCanceled(cancellationToken);
}
else
{
diff --git a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs
index 735b8b4e7d..984d60d35e 100644
--- a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs
+++ b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs
@@ -36,8 +36,7 @@ namespace osu.Game.Graphics.Containers
/// The easing type of the initial transform.
public void StartTracking(OsuLogo logo, double duration = 0, Easing easing = Easing.None)
{
- if (logo == null)
- throw new ArgumentNullException(nameof(logo));
+ ArgumentNullException.ThrowIfNull(logo);
if (logo.IsTracking && Logo == null)
throw new InvalidOperationException($"Cannot track an instance of {typeof(OsuLogo)} to multiple {typeof(LogoTrackingContainer)}s");
diff --git a/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs b/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs
new file mode 100644
index 0000000000..caed4b26b9
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Markdig;
+using Markdig.Extensions.GenericAttributes;
+using Markdig.Renderers;
+using Markdig.Syntax;
+
+namespace osu.Game.Graphics.Containers.Markdown.Extensions
+{
+ ///
+ /// A variant of
+ /// which only handles generic attributes in the current markdown and ignores inline generic attributes.
+ ///
+ ///
+ /// For rationale, see implementation of .
+ ///
+ public class BlockAttributeExtension : IMarkdownExtension
+ {
+ private readonly GenericAttributesExtension genericAttributesExtension = new GenericAttributesExtension();
+
+ public void Setup(MarkdownPipelineBuilder pipeline)
+ {
+ genericAttributesExtension.Setup(pipeline);
+
+ // GenericAttributesExtension registers a GenericAttributesParser in pipeline.InlineParsers.
+ // this conflicts with the CustomContainerExtension, leading to some custom containers (e.g. flags) not displaying.
+ // as a workaround, remove the inline parser here before it can do damage.
+ pipeline.InlineParsers.RemoveAll(parser => parser is GenericAttributesParser);
+ }
+
+ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) => genericAttributesExtension.Setup(pipeline, renderer);
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Extensions/OsuMarkdownExtensions.cs b/osu.Game/Graphics/Containers/Markdown/Extensions/OsuMarkdownExtensions.cs
new file mode 100644
index 0000000000..10542abe71
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Extensions/OsuMarkdownExtensions.cs
@@ -0,0 +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 Markdig;
+
+namespace osu.Game.Graphics.Containers.Markdown.Extensions
+{
+ public static class OsuMarkdownExtensions
+ {
+ ///
+ /// Uses the block attributes extension.
+ ///
+ /// The pipeline.
+ /// The modified pipeline.
+ public static MarkdownPipelineBuilder UseBlockAttributes(this MarkdownPipelineBuilder pipeline)
+ {
+ pipeline.Extensions.AddIfNotAlready();
+ return pipeline;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnote.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnote.cs
new file mode 100644
index 0000000000..e92d866eed
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnote.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Markdig.Extensions.Footnotes;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Containers.Markdown.Footnotes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+
+namespace osu.Game.Graphics.Containers.Markdown.Footnotes
+{
+ public partial class OsuMarkdownFootnote : MarkdownFootnote
+ {
+ public OsuMarkdownFootnote(Footnote footnote)
+ : base(footnote)
+ {
+ }
+
+ public override SpriteText CreateOrderMarker(int order) => CreateSpriteText().With(marker =>
+ {
+ marker.Text = LocalisableString.Format("{0}.", order);
+ });
+
+ public override MarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(textFlow =>
+ {
+ textFlow.Margin = new MarginPadding { Left = 30 };
+ });
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteBacklink.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteBacklink.cs
new file mode 100644
index 0000000000..22c02ea720
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteBacklink.cs
@@ -0,0 +1,62 @@
+// 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 Markdig.Extensions.Footnotes;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Graphics.Containers.Markdown.Footnotes
+{
+ public partial class OsuMarkdownFootnoteBacklink : OsuHoverContainer
+ {
+ private readonly FootnoteLink backlink;
+
+ private SpriteIcon spriteIcon = null!;
+
+ [Resolved]
+ private IMarkdownTextComponent parentTextComponent { get; set; } = null!;
+
+ protected override IEnumerable EffectTargets => spriteIcon.Yield();
+
+ public OsuMarkdownFootnoteBacklink(FootnoteLink backlink)
+ {
+ this.backlink = backlink;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider colourProvider, OsuMarkdownContainer markdownContainer, OverlayScrollContainer? scrollContainer)
+ {
+ float fontSize = parentTextComponent.CreateSpriteText().Font.Size;
+ Size = new Vector2(fontSize);
+
+ IdleColour = colourProvider.Light2;
+ HoverColour = colourProvider.Light1;
+
+ Add(spriteIcon = new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding { Left = 5 },
+ Size = new Vector2(fontSize / 2),
+ Icon = FontAwesome.Solid.ArrowUp,
+ });
+
+ if (scrollContainer != null)
+ {
+ Action = () =>
+ {
+ var footnoteLink = markdownContainer.ChildrenOfType().Single(footnoteLink => footnoteLink.FootnoteLink.Index == backlink.Index);
+ scrollContainer.ScrollIntoView(footnoteLink);
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteLink.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteLink.cs
new file mode 100644
index 0000000000..c9bd408e9e
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteLink.cs
@@ -0,0 +1,80 @@
+// 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 Markdig.Extensions.Footnotes;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Framework.Testing;
+using osu.Game.Overlays;
+
+namespace osu.Game.Graphics.Containers.Markdown.Footnotes
+{
+ public partial class OsuMarkdownFootnoteLink : OsuHoverContainer, IHasCustomTooltip
+ {
+ public readonly FootnoteLink FootnoteLink;
+
+ private SpriteText spriteText = null!;
+
+ [Resolved]
+ private IMarkdownTextComponent parentTextComponent { get; set; } = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ [Resolved]
+ private OsuMarkdownContainer markdownContainer { get; set; } = null!;
+
+ protected override IEnumerable EffectTargets => spriteText.Yield();
+
+ public OsuMarkdownFootnoteLink(FootnoteLink footnoteLink)
+ {
+ FootnoteLink = footnoteLink;
+
+ AutoSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuMarkdownContainer markdownContainer, OverlayScrollContainer? scrollContainer)
+ {
+ IdleColour = colourProvider.Light2;
+ HoverColour = colourProvider.Light1;
+
+ spriteText = parentTextComponent.CreateSpriteText();
+
+ Add(spriteText.With(t =>
+ {
+ float baseSize = t.Font.Size;
+ t.Font = t.Font.With(size: baseSize * 0.58f);
+ t.Margin = new MarginPadding { Bottom = 0.33f * baseSize };
+ t.Text = LocalisableString.Format("[{0}]", FootnoteLink.Index);
+ }));
+
+ if (scrollContainer != null)
+ {
+ Action = () =>
+ {
+ var footnote = markdownContainer.ChildrenOfType().Single(footnote => footnote.Footnote.Label == FootnoteLink.Footnote.Label);
+ scrollContainer.ScrollIntoView(footnote);
+ };
+ }
+ }
+
+ public object TooltipContent
+ {
+ get
+ {
+ var span = FootnoteLink.Footnote.LastChild.Span;
+ return markdownContainer.Text.Substring(span.Start, span.Length);
+ }
+ }
+
+ public ITooltip GetCustomTooltip() => new OsuMarkdownFootnoteTooltip(colourProvider);
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs
new file mode 100644
index 0000000000..af64913212
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs
@@ -0,0 +1,76 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Markdig.Extensions.Footnotes;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Graphics.Containers.Markdown.Footnotes
+{
+ public partial class OsuMarkdownFootnoteTooltip : CompositeDrawable, ITooltip
+ {
+ private readonly FootnoteMarkdownContainer markdownContainer;
+
+ [Cached]
+ private OverlayColourProvider colourProvider;
+
+ public OsuMarkdownFootnoteTooltip(OverlayColourProvider colourProvider)
+ {
+ this.colourProvider = colourProvider;
+
+ Masking = true;
+ Width = 200;
+ AutoSizeAxes = Axes.Y;
+ CornerRadius = 4;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background6
+ },
+ markdownContainer = new FootnoteMarkdownContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ DocumentMargin = new MarginPadding(),
+ DocumentPadding = new MarginPadding { Horizontal = 10, Vertical = 5 }
+ }
+ };
+ }
+
+ public void Move(Vector2 pos) => Position = pos;
+
+ public void SetContent(object content) => markdownContainer.SetContent((string)content);
+
+ private partial class FootnoteMarkdownContainer : OsuMarkdownContainer
+ {
+ private string? lastFootnote;
+
+ public void SetContent(string footnote)
+ {
+ if (footnote == lastFootnote)
+ return;
+
+ lastFootnote = Text = footnote;
+ }
+
+ public override MarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer();
+ }
+
+ private partial class FootnoteMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
+ {
+ protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink)
+ {
+ // we don't want footnote backlinks to show up in tooltips.
+ }
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs
index e884b5db69..5b1780a068 100644
--- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs
@@ -4,41 +4,25 @@
#nullable disable
using Markdig;
-using Markdig.Extensions.AutoLinks;
-using Markdig.Extensions.CustomContainers;
-using Markdig.Extensions.EmphasisExtras;
using Markdig.Extensions.Footnotes;
using Markdig.Extensions.Tables;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Containers.Markdown.Footnotes;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Graphics.Sprites;
+using osuTK;
namespace osu.Game.Graphics.Containers.Markdown
{
+ [Cached]
public partial class OsuMarkdownContainer : MarkdownContainer
{
- ///
- /// Allows this markdown container to parse and link footnotes.
- ///
- ///
- protected virtual bool Footnotes => false;
-
- ///
- /// Allows this markdown container to make URL text clickable.
- ///
- ///
- protected virtual bool Autolinks => false;
-
- ///
- /// Allows this markdown container to parse custom containers (used for flags and infoboxes).
- ///
- ///
- protected virtual bool CustomContainers => false;
-
public OsuMarkdownContainer()
{
LineSpacing = 21;
@@ -99,25 +83,17 @@ namespace osu.Game.Graphics.Containers.Markdown
return new OsuMarkdownUnorderedListItem(level);
}
- // reference: https://github.com/ppy/osu-web/blob/05488a96b25b5a09f2d97c54c06dd2bae59d1dc8/app/Libraries/Markdown/OsuMarkdown.php#L301
- protected override MarkdownPipeline CreateBuilder()
- {
- var pipeline = new MarkdownPipelineBuilder()
- .UseAutoIdentifiers()
- .UsePipeTables()
- .UseEmphasisExtras(EmphasisExtraOptions.Strikethrough)
- .UseYamlFrontMatter();
+ protected override MarkdownFootnoteGroup CreateFootnoteGroup(FootnoteGroup footnoteGroup) => base.CreateFootnoteGroup(footnoteGroup).With(g => g.Spacing = new Vector2(5));
- if (Footnotes)
- pipeline = pipeline.UseFootnotes();
+ protected override MarkdownFootnote CreateFootnote(Footnote footnote) => new OsuMarkdownFootnote(footnote);
- if (Autolinks)
- pipeline = pipeline.UseAutoLinks();
+ protected sealed override MarkdownPipeline CreateBuilder()
+ => Options.BuildPipeline();
- if (CustomContainers)
- pipeline.UseCustomContainers();
-
- return pipeline.Build();
- }
+ ///
+ /// Creates a instance which is used to determine
+ /// which CommonMark/Markdig extensions should be enabled for this .
+ ///
+ protected virtual OsuMarkdownContainerOptions Options => new OsuMarkdownContainerOptions();
}
}
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainerOptions.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainerOptions.cs
new file mode 100644
index 0000000000..1648ffbf90
--- /dev/null
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainerOptions.cs
@@ -0,0 +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 Markdig;
+using Markdig.Extensions.AutoLinks;
+using Markdig.Extensions.CustomContainers;
+using Markdig.Extensions.EmphasisExtras;
+using Markdig.Extensions.Footnotes;
+using osu.Game.Graphics.Containers.Markdown.Extensions;
+
+namespace osu.Game.Graphics.Containers.Markdown
+{
+ ///
+ /// Groups options of customising the set of available extensions to instances.
+ ///
+ public class OsuMarkdownContainerOptions
+ {
+ ///
+ /// Allows the to parse and link footnotes.
+ ///
+ ///
+ public bool Footnotes { get; init; }
+
+ ///
+ /// Allows the container to make URL text clickable.
+ ///
+ ///
+ public bool Autolinks { get; init; }
+
+ ///
+ /// Allows the to parse custom containers (used for flags and infoboxes).
+ ///
+ ///
+ public bool CustomContainers { get; init; }
+
+ ///
+ /// Allows the to parse custom attributes in block elements (used e.g. for custom anchor names in the wiki).
+ ///
+ ///
+ public bool BlockAttributes { get; init; }
+
+ ///
+ /// Returns a prepared according to the options specified by the current instance.
+ ///
+ ///
+ /// Compare: https://github.com/ppy/osu-web/blob/05488a96b25b5a09f2d97c54c06dd2bae59d1dc8/app/Libraries/Markdown/OsuMarkdown.php#L301
+ ///
+ public MarkdownPipeline BuildPipeline()
+ {
+ var pipeline = new MarkdownPipelineBuilder()
+ .UseAutoIdentifiers()
+ .UsePipeTables()
+ .UseEmphasisExtras(EmphasisExtraOptions.Strikethrough)
+ .UseYamlFrontMatter();
+
+ if (Footnotes)
+ pipeline = pipeline.UseFootnotes();
+
+ if (Autolinks)
+ pipeline = pipeline.UseAutoLinks();
+
+ if (CustomContainers)
+ pipeline = pipeline.UseCustomContainers();
+
+ if (BlockAttributes)
+ pipeline = pipeline.UseBlockAttributes();
+
+ return pipeline.Build();
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs
index 7de63fe09c..5f5b9acf56 100644
--- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs
@@ -6,6 +6,7 @@
using System;
using System.Linq;
using Markdig.Extensions.CustomContainers;
+using Markdig.Extensions.Footnotes;
using Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -13,6 +14,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Overlays;
using osu.Game.Users;
using osu.Game.Users.Drawables;
@@ -36,6 +38,10 @@ namespace osu.Game.Graphics.Containers.Markdown
Text = codeInline.Content
});
+ protected override void AddFootnoteLink(FootnoteLink footnoteLink) => AddDrawable(new OsuMarkdownFootnoteLink(footnoteLink));
+
+ protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink) => AddDrawable(new OsuMarkdownFootnoteBacklink(footnoteBacklink));
+
protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
=> CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic));
diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
index 4729ddf1a8..ce50dbdc39 100644
--- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
@@ -1,14 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
+using osuTK;
namespace osu.Game.Graphics.Containers
{
@@ -18,6 +18,12 @@ namespace osu.Game.Graphics.Containers
private readonly Container content = new Container { RelativeSizeAxes = Axes.Both };
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
+ // base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
+ base.ReceivePositionalInputAt(screenSpacePos)
+ // Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
+ && Content.ReceivePositionalInputAt(screenSpacePos);
+
protected override Container Content => content;
protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } };
@@ -38,11 +44,11 @@ namespace osu.Game.Graphics.Containers
content.AutoSizeAxes = AutoSizeAxes;
}
- InternalChildren = new Drawable[]
- {
- content,
- CreateHoverSounds(sampleSet)
- };
+ AddInternal(content);
+ Add(CreateHoverSounds(sampleSet));
}
+
+ protected override void ClearInternal(bool disposeChildren = true) =>
+ throw new InvalidOperationException($"Clearing {nameof(InternalChildren)} will cause critical failure. Use {nameof(Clear)} instead.");
}
}
diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs
index 123589c552..8dd6eac7bb 100644
--- a/osu.Game/Graphics/Containers/SectionsContainer.cs
+++ b/osu.Game/Graphics/Containers/SectionsContainer.cs
@@ -240,7 +240,9 @@ namespace osu.Game.Graphics.Containers
headerBackgroundContainer.Height = expandableHeaderSize + fixedHeaderSize;
headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0;
- float smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0;
+ var flowChildren = scrollContentContainer.FlowingChildren.OfType();
+
+ float smallestSectionHeight = flowChildren.Any() ? flowChildren.Min(d => d.Height) : 0;
// scroll offset is our fixed header height if we have it plus 10% of content height
// plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards
@@ -249,7 +251,7 @@ namespace osu.Game.Graphics.Containers
float scrollCentre = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_y_centre + selectionLenienceAboveSection;
- var presentChildren = Children.Where(c => c.IsPresent);
+ var presentChildren = flowChildren.Where(c => c.IsPresent);
if (lastClickedSection != null)
SelectedSection.Value = lastClickedSection;
diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
index b63e73e679..8cf47006ab 100644
--- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
@@ -234,7 +234,7 @@ namespace osu.Game.Graphics.Cursor
SampleChannel channel = tapSample.GetChannel();
// Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
- channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
+ channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * OsuGameBase.SFX_STEREO_STRENGTH;
channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
channel.Volume.Value = baseFrequency;
diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs
index d9bb2b610a..c62f53f1d4 100644
--- a/osu.Game/Graphics/DateTooltip.cs
+++ b/osu.Game/Graphics/DateTooltip.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osuTK;
@@ -69,8 +70,8 @@ namespace osu.Game.Graphics
{
DateTimeOffset localDate = date.ToLocalTime();
- dateText.Text = $"{localDate:d MMMM yyyy} ";
- timeText.Text = $"{localDate:HH:mm:ss \"UTC\"z}";
+ dateText.Text = LocalisableString.Interpolate($"{localDate:d MMMM yyyy} ");
+ timeText.Text = LocalisableString.Interpolate($"{localDate:HH:mm:ss \"UTC\"z}");
}
public void Move(Vector2 pos) => Position = pos;
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index 91161d5c71..c5659aaf57 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -22,38 +22,8 @@ namespace osu.Game.Graphics
public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255);
///
- /// Retrieves the colour for a .
+ /// Retrieves the colour for a given point in the star range.
///
- ///
- /// Sourced from the @diff-{rating} variables in https://github.com/ppy/osu-web/blob/71fbab8936d79a7929d13854f5e854b4f383b236/resources/assets/less/variables.less.
- ///
- public Color4 ForDifficultyRating(DifficultyRating difficulty, bool useLighterColour = false)
- {
- switch (difficulty)
- {
- case DifficultyRating.Easy:
- return Color4Extensions.FromHex("4ebfff");
-
- case DifficultyRating.Normal:
- return Color4Extensions.FromHex("66ff91");
-
- case DifficultyRating.Hard:
- return Color4Extensions.FromHex("f7e85d");
-
- case DifficultyRating.Insane:
- return Color4Extensions.FromHex("ff7e68");
-
- case DifficultyRating.Expert:
- return Color4Extensions.FromHex("fe3c71");
-
- case DifficultyRating.ExpertPlus:
- return Color4Extensions.FromHex("6662dd");
-
- default:
- throw new ArgumentOutOfRangeException(nameof(difficulty));
- }
- }
-
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
{
(0.1f, Color4Extensions.FromHex("aaaaaa")),
diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
index 67b63e120b..fc0770d896 100644
--- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
+++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
@@ -52,8 +52,8 @@ namespace osu.Game.Graphics.UserInterface
public readonly SpriteIcon Chevron;
- //don't allow clicking between transitions and don't make the chevron clickable
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && Text.ReceivePositionalInputAt(screenSpacePos);
+ //don't allow clicking between transitions
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && base.ReceivePositionalInputAt(screenSpacePos);
public override bool HandleNonPositionalInput => State == Visibility.Visible;
public override bool HandlePositionalInput => State == Visibility.Visible;
@@ -95,7 +95,7 @@ namespace osu.Game.Graphics.UserInterface
{
Text.Font = Text.Font.With(size: 18);
Text.Margin = new MarginPadding { Vertical = 8 };
- Padding = new MarginPadding { Right = padding + ChevronSize };
+ Margin = new MarginPadding { Right = padding + ChevronSize };
Add(Chevron = new SpriteIcon
{
Anchor = Anchor.CentreRight,
diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
index efbbaaca85..4eccb37613 100644
--- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
+++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
if (Link != null)
{
- items.Add(new OsuMenuItem("Open", MenuItemType.Standard, () => host.OpenUrlExternally(Link)));
+ items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => host.OpenUrlExternally(Link)));
items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, copyUrl));
}
diff --git a/osu.Game/Graphics/UserInterface/HistoryTextBox.cs b/osu.Game/Graphics/UserInterface/HistoryTextBox.cs
index d74a4f2cdb..b6dc1fcc9b 100644
--- a/osu.Game/Graphics/UserInterface/HistoryTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/HistoryTextBox.cs
@@ -45,6 +45,9 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnKeyDown(KeyDownEvent e)
{
+ if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
+ return false;
+
switch (e.Key)
{
case Key.Up:
diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs
index 4f56872f42..7921dcf593 100644
--- a/osu.Game/Graphics/UserInterface/Nub.cs
+++ b/osu.Game/Graphics/UserInterface/Nub.cs
@@ -114,8 +114,7 @@ namespace osu.Game.Graphics.UserInterface
get => current;
set
{
- if (value == null)
- throw new ArgumentNullException(nameof(value));
+ ArgumentNullException.ThrowIfNull(value);
current.UnbindBindings();
current.BindTo(value);
diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index fa61b06cff..805dfcaa95 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -13,6 +11,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
+using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
@@ -20,16 +19,12 @@ namespace osu.Game.Graphics.UserInterface
///
/// A button with added default sound effects.
///
- public partial class OsuButton : Button
+ public abstract partial class OsuButton : Button
{
public LocalisableString Text
{
- get => SpriteText?.Text ?? default;
- set
- {
- if (SpriteText != null)
- SpriteText.Text = value;
- }
+ get => SpriteText.Text;
+ set => SpriteText.Text = value;
}
private Color4? backgroundColour;
@@ -66,13 +61,19 @@ namespace osu.Game.Graphics.UserInterface
protected override Container Content { get; }
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
+ // base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
+ base.ReceivePositionalInputAt(screenSpacePos)
+ // Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
+ && Content.ReceivePositionalInputAt(screenSpacePos);
+
protected Box Hover;
protected Box Background;
protected SpriteText SpriteText;
private readonly Box flashLayer;
- public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
+ protected OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
{
Height = 40;
@@ -115,7 +116,7 @@ namespace osu.Game.Graphics.UserInterface
});
if (hoverSounds.HasValue)
- AddInternal(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
+ Add(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
index 9ef58f4c49..dc089e3410 100644
--- a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Graphics.UserInterface
{
public OsuEnumDropdown()
{
- Items = (T[])Enum.GetValues(typeof(T));
+ Items = Enum.GetValues();
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
index 1114c29afd..a65204e6b3 100644
--- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
@@ -277,7 +277,7 @@ namespace osu.Game.Graphics.UserInterface
{
var samples = sampleMap[feedbackSampleType];
- if (samples == null || samples.Length == 0)
+ if (samples.Length == 0)
return null;
return samples[RNG.Next(0, samples.Length)]?.GetChannel();
diff --git a/osu.Game/Graphics/UserInterface/RangeSlider.cs b/osu.Game/Graphics/UserInterface/RangeSlider.cs
index 483119cd58..4e23b06c2b 100644
--- a/osu.Game/Graphics/UserInterface/RangeSlider.cs
+++ b/osu.Game/Graphics/UserInterface/RangeSlider.cs
@@ -197,7 +197,7 @@ namespace osu.Game.Graphics.UserInterface
}, true);
}
- [BackgroundDependencyLoader]
+ [BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider)
{
if (colourProvider == null) return;
diff --git a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
index 8e6c3e5f3d..d47f936eb3 100644
--- a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
+++ b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
@@ -23,8 +23,7 @@ namespace osu.Game.IO.FileAbstraction
public void CloseStream(Stream stream)
{
- if (stream == null)
- throw new ArgumentNullException(nameof(stream));
+ ArgumentNullException.ThrowIfNull(stream);
stream.Close();
}
diff --git a/osu.Game/IO/HardLinkHelper.cs b/osu.Game/IO/HardLinkHelper.cs
new file mode 100644
index 0000000000..619bfdad6e
--- /dev/null
+++ b/osu.Game/IO/HardLinkHelper.cs
@@ -0,0 +1,190 @@
+// 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.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using Microsoft.Win32.SafeHandles;
+using osu.Framework;
+
+namespace osu.Game.IO
+{
+ internal static class HardLinkHelper
+ {
+ public static bool CheckAvailability(string testDestinationPath, string testSourcePath)
+ {
+ // For simplicity, only support desktop operating systems for now.
+ if (!RuntimeInfo.IsDesktop)
+ return false;
+
+ const string test_filename = "_hard_link_test";
+
+ testDestinationPath = Path.Combine(testDestinationPath, test_filename);
+ testSourcePath = Path.Combine(testSourcePath, test_filename);
+
+ cleanupFiles();
+
+ try
+ {
+ File.WriteAllText(testSourcePath, string.Empty);
+
+ // Test availability by creating an arbitrary hard link between the source and destination paths.
+ return TryCreateHardLink(testDestinationPath, testSourcePath);
+ }
+ catch
+ {
+ return false;
+ }
+ finally
+ {
+ cleanupFiles();
+ }
+
+ void cleanupFiles()
+ {
+ try
+ {
+ File.Delete(testDestinationPath);
+ File.Delete(testSourcePath);
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ ///
+ /// Attempts to create a hard link from to ,
+ /// using platform-specific native methods.
+ ///
+ ///
+ /// Hard links are only available on desktop platforms.
+ ///
+ /// Whether the hard link was successfully created.
+ public static bool TryCreateHardLink(string destinationPath, string sourcePath)
+ {
+ switch (RuntimeInfo.OS)
+ {
+ case RuntimeInfo.Platform.Windows:
+ return CreateHardLink(destinationPath, sourcePath, IntPtr.Zero);
+
+ case RuntimeInfo.Platform.Linux:
+ case RuntimeInfo.Platform.macOS:
+ return link(sourcePath, destinationPath) == 0;
+
+ default:
+ return false;
+ }
+ }
+
+ // For future use (to detect if a file is a hard link with other references existing on disk).
+ public static int GetFileLinkCount(string filePath)
+ {
+ int result = 0;
+
+ switch (RuntimeInfo.OS)
+ {
+ case RuntimeInfo.Platform.Windows:
+ SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
+
+ ByHandleFileInformation fileInfo;
+
+ if (GetFileInformationByHandle(handle, out fileInfo))
+ result = (int)fileInfo.NumberOfLinks;
+ CloseHandle(handle);
+ break;
+
+ case RuntimeInfo.Platform.Linux:
+ case RuntimeInfo.Platform.macOS:
+ if (stat(filePath, out var statbuf) == 0)
+ result = (int)statbuf.st_nlink;
+
+ break;
+ }
+
+ return result;
+ }
+
+ #region Windows native methods
+
+ [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern SafeFileHandle CreateFile(
+ string lpFileName,
+ [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
+ [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
+ IntPtr lpSecurityAttributes,
+ [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
+ [MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
+ IntPtr hTemplateFile);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool GetFileInformationByHandle(SafeFileHandle handle, out ByHandleFileInformation lpFileInformation);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool CloseHandle(SafeHandle hObject);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct ByHandleFileInformation
+ {
+ public readonly uint FileAttributes;
+ public readonly FILETIME CreationTime;
+ public readonly FILETIME LastAccessTime;
+ public readonly FILETIME LastWriteTime;
+ public readonly uint VolumeSerialNumber;
+ public readonly uint FileSizeHigh;
+ public readonly uint FileSizeLow;
+ public readonly uint NumberOfLinks;
+ public readonly uint FileIndexHigh;
+ public readonly uint FileIndexLow;
+ }
+
+ #endregion
+
+ #region Linux native methods
+
+#pragma warning disable IDE1006 // Naming rule violation
+
+ [DllImport("libc", SetLastError = true)]
+ public static extern int link(string oldpath, string newpath);
+
+ [DllImport("libc", SetLastError = true)]
+ private static extern int stat(string pathname, out struct_stat statbuf);
+
+ // ReSharper disable once InconsistentNaming
+ // Struct layout is likely non-portable across unices. Tread with caution.
+ [StructLayout(LayoutKind.Sequential)]
+ private struct struct_stat
+ {
+ public readonly long st_dev;
+ public readonly long st_ino;
+ public readonly long st_nlink;
+ public readonly int st_mode;
+ public readonly int st_uid;
+ public readonly int st_gid;
+ public readonly long st_rdev;
+ public readonly long st_size;
+ public readonly long st_blksize;
+ public readonly long st_blocks;
+ public readonly timespec st_atim;
+ public readonly timespec st_mtim;
+ public readonly timespec st_ctim;
+ }
+
+ // ReSharper disable once InconsistentNaming
+ [StructLayout(LayoutKind.Sequential)]
+ private struct timespec
+ {
+ public readonly long tv_sec;
+ public readonly long tv_nsec;
+ }
+
+#pragma warning restore IDE1006
+
+ #endregion
+ }
+}
diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
index 176e4e240d..de25d3e30e 100644
--- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
+++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
@@ -63,7 +63,7 @@ namespace osu.Game.IO.Serialization.Converters
throw new JsonException("Expected $type token.");
string typeName = lookupTable[(int)tok["$type"]];
- var instance = (T)Activator.CreateInstance(Type.GetType(typeName).AsNonNull());
+ var instance = (T)Activator.CreateInstance(Type.GetType(typeName).AsNonNull())!;
serializer.Populate(itemReader, instance);
list.Add(instance);
diff --git a/osu.Game/Localisation/BeatmapOverlayStrings.cs b/osu.Game/Localisation/BeatmapOverlayStrings.cs
new file mode 100644
index 0000000000..fc818f7596
--- /dev/null
+++ b/osu.Game/Localisation/BeatmapOverlayStrings.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class BeatmapOverlayStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOverlayStrings";
+
+ ///
+ /// "User content disclaimer"
+ ///
+ public static LocalisableString UserContentDisclaimerHeader => new TranslatableString(getKey(@"user_content_disclaimer"), @"User content disclaimer");
+
+ ///
+ /// "By turning off the "Featured Artist" filter, all user-uploaded content will be displayed.
+ ///
+ /// This includes content that may not be correctly licensed for osu! usage. Browse at your own risk."
+ ///
+ public static LocalisableString UserContentDisclaimerDescription => new TranslatableString(getKey(@"by_turning_off_the_featured"), @"By turning off the ""Featured Artist"" filter, all user-uploaded content will be displayed.
+
+This includes content that may not be correctly licensed for osu! usage. Browse at your own risk.");
+
+ ///
+ /// "I understand"
+ ///
+ public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Localisation/ContextMenuStrings.cs b/osu.Game/Localisation/ContextMenuStrings.cs
new file mode 100644
index 0000000000..8bc213016b
--- /dev/null
+++ b/osu.Game/Localisation/ContextMenuStrings.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class ContextMenuStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.ContextMenu";
+
+ ///
+ /// "View profile"
+ ///
+ public static LocalisableString ViewProfile => new TranslatableString(getKey(@"view_profile"), @"View profile");
+
+ ///
+ /// "View beatmap"
+ ///
+ public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
index deac7d8628..f0620245c3 100644
--- a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
+++ b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
@@ -15,10 +15,10 @@ namespace osu.Game.Localisation
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import");
///
- /// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation."
+ /// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way."
///
public static LocalisableString Description => new TranslatableString(getKey(@"description"),
- @"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation.");
+ @"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way.");
///
/// "previous osu! install"
diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs
index 3278b20983..a525af508b 100644
--- a/osu.Game/Localisation/GeneralSettingsStrings.cs
+++ b/osu.Game/Localisation/GeneralSettingsStrings.cs
@@ -64,6 +64,16 @@ namespace osu.Game.Localisation
///
public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard");
+ ///
+ /// "Learn more about lazer"
+ ///
+ public static LocalisableString LearnMoreAboutLazer => new TranslatableString(getKey(@"learn_more_about_lazer"), @"Learn more about lazer");
+
+ ///
+ /// "Check out the feature comparison and FAQ"
+ ///
+ public static LocalisableString LearnMoreAboutLazerTooltip => new TranslatableString(getKey(@"check_out_the_feature_comparison"), @"Check out the feature comparison and FAQ");
+
///
/// "You are running the latest release ({0})"
///
diff --git a/osu.Game/Models/RealmUser.cs b/osu.Game/Models/RealmUser.cs
index e20ffc0808..6997f04f44 100644
--- a/osu.Game/Models/RealmUser.cs
+++ b/osu.Game/Models/RealmUser.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Models
public bool IsBot => false;
- public bool Equals(RealmUser other)
+ public bool Equals(RealmUser? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index f2b9b6e968..757f6598e7 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -259,7 +259,11 @@ namespace osu.Game.Online.API
var friendsReq = new GetFriendsRequest();
friendsReq.Failure += _ => state.Value = APIState.Failing;
- friendsReq.Success += res => friends.AddRange(res);
+ friendsReq.Success += res =>
+ {
+ friends.Clear();
+ friends.AddRange(res);
+ };
if (!handleRequest(friendsReq))
{
diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs
index 176f10975d..45128375ab 100644
--- a/osu.Game/Online/API/APIMod.cs
+++ b/osu.Game/Online/API/APIMod.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Online.API
foreach (var (_, property) in mod.GetSettingsSourceProperties())
{
- var bindable = (IBindable)property.GetValue(mod);
+ var bindable = (IBindable)property.GetValue(mod)!;
if (!bindable.IsDefault)
Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue());
@@ -60,16 +60,16 @@ namespace osu.Game.Online.API
{
foreach (var (_, property) in resultMod.GetSettingsSourceProperties())
{
- if (!Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
+ if (!Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
continue;
try
{
- resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue);
+ resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod)!, settingValue);
}
catch (Exception ex)
{
- Logger.Log($"Failed to copy mod setting value '{settingValue ?? "null"}' to \"{property.Name}\": {ex.Message}");
+ Logger.Log($"Failed to copy mod setting value '{settingValue}' to \"{property.Name}\": {ex.Message}");
}
}
}
@@ -79,7 +79,7 @@ namespace osu.Game.Online.API
public bool ShouldSerializeSettings() => Settings.Count > 0;
- public bool Equals(APIMod other)
+ public bool Equals(APIMod? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs
index 7d1d26b75d..f2a2daccb5 100644
--- a/osu.Game/Online/API/Requests/GetScoresRequest.cs
+++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
@@ -11,10 +9,11 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using System.Text;
using System.Collections.Generic;
+using System.Linq;
namespace osu.Game.Online.API.Requests
{
- public class GetScoresRequest : APIRequest
+ public class GetScoresRequest : APIRequest, IEquatable
{
public const int MAX_SCORES_PER_REQUEST = 50;
@@ -23,7 +22,7 @@ namespace osu.Game.Online.API.Requests
private readonly IRulesetInfo ruleset;
private readonly IEnumerable mods;
- public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null)
+ public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable? mods = null)
{
if (beatmapInfo.OnlineID <= 0)
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(IBeatmapInfo.OnlineID)}.");
@@ -51,5 +50,16 @@ namespace osu.Game.Online.API.Requests
return query.ToString();
}
+
+ public bool Equals(GetScoresRequest? other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ return beatmapInfo.Equals(other.beatmapInfo)
+ && scope == other.scope
+ && ruleset.Equals(other.ruleset)
+ && mods.SequenceEqual(other.mods);
+ }
}
}
diff --git a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs
index d723786f23..e4134980b1 100644
--- a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs
@@ -32,6 +32,7 @@ namespace osu.Game.Online.API.Requests
Loved,
Pending,
Guest,
- Graveyard
+ Graveyard,
+ Nominated,
}
}
diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs
index bbaf241384..b57bb215aa 100644
--- a/osu.Game/Online/API/Requests/GetUsersRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests
{
public class GetUsersRequest : APIRequest
{
- private readonly int[] userIds;
+ public readonly int[] UserIds;
private const int max_ids_per_request = 50;
@@ -18,9 +18,9 @@ namespace osu.Game.Online.API.Requests
if (userIds.Length > max_ids_per_request)
throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {max_ids_per_request} IDs at once");
- this.userIds = userIds;
+ UserIds = userIds;
}
- protected override string Target => "users/?ids[]=" + string.Join("&ids[]=", userIds);
+ protected override string Target => "users/?ids[]=" + string.Join("&ids[]=", UserIds);
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
index 8a77801c3a..27ad2a746c 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
@@ -143,7 +143,7 @@ namespace osu.Game.Online.API.Requests.Responses
public bool Equals(IRulesetInfo? other) => other is APIRuleset r && this.MatchesOnlineID(r);
- public int CompareTo(IRulesetInfo other)
+ public int CompareTo(IRulesetInfo? other)
{
if (!(other is APIRuleset ruleset))
throw new ArgumentException($@"Object is not of type {nameof(APIRuleset)}.", nameof(other));
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
index 717a1de6b5..aeae3edde2 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
@@ -111,6 +111,12 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"language")]
public BeatmapSetOnlineLanguage Language { get; set; }
+ [JsonProperty(@"current_nominations")]
+ public BeatmapSetOnlineNomination[]? CurrentNominations { get; set; }
+
+ [JsonProperty(@"related_users")]
+ public APIUser[]? RelatedUsers { get; set; }
+
public string Source { get; set; } = string.Empty;
[JsonProperty(@"tags")]
diff --git a/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
index 2def18926f..c6a8a85407 100644
--- a/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string type
{
- set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.ToPascalCase());
+ set => Type = Enum.Parse(value.ToPascalCase());
}
public RecentActivityType Type;
@@ -29,7 +29,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string scoreRank
{
- set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value);
+ set => ScoreRank = Enum.Parse(value);
}
public ScoreRank ScoreRank;
diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs
index d3ddcffaf5..9cb0c0704d 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUser.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs
@@ -164,6 +164,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"guest_beatmapset_count")]
public int GuestBeatmapsetCount;
+ [JsonProperty(@"nominated_beatmapset_count")]
+ public int NominatedBeatmapsetCount;
+
[JsonProperty(@"scores_best_count")]
public int ScoresBestCount;
@@ -182,7 +185,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"playstyle")]
private string[] playStyle
{
- set => PlayStyles = value?.Select(str => Enum.Parse(typeof(APIPlayStyle), str, true)).Cast().ToArray();
+ set => PlayStyles = value?.Select(str => Enum.Parse(str, true)).ToArray();
}
public APIPlayStyle[] PlayStyles;
diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs
index 24b384b1d4..761e8aba8d 100644
--- a/osu.Game/Online/Chat/Channel.cs
+++ b/osu.Game/Online/Chat/Channel.cs
@@ -98,6 +98,11 @@ namespace osu.Game.Online.Chat
///
public Bindable HighlightedMessage = new Bindable();
+ ///
+ /// The current text box message while in this .
+ ///
+ public Bindable TextBoxMessage = new Bindable(string.Empty);
+
[JsonConstructor]
public Channel()
{
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index eaef940d5f..5d55374373 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -118,8 +118,7 @@ namespace osu.Game.Online.Chat
///
public void OpenChannel(string name)
{
- if (name == null)
- throw new ArgumentNullException(nameof(name));
+ ArgumentNullException.ThrowIfNull(name);
CurrentChannel.Value = AvailableChannels.FirstOrDefault(c => c.Name == name) ?? throw new ChannelNotFoundException(name);
}
@@ -130,8 +129,7 @@ namespace osu.Game.Online.Chat
/// The user the private channel is opened with.
public void OpenPrivateChannel(APIUser user)
{
- if (user == null)
- throw new ArgumentNullException(nameof(user));
+ ArgumentNullException.ThrowIfNull(user);
if (user.Id == api.LocalUser.Value.Id)
return;
@@ -529,6 +527,10 @@ namespace osu.Game.Online.Chat
{
Logger.Log($"Joined public channel {channel}");
joinChannel(channel, fetchInitialMessages);
+
+ // Required after joining public channels to mark the user as online in them.
+ // Todo: Temporary workaround for https://github.com/ppy/osu-web/issues/9602
+ SendAck();
};
req.Failure += e =>
{
diff --git a/osu.Game/Online/Chat/InfoMessage.cs b/osu.Game/Online/Chat/InfoMessage.cs
index d98c67de34..2ade99dcb2 100644
--- a/osu.Game/Online/Chat/InfoMessage.cs
+++ b/osu.Game/Online/Chat/InfoMessage.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using System;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.Chat
@@ -13,7 +10,6 @@ namespace osu.Game.Online.Chat
public InfoMessage(string message)
: base(null)
{
- Timestamp = DateTimeOffset.Now;
Content = message;
Sender = APIUser.SYSTEM_USER;
diff --git a/osu.Game/Online/Chat/LocalEchoMessage.cs b/osu.Game/Online/Chat/LocalEchoMessage.cs
index b226fe6cad..8a39515575 100644
--- a/osu.Game/Online/Chat/LocalEchoMessage.cs
+++ b/osu.Game/Online/Chat/LocalEchoMessage.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Online.Chat
{
public class LocalEchoMessage : LocalMessage
diff --git a/osu.Game/Online/Chat/LocalMessage.cs b/osu.Game/Online/Chat/LocalMessage.cs
index 5736f5cabf..57caca2287 100644
--- a/osu.Game/Online/Chat/LocalMessage.cs
+++ b/osu.Game/Online/Chat/LocalMessage.cs
@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
+using System;
namespace osu.Game.Online.Chat
{
@@ -13,6 +13,7 @@ namespace osu.Game.Online.Chat
protected LocalMessage(long? id)
: base(id)
{
+ Timestamp = DateTimeOffset.Now;
}
}
}
diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs
index 9f6f9c8d6b..8ea3ca0fc7 100644
--- a/osu.Game/Online/Chat/Message.cs
+++ b/osu.Game/Online/Chat/Message.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses;
@@ -59,19 +60,28 @@ namespace osu.Game.Online.Chat
/// The s' and s are according to
public List Links;
+ private static long constructionOrderStatic;
+ private readonly long constructionOrder;
+
public Message(long? id)
{
Id = id;
+
+ constructionOrder = Interlocked.Increment(ref constructionOrderStatic);
}
public int CompareTo(Message other)
{
- if (!Id.HasValue)
- return other.Id.HasValue ? 1 : Timestamp.CompareTo(other.Timestamp);
- if (!other.Id.HasValue)
- return -1;
+ if (Id.HasValue && other.Id.HasValue)
+ return Id.Value.CompareTo(other.Id.Value);
- return Id.Value.CompareTo(other.Id.Value);
+ int timestampComparison = Timestamp.CompareTo(other.Timestamp);
+
+ if (timestampComparison != 0)
+ return timestampComparison;
+
+ // Timestamp might not be accurate enough to make a stable sorting decision.
+ return constructionOrder.CompareTo(other.constructionOrder);
}
public virtual bool Equals(Message other)
@@ -85,6 +95,6 @@ namespace osu.Game.Online.Chat
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode();
- public override string ToString() => $"[{ChannelId}] ({Id}) {Sender}: {Content}";
+ public override string ToString() => $"({(Id?.ToString() ?? "null")}) {Timestamp} {Sender}: {Content}";
}
}
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 7a040d9446..523185a7cb 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Online.Chat
{
int index = m.Index - captureOffset;
- string? displayText = string.Format(display,
+ string displayText = string.Format(display,
m.Groups[0],
m.Groups["text"].Value,
m.Groups["url"].Value).Trim();
@@ -109,7 +109,7 @@ namespace osu.Game.Online.Chat
foreach (Match m in regex.Matches(result.Text, startIndex))
{
int index = m.Index;
- string? linkText = m.Groups["link"].Value;
+ string linkText = m.Groups["link"].Value;
int indexLength = linkText.Length;
var details = GetLinkDetails(linkText);
@@ -125,7 +125,7 @@ namespace osu.Game.Online.Chat
public static LinkDetails GetLinkDetails(string url)
{
- string[]? args = url.Split('/', StringSplitOptions.RemoveEmptyEntries);
+ string[] args = url.Split('/', StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
switch (args[0])
@@ -341,6 +341,8 @@ namespace osu.Game.Online.Chat
OpenWiki,
Custom,
OpenChangelog,
+ FilterBeatmapSetGenre,
+ FilterBeatmapSetLanguage,
}
public class Link : IComparable
@@ -362,6 +364,6 @@ namespace osu.Game.Online.Chat
public bool Overlaps(Link otherLink) => Index < otherLink.Index + otherLink.Length && otherLink.Index < Index + Length;
- public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
+ public int CompareTo(Link? otherLink) => Index > otherLink?.Index ? 1 : -1;
}
}
diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs
index 4872d93467..9b2ad666b2 100644
--- a/osu.Game/Online/Chat/MessageNotifier.cs
+++ b/osu.Game/Online/Chat/MessageNotifier.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
@@ -61,12 +62,16 @@ namespace osu.Game.Online.Chat
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
+ Debug.Assert(e.NewItems != null);
+
foreach (var channel in e.NewItems.Cast())
channel.NewMessagesArrived += checkNewMessages;
break;
case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(e.OldItems != null);
+
foreach (var channel in e.OldItems.Cast())
channel.NewMessagesArrived -= checkNewMessages;
diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
index 7fd6f99102..0a5434822b 100644
--- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs
+++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
@@ -111,8 +111,13 @@ namespace osu.Game.Online.Chat
{
drawableChannel?.Expire();
+ if (e.OldValue != null)
+ TextBox?.Current.UnbindFrom(e.OldValue.TextBoxMessage);
+
if (e.NewValue == null) return;
+ TextBox?.Current.BindTo(e.NewValue.TextBoxMessage);
+
drawableChannel = CreateDrawableChannel(e.NewValue);
drawableChannel.CreateChatLineAction = CreateMessage;
drawableChannel.Padding = new MarginPadding { Bottom = postingTextBox ? text_box_height : 0 };
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index ca6d2932f7..8fd79bd703 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Net;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
@@ -59,17 +60,21 @@ namespace osu.Game.Online
var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options =>
{
- // Use HttpClient.DefaultProxy once on net6 everywhere.
- // The credential setter can also be removed at this point.
- options.Proxy = WebRequest.DefaultWebProxy;
- if (options.Proxy != null)
- options.Proxy.Credentials = CredentialCache.DefaultCredentials;
+ // Configuring proxies is not supported on iOS, see https://github.com/xamarin/xamarin-macios/issues/14632.
+ if (RuntimeInfo.OS != RuntimeInfo.Platform.iOS)
+ {
+ // Use HttpClient.DefaultProxy once on net6 everywhere.
+ // The credential setter can also be removed at this point.
+ options.Proxy = WebRequest.DefaultWebProxy;
+ if (options.Proxy != null)
+ options.Proxy.Credentials = CredentialCache.DefaultCredentials;
+ }
options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
options.Headers.Add("OsuVersionHash", versionHash);
});
- if (RuntimeInfo.SupportsJIT && preferMessagePack)
+ if (RuntimeFeature.IsDynamicCodeCompiled && preferMessagePack)
{
builder.AddMessagePackProtocol(options =>
{
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
index 170f266307..0b2e401f57 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
@@ -136,9 +136,8 @@ namespace osu.Game.Online.Leaderboards
{
if (displayedScore != null)
{
- timestampLabel.Text = prefer24HourTime.Value
- ? $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy HH:mm}"
- : $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy h:mm tt}";
+ timestampLabel.Text = LocalisableString.Format("Played on {0}",
+ displayedScore.Date.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt"));
}
}
diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
index ba7ccb24f7..57311419f7 100644
--- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs
+++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
@@ -68,7 +68,7 @@ namespace osu.Game.Online.Metadata
while (true)
{
Logger.Log($"Requesting catch-up from {lastQueueId.Value}");
- var catchUpChanges = await GetChangesSince(lastQueueId.Value);
+ var catchUpChanges = await GetChangesSince(lastQueueId.Value).ConfigureAwait(true);
lastQueueId.Value = catchUpChanges.LastProcessedQueueID;
@@ -78,7 +78,7 @@ namespace osu.Game.Online.Metadata
break;
}
- await ProcessChanges(catchUpChanges.BeatmapSetIDs);
+ await ProcessChanges(catchUpChanges.BeatmapSetIDs).ConfigureAwait(true);
}
}
catch (Exception e)
@@ -101,7 +101,7 @@ namespace osu.Game.Online.Metadata
if (!catchingUp)
lastQueueId.Value = updates.LastProcessedQueueID;
- await ProcessChanges(updates.BeatmapSetIDs);
+ await ProcessChanges(updates.BeatmapSetIDs).ConfigureAwait(false);
}
public override Task GetChangesSince(int queueId)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index bd87f2d43e..2be7327234 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -822,7 +822,7 @@ namespace osu.Game.Online.Multiplayer
{
if (cancellationToken.IsCancellationRequested)
{
- tcs.SetCanceled();
+ tcs.SetCanceled(cancellationToken);
return;
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
index 681a839b89..d70a2797c4 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
@@ -54,10 +54,10 @@ namespace osu.Game.Online.Multiplayer
return UserID == other.UserID;
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != GetType()) return false;
+ if (obj?.GetType() != GetType()) return false;
return Equals((MultiplayerRoomUser)obj);
}
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 386a3d5262..8ff0ce4065 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -80,7 +80,7 @@ namespace osu.Game.Online.Multiplayer
try
{
- return await connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);
+ return await connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty).ConfigureAwait(false);
}
catch (HubException exception)
{
@@ -88,8 +88,8 @@ namespace osu.Game.Online.Multiplayer
{
Debug.Assert(connector != null);
- await connector.Reconnect();
- return await JoinRoom(roomId, password);
+ await connector.Reconnect().ConfigureAwait(false);
+ return await JoinRoom(roomId, password).ConfigureAwait(false);
}
throw;
diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs
index d2c2e6673c..34ce186cb8 100644
--- a/osu.Game/Online/Notifications/NotificationsClientConnector.cs
+++ b/osu.Game/Online/Notifications/NotificationsClientConnector.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Online.Notifications
protected sealed override async Task BuildConnectionAsync(CancellationToken cancellationToken)
{
- var client = await BuildNotificationClientAsync(cancellationToken);
+ var client = await BuildNotificationClientAsync(cancellationToken).ConfigureAwait(false);
client.ChannelJoined = c => ChannelJoined?.Invoke(c);
client.ChannelParted = c => ChannelParted?.Invoke(c);
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
index d8d78297e3..73e5dcec6f 100644
--- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
+using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
@@ -36,11 +37,11 @@ namespace osu.Game.Online.Notifications.WebSocket
public override async Task ConnectAsync(CancellationToken cancellationToken)
{
await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false);
- await sendMessage(new StartChatRequest(), CancellationToken.None);
+ await sendMessage(new StartChatRequest(), CancellationToken.None).ConfigureAwait(false);
runReadLoop(cancellationToken);
- await base.ConnectAsync(cancellationToken);
+ await base.ConnectAsync(cancellationToken).ConfigureAwait(false);
}
private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () =>
@@ -52,7 +53,7 @@ namespace osu.Game.Online.Notifications.WebSocket
{
try
{
- WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken);
+ WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
switch (result.MessageType)
{
@@ -72,7 +73,7 @@ namespace osu.Game.Online.Notifications.WebSocket
break;
}
- await onMessageReceivedAsync(message);
+ await onMessageReceivedAsync(message).ConfigureAwait(false);
}
break;
@@ -81,12 +82,12 @@ namespace osu.Game.Online.Notifications.WebSocket
throw new NotImplementedException("Binary message type not supported.");
case WebSocketMessageType.Close:
- throw new Exception("Connection closed by remote host.");
+ throw new WebException("Connection closed by remote host.");
}
}
catch (Exception ex)
{
- await InvokeClosed(ex);
+ await InvokeClosed(ex).ConfigureAwait(false);
return;
}
}
@@ -109,7 +110,7 @@ namespace osu.Game.Online.Notifications.WebSocket
if (socket.State != WebSocketState.Open)
return;
- await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken);
+ await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
private async Task onMessageReceivedAsync(SocketMessage message)
@@ -141,7 +142,7 @@ namespace osu.Game.Online.Notifications.WebSocket
Debug.Assert(messageData != null);
foreach (var msg in messageData.Messages)
- HandleChannelJoined(await getChannel(msg.ChannelId));
+ HandleChannelJoined(await getChannel(msg.ChannelId).ConfigureAwait(false));
HandleMessages(messageData.Messages);
break;
@@ -150,7 +151,7 @@ namespace osu.Game.Online.Notifications.WebSocket
private async Task getChannel(long channelId)
{
- if (channelsMap.TryGetValue(channelId, out Channel channel))
+ if (channelsMap.TryGetValue(channelId, out Channel? channel))
return channel;
var tsc = new TaskCompletionSource();
@@ -166,13 +167,13 @@ namespace osu.Game.Online.Notifications.WebSocket
API.Queue(req);
- return await tsc.Task;
+ return await tsc.Task.ConfigureAwait(false);
}
public override async ValueTask DisposeAsync()
{
- await base.DisposeAsync();
- await closeAsync();
+ await base.DisposeAsync().ConfigureAwait(false);
+ await closeAsync().ConfigureAwait(false);
socket.Dispose();
}
}
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
index 21335a3b59..f50369a06c 100644
--- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Online.Notifications.WebSocket
req.Failure += ex => tcs.SetException(ex);
api.Queue(req);
- string endpoint = await tcs.Task;
+ string endpoint = await tcs.Task.ConfigureAwait(false);
ClientWebSocket socket = new ClientWebSocket();
socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}");
diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs
index be76644745..e33924047d 100644
--- a/osu.Game/Online/PersistentEndpointClientConnector.cs
+++ b/osu.Game/Online/PersistentEndpointClientConnector.cs
@@ -65,11 +65,11 @@ namespace osu.Game.Online
{
case APIState.Failing:
case APIState.Offline:
- await disconnect(true);
+ await disconnect(true).ConfigureAwait(true);
break;
case APIState.Online:
- await connect();
+ await connect().ConfigureAwait(true);
break;
}
}
@@ -147,10 +147,10 @@ namespace osu.Game.Online
{
bool hasBeenCancelled = cancellationToken.IsCancellationRequested;
- await disconnect(true);
+ await disconnect(true).ConfigureAwait(false);
if (ex != null)
- await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
+ await handleErrorAndDelay(ex, CancellationToken.None).ConfigureAwait(false);
else
Logger.Log($"{ClientName} disconnected", LoggingTarget.Network);
diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs
index 316452280d..003ec50afd 100644
--- a/osu.Game/Online/ProductionEndpointConfiguration.cs
+++ b/osu.Game/Online/ProductionEndpointConfiguration.cs
@@ -9,7 +9,8 @@ namespace osu.Game.Online
{
public ProductionEndpointConfiguration()
{
- WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh";
+ WebsiteRootUrl = @"https://osu.ppy.sh";
+ APIEndpointUrl = @"https://lazer.ppy.sh";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
APIClientID = "5";
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
diff --git a/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs b/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
index a999cb47f8..86708bee82 100644
--- a/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
+++ b/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
@@ -33,7 +33,8 @@ namespace osu.Game.Online
object? instance = Activator.CreateInstance(resolvedType);
- jsonSerializer.Populate(obj["$value"]!.CreateReader(), instance);
+ if (instance != null)
+ jsonSerializer.Populate(obj["$value"]!.CreateReader(), instance);
return instance;
}
diff --git a/osu.Game/Online/Solo/SoloStatisticsUpdate.cs b/osu.Game/Online/Solo/SoloStatisticsUpdate.cs
new file mode 100644
index 0000000000..cb9dac97c7
--- /dev/null
+++ b/osu.Game/Online/Solo/SoloStatisticsUpdate.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Online.Solo
+{
+ ///
+ /// Contains data about the change in a user's profile statistics after completing a score.
+ ///
+ public class SoloStatisticsUpdate
+ {
+ ///
+ /// The score set by the user that triggered the update.
+ ///
+ public ScoreInfo Score { get; }
+
+ ///
+ /// The user's profile statistics prior to the score being set.
+ ///
+ public UserStatistics Before { get; }
+
+ ///
+ /// The user's profile statistics after the score was set.
+ ///
+ public UserStatistics After { get; }
+
+ ///
+ /// Creates a new .
+ ///
+ /// The score set by the user that triggered the update.
+ /// The user's profile statistics prior to the score being set.
+ /// The user's profile statistics after the score was set.
+ public SoloStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after)
+ {
+ Score = score;
+ Before = before;
+ After = after;
+ }
+ }
+}
diff --git a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs
new file mode 100644
index 0000000000..46449fea73
--- /dev/null
+++ b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs
@@ -0,0 +1,162 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Game.Extensions;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Spectator;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Online.Solo
+{
+ ///
+ /// A persistent component that binds to the spectator server and API in order to deliver updates about the logged in user's gameplay statistics.
+ ///
+ public partial class SoloStatisticsWatcher : Component
+ {
+ [Resolved]
+ private SpectatorClient spectatorClient { get; set; } = null!;
+
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
+
+ private readonly Dictionary callbacks = new Dictionary();
+ private long? lastProcessedScoreId;
+
+ private Dictionary? latestStatistics;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true);
+ spectatorClient.OnUserScoreProcessed += userScoreProcessed;
+ }
+
+ ///
+ /// Registers for a user statistics update after the given has been processed server-side.
+ ///
+ /// The score to listen for the statistics update for.
+ /// The callback to be invoked once the statistics update has been prepared.
+ /// An representing the subscription. Disposing it is equivalent to unsubscribing from future notifications.
+ public IDisposable RegisterForStatisticsUpdateAfter(ScoreInfo score, Action onUpdateReady)
+ {
+ Schedule(() =>
+ {
+ if (!api.IsLoggedIn)
+ return;
+
+ if (!score.Ruleset.IsLegacyRuleset() || score.OnlineID <= 0)
+ return;
+
+ var callback = new StatisticsUpdateCallback(score, onUpdateReady);
+
+ if (lastProcessedScoreId == score.OnlineID)
+ {
+ requestStatisticsUpdate(api.LocalUser.Value.Id, callback);
+ return;
+ }
+
+ callbacks.Add(score.OnlineID, callback);
+ });
+
+ return new InvokeOnDisposal(() => Schedule(() => callbacks.Remove(score.OnlineID)));
+ }
+
+ private void onUserChanged(APIUser? localUser) => Schedule(() =>
+ {
+ callbacks.Clear();
+ lastProcessedScoreId = null;
+ latestStatistics = null;
+
+ if (localUser == null || localUser.OnlineID <= 1)
+ return;
+
+ var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
+ userRequest.Success += initialiseUserStatistics;
+ api.Queue(userRequest);
+ });
+
+ private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() =>
+ {
+ var user = response.Users.SingleOrDefault();
+
+ // possible if the user is restricted or similar.
+ if (user == null)
+ return;
+
+ latestStatistics = new Dictionary();
+
+ if (user.RulesetsStatistics != null)
+ {
+ foreach (var rulesetStats in user.RulesetsStatistics)
+ latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
+ }
+ });
+
+ private void userScoreProcessed(int userId, long scoreId)
+ {
+ if (userId != api.LocalUser.Value?.OnlineID)
+ return;
+
+ lastProcessedScoreId = scoreId;
+
+ if (!callbacks.TryGetValue(scoreId, out var callback))
+ return;
+
+ requestStatisticsUpdate(userId, callback);
+ callbacks.Remove(scoreId);
+ }
+
+ private void requestStatisticsUpdate(int userId, StatisticsUpdateCallback callback)
+ {
+ var request = new GetUserRequest(userId, callback.Score.Ruleset);
+ request.Success += user => Schedule(() => dispatchStatisticsUpdate(callback, user.Statistics));
+ api.Queue(request);
+ }
+
+ private void dispatchStatisticsUpdate(StatisticsUpdateCallback callback, UserStatistics updatedStatistics)
+ {
+ string rulesetName = callback.Score.Ruleset.ShortName;
+
+ if (latestStatistics == null)
+ return;
+
+ latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
+ latestRulesetStatistics ??= new UserStatistics();
+
+ var update = new SoloStatisticsUpdate(callback.Score, latestRulesetStatistics, updatedStatistics);
+ callback.OnUpdateReady.Invoke(update);
+
+ latestStatistics[rulesetName] = updatedStatistics;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ if (spectatorClient.IsNotNull())
+ spectatorClient.OnUserScoreProcessed -= userScoreProcessed;
+
+ base.Dispose(isDisposing);
+ }
+
+ private class StatisticsUpdateCallback
+ {
+ public ScoreInfo Score { get; }
+ public Action OnUpdateReady { get; }
+
+ public StatisticsUpdateCallback(ScoreInfo score, Action onUpdateReady)
+ {
+ Score = score;
+ OnUpdateReady = onUpdateReady;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs
index ccba280001..605ebc4ef0 100644
--- a/osu.Game/Online/Spectator/ISpectatorClient.cs
+++ b/osu.Game/Online/Spectator/ISpectatorClient.cs
@@ -32,5 +32,12 @@ namespace osu.Game.Online.Spectator
/// The user.
/// The frame data.
Task UserSentFrames(int userId, FrameDataBundle data);
+
+ ///
+ /// Signals that a user's submitted score was fully processed.
+ ///
+ /// The ID of the user who achieved the score.
+ /// The ID of the score.
+ Task UserScoreProcessed(int userId, long scoreId);
}
}
diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs
index 25785f60a4..fa9d04792a 100644
--- a/osu.Game/Online/Spectator/ISpectatorServer.cs
+++ b/osu.Game/Online/Spectator/ISpectatorServer.cs
@@ -15,8 +15,9 @@ namespace osu.Game.Online.Spectator
///
/// Signal the start of a new play session.
///
+ /// The score submission token.
/// The state of gameplay.
- Task BeginPlaySession(SpectatorState state);
+ Task BeginPlaySession(long? scoreToken, SpectatorState state);
///
/// Send a bundle of frame data for the current play session.
diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
index d69bd81b57..3118e05053 100644
--- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
+++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
@@ -41,13 +41,14 @@ namespace osu.Game.Online.Spectator
connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
+ connection.On(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed);
};
IsConnected.BindTo(connector.IsConnected);
}
}
- protected override async Task BeginPlayingInternal(SpectatorState state)
+ protected override async Task BeginPlayingInternal(long? scoreToken, SpectatorState state)
{
if (!IsConnected.Value)
return;
@@ -56,7 +57,7 @@ namespace osu.Game.Online.Spectator
try
{
- await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
+ await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), scoreToken, state).ConfigureAwait(false);
}
catch (Exception exception)
{
@@ -64,8 +65,8 @@ namespace osu.Game.Online.Spectator
{
Debug.Assert(connector != null);
- await connector.Reconnect();
- await BeginPlayingInternal(state);
+ await connector.Reconnect().ConfigureAwait(false);
+ await BeginPlayingInternal(scoreToken, state).ConfigureAwait(false);
}
// Exceptions can occur if, for instance, the locally played beatmap doesn't have a server-side counterpart.
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index b0ee0bc37b..b60cef2835 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -64,6 +64,11 @@ namespace osu.Game.Online.Spectator
///
public virtual event Action? OnUserFinishedPlaying;
+ ///
+ /// Called whenever a user-submitted score has been fully processed.
+ ///
+ public virtual event Action? OnUserScoreProcessed;
+
///
/// A dictionary containing all users currently being watched, with the number of watching components for each user.
///
@@ -76,6 +81,7 @@ namespace osu.Game.Online.Spectator
private IBeatmap? currentBeatmap;
private Score? currentScore;
+ private long? currentScoreToken;
private readonly Queue pendingFrameBundles = new Queue();
@@ -108,7 +114,7 @@ namespace osu.Game.Online.Spectator
// re-send state in case it wasn't received
if (IsPlaying)
// TODO: this is likely sent out of order after a reconnect scenario. needs further consideration.
- BeginPlayingInternal(currentState);
+ BeginPlayingInternal(currentScoreToken, currentState);
}
else
{
@@ -159,7 +165,14 @@ namespace osu.Game.Online.Spectator
return Task.CompletedTask;
}
- public void BeginPlaying(GameplayState state, Score score)
+ Task ISpectatorClient.UserScoreProcessed(int userId, long scoreId)
+ {
+ Schedule(() => OnUserScoreProcessed?.Invoke(userId, scoreId));
+
+ return Task.CompletedTask;
+ }
+
+ public void BeginPlaying(long? scoreToken, GameplayState state, Score score)
{
// This schedule is only here to match the one below in `EndPlaying`.
Schedule(() =>
@@ -174,12 +187,13 @@ namespace osu.Game.Online.Spectator
currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentState.State = SpectatedUserState.Playing;
- currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues;
+ currentState.MaximumStatistics = state.ScoreProcessor.MaximumStatistics;
currentBeatmap = state.Beatmap;
currentScore = score;
+ currentScoreToken = scoreToken;
- BeginPlayingInternal(currentState);
+ BeginPlayingInternal(currentScoreToken, currentState);
});
}
@@ -264,7 +278,7 @@ namespace osu.Game.Online.Spectator
});
}
- protected abstract Task BeginPlayingInternal(SpectatorState state);
+ protected abstract Task BeginPlayingInternal(long? scoreToken, SpectatorState state);
protected abstract Task SendFramesInternal(FrameDataBundle bundle);
diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
index c97871c3aa..1c505ea107 100644
--- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
+++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
@@ -152,12 +152,12 @@ namespace osu.Game.Online.Spectator
scoreInfo.MaxCombo = frame.Header.MaxCombo;
scoreInfo.Statistics = frame.Header.Statistics;
+ scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics;
Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo;
- scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _);
- TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues);
+ TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
}
protected override void Dispose(bool isDisposing)
@@ -184,7 +184,7 @@ namespace osu.Game.Online.Spectator
Header = header;
}
- public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
+ public int CompareTo(TimedFrame? other) => Time.CompareTo(other?.Time);
}
}
}
diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs
index 766b274e63..91df05bf96 100644
--- a/osu.Game/Online/Spectator/SpectatorState.cs
+++ b/osu.Game/Online/Spectator/SpectatorState.cs
@@ -9,7 +9,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MessagePack;
using osu.Game.Online.API;
-using osu.Game.Scoring;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Online.Spectator
{
@@ -31,7 +31,7 @@ namespace osu.Game.Online.Spectator
public SpectatedUserState State { get; set; }
[Key(4)]
- public ScoringValues MaximumScoringValues { get; set; }
+ public Dictionary MaximumStatistics { get; set; } = new Dictionary();
public bool Equals(SpectatorState other)
{
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 5565fa7ef3..6efa3c4172 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -16,6 +16,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
+using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
@@ -45,6 +46,7 @@ using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
+using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Music;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Toolbar;
@@ -332,7 +334,7 @@ namespace osu.Game
/// The link to load.
public void HandleLink(LinkDetails link) => Schedule(() =>
{
- string argString = link.Argument.ToString();
+ string argString = link.Argument.ToString() ?? string.Empty;
switch (link.Action)
{
@@ -352,7 +354,18 @@ namespace osu.Game
break;
case LinkAction.SearchBeatmapSet:
- SearchBeatmapSet(argString);
+ if (link.Argument is RomanisableString romanisable)
+ SearchBeatmapSet(romanisable.GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript));
+ else
+ SearchBeatmapSet(argString);
+ break;
+
+ case LinkAction.FilterBeatmapSetGenre:
+ FilterBeatmapSetGenre((SearchGenre)link.Argument);
+ break;
+
+ case LinkAction.FilterBeatmapSetLanguage:
+ FilterBeatmapSetLanguage((SearchLanguage)link.Argument);
break;
case LinkAction.OpenEditorTimestamp:
@@ -406,6 +419,16 @@ namespace osu.Game
if (url.StartsWith('/'))
url = $"{API.APIEndpointUrl}{url}";
+ if (!url.CheckIsValidUrl())
+ {
+ Notifications.Post(new SimpleErrorNotification
+ {
+ Text = $"The URL {url} has an unsupported or dangerous protocol and will not be opened.",
+ });
+
+ return;
+ }
+
externalLinkOpener.OpenUrlExternally(url, bypassExternalUrlWarning);
});
@@ -449,6 +472,10 @@ namespace osu.Game
/// The query to search for.
public void SearchBeatmapSet(string query) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithSearch(query));
+ public void FilterBeatmapSetGenre(SearchGenre genre) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithGenreFilter(genre));
+
+ public void FilterBeatmapSetLanguage(SearchLanguage language) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithLanguageFilter(language));
+
///
/// Show a wiki's page as an overlay
///
@@ -616,14 +643,14 @@ namespace osu.Game
}, validScreens: validScreens);
}
- public override Task Import(params ImportTask[] imports)
+ public override Task Import(ImportTask[] imports, ImportParameters parameters = default)
{
// encapsulate task as we don't want to begin the import process until in a ready state.
// ReSharper disable once AsyncVoidLambda
// TODO: This is bad because `new Task` doesn't have a Func override.
// Only used for android imports and a bit of a mess. Probably needs rethinking overall.
- var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false));
+ var importTask = new Task(async () => await base.Import(imports, parameters).ConfigureAwait(false));
waitForReady(() => this, _ => importTask.Start());
@@ -712,7 +739,7 @@ namespace osu.Game
{
base.LoadComplete();
- var languages = Enum.GetValues(typeof(Language)).OfType();
+ var languages = Enum.GetValues();
var mappings = languages.Select(language =>
{
@@ -1029,7 +1056,7 @@ namespace osu.Game
Logger.NewEntry += entry =>
{
- if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database) return;
+ if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return;
const int short_term_display_limit = 3;
@@ -1043,7 +1070,7 @@ namespace osu.Game
}
else if (recentLogCount == short_term_display_limit)
{
- string logFile = $@"{entry.Target.ToString().ToLowerInvariant()}.log";
+ string logFile = $@"{entry.Target.Value.ToString().ToLowerInvariant()}.log";
Schedule(() => Notifications.Post(new SimpleNotification
{
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 1d5f5a75e5..36e248c1f2 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -46,6 +46,7 @@ using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Solo;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
@@ -83,6 +84,8 @@ namespace osu.Game
public const int SAMPLE_CONCURRENCY = 6;
+ public const double SFX_STEREO_STRENGTH = 0.75;
+
///
/// Length of debounce (in milliseconds) for commonly occuring sample playbacks that could stack.
///
@@ -191,6 +194,7 @@ namespace osu.Game
protected MultiplayerClient MultiplayerClient { get; private set; }
private MetadataClient metadataClient;
+ private SoloStatisticsWatcher soloStatisticsWatcher;
private RealmAccess realm;
@@ -299,6 +303,7 @@ namespace osu.Game
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
+ dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher());
AddInternal(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
@@ -344,6 +349,7 @@ namespace osu.Game
AddInternal(spectatorClient);
AddInternal(MultiplayerClient);
AddInternal(metadataClient);
+ AddInternal(soloStatisticsWatcher);
AddInternal(rulesetConfigCache);
@@ -601,7 +607,7 @@ namespace osu.Game
try
{
- foreach (ModType type in Enum.GetValues(typeof(ModType)))
+ foreach (ModType type in Enum.GetValues())
{
dict[type] = instance.GetModsFor(type)
// Rulesets should never return null mods, but let's be defensive just in case.
diff --git a/osu.Game/OsuGameBase_Importing.cs b/osu.Game/OsuGameBase_Importing.cs
index e34e48f21d..cf65460bab 100644
--- a/osu.Game/OsuGameBase_Importing.cs
+++ b/osu.Game/OsuGameBase_Importing.cs
@@ -44,13 +44,13 @@ namespace osu.Game
}
}
- public virtual async Task Import(params ImportTask[] tasks)
+ public virtual async Task Import(ImportTask[] tasks, ImportParameters parameters = default)
{
var tasksPerExtension = tasks.GroupBy(t => Path.GetExtension(t.Path).ToLowerInvariant());
await Task.WhenAll(tasksPerExtension.Select(taskGroup =>
{
var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key));
- return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask;
+ return importer?.Import(taskGroup.ToArray(), parameters) ?? Task.CompletedTask;
})).ConfigureAwait(false);
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index c5c252fb5d..37a29b1c50 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -145,6 +145,12 @@ namespace osu.Game.Overlays.BeatmapListing
public void Search(string query)
=> Schedule(() => searchControl.Query.Value = query);
+ public void FilterGenre(SearchGenre genre)
+ => Schedule(() => searchControl.Genre.Value = genre);
+
+ public void FilterLanguage(SearchLanguage language)
+ => Schedule(() => searchControl.Language.Value = language);
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
index f28ec9c295..23de1cf76d 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
@@ -146,6 +146,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
});
+ generalFilter.Current.Add(SearchGeneral.FeaturedArtists);
categoryFilter.Current.Value = SearchCategory.Leaderboard;
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs
index 10ec66e396..a4a914db55 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs
@@ -3,10 +3,18 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Configuration;
using osu.Game.Graphics;
+using osu.Game.Localisation;
+using osu.Game.Overlays.Dialog;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
+using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Overlays.BeatmapListing
{
@@ -32,6 +40,8 @@ namespace osu.Game.Overlays.BeatmapListing
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
{
+ private Bindable disclaimerShown;
+
public FeaturedArtistsTabItem()
: base(SearchGeneral.FeaturedArtists)
{
@@ -40,7 +50,60 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved]
private OsuColour colours { get; set; }
+ [Resolved]
+ private SessionStatics sessionStatics { get; set; }
+
+ [Resolved(canBeNull: true)]
+ private IDialogOverlay dialogOverlay { get; set; }
+
protected override Color4 GetStateColour() => colours.Orange1;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ disclaimerShown = sessionStatics.GetBindable(Static.FeaturedArtistDisclaimerShownOnce);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ if (!disclaimerShown.Value && dialogOverlay != null)
+ {
+ dialogOverlay.Push(new FeaturedArtistConfirmDialog(() =>
+ {
+ disclaimerShown.Value = true;
+ base.OnClick(e);
+ }));
+
+ return true;
+ }
+
+ return base.OnClick(e);
+ }
+ }
+ }
+
+ internal partial class FeaturedArtistConfirmDialog : PopupDialog
+ {
+ public FeaturedArtistConfirmDialog(Action confirm)
+ {
+ HeaderText = BeatmapOverlayStrings.UserContentDisclaimerHeader;
+ BodyText = BeatmapOverlayStrings.UserContentDisclaimerDescription;
+
+ Icon = FontAwesome.Solid.ExclamationTriangle;
+
+ Buttons = new PopupDialogButton[]
+ {
+ new PopupDialogDangerousButton
+ {
+ Text = BeatmapOverlayStrings.UserContentConfirmButtonText,
+ Action = confirm
+ },
+ new PopupDialogCancelButton
+ {
+ Text = CommonStrings.ButtonsCancel,
+ },
+ };
}
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
index 79a794a9ad..abd2643a41 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
@@ -5,12 +5,14 @@
using System;
using System.Collections.Generic;
+using System.Collections.Specialized;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osuTK;
@@ -18,6 +20,7 @@ using osuTK;
namespace osu.Game.Overlays.BeatmapListing
{
public partial class BeatmapSearchMultipleSelectionFilterRow : BeatmapSearchFilterRow>
+ where T : Enum
{
public new readonly BindableList Current = new BindableList();
@@ -31,7 +34,7 @@ namespace osu.Game.Overlays.BeatmapListing
[BackgroundDependencyLoader]
private void load()
{
- Current.BindTo(filter.Current);
+ filter.Current.BindTo(Current);
}
protected sealed override Drawable CreateFilter() => filter = CreateMultipleSelectionFilter();
@@ -64,6 +67,14 @@ namespace osu.Game.Overlays.BeatmapListing
foreach (var item in Children)
item.Active.BindValueChanged(active => toggleItem(item.Value, active.NewValue));
+
+ Current.BindCollectionChanged(currentChanged, true);
+ }
+
+ private void currentChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ foreach (var c in Children)
+ c.Active.Value = Current.Contains(c.Value);
}
///
@@ -79,7 +90,10 @@ namespace osu.Game.Overlays.BeatmapListing
private void toggleItem(T value, bool active)
{
if (active)
- Current.Add(value);
+ {
+ if (!Current.Contains(value))
+ Current.Add(value);
+ }
else
Current.Remove(value);
}
@@ -87,9 +101,30 @@ namespace osu.Game.Overlays.BeatmapListing
protected partial class MultipleSelectionFilterTabItem : FilterTabItem
{
+ private readonly Box selectedUnderline;
+
+ protected override bool HighlightOnHoverWhenActive => true;
+
public MultipleSelectionFilterTabItem(T value)
: base(value)
{
+ // This doesn't match any actual design, but should make it easier for the user to understand
+ // that filters are applied until we settle on a final design.
+ AddInternal(selectedUnderline = new Box
+ {
+ Depth = float.MaxValue,
+ RelativeSizeAxes = Axes.X,
+ Height = 1.5f,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.CentreLeft,
+ });
+ }
+
+ protected override void UpdateState()
+ {
+ base.UpdateState();
+ selectedUnderline.FadeTo(Active.Value ? 1 : 0, 200, Easing.OutQuint);
+ selectedUnderline.FadeColour(IsHovered ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
}
protected override bool OnClick(ClickEvent e)
diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
index 7b95ae8ea8..c33d5056fa 100644
--- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
+++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Overlays.BeatmapListing
public partial class FilterTabItem : TabItem
{
[Resolved]
- private OverlayColourProvider colourProvider { get; set; }
+ protected OverlayColourProvider ColourProvider { get; private set; }
private OsuSpriteText text;
@@ -52,38 +52,42 @@ namespace osu.Game.Overlays.BeatmapListing
{
base.LoadComplete();
- updateState();
+ UpdateState();
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
- updateState();
+ UpdateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
- updateState();
+ UpdateState();
}
- protected override void OnActivated() => updateState();
+ protected override void OnActivated() => UpdateState();
- protected override void OnDeactivated() => updateState();
+ protected override void OnDeactivated() => UpdateState();
///
/// Returns the label text to be used for the supplied .
///
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
- private void updateState()
+ protected virtual bool HighlightOnHoverWhenActive => false;
+
+ protected virtual void UpdateState()
{
- text.FadeColour(IsHovered ? colourProvider.Light1 : GetStateColour(), 200, Easing.OutQuint);
- text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
+ bool highlightHover = IsHovered && (!Active.Value || HighlightOnHoverWhenActive);
+
+ text.FadeColour(highlightHover ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
+ text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular);
}
- protected virtual Color4 GetStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
+ protected virtual Color4 GetStateColour() => Active.Value ? ColourProvider.Content1 : ColourProvider.Light2;
}
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index d6d4f1a67b..73961487ed 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -110,6 +110,18 @@ namespace osu.Game.Overlays
ScrollFlow.ScrollToStart();
}
+ public void ShowWithGenreFilter(SearchGenre genre)
+ {
+ ShowWithSearch(string.Empty);
+ filterControl.FilterGenre(genre);
+ }
+
+ public void ShowWithLanguageFilter(SearchLanguage language)
+ {
+ ShowWithSearch(string.Empty);
+ filterControl.FilterLanguage(language);
+ }
+
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
index 0318dad0e3..26e6b1f158 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
@@ -3,26 +3,29 @@
#nullable disable
+using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
-using osu.Game.Graphics.Cursor;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
@@ -41,12 +44,10 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly UpdateableOnlineBeatmapSetCover cover;
private readonly Box coverGradient;
- private readonly OsuSpriteText title, artist;
+ private readonly LinkFlowContainer title, artist;
private readonly AuthorInfo author;
- private readonly ExplicitContentBeatmapBadge explicitContent;
- private readonly SpotlightBeatmapBadge spotlight;
- private readonly FeaturedArtistBeatmapBadge featuredArtist;
+ private ExternalLinkButton externalLink;
private readonly FillFlowContainer downloadButtonsContainer;
private readonly BeatmapAvailability beatmapAvailability;
@@ -65,8 +66,6 @@ namespace osu.Game.Overlays.BeatmapSet
public BeatmapSetHeaderContent()
{
- ExternalLinkButton externalLink;
-
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new Container
@@ -91,118 +90,74 @@ namespace osu.Game.Overlays.BeatmapSet
},
},
},
- new OsuContextMenuContainer
+ new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Child = new Container
+ Padding = new MarginPadding
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding
+ Vertical = BeatmapSetOverlay.Y_PADDING,
+ Left = BeatmapSetOverlay.X_PADDING,
+ Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
+ },
+ Children = new Drawable[]
+ {
+ fadeContent = new FillFlowContainer
{
- Vertical = BeatmapSetOverlay.Y_PADDING,
- Left = BeatmapSetOverlay.X_PADDING,
- Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
- },
- Children = new Drawable[]
- {
- fadeContent = new FillFlowContainer
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ new Container
{
- new Container
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = Picker = new BeatmapPicker(),
+ },
+ title = new MetadataFlowContainer(s =>
+ {
+ s.Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true);
+ })
+ {
+ Margin = new MarginPadding { Top = 15 },
+ },
+ artist = new MetadataFlowContainer(s =>
+ {
+ s.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true);
+ })
+ {
+ Margin = new MarginPadding { Bottom = 20 },
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = author = new AuthorInfo(),
+ },
+ beatmapAvailability = new BeatmapAvailability(),
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = buttons_height,
+ Margin = new MarginPadding { Top = 10 },
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Child = Picker = new BeatmapPicker(),
- },
- new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Top = 15 },
- Children = new Drawable[]
+ favouriteButton = new FavouriteButton
{
- title = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true)
- },
- externalLink = new ExternalLinkButton
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font
- },
- explicitContent = new ExplicitContentBeatmapBadge
- {
- Alpha = 0f,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Left = 10, Bottom = 4 },
- },
- spotlight = new SpotlightBeatmapBadge
- {
- Alpha = 0f,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Left = 10, Bottom = 4 },
- }
- }
- },
- new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Bottom = 20 },
- Children = new Drawable[]
- {
- artist = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true),
- },
- featuredArtist = new FeaturedArtistBeatmapBadge
- {
- Alpha = 0f,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Left = 10 }
- }
- }
- },
- new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Child = author = new AuthorInfo(),
- },
- beatmapAvailability = new BeatmapAvailability(),
- new Container
- {
- RelativeSizeAxes = Axes.X,
- Height = buttons_height,
- Margin = new MarginPadding { Top = 10 },
- Children = new Drawable[]
- {
- favouriteButton = new FavouriteButton
- {
- BeatmapSet = { BindTarget = BeatmapSet }
- },
- downloadButtonsContainer = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
- Spacing = new Vector2(buttons_spacing),
- },
+ BeatmapSet = { BindTarget = BeatmapSet }
},
- },
+ downloadButtonsContainer = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
+ Spacing = new Vector2(buttons_spacing),
+ },
+ }
},
},
- }
- },
+ },
+ }
},
loading = new LoadingSpinner
{
@@ -237,12 +192,17 @@ namespace osu.Game.Overlays.BeatmapSet
Picker.Beatmap.ValueChanged += b =>
{
Details.BeatmapInfo = b.NewValue;
- externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineID}";
+ updateExternalLink();
onlineStatusPill.Status = b.NewValue?.Status ?? BeatmapOnlineStatus.None;
};
}
+ private void updateExternalLink()
+ {
+ if (externalLink != null) externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{Picker.Beatmap.Value?.Ruleset.ShortName}/{Picker.Beatmap.Value?.OnlineID}";
+ }
+
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
@@ -275,12 +235,38 @@ namespace osu.Game.Overlays.BeatmapSet
loading.Hide();
- title.Text = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title);
- artist.Text = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist);
+ var titleText = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title);
+ var artistText = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist);
- explicitContent.Alpha = setInfo.NewValue.HasExplicitContent ? 1 : 0;
- spotlight.Alpha = setInfo.NewValue.FeaturedInSpotlight ? 1 : 0;
- featuredArtist.Alpha = setInfo.NewValue.TrackId != null ? 1 : 0;
+ title.Clear();
+ artist.Clear();
+
+ title.AddLink(titleText, LinkAction.SearchBeatmapSet, titleText);
+
+ title.AddArbitraryDrawable(Empty().With(d => d.Width = 5));
+ title.AddArbitraryDrawable(externalLink = new ExternalLinkButton());
+
+ if (setInfo.NewValue.HasExplicitContent)
+ {
+ title.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
+ title.AddArbitraryDrawable(new ExplicitContentBeatmapBadge());
+ }
+
+ if (setInfo.NewValue.FeaturedInSpotlight)
+ {
+ title.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
+ title.AddArbitraryDrawable(new SpotlightBeatmapBadge());
+ }
+
+ artist.AddLink(artistText, LinkAction.SearchBeatmapSet, artistText);
+
+ if (setInfo.NewValue.TrackId != null)
+ {
+ artist.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
+ artist.AddArbitraryDrawable(new FeaturedArtistBeatmapBadge());
+ }
+
+ updateExternalLink();
onlineStatusPill.FadeIn(500, Easing.OutQuint);
@@ -327,5 +313,32 @@ namespace osu.Game.Overlays.BeatmapSet
break;
}
}
+
+ public partial class MetadataFlowContainer : LinkFlowContainer
+ {
+ public MetadataFlowContainer(Action defaultCreationParameters = null)
+ : base(defaultCreationParameters)
+ {
+ TextAnchor = Anchor.CentreLeft;
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ }
+
+ protected override DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new MetadataLinkCompiler(textPart);
+
+ public partial class MetadataLinkCompiler : DrawableLinkCompiler
+ {
+ public MetadataLinkCompiler(ITextPart part)
+ : base(part)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ IdleColour = Color4.White;
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs
index 514a4ea8cd..58739eb471 100644
--- a/osu.Game/Overlays/BeatmapSet/Info.cs
+++ b/osu.Game/Overlays/BeatmapSet/Info.cs
@@ -3,14 +3,17 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.BeatmapListing;
namespace osu.Game.Overlays.BeatmapSet
{
@@ -34,7 +37,10 @@ namespace osu.Game.Overlays.BeatmapSet
public Info()
{
- MetadataSection source, tags, genre, language;
+ MetadataSectionNominators nominators;
+ MetadataSection source, tags;
+ MetadataSectionGenre genre;
+ MetadataSectionLanguage language;
OsuSpriteText notRankedPlaceholder;
RelativeSizeAxes = Axes.X;
@@ -59,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapSet
Child = new Container
{
RelativeSizeAxes = Axes.Both,
- Child = new MetadataSection(MetadataType.Description),
+ Child = new MetadataSectionDescription(),
},
},
new Container
@@ -76,12 +82,13 @@ namespace osu.Game.Overlays.BeatmapSet
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
- Children = new[]
+ Children = new Drawable[]
{
- source = new MetadataSection(MetadataType.Source),
- genre = new MetadataSection(MetadataType.Genre) { Width = 0.5f },
- language = new MetadataSection(MetadataType.Language) { Width = 0.5f },
- tags = new MetadataSection(MetadataType.Tags),
+ nominators = new MetadataSectionNominators(),
+ source = new MetadataSectionSource(),
+ genre = new MetadataSectionGenre { Width = 0.5f },
+ language = new MetadataSectionLanguage { Width = 0.5f },
+ tags = new MetadataSectionTags(),
},
},
},
@@ -118,10 +125,11 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet.ValueChanged += b =>
{
- source.Text = b.NewValue?.Source ?? string.Empty;
- tags.Text = b.NewValue?.Tags ?? string.Empty;
- genre.Text = b.NewValue?.Genre.Name ?? string.Empty;
- language.Text = b.NewValue?.Language.Name ?? string.Empty;
+ nominators.Metadata = (b.NewValue?.CurrentNominations ?? Array.Empty(), b.NewValue?.RelatedUsers ?? Array.Empty());
+ source.Metadata = b.NewValue?.Source ?? string.Empty;
+ tags.Metadata = b.NewValue?.Tags ?? string.Empty;
+ genre.Metadata = b.NewValue?.Genre ?? new BeatmapSetOnlineGenre { Id = (int)SearchGenre.Unspecified };
+ language.Metadata = b.NewValue?.Language ?? new BeatmapSetOnlineLanguage { Id = (int)SearchLanguage.Unspecified };
bool setHasLeaderboard = b.NewValue?.Status > 0;
successRate.Alpha = setHasLeaderboard ? 1 : 0;
notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1;
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSection.cs b/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
index 6390c52ff3..d32d8e83fb 100644
--- a/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
@@ -11,26 +9,45 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Online.Chat;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
- public partial class MetadataSection : Container
+ public abstract partial class MetadataSection : MetadataSection
+ {
+ public override string Metadata
+ {
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ this.FadeOut(TRANSITION_DURATION);
+ return;
+ }
+
+ base.Metadata = value;
+ }
+ }
+
+ protected MetadataSection(MetadataType type, Action? searchAction = null)
+ : base(type, searchAction)
+ {
+ }
+ }
+
+ public abstract partial class MetadataSection : Container
{
private readonly FillFlowContainer textContainer;
- private readonly MetadataType type;
- private TextFlowContainer textFlow;
+ private TextFlowContainer? textFlow;
- private readonly Action searchAction;
+ protected readonly Action? SearchAction;
- private const float transition_duration = 250;
+ protected const float TRANSITION_DURATION = 250;
- public MetadataSection(MetadataType type, Action searchAction = null)
+ protected MetadataSection(MetadataType type, Action? searchAction = null)
{
- this.type = type;
- this.searchAction = searchAction;
+ SearchAction = searchAction;
Alpha = 0;
@@ -53,7 +70,7 @@ namespace osu.Game.Overlays.BeatmapSet
AutoSizeAxes = Axes.Y,
Child = new OsuSpriteText
{
- Text = this.type.GetLocalisableDescription(),
+ Text = type.GetLocalisableDescription(),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
},
},
@@ -61,23 +78,23 @@ namespace osu.Game.Overlays.BeatmapSet
};
}
- public string Text
+ public virtual T Metadata
{
set
{
- if (string.IsNullOrEmpty(value))
+ if (value == null)
{
- this.FadeOut(transition_duration);
+ this.FadeOut(TRANSITION_DURATION);
return;
}
- this.FadeIn(transition_duration);
+ this.FadeIn(TRANSITION_DURATION);
- setTextAsync(value);
+ setTextFlowAsync(value);
}
}
- private void setTextAsync(string text)
+ private void setTextFlowAsync(T metadata)
{
LoadComponentAsync(new LinkFlowContainer(s => s.Font = s.Font.With(size: 14))
{
@@ -88,44 +105,15 @@ namespace osu.Game.Overlays.BeatmapSet
{
textFlow?.Expire();
- switch (type)
- {
- case MetadataType.Tags:
- string[] tags = text.Split(" ");
-
- for (int i = 0; i <= tags.Length - 1; i++)
- {
- string tag = tags[i];
-
- if (searchAction != null)
- loaded.AddLink(tag, () => searchAction(tag));
- else
- loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
-
- if (i != tags.Length - 1)
- loaded.AddText(" ");
- }
-
- break;
-
- case MetadataType.Source:
- if (searchAction != null)
- loaded.AddLink(text, () => searchAction(text));
- else
- loaded.AddLink(text, LinkAction.SearchBeatmapSet, text);
-
- break;
-
- default:
- loaded.AddText(text);
- break;
- }
+ AddMetadata(metadata, loaded);
textContainer.Add(textFlow = loaded);
// fade in if we haven't yet.
- textContainer.FadeIn(transition_duration);
+ textContainer.FadeIn(TRANSITION_DURATION);
});
}
+
+ protected abstract void AddMetadata(T metadata, LinkFlowContainer loaded);
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionDescription.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionDescription.cs
new file mode 100644
index 0000000000..e6837951c9
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionDescription.cs
@@ -0,0 +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 osu.Game.Graphics.Containers;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionDescription : MetadataSection
+ {
+ public MetadataSectionDescription(Action? searchAction = null)
+ : base(MetadataType.Description, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
+ {
+ loaded.AddText(metadata);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionGenre.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionGenre.cs
new file mode 100644
index 0000000000..d41115f2b8
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionGenre.cs
@@ -0,0 +1,30 @@
+// 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.Extensions;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays.BeatmapListing;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionGenre : MetadataSection
+ {
+ public MetadataSectionGenre(Action? searchAction = null)
+ : base(MetadataType.Genre, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(BeatmapSetOnlineGenre metadata, LinkFlowContainer loaded)
+ {
+ var genre = (SearchGenre)metadata.Id;
+
+ if (Enum.IsDefined(genre))
+ loaded.AddLink(genre.GetLocalisableDescription(), LinkAction.FilterBeatmapSetGenre, genre);
+ else
+ loaded.AddText(metadata.Name);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionLanguage.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionLanguage.cs
new file mode 100644
index 0000000000..e831b1eaca
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionLanguage.cs
@@ -0,0 +1,30 @@
+// 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.Extensions;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays.BeatmapListing;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionLanguage : MetadataSection
+ {
+ public MetadataSectionLanguage(Action? searchAction = null)
+ : base(MetadataType.Language, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(BeatmapSetOnlineLanguage metadata, LinkFlowContainer loaded)
+ {
+ var language = (SearchLanguage)metadata.Id;
+
+ if (Enum.IsDefined(language))
+ loaded.AddLink(language.GetLocalisableDescription(), LinkAction.FilterBeatmapSetLanguage, language);
+ else
+ loaded.AddText(metadata.Name);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionNominators.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionNominators.cs
new file mode 100644
index 0000000000..76dbda3d5e
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionNominators.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Resources.Localisation.Web;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionNominators : MetadataSection<(BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers)>
+ {
+ public override (BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers) Metadata
+ {
+ set
+ {
+ if (value.CurrentNominations.Length == 0)
+ {
+ this.FadeOut(TRANSITION_DURATION);
+ return;
+ }
+
+ base.Metadata = value;
+ }
+ }
+
+ public MetadataSectionNominators(Action<(BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers)>? searchAction = null)
+ : base(MetadataType.Nominators, searchAction)
+ {
+ }
+
+ protected override void AddMetadata((BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers) metadata, LinkFlowContainer loaded)
+ {
+ int[] nominatorIds = metadata.CurrentNominations.Select(n => n.UserId).ToArray();
+
+ int nominatorsFound = 0;
+
+ foreach (int nominatorId in nominatorIds)
+ {
+ foreach (var user in metadata.RelatedUsers)
+ {
+ if (nominatorId != user.OnlineID) continue;
+
+ nominatorsFound++;
+
+ loaded.AddUserLink(new APIUser
+ {
+ Username = user.Username,
+ Id = nominatorId,
+ });
+
+ if (nominatorsFound < nominatorIds.Length)
+ loaded.AddText(CommonStrings.ArrayAndWordsConnector);
+
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionSource.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionSource.cs
new file mode 100644
index 0000000000..544dc0dfe4
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionSource.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionSource : MetadataSection
+ {
+ public MetadataSectionSource(Action? searchAction = null)
+ : base(MetadataType.Source, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
+ {
+ if (SearchAction != null)
+ loaded.AddLink(metadata, () => SearchAction(metadata));
+ else
+ loaded.AddLink(metadata, LinkAction.SearchBeatmapSet, metadata);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs
new file mode 100644
index 0000000000..fc16ba19d8
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs
@@ -0,0 +1,35 @@
+// 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.Game.Graphics.Containers;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public partial class MetadataSectionTags : MetadataSection
+ {
+ public MetadataSectionTags(Action? searchAction = null)
+ : base(MetadataType.Tags, searchAction)
+ {
+ }
+
+ protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
+ {
+ string[] tags = metadata.Split(" ");
+
+ for (int i = 0; i <= tags.Length - 1; i++)
+ {
+ string tag = tags[i];
+
+ if (SearchAction != null)
+ loaded.AddLink(tag, () => SearchAction(tag));
+ else
+ loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
+
+ if (i != tags.Length - 1)
+ loaded.AddText(" ");
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataType.cs b/osu.Game/Overlays/BeatmapSet/MetadataType.cs
index 924e020641..dc96ce99e9 100644
--- a/osu.Game/Overlays/BeatmapSet/MetadataType.cs
+++ b/osu.Game/Overlays/BeatmapSet/MetadataType.cs
@@ -23,6 +23,9 @@ namespace osu.Game.Overlays.BeatmapSet
Genre,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoLanguage))]
- Language
+ Language,
+
+ [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoNominators))]
+ Nominators,
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 006eec2838..425f40258e 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -95,8 +95,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersScore, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)),
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, minSize: 60, maxSize: 70)),
new TableColumn("", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 25)), // flag
- new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersPlayer, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 125)),
- new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120))
+ new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersPlayer, Anchor.CentreLeft, new Dimension(minSize: 125)),
+ new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, Anchor.CentreLeft, new Dimension(minSize: 70, maxSize: 120))
};
// All statistics across all scores, unordered.
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
var displayName = ruleset.GetDisplayNameForHitResult(result);
- columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
+ columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60)));
statisticResultTypes.Add((result, displayName));
}
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index fd7a3f8791..96d5203d14 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -68,11 +68,15 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
+ Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 20 },
- Children = new Drawable[]
+ Child = new FillFlowContainer
{
- new OsuHoverContainer
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Child = new OsuHoverContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs
index d30fd97652..d7c9ff67fe 100644
--- a/osu.Game/Overlays/Changelog/ChangelogListing.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = 20 },
- Text = build.CreatedAt.Date.ToString("dd MMMM yyyy"),
+ Text = build.CreatedAt.Date.ToLocalisableString("dd MMMM yyyy"),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 24),
});
diff --git a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs
index ddee6ff8bb..13a19de22a 100644
--- a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs
@@ -4,10 +4,10 @@
#nullable disable
using System;
-using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -104,27 +104,29 @@ namespace osu.Game.Overlays.Changelog
{
var fill = base.CreateHeader();
- foreach (var existing in fill.Children.OfType())
+ var nestedFill = (FillFlowContainer)fill.Child;
+
+ var buildDisplay = (OsuHoverContainer)nestedFill.Child;
+
+ buildDisplay.Scale = new Vector2(1.25f);
+ buildDisplay.Action = null;
+
+ fill.Add(date = new OsuSpriteText
{
- existing.Scale = new Vector2(1.25f);
- existing.Action = null;
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = Build.CreatedAt.Date.ToLocalisableString("dd MMMM yyyy"),
+ Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14),
+ Margin = new MarginPadding { Top = 5 },
+ Scale = new Vector2(1.25f),
+ });
- existing.Add(date = new OsuSpriteText
- {
- Text = Build.CreatedAt.Date.ToString("dd MMMM yyyy"),
- Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14),
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.TopCentre,
- Margin = new MarginPadding { Top = 5 },
- });
- }
-
- fill.Insert(-1, new NavigationIconButton(Build.Versions?.Previous)
+ nestedFill.Insert(-1, new NavigationIconButton(Build.Versions?.Previous)
{
Icon = FontAwesome.Solid.ChevronLeft,
SelectBuild = b => SelectBuild(b)
});
- fill.Insert(1, new NavigationIconButton(Build.Versions?.Next)
+ nestedFill.Insert(1, new NavigationIconButton(Build.Versions?.Next)
{
Icon = FontAwesome.Solid.ChevronRight,
SelectBuild = b => SelectBuild(b)
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index 90863a90a2..671d649dcf 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Overlays
/// are specified, the header will instantly display them.
public void ShowBuild([NotNull] APIChangelogBuild build)
{
- if (build == null) throw new ArgumentNullException(nameof(build));
+ ArgumentNullException.ThrowIfNull(build);
Current.Value = build;
Show();
@@ -78,8 +78,8 @@ namespace osu.Game.Overlays
public void ShowBuild([NotNull] string updateStream, [NotNull] string version)
{
- if (updateStream == null) throw new ArgumentNullException(nameof(updateStream));
- if (version == null) throw new ArgumentNullException(nameof(version));
+ ArgumentNullException.ThrowIfNull(updateStream);
+ ArgumentNullException.ThrowIfNull(version);
performAfterFetch(() =>
{
diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs
index 2b8718939e..70c3bf181c 100644
--- a/osu.Game/Overlays/Chat/ChatLine.cs
+++ b/osu.Game/Overlays/Chat/ChatLine.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -175,9 +176,7 @@ namespace osu.Game.Overlays.Chat
private void updateTimestamp()
{
- drawableTimestamp.Text = prefer24HourTime.Value
- ? $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"
- : $@"{message.Timestamp.LocalDateTime:hh:mm:ss tt}";
+ drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt");
}
}
}
diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs
index bcf5c1a409..682c96a695 100644
--- a/osu.Game/Overlays/Chat/ChatTextBar.cs
+++ b/osu.Game/Overlays/Chat/ChatTextBar.cs
@@ -128,9 +128,8 @@ namespace osu.Game.Overlays.Chat
chattingTextContainer.FadeTo(showSearch ? 0 : 1);
searchIconContainer.FadeTo(showSearch ? 1 : 0);
- // Clear search terms if any exist when switching back to chat mode
- if (!showSearch)
- OnSearchTermsChanged?.Invoke(string.Empty);
+ if (showSearch)
+ OnSearchTermsChanged?.Invoke(chatTextBox.Current.Value);
}, true);
currentChannel.BindValueChanged(change =>
@@ -151,6 +150,12 @@ namespace osu.Game.Overlays.Chat
chattingText.Text = ChatStrings.TalkingIn(newChannel.Name);
break;
}
+
+ if (change.OldValue != null)
+ chatTextBox.Current.UnbindFrom(change.OldValue.TextBoxMessage);
+
+ if (newChannel != null)
+ chatTextBox.Current.BindTo(newChannel.TextBoxMessage);
}, true);
}
diff --git a/osu.Game/Overlays/Chat/ChatTextBox.cs b/osu.Game/Overlays/Chat/ChatTextBox.cs
index 780c85a9c1..7cd005698e 100644
--- a/osu.Game/Overlays/Chat/ChatTextBox.cs
+++ b/osu.Game/Overlays/Chat/ChatTextBox.cs
@@ -24,7 +24,6 @@ namespace osu.Game.Overlays.Chat
bool showSearch = change.NewValue;
PlaceholderText = showSearch ? HomeStrings.SearchPlaceholder : ChatStrings.InputPlaceholder;
- Text = string.Empty;
}, true);
}
diff --git a/osu.Game/Overlays/Chat/DrawableUsername.cs b/osu.Game/Overlays/Chat/DrawableUsername.cs
index 7026d519a5..8cd16047f3 100644
--- a/osu.Game/Overlays/Chat/DrawableUsername.cs
+++ b/osu.Game/Overlays/Chat/DrawableUsername.cs
@@ -17,9 +17,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
+using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@@ -30,7 +32,7 @@ namespace osu.Game.Overlays.Chat
public Color4 AccentColour { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
- Child.ReceivePositionalInputAt(screenSpacePos);
+ colouredDrawable.ReceivePositionalInputAt(screenSpacePos);
public float FontSize
{
@@ -87,13 +89,13 @@ namespace osu.Game.Overlays.Chat
{
AccentColour = default_colours[user.Id % default_colours.Length];
- Child = colouredDrawable = drawableText;
+ Add(colouredDrawable = drawableText);
}
else
{
AccentColour = Color4Extensions.FromHex(user.Colour);
- Child = new Container
+ Add(new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@@ -127,7 +129,7 @@ namespace osu.Game.Overlays.Chat
}
}
}
- };
+ });
}
}
@@ -148,11 +150,11 @@ namespace osu.Game.Overlays.Chat
List