diff --git a/.github/workflows/test-diffcalc.yml b/.github/workflows/test-diffcalc.yml
new file mode 100644
index 0000000000..4274d01bab
--- /dev/null
+++ b/.github/workflows/test-diffcalc.yml
@@ -0,0 +1,163 @@
+# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
+# Usage:
+# !pp check 0 | Runs only the osu! ruleset.
+# !pp check 0 2 | Runs only the osu! and catch rulesets.
+#
+
+name: Diffcalc Consistency Checks
+on:
+ issue_comment:
+ types: [ created ]
+
+env:
+ DB_USER: root
+ DB_HOST: 127.0.0.1
+ CONCURRENCY: 4
+ ALLOW_DOWNLOAD: 1
+ SAVE_DOWNLOADED: 1
+
+jobs:
+ diffcalc:
+ name: Diffcalc
+ runs-on: ubuntu-latest
+ continue-on-error: true
+
+ if: |
+ github.event.issue.pull_request &&
+ contains(github.event.comment.body, '!pp check') &&
+ (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
+
+ strategy:
+ fail-fast: false
+ matrix:
+ ruleset:
+ - { name: osu, id: 0 }
+ - { name: taiko, id: 1 }
+ - { name: catch, id: 2 }
+ - { name: mania, id: 3 }
+
+ services:
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ALLOW_EMPTY_PASSWORD: yes
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+
+ steps:
+ - name: Verify ruleset
+ if: contains(github.event.comment.body, matrix.ruleset.id) == false
+ run: |
+ echo "${{ github.event.comment.body }} doesn't contain ${{ matrix.ruleset.id }}"
+ exit 1
+
+ - name: Verify MySQL connection from host
+ run: |
+ sudo apt-get install -y mysql-client
+ mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "SHOW DATABASES"
+
+ - name: Create directory structure
+ run: |
+ mkdir -p $GITHUB_WORKSPACE/master/
+ mkdir -p $GITHUB_WORKSPACE/pr/
+
+ # Checkout osu
+ - name: Checkout osu (master)
+ uses: actions/checkout@v2
+ with:
+ repository: ppy/osu
+ path: 'master/osu'
+ - name: Checkout osu (pr)
+ uses: actions/checkout@v2
+ with:
+ path: 'pr/osu'
+
+ # Checkout osu-difficulty-calculator
+ - name: Checkout osu-difficulty-calculator (master)
+ uses: actions/checkout@v2
+ with:
+ repository: ppy/osu-difficulty-calculator
+ path: 'master/osu-difficulty-calculator'
+ - name: Checkout osu-difficulty-calculator (pr)
+ uses: actions/checkout@v2
+ with:
+ repository: ppy/osu-difficulty-calculator
+ path: 'pr/osu-difficulty-calculator'
+
+ - name: Install .NET 5.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "5.0.x"
+
+ # Sanity checks to make sure diffcalc is not run when incompatible.
+ - name: Build diffcalc (master)
+ run: |
+ cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator
+ ./UseLocalOsu.sh
+ dotnet build
+ - name: Build diffcalc (pr)
+ run: |
+ cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator
+ ./UseLocalOsu.sh
+ dotnet build
+
+ # Initial data imports
+ - name: Download + import data
+ run: |
+ PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
+ BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
+
+ # Set env variable for further steps.
+ echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV
+
+ cd $GITHUB_WORKSPACE
+
+ wget https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2
+ wget https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2
+ tar -xf $PERFORMANCE_DATA_NAME.tar.bz2
+ tar -xf $BEATMAPS_DATA_NAME.tar.bz2
+
+ cd $GITHUB_WORKSPACE/$PERFORMANCE_DATA_NAME
+
+ mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_master"
+ mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_pr"
+
+ cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_master
+ cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_pr
+
+ # Run diffcalc
+ - name: Run diffcalc (master)
+ env:
+ DB_NAME: osu_master
+ run: |
+ cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
+ dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
+ - name: Run diffcalc (pr)
+ env:
+ DB_NAME: osu_pr
+ run: |
+ cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator
+ dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
+
+ # Print diffs
+ - name: Print diffs
+ run: |
+ mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "
+ SELECT
+ m.beatmap_id,
+ m.mods,
+ m.diff_unified as 'sr_master',
+ p.diff_unified as 'sr_pr',
+ (p.diff_unified - m.diff_unified) as 'diff'
+ FROM osu_master.osu_beatmap_difficulty m
+ JOIN osu_pr.osu_beatmap_difficulty p
+ ON m.beatmap_id = p.beatmap_id
+ AND m.mode = p.mode
+ AND m.mods = p.mods
+ WHERE abs(m.diff_unified - p.diff_unified) > 0.1
+ ORDER BY abs(m.diff_unified - p.diff_unified)
+ DESC
+ LIMIT 10000;"
+
+ # Todo: Run ppcalc
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
index 8fa7608b8e..498a710df9 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
@@ -1,8 +1,8 @@
-
-
-
+
+
+
@@ -14,7 +14,7 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 8f922f74a7..786ce2589d 100644
--- a/README.md
+++ b/README.md
@@ -31,12 +31,11 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:**
-| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(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.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
- 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.
-- When running on Windows 7 or 8.1, *[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net50&pivots=os-windows#dependencies)** may be required to correctly run .NET 5 applications if your operating system is not up-to-date with the latest service packs.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
## Developing a custom ruleset
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 3dd6be7307..e28053d0ca 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 0c4bfe0ed7..027bd0b7e2 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index bb0a487274..e2c715d385 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 0c4bfe0ed7..027bd0b7e2 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/osu.Android.props b/osu.Android.props
index 8de516240f..d4331a5e65 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 063e02d349..0bcbfc4baf 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -20,8 +20,21 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
- [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-archive")]
- [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed", "application/x-osu-archive" })]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
+ [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[]
+ {
+ "application/zip",
+ "application/octet-stream",
+ "application/download",
+ "application/x-zip",
+ "application/x-zip-compressed",
+ // newer official mime types (see https://osu.ppy.sh/wiki/en/osu%21_File_Formats).
+ "application/x-osu-beatmap-archive",
+ "application/x-osu-skin-archive",
+ "application/x-osu-replay",
+ })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
@@ -66,12 +79,14 @@ namespace osu.Android
case Intent.ActionSendMultiple:
{
var uris = new List();
+
for (int i = 0; i < intent.ClipData?.ItemCount; i++)
{
var content = intent.ClipData?.GetItemAt(i);
if (content != null)
uris.Add(content.Uri);
}
+
handleImportFromUris(uris.ToArray());
break;
}
diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
index 832d26b0ef..dcb88efeb6 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -139,8 +139,8 @@ namespace osu.Desktop
{
switch (activity)
{
- case UserActivity.SoloGame solo:
- return solo.Beatmap.ToString();
+ case UserActivity.InGame game:
+ return game.Beatmap.ToString();
case UserActivity.Editing edit:
return edit.Beatmap.ToString();
diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
index efc3f21149..dbfd170ea1 100644
--- a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
+++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
@@ -5,23 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
-using osu.Game;
using osu.Game.Configuration;
+using osu.Game.Screens.Play;
namespace osu.Desktop.Windows
{
public class GameplayWinKeyBlocker : Component
{
private Bindable disableWinKey;
- private Bindable localUserPlaying;
+ private IBindable localUserPlaying;
[Resolved]
private GameHost host { get; set; }
[BackgroundDependencyLoader]
- private void load(OsuGame game, OsuConfigManager config)
+ private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)
{
- localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
+ localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey);
diff --git a/osu.Game.Benchmarks/BenchmarkMod.cs b/osu.Game.Benchmarks/BenchmarkMod.cs
new file mode 100644
index 0000000000..c5375e9f09
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkMod.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 System;
+using BenchmarkDotNet.Attributes;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkMod : BenchmarkTest
+ {
+ private OsuModDoubleTime mod;
+
+ [Params(1, 10, 100)]
+ public int Times { get; set; }
+
+ public override void SetUp()
+ {
+ base.SetUp();
+ mod = new OsuModDoubleTime();
+ }
+
+ [Benchmark]
+ public int ModHashCode()
+ {
+ var hashCode = new HashCode();
+
+ for (int i = 0; i < Times; i++)
+ hashCode.Add(mod);
+
+ return hashCode.ToHashCode();
+ }
+ }
+}
diff --git a/osu.Game.Benchmarks/BenchmarkRuleset.cs b/osu.Game.Benchmarks/BenchmarkRuleset.cs
new file mode 100644
index 0000000000..2835ec9499
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkRuleset.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 BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+using osu.Game.Online.API;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkRuleset : BenchmarkTest
+ {
+ private OsuRuleset ruleset;
+ private APIMod apiModDoubleTime;
+ private APIMod apiModDifficultyAdjust;
+
+ public override void SetUp()
+ {
+ base.SetUp();
+ ruleset = new OsuRuleset();
+ apiModDoubleTime = new APIMod { Acronym = "DT" };
+ apiModDifficultyAdjust = new APIMod { Acronym = "DA" };
+ }
+
+ [Benchmark]
+ public void BenchmarkToModDoubleTime()
+ {
+ apiModDoubleTime.ToMod(ruleset);
+ }
+
+ [Benchmark]
+ public void BenchmarkToModDifficultyAdjust()
+ {
+ apiModDifficultyAdjust.ToMod(ruleset);
+ }
+
+ [Benchmark]
+ public void BenchmarkGetAllMods()
+ {
+ ruleset.CreateAllMods().Consume(new Consumer());
+ }
+
+ [Benchmark]
+ public void BenchmarkGetAllModsForReference()
+ {
+ ruleset.AllMods.Consume(new Consumer());
+ }
+
+ [Benchmark]
+ public void BenchmarkGetForAcronym()
+ {
+ ruleset.CreateModFromAcronym("DT");
+ }
+
+ [Benchmark]
+ public void BenchmarkGetForType()
+ {
+ ruleset.CreateMod();
+ }
+ }
+}
diff --git a/osu.Game.Benchmarks/Program.cs b/osu.Game.Benchmarks/Program.cs
index c55075fea6..439ced53ab 100644
--- a/osu.Game.Benchmarks/Program.cs
+++ b/osu.Game.Benchmarks/Program.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 BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
namespace osu.Game.Benchmarks
@@ -11,7 +12,7 @@ namespace osu.Game.Benchmarks
{
BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly)
- .Run(args);
+ .Run(args, DefaultConfig.Instance.WithOption(ConfigOptions.DisableOptimizationsValidator, true));
}
}
}
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index da8a0540f4..03f39f226c 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
index f5ef5c5e18..5e73a89069 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
@@ -210,9 +210,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
new Vector2(50, 200),
}), 0.5);
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
- AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.PerfectCurve);
+ AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PerfectCurve);
addAddVertexSteps(150, 150);
- AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.Linear);
+ AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.Linear);
}
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>
diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
index 5e4b6d9e1a..8fa96fb8c9 100644
--- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
@@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Catch.Tests
} while (rng.Next(2) != 0);
int length = sliderPath.ControlPoints.Count - start + 1;
- sliderPath.ControlPoints[start].Type.Value = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
+ sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
} while (rng.Next(3) != 0);
if (rng.Next(5) == 0)
@@ -210,13 +210,13 @@ namespace osu.Game.Rulesets.Catch.Tests
path.ConvertToSliderPath(sliderPath, sliderStartY);
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
- Assert.That(sliderPath.ControlPoints[0].Position.Value.X, Is.EqualTo(path.Vertices[0].X));
+ Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
assertInvariants(path.Vertices, true);
foreach (var point in sliderPath.ControlPoints)
{
- Assert.That(point.Type.Value, Is.EqualTo(PathType.Linear).Or.Null);
- Assert.That(sliderStartY + point.Position.Value.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
+ Assert.That(point.Type, Is.EqualTo(PathType.Linear).Or.Null);
+ Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
}
for (int i = 0; i < 10; i++)
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index a3307c9224..6abfbdbe21 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
AddSliderStep("circle size", 0, 8, 5, createCatcher);
AddToggleStep("hyper dash", t => this.ChildrenOfType().ForEach(area => area.ToggleHyperDash(t)));
+ AddToggleStep("toggle hit lighting", lighting => config.SetValue(OsuSetting.HitLighting, lighting));
AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
AddStep("catch many random fruit", () =>
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 484da8e22e..6457ec92da 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index 3a5322ce82..a891ec6c0a 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case JuiceStream juiceStream:
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
- lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.Value.X;
+ lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.X;
// Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
lastStartTime = juiceStream.StartTime;
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
index e736d68740..371e901c69 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
@@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Catch
Banana,
Droplet,
Catcher,
- CatchComboCounter
+ CatchComboCounter,
+ HitExplosion
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 4372ed938c..cfb3fe40be 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
- public class Movement : StrainSkill
+ public class Movement : StrainDecaySkill
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
index 8aaeef045f..1a43a10c81 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
path.ConvertFromSliderPath(sliderPath);
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
- if (sliderPath.ControlPoints.Any(p => p.Type.Value != null && p.Type.Value != PathType.Linear))
+ if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
{
path.ResampleVertices(hitObject.NestedHitObjects
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
index 36072d7fcb..8cb0804ab7 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
@@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Catch.Edit
juiceStream.OriginalX = selectionRange.GetFlippedPosition(juiceStream.OriginalX);
foreach (var point in juiceStream.Path.ControlPoints)
- point.Position.Value *= new Vector2(-1, 1);
+ point.Position *= new Vector2(-1, 1);
EditorBeatmap.Update(juiceStream);
return true;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs
index 932c8cad85..a97e940a64 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs
@@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Catch.Mods
///
private static void mirrorJuiceStreamPath(JuiceStream juiceStream)
{
- var controlPoints = juiceStream.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
+ var controlPoints = juiceStream.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints)
- point.Position.Value = new Vector2(-point.Position.Value.X, point.Position.Value.Y);
+ point.Position = new Vector2(-point.Position.X, point.Position.Y);
juiceStream.Path = new SliderPath(controlPoints, juiceStream.Path.ExpectedDistance.Value);
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 3088d024d1..a8ad34fcbe 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Catch.Objects
if (value != null)
{
- path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position.Value, c.Type.Value)));
+ path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
index f1cdb39e91..7207833fe6 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
@@ -234,7 +234,7 @@ namespace osu.Game.Rulesets.Catch.Objects
for (int i = 1; i < vertices.Count; i++)
{
- sliderPath.ControlPoints[^1].Type.Value = PathType.Linear;
+ sliderPath.ControlPoints[^1].Type = PathType.Linear;
float deltaX = vertices[i].X - lastPosition.X;
double length = vertices[i].Distance - currentDistance;
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultHitExplosion.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultHitExplosion.cs
new file mode 100644
index 0000000000..e1fad564a3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultHitExplosion.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.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Utils;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+ public class DefaultHitExplosion : CompositeDrawable, IHitExplosion
+ {
+ private CircularContainer largeFaint;
+ private CircularContainer smallFaint;
+ private CircularContainer directionalGlow1;
+ private CircularContainer directionalGlow2;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Size = new Vector2(20);
+ Anchor = Anchor.BottomCentre;
+ Origin = Anchor.BottomCentre;
+
+ // scale roughly in-line with visual appearance of notes
+ const float initial_height = 10;
+
+ InternalChildren = new Drawable[]
+ {
+ largeFaint = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Blending = BlendingParameters.Additive,
+ },
+ smallFaint = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Blending = BlendingParameters.Additive,
+ },
+ directionalGlow1 = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Size = new Vector2(0.01f, initial_height),
+ Blending = BlendingParameters.Additive,
+ },
+ directionalGlow2 = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Size = new Vector2(0.01f, initial_height),
+ Blending = BlendingParameters.Additive,
+ }
+ };
+ }
+
+ public void Animate(HitExplosionEntry entry)
+ {
+ X = entry.Position;
+ Scale = new Vector2(entry.HitObject.Scale);
+ setColour(entry.ObjectColour);
+
+ using (BeginAbsoluteSequence(entry.LifetimeStart))
+ applyTransforms(entry.HitObject.RandomSeed);
+ }
+
+ private void applyTransforms(int randomSeed)
+ {
+ const double duration = 400;
+
+ // we want our size to be very small so the glow dominates it.
+ largeFaint.Size = new Vector2(0.8f);
+ largeFaint
+ .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
+ .FadeOut(duration * 2);
+
+ const float angle_variangle = 15; // should be less than 45
+ directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
+ directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
+
+ this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
+ }
+
+ private void setColour(Color4 objectColour)
+ {
+ const float roundness = 100;
+
+ largeFaint.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+ Roundness = 160,
+ Radius = 200,
+ };
+
+ smallFaint.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+ Roundness = 20,
+ Radius = 50,
+ };
+
+ directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
+ Roundness = roundness,
+ Radius = 40,
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index 5e744ec001..10fc4e78b2 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -70,13 +70,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
if (version < 2.3m)
{
- if (GetTexture(@"fruit-ryuuta") != null ||
- GetTexture(@"fruit-ryuuta-0") != null)
+ if (hasOldStyleCatcherSprite())
return new LegacyCatcherOld();
}
- if (GetTexture(@"fruit-catcher-idle") != null ||
- GetTexture(@"fruit-catcher-idle-0") != null)
+ if (hasNewStyleCatcherSprite())
return new LegacyCatcherNew();
return null;
@@ -86,12 +84,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new LegacyCatchComboCounter(Skin);
return null;
+
+ case CatchSkinComponents.HitExplosion:
+ if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
+ return new LegacyHitExplosion();
+
+ return null;
}
}
return base.GetDrawableComponent(component);
}
+ private bool hasOldStyleCatcherSprite() =>
+ GetTexture(@"fruit-ryuuta") != null
+ || GetTexture(@"fruit-ryuuta-0") != null;
+
+ private bool hasNewStyleCatcherSprite() =>
+ GetTexture(@"fruit-catcher-idle") != null
+ || GetTexture(@"fruit-catcher-idle-0") != null;
+
public override IBindable GetConfig(TLookup lookup)
{
switch (lookup)
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
new file mode 100644
index 0000000000..c262b0a4ac
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
@@ -0,0 +1,94 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Legacy
+{
+ public class LegacyHitExplosion : CompositeDrawable, IHitExplosion
+ {
+ [Resolved]
+ private Catcher catcher { get; set; }
+
+ private const float catch_margin = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2;
+
+ private readonly Sprite explosion1;
+ private readonly Sprite explosion2;
+
+ public LegacyHitExplosion()
+ {
+ Anchor = Anchor.BottomCentre;
+ Origin = Anchor.BottomCentre;
+ RelativeSizeAxes = Axes.Both;
+ Scale = new Vector2(0.5f);
+
+ InternalChildren = new[]
+ {
+ explosion1 = new Sprite
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.CentreLeft,
+ Alpha = 0,
+ Blending = BlendingParameters.Additive,
+ Rotation = -90
+ },
+ explosion2 = new Sprite
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.CentreLeft,
+ Alpha = 0,
+ Blending = BlendingParameters.Additive,
+ Rotation = -90
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(SkinManager skins)
+ {
+ var defaultLegacySkin = skins.DefaultLegacySkin;
+
+ // sprite names intentionally swapped to match stable member naming / ease of cross-referencing
+ explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2");
+ explosion2.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-1");
+ }
+
+ public void Animate(HitExplosionEntry entry)
+ {
+ Colour = entry.ObjectColour;
+
+ using (BeginAbsoluteSequence(entry.LifetimeStart))
+ {
+ float halfCatchWidth = catcher.CatchWidth / 2;
+ float explosionOffset = Math.Clamp(entry.Position, -halfCatchWidth + catch_margin * 3, halfCatchWidth - catch_margin * 3);
+
+ if (!(entry.HitObject is Droplet))
+ {
+ float scale = Math.Clamp(entry.JudgementResult.ComboAtJudgement / 200f, 0.35f, 1.125f);
+
+ explosion1.Scale = new Vector2(1, 0.9f);
+ explosion1.Position = new Vector2(explosionOffset, 0);
+
+ explosion1.FadeOutFromOne(300);
+ explosion1.ScaleTo(new Vector2(16 * scale, 1.1f), 160, Easing.Out);
+ }
+
+ explosion2.Scale = new Vector2(0.9f, 1);
+ explosion2.Position = new Vector2(explosionOffset, 0);
+
+ explosion2.FadeOutFromOne(700);
+ explosion2.ScaleTo(new Vector2(0.9f, 1.3f), 500, Easing.Out);
+
+ this.Delay(700).FadeOutFromOne();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 9fd4610e6e..5cd85aac56 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -23,6 +23,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
+ [Cached]
public class Catcher : SkinReloadableDrawable
{
///
@@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// Width of the area that can be used to attempt catches during gameplay.
///
- private readonly float catchWidth;
+ public readonly float CatchWidth;
private readonly SkinnableCatcher body;
@@ -133,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (difficulty != null)
Scale = calculateScale(difficulty);
- catchWidth = CalculateCatchWidth(Scale);
+ CatchWidth = CalculateCatchWidth(Scale);
InternalChildren = new Drawable[]
{
@@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (!(hitObject is PalpableCatchHitObject fruit))
return false;
- float halfCatchWidth = catchWidth * 0.5f;
+ float halfCatchWidth = CatchWidth * 0.5f;
return fruit.EffectiveX >= X - halfCatchWidth &&
fruit.EffectiveX <= X + halfCatchWidth;
}
@@ -216,7 +217,7 @@ namespace osu.Game.Rulesets.Catch.UI
placeCaughtObject(palpableObject, positionInStack);
if (hitLighting.Value)
- addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value);
+ addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
}
// droplet doesn't affect the catcher state
@@ -365,8 +366,8 @@ namespace osu.Game.Rulesets.Catch.UI
return position;
}
- private void addLighting(CatchHitObject hitObject, float x, Color4 colour) =>
- hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed));
+ private void addLighting(JudgementResult judgementResult, Color4 colour, float x) =>
+ hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, judgementResult, colour, x));
private CaughtObject getCaughtObject(PalpableCatchHitObject source)
{
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
index d9ab428231..955b1e6edb 100644
--- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
@@ -1,129 +1,56 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Rulesets.Objects.Pooling;
-using osu.Game.Utils;
-using osuTK;
-using osuTK.Graphics;
+using osu.Game.Skinning;
+
+#nullable enable
namespace osu.Game.Rulesets.Catch.UI
{
public class HitExplosion : PoolableDrawableWithLifetime
{
- private readonly CircularContainer largeFaint;
- private readonly CircularContainer smallFaint;
- private readonly CircularContainer directionalGlow1;
- private readonly CircularContainer directionalGlow2;
+ private readonly SkinnableDrawable skinnableExplosion;
public HitExplosion()
{
- Size = new Vector2(20);
- Anchor = Anchor.TopCentre;
+ RelativeSizeAxes = Axes.Both;
+ Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
- // scale roughly in-line with visual appearance of notes
- const float initial_height = 10;
-
- InternalChildren = new Drawable[]
+ InternalChild = skinnableExplosion = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.HitExplosion), _ => new DefaultHitExplosion())
{
- largeFaint = new CircularContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Blending = BlendingParameters.Additive,
- },
- smallFaint = new CircularContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Blending = BlendingParameters.Additive,
- },
- directionalGlow1 = new CircularContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Size = new Vector2(0.01f, initial_height),
- Blending = BlendingParameters.Additive,
- },
- directionalGlow2 = new CircularContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Size = new Vector2(0.01f, initial_height),
- Blending = BlendingParameters.Additive,
- }
+ CentreComponent = false,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre
};
}
protected override void OnApply(HitExplosionEntry entry)
{
- X = entry.Position;
- Scale = new Vector2(entry.Scale);
- setColour(entry.ObjectColour);
-
- using (BeginAbsoluteSequence(entry.LifetimeStart))
- applyTransforms(entry.RNGSeed);
+ base.OnApply(entry);
+ if (IsLoaded)
+ apply(entry);
}
- private void applyTransforms(int randomSeed)
+ protected override void LoadComplete()
{
+ base.LoadComplete();
+ apply(Entry);
+ }
+
+ private void apply(HitExplosionEntry? entry)
+ {
+ if (entry == null)
+ return;
+
+ ApplyTransformsAt(double.MinValue, true);
ClearTransforms(true);
- const double duration = 400;
-
- // we want our size to be very small so the glow dominates it.
- largeFaint.Size = new Vector2(0.8f);
- largeFaint
- .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
- .FadeOut(duration * 2);
-
- const float angle_variangle = 15; // should be less than 45
- directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
- directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
-
- this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire();
- }
-
- private void setColour(Color4 objectColour)
- {
- const float roundness = 100;
-
- largeFaint.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
- Roundness = 160,
- Radius = 200,
- };
-
- smallFaint.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
- Roundness = 20,
- Radius = 50,
- };
-
- directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
- Roundness = roundness,
- Radius = 40,
- };
+ (skinnableExplosion.Drawable as IHitExplosion)?.Animate(entry);
+ LifetimeEnd = skinnableExplosion.Drawable.LatestTransformEndTime;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs
index 094d88243a..6df13e52ef 100644
--- a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.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 osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Pooling;
@@ -14,6 +15,8 @@ namespace osu.Game.Rulesets.Catch.UI
public HitExplosionContainer()
{
+ RelativeSizeAxes = Axes.Both;
+
AddInternal(pool = new DrawablePool(10));
}
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs
index b142962a8a..88871c77f6 100644
--- a/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs
@@ -2,24 +2,42 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Performance;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Judgements;
using osuTK.Graphics;
+#nullable enable
+
namespace osu.Game.Rulesets.Catch.UI
{
public class HitExplosionEntry : LifetimeEntry
{
- public readonly float Position;
- public readonly float Scale;
- public readonly Color4 ObjectColour;
- public readonly int RNGSeed;
+ ///
+ /// The judgement result that triggered this explosion.
+ ///
+ public JudgementResult JudgementResult { get; }
- public HitExplosionEntry(double startTime, float position, float scale, Color4 objectColour, int rngSeed)
+ ///
+ /// The hitobject which triggered this explosion.
+ ///
+ public CatchHitObject HitObject => (CatchHitObject)JudgementResult.HitObject;
+
+ ///
+ /// The accent colour of the object caught.
+ ///
+ public Color4 ObjectColour { get; }
+
+ ///
+ /// The position at which the object was caught.
+ ///
+ public float Position { get; }
+
+ public HitExplosionEntry(double startTime, JudgementResult judgementResult, Color4 objectColour, float position)
{
LifetimeStart = startTime;
Position = position;
- Scale = scale;
+ JudgementResult = judgementResult;
ObjectColour = objectColour;
- RNGSeed = rngSeed;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/IHitExplosion.cs b/osu.Game.Rulesets.Catch/UI/IHitExplosion.cs
new file mode 100644
index 0000000000..c744c00d9a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/IHitExplosion.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// Common interface for all hit explosion skinnables.
+ ///
+ public interface IHitExplosion
+ {
+ ///
+ /// Begins animating this .
+ ///
+ void Animate(HitExplosionEntry entry);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
new file mode 100644
index 0000000000..0f520215a1
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
@@ -0,0 +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 System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Editor
+{
+ public class TestSceneManiaComposeScreen : EditorClockTestScene
+ {
+ [Resolved]
+ private SkinManager skins { get; set; }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("setup compose screen", () =>
+ {
+ var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }))
+ {
+ BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
+ };
+
+ Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
+
+ Child = new DependencyProvidingContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(EditorBeatmap), editorBeatmap),
+ (typeof(IBeatSnapProvider), editorBeatmap),
+ },
+ Child = new ComposeScreen { State = { Value = Visibility.Visible } },
+ };
+ });
+
+ AddUntilStep("wait for composer", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+ }
+
+ [Test]
+ public void TestDefaultSkin()
+ {
+ AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default);
+ }
+
+ [Test]
+ public void TestLegacySkin()
+ {
+ AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
index e14ad92842..449a6ff23d 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
@@ -13,6 +14,10 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Framework.Bindables;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Tests
{
@@ -22,14 +27,65 @@ namespace osu.Game.Rulesets.Mania.Tests
[Resolved]
private RulesetConfigCache configCache { get; set; }
- private readonly Bindable configTimingBasedNoteColouring = new Bindable();
+ private Bindable configTimingBasedNoteColouring;
- protected override void LoadComplete()
+ private ManualClock clock;
+ private DrawableManiaRuleset drawableRuleset;
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("setup hierarchy", () => Child = new Container
+ {
+ Clock = new FramedClock(clock = new ManualClock()),
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new[]
+ {
+ drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
+ }
+ });
+ AddStep("retrieve config bindable", () =>
+ {
+ var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
+ configTimingBasedNoteColouring = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring);
+ });
+ }
+
+ [Test]
+ public void TestSimple()
+ {
+ AddStep("enable", () => configTimingBasedNoteColouring.Value = true);
+ AddStep("disable", () => configTimingBasedNoteColouring.Value = false);
+ }
+
+ [Test]
+ public void TestToggleOffScreen()
+ {
+ AddStep("enable", () => configTimingBasedNoteColouring.Value = true);
+
+ seekTo(10000);
+ AddStep("disable", () => configTimingBasedNoteColouring.Value = false);
+ seekTo(0);
+ AddAssert("all notes not coloured", () => this.ChildrenOfType().All(note => note.Colour == Colour4.White));
+
+ seekTo(10000);
+ AddStep("enable again", () => configTimingBasedNoteColouring.Value = true);
+ seekTo(0);
+ AddAssert("some notes coloured", () => this.ChildrenOfType().Any(note => note.Colour != Colour4.White));
+ }
+
+ private void seekTo(double time)
+ {
+ AddStep($"seek to {time}", () => clock.CurrentTime = time);
+ AddUntilStep("wait for seek", () => Precision.AlmostEquals(drawableRuleset.FrameStableClock.CurrentTime, time, 1));
+ }
+
+ private ManiaBeatmap createTestBeatmap()
{
const double beat_length = 500;
- var ruleset = new ManiaRuleset();
-
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
{
HitObjects =
@@ -45,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Tests
new Note { StartTime = beat_length }
},
ControlPointInfo = new ControlPointInfo(),
- BeatmapInfo = { Ruleset = ruleset.RulesetInfo },
+ BeatmapInfo = { Ruleset = Ruleset.Value },
};
foreach (var note in beatmap.HitObjects)
@@ -57,24 +113,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
BeatLength = beat_length
});
-
- Child = new Container
- {
- Clock = new FramedClock(new ManualClock()),
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new[]
- {
- ruleset.CreateDrawableRulesetWith(beatmap)
- }
- };
-
- var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
- config.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
-
- AddStep("Enable", () => configTimingBasedNoteColouring.Value = true);
- AddStep("Disable", () => configTimingBasedNoteColouring.Value = false);
+ return beatmap;
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 6df555617b..674a22df98 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
index 2ba2ee6b4a..01d930d585 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
- public class Strain : StrainSkill
+ public class Strain : StrainDecaySkill
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
return individualStrain + overallStrain - CurrentStrain;
}
- protected override double GetPeakStrain(double offset)
+ protected override double CalculateInitialStrain(double offset)
=> applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base)
+ applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base);
diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs
new file mode 100644
index 0000000000..a206aafb8a
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Screens.Edit.Setup;
+
+namespace osu.Game.Rulesets.Mania.Edit.Setup
+{
+ public class ManiaSetupSection : RulesetSetupSection
+ {
+ private LabelledSwitchButton specialStyle;
+
+ public ManiaSetupSection()
+ : base(new ManiaRuleset().RulesetInfo)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ specialStyle = new LabelledSwitchButton
+ {
+ Label = "Use special (N+1) style",
+ Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 5k (4+1) or 8key (7+1) configurations.",
+ Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ specialStyle.Current.BindValueChanged(_ => updateBeatmap());
+ }
+
+ private void updateBeatmap()
+ {
+ Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index f4b6e10af4..1f79dae280 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -27,11 +27,13 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Mania.Edit.Setup;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Scoring;
+using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania
@@ -390,6 +392,8 @@ namespace osu.Game.Rulesets.Mania
{
return new ManiaFilterCriteria();
}
+
+ public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
}
public enum PlayfieldType
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index d1310d42eb..2923a2af2f 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -275,9 +275,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
return false;
beginHoldAt(Time.Current - Head.HitObject.StartTime);
- Head.UpdateResult();
- return true;
+ return Head.UpdateResult();
}
private void beginHoldAt(double timeOffset)
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index be600f0d47..8458345998 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Origin = Anchor.TopCentre;
}
- public void UpdateResult() => base.UpdateResult(true);
+ public bool UpdateResult() => base.UpdateResult(true);
protected override void UpdateInitialTransforms()
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 5aff4e200b..9ac223a0d7 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -6,7 +6,6 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI;
@@ -29,11 +28,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
[Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; }
- ///
- /// Gets the samples that are played by this object during gameplay.
- ///
- public ISampleInfo[] GetGameplaySamples() => Samples.Samples;
-
protected override float SamplePlaybackPosition
{
get
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index 33d872dfb6..d53c28868d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true);
}
+ protected override void OnApply()
+ {
+ base.OnApply();
+ updateSnapColour();
+ }
+
protected override void OnDirectionChanged(ValueChangedEvent e)
{
base.OnDirectionChanged(e);
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 9b5893b268..f5e30efd91 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Linq;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -19,6 +18,7 @@ using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -28,12 +28,6 @@ namespace osu.Game.Rulesets.Mania.UI
public const float COLUMN_WIDTH = 80;
public const float SPECIAL_COLUMN_WIDTH = 70;
- ///
- /// For hitsounds played by this (i.e. not as a result of hitting a hitobject),
- /// a certain number of samples are allowed to be played concurrently so that it feels better when spam-pressing the key.
- ///
- private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY;
-
///
/// The index of this column as part of the whole playfield.
///
@@ -45,10 +39,10 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
private readonly DrawablePool hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy;
- private readonly Container hitSounds;
-
public Container UnderlayElements => HitObjectArea.UnderlayElements;
+ private readonly GameplaySampleTriggerSource sampleTriggerSource;
+
public Column(int index)
{
Index = index;
@@ -64,6 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new[]
{
hitExplosionPool = new DrawablePool(5),
+ sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
@@ -72,12 +67,6 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both
},
background,
- hitSounds = new Container
- {
- Name = "Column samples pool",
- RelativeSizeAxes = Axes.Both,
- Children = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray()
- },
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
@@ -133,29 +122,12 @@ namespace osu.Game.Rulesets.Mania.UI
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
}
- private int nextHitSoundIndex;
-
public bool OnPressed(ManiaAction action)
{
if (action != Action.Value)
return false;
- var nextObject =
- HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
- // fallback to non-alive objects to find next off-screen object
- HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
- HitObjectContainer.Objects.LastOrDefault();
-
- if (nextObject is DrawableManiaHitObject maniaObject)
- {
- var hitSound = hitSounds[nextHitSoundIndex];
-
- hitSound.Samples = maniaObject.GetGameplaySamples();
- hitSound.Play();
-
- nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds;
- }
-
+ sampleTriggerSource.Play();
return true;
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index 34d972e60f..8581f016b1 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -37,12 +37,11 @@ namespace osu.Game.Rulesets.Mania.UI
public override void PlayAnimation()
{
- base.PlayAnimation();
-
switch (Result)
{
case HitResult.None:
case HitResult.Miss:
+ base.PlayAnimation();
break;
default:
@@ -52,6 +51,8 @@ namespace osu.Game.Rulesets.Mania.UI
this.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
+
+ // osu!mania uses a custom fade length, so the base call is intentionally omitted.
break;
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
index 35b79aa8ac..5a1aa42ed1 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("last connection displayed", () =>
{
- var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position.Value == new Vector2(300));
+ var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position == new Vector2(300));
return lastConnection.DrawWidth > 50;
});
}
@@ -166,14 +166,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"move mouse to control point {index}", () =>
{
- Vector2 position = slider.Path.ControlPoints[index].Position.Value;
+ Vector2 position = slider.Path.ControlPoints[index].Position;
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
});
}
private void assertControlPointPathType(int controlPointIndex, PathType? type)
{
- AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type.Value == type);
+ AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type == type);
}
private void addContextMenuItemStep(string contextMenuText)
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs
index 24b947c854..6bfe7f892b 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs
@@ -108,9 +108,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestDragControlPointPathAfterChangingType()
{
- AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type.Value = PathType.Bezier);
+ AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.Bezier);
AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10))));
- AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type.Value = PathType.PerfectCurve);
+ AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PerfectCurve);
moveMouseToControlPoint(4);
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
@@ -137,15 +137,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"move mouse to control point {index}", () =>
{
- Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
+ Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}
- private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type.Value == type);
+ private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type == type);
private void assertControlPointPosition(int index, Vector2 position) =>
- AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position.Value, 1));
+ AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position, 1));
private class TestSliderBlueprint : SliderSelectionBlueprint
{
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
index 8235e1bc79..e724015905 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
@@ -385,10 +385,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected);
- private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type.Value == type);
+ private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type == type);
private void assertControlPointPosition(int index, Vector2 position) =>
- AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position.Value, 1));
+ AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position, 1));
private Slider getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs
index 0d828a79c8..cc43eb3852 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs
@@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"move mouse to control point {index}", () =>
{
- Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
+ Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 68be34d153..f5f1159542 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
index e47edc37cc..7bcd867a9c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
@@ -10,7 +10,7 @@ using osu.Framework.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
- public abstract class OsuStrainSkill : StrainSkill
+ public abstract class OsuStrainSkill : StrainDecaySkill
{
///
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
index eb7011e8b0..d66c9ea4bf 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
private void updateConnectingPath()
{
- Position = slider.StackedPosition + ControlPoint.Position.Value;
+ Position = slider.StackedPosition + ControlPoint.Position;
path.ClearVertices();
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return;
path.AddVertex(Vector2.Zero);
- path.AddVertex(slider.Path.ControlPoints[nextIndex].Position.Value - ControlPoint.Position.Value);
+ path.AddVertex(slider.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index 5b476526c9..2cc95e1891 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IBindable sliderPosition;
private IBindable sliderScale;
- private IBindable controlPointPosition;
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
{
@@ -69,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
updatePathType();
});
- controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
+ controlPoint.Changed += updateMarkerDisplay;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@@ -117,9 +116,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
sliderPosition = slider.PositionBindable.GetBoundCopy();
sliderPosition.BindValueChanged(_ => updateMarkerDisplay());
- controlPointPosition = ControlPoint.Position.GetBoundCopy();
- controlPointPosition.BindValueChanged(_ => updateMarkerDisplay());
-
sliderScale = slider.ScaleBindable.GetBoundCopy();
sliderScale.BindValueChanged(_ => updateMarkerDisplay());
@@ -174,8 +170,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (e.Button == MouseButton.Left)
{
- dragStartPosition = ControlPoint.Position.Value;
- dragPathType = PointsInSegment[0].Type.Value;
+ dragStartPosition = ControlPoint.Position;
+ dragPathType = PointsInSegment[0].Type;
changeHandler?.BeginChange();
return true;
@@ -186,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override void OnDrag(DragEvent e)
{
- Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position.Value).ToArray();
+ Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = slider.Position;
var oldStartTime = slider.StartTime;
@@ -202,15 +198,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
- slider.Path.ControlPoints[i].Position.Value -= movementDelta;
+ slider.Path.ControlPoints[i].Position -= movementDelta;
}
else
- ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
+ ControlPoint.Position = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
if (!slider.Path.HasValidLength)
{
for (var i = 0; i < slider.Path.ControlPoints.Count; i++)
- slider.Path.ControlPoints[i].Position.Value = oldControlPoints[i];
+ slider.Path.ControlPoints[i].Position = oldControlPoints[i];
slider.Position = oldPosition;
slider.StartTime = oldStartTime;
@@ -218,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
- PointsInSegment[0].Type.Value = dragPathType;
+ PointsInSegment[0].Type = dragPathType;
}
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
@@ -230,19 +226,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
private void updatePathType()
{
- if (ControlPoint.Type.Value != PathType.PerfectCurve)
+ if (ControlPoint.Type != PathType.PerfectCurve)
return;
if (PointsInSegment.Count > 3)
- ControlPoint.Type.Value = PathType.Bezier;
+ ControlPoint.Type = PathType.Bezier;
if (PointsInSegment.Count != 3)
return;
- ReadOnlySpan points = PointsInSegment.Select(p => p.Position.Value).ToArray();
+ ReadOnlySpan points = PointsInSegment.Select(p => p.Position).ToArray();
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
- ControlPoint.Type.Value = PathType.Bezier;
+ ControlPoint.Type = PathType.Bezier;
}
///
@@ -250,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
private void updateMarkerDisplay()
{
- Position = slider.StackedPosition + ControlPoint.Position.Value;
+ Position = slider.StackedPosition + ControlPoint.Position;
markerRing.Alpha = IsSelected.Value ? 1 : 0;
@@ -265,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private Color4 getColourFromNodeType()
{
- if (!(ControlPoint.Type.Value is PathType pathType))
+ if (!(ControlPoint.Type is PathType pathType))
return colours.Yellow;
switch (pathType)
@@ -284,6 +280,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
- public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
+ public LocalisableString TooltipText => ControlPoint.Type.ToString() ?? string.Empty;
}
}
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 5bbdf9688f..6269a41350 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -173,12 +173,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
int thirdPointIndex = indexInSegment + 2;
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
- piece.PointsInSegment[thirdPointIndex].Type.Value = piece.PointsInSegment[0].Type.Value;
+ piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
break;
}
- piece.ControlPoint.Type.Value = type;
+ piece.ControlPoint.Type = type;
}
[Resolved(CanBeNull = true)]
@@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private MenuItem createMenuItemForPathType(PathType? type)
{
int totalCount = Pieces.Count(p => p.IsSelected.Value);
- int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type);
+ int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type);
var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ =>
{
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 8b20df9a68..b9e4ed6fcb 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
- segmentStart.Type.Value = PathType.Linear;
+ segmentStart.Type = PathType.Linear;
currentSegmentLength = 1;
}
@@ -153,15 +153,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
case 1:
case 2:
- segmentStart.Type.Value = PathType.Linear;
+ segmentStart.Type = PathType.Linear;
break;
case 3:
- segmentStart.Type.Value = PathType.PerfectCurve;
+ segmentStart.Type = PathType.PerfectCurve;
break;
default:
- segmentStart.Type.Value = PathType.Bezier;
+ segmentStart.Type = PathType.Bezier;
break;
}
}
@@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// The cursor does not overlap a previous control point, so it can be added if not already existing.
if (cursor == null)
{
- HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
+ HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = Vector2.Zero });
// The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier).
currentSegmentLength++;
@@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Update the cursor position.
- cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
+ cursor.Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
else if (cursor != null)
{
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index e810d2fe0c..89724876fa 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
Debug.Assert(placementControlPointIndex != null);
- HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position.Value = e.MousePosition - HitObject.Position;
+ HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position = e.MousePosition - HitObject.Position;
}
protected override void OnDragEnd(DragEndEvent e)
@@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
for (int i = 0; i < controlPoints.Count - 1; i++)
{
- float dist = new Line(controlPoints[i].Position.Value, controlPoints[i + 1].Position.Value).DistanceToPoint(position);
+ float dist = new Line(controlPoints[i].Position, controlPoints[i + 1].Position).DistanceToPoint(position);
if (dist < minDistance)
{
@@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Move the control points from the insertion index onwards to make room for the insertion
- controlPoints.Insert(insertionIndex, new PathControlPoint { Position = { Value = position } });
+ controlPoints.Insert(insertionIndex, new PathControlPoint { Position = position });
return insertionIndex;
}
@@ -207,8 +207,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
// The first control point in the slider must have a type, so take it from the previous "first" one
// Todo: Should be handled within SliderPath itself
- if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type.Value == null)
- controlPoints[1].Type.Value = controlPoints[0].Type.Value;
+ if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null)
+ controlPoints[1].Type = controlPoints[0].Type;
controlPoints.Remove(c);
}
@@ -222,9 +222,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position
// So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
- Vector2 first = controlPoints[0].Position.Value;
+ Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints)
- c.Position.Value -= first;
+ c.Position -= first;
HitObject.Position += first;
}
diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs
index 0e61c02e2d..c89527d8bd 100644
--- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs
+++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs
@@ -41,6 +41,11 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override GameplayCursorContainer CreateCursor() => null;
+ public OsuEditorPlayfield()
+ {
+ HitPolicy = new AnyOrderHitPolicy();
+ }
+
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
@@ -59,11 +64,14 @@ namespace osu.Game.Rulesets.Osu.Edit
if (hitObject is DrawableHitCircle circle)
{
- circle.ApproachCircle
- .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
- .Expire();
+ using (circle.BeginAbsoluteSequence(circle.HitStateUpdateTime))
+ {
+ circle.ApproachCircle
+ .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
+ .Expire();
- circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
+ circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
+ }
}
if (hitObject is IHasMainCirclePiece mainPieceContainer)
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 358a44e0e6..4a57d36eb4 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Osu.Edit
{
foreach (var point in slider.Path.ControlPoints)
{
- point.Position.Value = new Vector2(
- (direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
- (direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
+ point.Position = new Vector2(
+ (direction == Direction.Horizontal ? -1 : 1) * point.Position.X,
+ (direction == Direction.Vertical ? -1 : 1) * point.Position.Y
);
}
}
@@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (h is IHasPath path)
{
foreach (var point in path.Path.ControlPoints)
- point.Position.Value = RotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
+ point.Position = RotatePointAroundOrigin(point.Position, Vector2.Zero, delta);
}
}
@@ -163,9 +163,9 @@ namespace osu.Game.Rulesets.Osu.Edit
private void scaleSlider(Slider slider, Vector2 scale)
{
- referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList();
+ referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
- Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
+ Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
@@ -178,13 +178,13 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var point in slider.Path.ControlPoints)
{
- oldControlPoints.Enqueue(point.Position.Value);
- point.Position.Value *= pathRelativeDeltaScale;
+ oldControlPoints.Enqueue(point.Position);
+ point.Position *= pathRelativeDeltaScale;
}
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
- slider.Path.ControlPoints[i].Type.Value = referencePathTypes[i];
+ slider.Path.ControlPoints[i].Type = referencePathTypes[i];
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
@@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Edit
return;
foreach (var point in slider.Path.ControlPoints)
- point.Position.Value = oldControlPoints.Dequeue();
+ point.Position = oldControlPoints.Dequeue();
}
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs
new file mode 100644
index 0000000000..8cb778a2e1
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Screens.Edit.Setup;
+
+namespace osu.Game.Rulesets.Osu.Edit.Setup
+{
+ public class OsuSetupSection : RulesetSetupSection
+ {
+ private LabelledSliderBar stackLeniency;
+
+ public OsuSetupSection()
+ : base(new OsuRuleset().RulesetInfo)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new[]
+ {
+ stackLeniency = new LabelledSliderBar
+ {
+ Label = "Stack Leniency",
+ Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
+ Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
+ {
+ Default = 0.7f,
+ MinValue = 0,
+ MaxValue = 1,
+ Precision = 0.1f
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ stackLeniency.Current.BindValueChanged(_ => updateBeatmap());
+ }
+
+ private void updateBeatmap()
+ {
+ Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index 636cd63c69..3102db270e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
@@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield.HitObjectContainer, drawableRuleset.Beatmap));
+ drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield, drawableRuleset.Beatmap));
}
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
@@ -128,8 +129,21 @@ namespace osu.Game.Rulesets.Osu.Mods
protected override void Update()
{
- float start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
- float end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
+ float start, end;
+
+ if (Precision.AlmostEquals(restrictTo.Rotation, 0))
+ {
+ start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
+ end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
+ }
+ else
+ {
+ float center = restrictTo.ToSpaceOfOtherDrawable(restrictTo.OriginPosition, Parent).X;
+ float halfDiagonal = (restrictTo.DrawSize / 2).LengthFast;
+
+ start = center - halfDiagonal;
+ end = center + halfDiagonal;
+ }
float rawWidth = end - start;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs
index 82bca0a4e2..30ff6b8984 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs
@@ -4,6 +4,7 @@
#nullable enable
using System;
+using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Objects;
@@ -20,8 +21,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
Start = start;
LifetimeStart = Start.StartTime;
-
- bindEvents();
}
private OsuHitObject? end;
@@ -41,31 +40,39 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
}
}
+ private bool wasBound;
+
private void bindEvents()
{
UnbindEvents();
+ if (End == null)
+ return;
+
// Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects.
Start.DefaultsApplied += onDefaultsApplied;
Start.PositionBindable.ValueChanged += onPositionChanged;
- if (End != null)
- {
- End.DefaultsApplied += onDefaultsApplied;
- End.PositionBindable.ValueChanged += onPositionChanged;
- }
+ End.DefaultsApplied += onDefaultsApplied;
+ End.PositionBindable.ValueChanged += onPositionChanged;
+
+ wasBound = true;
}
public void UnbindEvents()
{
+ if (!wasBound)
+ return;
+
+ Debug.Assert(End != null);
+
Start.DefaultsApplied -= onDefaultsApplied;
Start.PositionBindable.ValueChanged -= onPositionChanged;
- if (End != null)
- {
- End.DefaultsApplied -= onDefaultsApplied;
- End.PositionBindable.ValueChanged -= onPositionChanged;
- }
+ End.DefaultsApplied -= onDefaultsApplied;
+ End.PositionBindable.ValueChanged -= onPositionChanged;
+
+ wasBound = false;
}
private void onDefaultsApplied(HitObject obj) => refreshLifetimes();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 79655c33e4..e4df41a4fe 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -74,10 +74,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override void PlayAnimation()
{
- base.PlayAnimation();
-
if (Result != HitResult.Miss)
- JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
+ {
+ JudgementText
+ .ScaleTo(new Vector2(0.8f, 1))
+ .ScaleTo(new Vector2(1.2f, 1), 1800, Easing.OutQuint);
+ }
+
+ base.PlayAnimation();
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 8ba9597dc3..c4420b1e87 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Objects
if (value != null)
{
- path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position.Value, c.Type.Value)));
+ path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index b13cdff1ec..f4a93a571d 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -30,9 +30,11 @@ using osu.Game.Skinning;
using System;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Game.Rulesets.Osu.Edit.Setup;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.Statistics;
+using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu
@@ -305,5 +307,7 @@ namespace osu.Game.Rulesets.Osu
}
};
}
+
+ public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
index 4dd7b2d69c..8602ebc88b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
pathVersion.BindValueChanged(_ => Refresh());
accentColour = drawableObject.AccentColour.GetBoundCopy();
- accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true);
+ accentColour.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
}
- private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour)
- => AccentColour = skin.GetConfig(OsuSkinColour.SliderTrackOverride)?.Value ?? defaultAccentColour;
+ protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) =>
+ skin.GetConfig(OsuSkinColour.SliderTrackOverride)?.Value ?? hitObjectAccentColour;
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs
index ed4e04184b..7b7a89d5e2 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
///
/// A which changes its curve depending on the snaking progress.
///
- public class SnakingSliderBody : SliderBody, ISliderProgress
+ public abstract class SnakingSliderBody : SliderBody, ISliderProgress
{
public readonly List CurrentCurve = new List();
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
index f6fd3e36ab..75d847d54d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
@@ -9,6 +9,7 @@ using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
@@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private double lastTrailTime;
private IBindable cursorSize;
+ private Vector2? currentPosition;
+
public LegacyCursorTrail(ISkin skin)
{
this.skin = skin;
@@ -54,22 +57,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
protected override double FadeDuration => disjointTrail ? 150 : 500;
+ protected override float FadeExponent => 1;
protected override bool InterpolateMovements => !disjointTrail;
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
+ protected override bool AvoidDrawingNearCursor => !disjointTrail;
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!disjointTrail || !currentPosition.HasValue)
+ return;
+
+ if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
+ {
+ lastTrailTime = Time.Current;
+ AddTrail(currentPosition.Value);
+ }
+ }
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (!disjointTrail)
return base.OnMouseMove(e);
- if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
- {
- lastTrailTime = Time.Current;
- return base.OnMouseMove(e);
- }
+ currentPosition = e.ScreenSpaceMousePosition;
+ // Intentionally block the base call as we're adding the trails ourselves.
return false;
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs
index 1c8dfeac52..29a0745193 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK.Graphics;
@@ -14,6 +15,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
protected override DrawableSliderPath CreateSliderPath() => new LegacyDrawableSliderPath();
+ protected override Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour)
+ {
+ // legacy skins use a constant value for slider track alpha, regardless of the source colour.
+ return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(0.7f);
+ }
+
private class LegacyDrawableSliderPath : DrawableSliderPath
{
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
@@ -22,8 +29,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// Roughly matches osu!stable's slider border portions.
=> base.CalculatedBorderPortion * 0.77f;
- public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, 0.7f);
-
protected override Color4 ColourAt(float position)
{
float realBorderPortion = shadow_portion + CalculatedBorderPortion;
diff --git a/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs
new file mode 100644
index 0000000000..b4de91562b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.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 osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.UI
+{
+ ///
+ /// An which allows hitobjects to be hit in any order.
+ ///
+ public class AnyOrderHitPolicy : IHitPolicy
+ {
+ public IHitObjectContainer HitObjectContainer { get; set; }
+
+ public bool IsHittable(DrawableHitObject hitObject, double time) => true;
+
+ public void HandleHit(DrawableHitObject hitObject)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 7f86e9daf7..62cab4d6d7 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
private const int max_sprites = 2048;
+ ///
+ /// An exponentiating factor to ease the trail fade.
+ ///
+ protected virtual float FadeExponent => 1.7f;
+
private readonly TrailPart[] parts = new TrailPart[max_sprites];
private int currentIndex;
private IShader shader;
@@ -133,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected virtual bool InterpolateMovements => true;
protected virtual float IntervalMultiplier => 1.0f;
+ protected virtual bool AvoidDrawingNearCursor => false;
private Vector2? lastPosition;
private readonly InputResampler resampler = new InputResampler();
@@ -141,43 +147,45 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected override bool OnMouseMove(MouseMoveEvent e)
{
- Vector2 pos = e.ScreenSpaceMousePosition;
+ AddTrail(e.ScreenSpaceMousePosition);
+ return base.OnMouseMove(e);
+ }
- if (lastPosition == null)
+ protected void AddTrail(Vector2 position)
+ {
+ if (InterpolateMovements)
{
- lastPosition = pos;
- resampler.AddPosition(lastPosition.Value);
- return base.OnMouseMove(e);
- }
-
- foreach (Vector2 pos2 in resampler.AddPosition(pos))
- {
- Trace.Assert(lastPosition.HasValue);
-
- if (InterpolateMovements)
+ if (!lastPosition.HasValue)
{
- // ReSharper disable once PossibleInvalidOperationException
+ lastPosition = position;
+ resampler.AddPosition(lastPosition.Value);
+ return;
+ }
+
+ foreach (Vector2 pos2 in resampler.AddPosition(position))
+ {
+ Trace.Assert(lastPosition.HasValue);
+
Vector2 pos1 = lastPosition.Value;
Vector2 diff = pos2 - pos1;
float distance = diff.Length;
Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier;
+ float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
- for (float d = interval; d < distance; d += interval)
+ for (float d = interval; d < stopAt; d += interval)
{
lastPosition = pos1 + direction * d;
addPart(lastPosition.Value);
}
}
- else
- {
- lastPosition = pos2;
- addPart(lastPosition.Value);
- }
}
-
- return base.OnMouseMove(e);
+ else
+ {
+ lastPosition = position;
+ addPart(lastPosition.Value);
+ }
}
private void addPart(Vector2 screenSpacePosition)
@@ -206,10 +214,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Texture texture;
private float time;
+ private float fadeExponent;
private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size;
-
private Vector2 originPosition;
private readonly QuadBatch vertexBatch = new QuadBatch(max_sprites, 1);
@@ -227,6 +235,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
texture = Source.texture;
size = Source.partSize;
time = Source.time;
+ fadeExponent = Source.FadeExponent;
originPosition = Vector2.Zero;
@@ -249,6 +258,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
shader.Bind();
shader.GetUniform("g_FadeClock").UpdateValue(ref time);
+ shader.GetUniform("g_FadeExponent").UpdateValue(ref fadeExponent);
texture.TextureGL.Bind();
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
index 57ec51cf64..bfd6ac3ad3 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -119,9 +119,9 @@ namespace osu.Game.Rulesets.Osu.Utils
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));
- var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
+ var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints)
- point.Position.Value = new Vector2(-point.Position.Value.X, point.Position.Value.Y);
+ point.Position = new Vector2(-point.Position.X, point.Position.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
@@ -140,9 +140,9 @@ namespace osu.Game.Rulesets.Osu.Utils
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));
- var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
+ var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints)
- point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
+ point.Position = new Vector2(point.Position.X, -point.Position.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
index f9b8e9a985..269a855219 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Origin = Anchor.Centre,
Children = new Drawable[]
{
- new TaikoPlayfield(new ControlPointInfo()),
+ new TaikoPlayfield(),
hoc = new ScrollingHitObjectContainer()
}
};
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Origin = Anchor.Centre,
Children = new Drawable[]
{
- new TaikoPlayfield(new ControlPointInfo()),
+ new TaikoPlayfield(),
hoc = new ScrollingHitObjectContainer()
}
};
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
index 055a292fe8..24db046748 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
@@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.UI;
using osuTK;
@@ -17,6 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
+ var playfield = new TaikoPlayfield();
+
+ var beatmap = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo).GetPlayableBeatmap(new TaikoRuleset().RulesetInfo);
+
+ foreach (var h in beatmap.HitObjects)
+ playfield.Add(h);
+
SetContents(_ => new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{
RelativeSizeAxes = Axes.Both,
@@ -25,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
- Child = new InputDrum(new ControlPointInfo())
+ Child = new InputDrum(playfield.HitObjectContainer)
}
});
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
index f96297a06d..6f2fcd08f1 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Beatmap.Value.Track.Start();
});
- AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield(new ControlPointInfo())
+ AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 532fdc5cb0..b9b295767e 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
deleted file mode 100644
index e4dc261363..0000000000
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
+++ /dev/null
@@ -1,104 +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.Collections.Generic;
-using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Audio;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Skinning;
-
-namespace osu.Game.Rulesets.Taiko.Audio
-{
- ///
- /// Stores samples for the input drum.
- /// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point.
- ///
- public class DrumSampleContainer : LifetimeManagementContainer
- {
- private readonly ControlPointInfo controlPoints;
- private readonly Dictionary mappings = new Dictionary();
-
- private readonly IBindableList samplePoints = new BindableList();
-
- public DrumSampleContainer(ControlPointInfo controlPoints)
- {
- this.controlPoints = controlPoints;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- samplePoints.BindTo(controlPoints.SamplePoints);
- samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true);
- }
-
- private void recreateMappings()
- {
- mappings.Clear();
- ClearInternal();
-
- SampleControlPoint[] points = samplePoints.Count == 0
- ? new[] { controlPoints.SamplePointAt(double.MinValue) }
- : samplePoints.ToArray();
-
- for (int i = 0; i < points.Length; i++)
- {
- var samplePoint = points[i];
-
- var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue;
- var lifetimeEnd = i + 1 < points.Length ? points[i + 1].Time : double.MaxValue;
-
- AddInternal(mappings[samplePoint.Time] = new DrumSample(samplePoint)
- {
- LifetimeStart = lifetimeStart,
- LifetimeEnd = lifetimeEnd
- });
- }
- }
-
- public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
-
- public class DrumSample : CompositeDrawable
- {
- public override bool RemoveWhenNotAlive => false;
-
- public PausableSkinnableSound Centre { get; private set; }
- public PausableSkinnableSound Rim { get; private set; }
-
- private readonly SampleControlPoint samplePoint;
-
- private Bindable sampleBank;
- private BindableNumber sampleVolume;
-
- public DrumSample(SampleControlPoint samplePoint)
- {
- this.samplePoint = samplePoint;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- sampleBank = samplePoint.SampleBankBindable.GetBoundCopy();
- sampleBank.BindValueChanged(_ => recreate());
-
- sampleVolume = samplePoint.SampleVolumeBindable.GetBoundCopy();
- sampleVolume.BindValueChanged(_ => recreate());
-
- recreate();
- }
-
- private void recreate()
- {
- InternalChildren = new Drawable[]
- {
- Centre = new PausableSkinnableSound(samplePoint.GetSampleInfo()),
- Rim = new PausableSkinnableSound(samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP))
- };
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 90c99316b1..9b73e644c5 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -18,12 +18,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
internal class TaikoBeatmapConverter : BeatmapConverter
{
- ///
- /// osu! is generally slower than taiko, so a factor is added to increase
- /// speed. This must be used everywhere slider length or beat length is used.
- ///
- public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f;
-
///
/// Because swells are easier in taiko than spinners are in osu!,
/// legacy taiko multiplies a factor when converting the number of required hits.
@@ -52,10 +46,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
- // Rewrite the beatmap info to add the slider velocity multiplier
- original.BeatmapInfo = original.BeatmapInfo.Clone();
- original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
- original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
+ if (!(original.BeatmapInfo.BaseDifficulty is TaikoMutliplierAppliedDifficulty))
+ {
+ // Rewrite the beatmap info to add the slider velocity multiplier
+ original.BeatmapInfo = original.BeatmapInfo.Clone();
+ original.BeatmapInfo.BaseDifficulty = new TaikoMutliplierAppliedDifficulty(original.BeatmapInfo.BaseDifficulty);
+ }
Beatmap converted = base.ConvertBeatmap(original, cancellationToken);
@@ -155,7 +151,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
- double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
+ double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
@@ -194,5 +190,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}
protected override Beatmap CreateBeatmap() => new TaikoBeatmap();
+
+ private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty
+ {
+ public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty)
+ {
+ difficulty.CopyTo(this);
+ SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
index 769d021362..0c17ca66b9 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Calculates the colour coefficient of taiko difficulty.
///
- public class Colour : StrainSkill
+ public class Colour : StrainDecaySkill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
index a32f6ebe0d..973e55f4b4 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Calculates the rhythm coefficient of taiko difficulty.
///
- public class Rhythm : StrainSkill
+ public class Rhythm : StrainDecaySkill
{
protected override double SkillMultiplier => 10;
protected override double StrainDecayBase => 0;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index 4cceadb23f..54cf233d69 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
///
- public class Stamina : StrainSkill
+ public class Stamina : StrainDecaySkill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index c0377c67a5..b0634295d0 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -6,10 +6,10 @@ using System;
using System.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Judgements;
using osuTK;
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
double IHasDistance.Distance => Duration * Velocity;
SliderPath IHasPath.Path
- => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER);
+ => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER);
#endregion
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
index 795885d4b9..9d35093591 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
@@ -7,7 +7,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
-using osu.Game.Rulesets.Taiko.Audio;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
using osuTK;
@@ -111,7 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public readonly Sprite Centre;
[Resolved]
- private DrumSampleContainer sampleContainer { get; set; }
+ private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public LegacyHalfDrum(bool flipped)
{
@@ -143,17 +144,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public bool OnPressed(TaikoAction action)
{
Drawable target = null;
- var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction)
{
target = Centre;
- drumSample.Centre?.Play();
+ sampleTriggerSource.Play(HitType.Centre);
}
else if (action == RimAction)
{
target = Rim;
- drumSample.Rim?.Play();
+ sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index 1ad1e4495c..876fa207bf 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -3,6 +3,7 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -16,5 +17,26 @@ namespace osu.Game.Rulesets.Taiko.UI
this.MoveToY(-100, 500);
base.ApplyHitAnimations();
}
+
+ protected override Drawable CreateDefaultJudgement(HitResult result) => new TaikoJudgementPiece(result);
+
+ private class TaikoJudgementPiece : DefaultJudgementPiece
+ {
+ public TaikoJudgementPiece(HitResult result)
+ : base(result)
+ {
+ }
+
+ public override void PlayAnimation()
+ {
+ if (Result != HitResult.Miss)
+ {
+ JudgementText.ScaleTo(0.9f);
+ JudgementText.ScaleTo(1, 500, Easing.OutElastic);
+ }
+
+ base.PlayAnimation();
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 650ce1f5a3..6ddbf3c16b 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
- protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo);
+ protected override Playfield CreatePlayfield() => new TaikoPlayfield();
public override DrawableHitObject CreateDrawableRepresentation(TaikoHitObject h) => null;
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs
new file mode 100644
index 0000000000..3279d128d3
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.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.Game.Audio;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ public class DrumSampleTriggerSource : GameplaySampleTriggerSource
+ {
+ public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer)
+ : base(hitObjectContainer)
+ {
+ }
+
+ public void Play(HitType hitType)
+ {
+ var hitObject = GetMostValidObject();
+
+ if (hitObject == null)
+ return;
+
+ PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) });
+ }
+
+ public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 1ca1be1bdf..ddfaf64549 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -2,18 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Taiko.Audio;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float middle_split = 0.025f;
[Cached]
- private DrumSampleContainer sampleContainer;
+ private DrumSampleTriggerSource sampleTriggerSource;
- public InputDrum(ControlPointInfo controlPoints)
+ public InputDrum(HitObjectContainer hitObjectContainer)
{
- sampleContainer = new DrumSampleContainer(controlPoints);
+ sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
RelativeSizeAxes = Axes.Both;
}
@@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
}),
- sampleContainer
+ sampleTriggerSource
};
}
@@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centreHit;
[Resolved]
- private DrumSampleContainer sampleContainer { get; set; }
+ private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public TaikoHalfDrum(bool flipped)
{
@@ -156,21 +156,19 @@ namespace osu.Game.Rulesets.Taiko.UI
Drawable target = null;
Drawable back = null;
- var drumSample = sampleContainer.SampleAt(Time.Current);
-
if (action == CentreAction)
{
target = centreHit;
back = centre;
- drumSample.Centre?.Play();
+ sampleTriggerSource.Play(HitType.Centre);
}
else if (action == RimAction)
{
target = rimHit;
back = rim;
- drumSample.Rim?.Play();
+ sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 0d9e08b8b7..d650cab729 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -8,7 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
@@ -27,8 +26,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public class TaikoPlayfield : ScrollingPlayfield
{
- private readonly ControlPointInfo controlPoints;
-
///
/// Default height of a when inside a .
///
@@ -56,11 +53,6 @@ namespace osu.Game.Rulesets.Taiko.UI
private Container hitTargetOffsetContent;
- public TaikoPlayfield(ControlPointInfo controlPoints)
- {
- this.controlPoints = controlPoints;
- }
-
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -131,7 +123,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Children = new Drawable[]
{
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
- new InputDrum(controlPoints)
+ new InputDrum(HitObjectContainer)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyCacheTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyCacheTest.cs
deleted file mode 100644
index d407c0663f..0000000000
--- a/osu.Game.Tests/Beatmaps/BeatmapDifficultyCacheTest.cs
+++ /dev/null
@@ -1,56 +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 NUnit.Framework;
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu.Mods;
-
-namespace osu.Game.Tests.Beatmaps
-{
- [TestFixture]
- public class BeatmapDifficultyCacheTest
- {
- [Test]
- public void TestKeyEqualsWithDifferentModInstances()
- {
- var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
- var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
-
- Assert.That(key1, Is.EqualTo(key2));
- }
-
- [Test]
- public void TestKeyEqualsWithDifferentModOrder()
- {
- var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
- var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
-
- Assert.That(key1, Is.EqualTo(key2));
- }
-
- [TestCase(1.3, DifficultyRating.Easy)]
- [TestCase(1.993, DifficultyRating.Easy)]
- [TestCase(1.998, DifficultyRating.Normal)]
- [TestCase(2.4, DifficultyRating.Normal)]
- [TestCase(2.693, DifficultyRating.Normal)]
- [TestCase(2.698, DifficultyRating.Hard)]
- [TestCase(3.5, DifficultyRating.Hard)]
- [TestCase(3.993, DifficultyRating.Hard)]
- [TestCase(3.997, DifficultyRating.Insane)]
- [TestCase(5.0, DifficultyRating.Insane)]
- [TestCase(5.292, DifficultyRating.Insane)]
- [TestCase(5.297, DifficultyRating.Expert)]
- [TestCase(6.2, DifficultyRating.Expert)]
- [TestCase(6.493, DifficultyRating.Expert)]
- [TestCase(6.498, DifficultyRating.ExpertPlus)]
- [TestCase(8.3, DifficultyRating.ExpertPlus)]
- public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
- {
- var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
-
- Assert.AreEqual(expectedBracket, actualBracket);
- }
- }
-}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 4fe1cf3790..a4bf8c92e3 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -3,15 +3,12 @@
using System;
using System.IO;
-using NUnit.Framework;
-using osuTK;
-using osuTK.Graphics;
-using osu.Game.Tests.Resources;
using System.Linq;
+using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats;
+using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO;
using osu.Game.Rulesets.Catch;
@@ -19,9 +16,13 @@ using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Skinning;
+using osu.Game.Tests.Resources;
+using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Tests.Beatmaps.Formats
{
@@ -58,12 +59,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile);
Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
Assert.AreEqual(164471, metadata.PreviewTime);
- Assert.IsFalse(beatmapInfo.Countdown);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
Assert.IsTrue(beatmapInfo.RulesetID == 0);
Assert.IsFalse(beatmapInfo.LetterboxInBreaks);
Assert.IsFalse(beatmapInfo.SpecialStyle);
Assert.IsFalse(beatmapInfo.WidescreenStoryboard);
+ Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate);
+ Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
+ Assert.AreEqual(0, beatmapInfo.CountdownOffset);
}
}
@@ -165,7 +168,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
- var controlPoints = beatmap.ControlPointInfo;
+ var controlPoints = (LegacyControlPointInfo)beatmap.ControlPointInfo;
Assert.AreEqual(4, controlPoints.TimingPoints.Count);
Assert.AreEqual(5, controlPoints.DifficultyPoints.Count);
@@ -239,7 +242,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("overlapping-control-points.osu"))
using (var stream = new LineBufferedReader(resStream))
{
- var controlPoints = decoder.Decode(stream).ControlPointInfo;
+ var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(4));
Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3));
@@ -665,111 +668,111 @@ namespace osu.Game.Tests.Beatmaps.Formats
// Multi-segment
var first = ((IHasPath)decoded.HitObjects[0]).Path;
- Assert.That(first.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
- Assert.That(first.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
- Assert.That(first.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
- Assert.That(first.ControlPoints[1].Type.Value, Is.EqualTo(null));
+ Assert.That(first.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
+ Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
+ Assert.That(first.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
+ Assert.That(first.ControlPoints[1].Type, Is.EqualTo(null));
- Assert.That(first.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
- Assert.That(first.ControlPoints[2].Type.Value, Is.EqualTo(PathType.Bezier));
- Assert.That(first.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(68, 15)));
- Assert.That(first.ControlPoints[3].Type.Value, Is.EqualTo(null));
- Assert.That(first.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(259, -132)));
- Assert.That(first.ControlPoints[4].Type.Value, Is.EqualTo(null));
- Assert.That(first.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(92, -107)));
- Assert.That(first.ControlPoints[5].Type.Value, Is.EqualTo(null));
+ Assert.That(first.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
+ Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.Bezier));
+ Assert.That(first.ControlPoints[3].Position, Is.EqualTo(new Vector2(68, 15)));
+ Assert.That(first.ControlPoints[3].Type, Is.EqualTo(null));
+ Assert.That(first.ControlPoints[4].Position, Is.EqualTo(new Vector2(259, -132)));
+ Assert.That(first.ControlPoints[4].Type, Is.EqualTo(null));
+ Assert.That(first.ControlPoints[5].Position, Is.EqualTo(new Vector2(92, -107)));
+ Assert.That(first.ControlPoints[5].Type, Is.EqualTo(null));
// Single-segment
var second = ((IHasPath)decoded.HitObjects[1]).Path;
- Assert.That(second.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
- Assert.That(second.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
- Assert.That(second.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
- Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null));
- Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
- Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null));
+ Assert.That(second.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
+ Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
+ Assert.That(second.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
+ Assert.That(second.ControlPoints[1].Type, Is.EqualTo(null));
+ Assert.That(second.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
+ Assert.That(second.ControlPoints[2].Type, Is.EqualTo(null));
// Implicit multi-segment
var third = ((IHasPath)decoded.HitObjects[2]).Path;
- Assert.That(third.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
- Assert.That(third.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
- Assert.That(third.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(0, 192)));
- Assert.That(third.ControlPoints[1].Type.Value, Is.EqualTo(null));
- Assert.That(third.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(224, 192)));
- Assert.That(third.ControlPoints[2].Type.Value, Is.EqualTo(null));
+ Assert.That(third.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
+ Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
+ Assert.That(third.ControlPoints[1].Position, Is.EqualTo(new Vector2(0, 192)));
+ Assert.That(third.ControlPoints[1].Type, Is.EqualTo(null));
+ Assert.That(third.ControlPoints[2].Position, Is.EqualTo(new Vector2(224, 192)));
+ Assert.That(third.ControlPoints[2].Type, Is.EqualTo(null));
- Assert.That(third.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(224, 0)));
- Assert.That(third.ControlPoints[3].Type.Value, Is.EqualTo(PathType.Bezier));
- Assert.That(third.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(224, -192)));
- Assert.That(third.ControlPoints[4].Type.Value, Is.EqualTo(null));
- Assert.That(third.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(480, -192)));
- Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null));
- Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0)));
- Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null));
+ Assert.That(third.ControlPoints[3].Position, Is.EqualTo(new Vector2(224, 0)));
+ Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.Bezier));
+ Assert.That(third.ControlPoints[4].Position, Is.EqualTo(new Vector2(224, -192)));
+ Assert.That(third.ControlPoints[4].Type, Is.EqualTo(null));
+ Assert.That(third.ControlPoints[5].Position, Is.EqualTo(new Vector2(480, -192)));
+ Assert.That(third.ControlPoints[5].Type, Is.EqualTo(null));
+ Assert.That(third.ControlPoints[6].Position, Is.EqualTo(new Vector2(480, 0)));
+ Assert.That(third.ControlPoints[6].Type, Is.EqualTo(null));
// Last control point duplicated
var fourth = ((IHasPath)decoded.HitObjects[3]).Path;
- Assert.That(fourth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
- Assert.That(fourth.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
- Assert.That(fourth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(1, 1)));
- Assert.That(fourth.ControlPoints[1].Type.Value, Is.EqualTo(null));
- Assert.That(fourth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(2, 2)));
- Assert.That(fourth.ControlPoints[2].Type.Value, Is.EqualTo(null));
- Assert.That(fourth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(3, 3)));
- Assert.That(fourth.ControlPoints[3].Type.Value, Is.EqualTo(null));
- Assert.That(fourth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(3, 3)));
- Assert.That(fourth.ControlPoints[4].Type.Value, Is.EqualTo(null));
+ Assert.That(fourth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
+ Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
+ Assert.That(fourth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
+ Assert.That(fourth.ControlPoints[1].Type, Is.EqualTo(null));
+ Assert.That(fourth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
+ Assert.That(fourth.ControlPoints[2].Type, Is.EqualTo(null));
+ Assert.That(fourth.ControlPoints[3].Position, Is.EqualTo(new Vector2(3, 3)));
+ Assert.That(fourth.ControlPoints[3].Type, Is.EqualTo(null));
+ Assert.That(fourth.ControlPoints[4].Position, Is.EqualTo(new Vector2(3, 3)));
+ Assert.That(fourth.ControlPoints[4].Type, Is.EqualTo(null));
// Last control point in segment duplicated
var fifth = ((IHasPath)decoded.HitObjects[4]).Path;
- Assert.That(fifth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
- Assert.That(fifth.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
- Assert.That(fifth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(1, 1)));
- Assert.That(fifth.ControlPoints[1].Type.Value, Is.EqualTo(null));
- Assert.That(fifth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(2, 2)));
- Assert.That(fifth.ControlPoints[2].Type.Value, Is.EqualTo(null));
- Assert.That(fifth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(3, 3)));
- Assert.That(fifth.ControlPoints[3].Type.Value, Is.EqualTo(null));
- Assert.That(fifth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(3, 3)));
- Assert.That(fifth.ControlPoints[4].Type.Value, Is.EqualTo(null));
+ Assert.That(fifth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
+ Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
+ Assert.That(fifth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
+ Assert.That(fifth.ControlPoints[1].Type, Is.EqualTo(null));
+ Assert.That(fifth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
+ Assert.That(fifth.ControlPoints[2].Type, Is.EqualTo(null));
+ Assert.That(fifth.ControlPoints[3].Position, Is.EqualTo(new Vector2(3, 3)));
+ Assert.That(fifth.ControlPoints[3].Type, Is.EqualTo(null));
+ Assert.That(fifth.ControlPoints[4].Position, Is.EqualTo(new Vector2(3, 3)));
+ Assert.That(fifth.ControlPoints[4].Type, Is.EqualTo(null));
- Assert.That(fifth.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(4, 4)));
- Assert.That(fifth.ControlPoints[5].Type.Value, Is.EqualTo(PathType.Bezier));
- Assert.That(fifth.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(5, 5)));
- Assert.That(fifth.ControlPoints[6].Type.Value, Is.EqualTo(null));
+ Assert.That(fifth.ControlPoints[5].Position, Is.EqualTo(new Vector2(4, 4)));
+ Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.Bezier));
+ Assert.That(fifth.ControlPoints[6].Position, Is.EqualTo(new Vector2(5, 5)));
+ Assert.That(fifth.ControlPoints[6].Type, Is.EqualTo(null));
// Implicit perfect-curve multi-segment(Should convert to bezier to match stable)
var sixth = ((IHasPath)decoded.HitObjects[5]).Path;
- Assert.That(sixth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
- Assert.That(sixth.ControlPoints[0].Type.Value == PathType.Bezier);
- Assert.That(sixth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(75, 145)));
- Assert.That(sixth.ControlPoints[1].Type.Value == null);
- Assert.That(sixth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(170, 75)));
+ Assert.That(sixth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
+ Assert.That(sixth.ControlPoints[0].Type == PathType.Bezier);
+ Assert.That(sixth.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
+ Assert.That(sixth.ControlPoints[1].Type == null);
+ Assert.That(sixth.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
- Assert.That(sixth.ControlPoints[2].Type.Value == PathType.Bezier);
- Assert.That(sixth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(300, 145)));
- Assert.That(sixth.ControlPoints[3].Type.Value == null);
- Assert.That(sixth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(410, 20)));
- Assert.That(sixth.ControlPoints[4].Type.Value == null);
+ Assert.That(sixth.ControlPoints[2].Type == PathType.Bezier);
+ Assert.That(sixth.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
+ Assert.That(sixth.ControlPoints[3].Type == null);
+ Assert.That(sixth.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
+ Assert.That(sixth.ControlPoints[4].Type == null);
// Explicit perfect-curve multi-segment(Should not convert to bezier)
var seventh = ((IHasPath)decoded.HitObjects[6]).Path;
- Assert.That(seventh.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
- Assert.That(seventh.ControlPoints[0].Type.Value == PathType.PerfectCurve);
- Assert.That(seventh.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(75, 145)));
- Assert.That(seventh.ControlPoints[1].Type.Value == null);
- Assert.That(seventh.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(170, 75)));
+ Assert.That(seventh.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
+ Assert.That(seventh.ControlPoints[0].Type == PathType.PerfectCurve);
+ Assert.That(seventh.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
+ Assert.That(seventh.ControlPoints[1].Type == null);
+ Assert.That(seventh.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
- Assert.That(seventh.ControlPoints[2].Type.Value == PathType.PerfectCurve);
- Assert.That(seventh.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(300, 145)));
- Assert.That(seventh.ControlPoints[3].Type.Value == null);
- Assert.That(seventh.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(410, 20)));
- Assert.That(seventh.ControlPoints[4].Type.Value == null);
+ Assert.That(seventh.ControlPoints[2].Type == PathType.PerfectCurve);
+ Assert.That(seventh.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
+ Assert.That(seventh.ControlPoints[3].Type == null);
+ Assert.That(seventh.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
+ Assert.That(seventh.ControlPoints[4].Type == null);
}
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index 059432eeaf..896aa53f82 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -14,6 +14,7 @@ using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
+using osu.Game.Beatmaps.Legacy;
using osu.Game.IO;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Catch;
@@ -49,6 +50,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
}
+ [TestCaseSource(nameof(allBeatmaps))]
+ public void TestEncodeDecodeStabilityDoubleConvert(string name)
+ {
+ var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
+ var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
+
+ // run an extra convert. this is expected to be stable.
+ decodedAfterEncode.beatmap = convert(decodedAfterEncode.beatmap);
+
+ sort(decoded.beatmap);
+ sort(decodedAfterEncode.beatmap);
+
+ Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
+ Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
+ }
+
+ [TestCaseSource(nameof(allBeatmaps))]
+ public void TestEncodeDecodeStabilityWithNonLegacyControlPoints(string name)
+ {
+ var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
+
+ // we are testing that the transfer of relevant data to hitobjects (from legacy control points) sticks through encode/decode.
+ // before the encode step, the legacy information is removed here.
+ decoded.beatmap.ControlPointInfo = removeLegacyControlPointTypes(decoded.beatmap.ControlPointInfo);
+
+ var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
+
+ // in this process, we may lose some detail in the control points section.
+ // let's focus on only the hitobjects.
+ var originalHitObjects = decoded.beatmap.HitObjects.Serialize();
+ var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize();
+
+ Assert.That(newHitObjects, Is.EqualTo(originalHitObjects));
+
+ ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
+ {
+ // emulate non-legacy control points by cloning the non-legacy portion.
+ // the assertion is that the encoder can recreate this losslessly from hitobject data.
+ Assert.IsInstanceOf(controlPointInfo);
+
+ var newControlPoints = new ControlPointInfo();
+
+ foreach (var point in controlPointInfo.AllControlPoints)
+ {
+ // completely ignore "legacy" types, which have been moved to HitObjects.
+ // even though these would mostly be ignored by the Add call, they will still be available in groups,
+ // which isn't what we want to be testing here.
+ if (point is SampleControlPoint)
+ continue;
+
+ newControlPoints.Add(point.Time, point.DeepClone());
+ }
+
+ return newControlPoints;
+ }
+ }
+
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
@@ -116,7 +174,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
- private Stream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
+ private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream();
@@ -169,7 +227,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
- protected override ISkin GetSkin() => throw new NotImplementedException();
+ protected internal override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
index e97c83e2c2..1fc3abef9a 100644
--- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
@@ -50,12 +50,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
var beatmap = decodeAsJson(normal);
var beatmapInfo = beatmap.BeatmapInfo;
Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
- Assert.AreEqual(false, beatmapInfo.Countdown);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
Assert.AreEqual(false, beatmapInfo.SpecialStyle);
Assert.IsTrue(beatmapInfo.RulesetID == 0);
Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks);
Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard);
+ Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
+ Assert.AreEqual(0, beatmapInfo.CountdownOffset);
}
[Test]
diff --git a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs
new file mode 100644
index 0000000000..dcfeea5db8
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs
@@ -0,0 +1,173 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Tests.Beatmaps.IO;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Beatmaps
+{
+ [HeadlessTest]
+ public class TestSceneBeatmapDifficultyCache : OsuTestScene
+ {
+ public const double BASE_STARS = 5.55;
+
+ private BeatmapSetInfo importedSet;
+
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; }
+
+ private TestBeatmapDifficultyCache difficultyCache;
+
+ private IBindable starDifficultyBindable;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGameBase osu)
+ {
+ importedSet = ImportBeatmapTest.LoadQuickOszIntoOsu(osu).Result;
+ }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("setup difficulty cache", () =>
+ {
+ SelectedMods.Value = Array.Empty();
+
+ Child = difficultyCache = new TestBeatmapDifficultyCache();
+
+ starDifficultyBindable = difficultyCache.GetBindableDifficulty(importedSet.Beatmaps.First());
+ });
+
+ AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value?.Stars == BASE_STARS);
+ }
+
+ [Test]
+ public void TestStarDifficultyChangesOnModSettings()
+ {
+ OsuModDoubleTime dt = null;
+
+ AddStep("set computation function", () => difficultyCache.ComputeDifficulty = lookup =>
+ {
+ var modRateAdjust = (ModRateAdjust)lookup.OrderedMods.SingleOrDefault(mod => mod is ModRateAdjust);
+ return new StarDifficulty(BASE_STARS + modRateAdjust?.SpeedChange.Value ?? 0, 0);
+ });
+
+ AddStep("change selected mod to DT", () => SelectedMods.Value = new[] { dt = new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
+ AddUntilStep($"star difficulty -> {BASE_STARS + 1.5}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.5);
+
+ AddStep("change DT speed to 1.25", () => dt.SpeedChange.Value = 1.25);
+ AddUntilStep($"star difficulty -> {BASE_STARS + 1.25}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.25);
+
+ AddStep("change selected mod to NC", () => SelectedMods.Value = new[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } });
+ AddUntilStep($"star difficulty -> {BASE_STARS + 1.75}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.75);
+ }
+
+ [Test]
+ public void TestStarDifficultyAdjustHashCodeConflict()
+ {
+ OsuModDifficultyAdjust difficultyAdjust = null;
+
+ AddStep("set computation function", () => difficultyCache.ComputeDifficulty = lookup =>
+ {
+ var modDifficultyAdjust = (ModDifficultyAdjust)lookup.OrderedMods.SingleOrDefault(mod => mod is ModDifficultyAdjust);
+ return new StarDifficulty(BASE_STARS * (modDifficultyAdjust?.OverallDifficulty.Value ?? 1), 0);
+ });
+
+ AddStep("change selected mod to DA", () => SelectedMods.Value = new[] { difficultyAdjust = new OsuModDifficultyAdjust() });
+ AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value?.Stars == BASE_STARS);
+
+ AddStep("change DA difficulty to 0.5", () => difficultyAdjust.OverallDifficulty.Value = 0.5f);
+ AddUntilStep($"star difficulty -> {BASE_STARS * 0.5f}", () => starDifficultyBindable.Value?.Stars == BASE_STARS / 2);
+
+ // hash code of 0 (the value) conflicts with the hash code of null (the initial/default value).
+ // it's important that the mod reference and its underlying bindable references stay the same to demonstrate this failure.
+ AddStep("change DA difficulty to 0", () => difficultyAdjust.OverallDifficulty.Value = 0);
+ AddUntilStep("star difficulty -> 0", () => starDifficultyBindable.Value?.Stars == 0);
+ }
+
+ [Test]
+ public void TestKeyEqualsWithDifferentModInstances()
+ {
+ var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
+ var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
+
+ Assert.That(key1, Is.EqualTo(key2));
+ Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
+ }
+
+ [Test]
+ public void TestKeyEqualsWithDifferentModOrder()
+ {
+ var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
+ var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
+
+ Assert.That(key1, Is.EqualTo(key2));
+ Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
+ }
+
+ [Test]
+ public void TestKeyDoesntEqualWithDifferentModSettings()
+ {
+ var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
+ var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
+
+ Assert.That(key1, Is.Not.EqualTo(key2));
+ Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode()));
+ }
+
+ [Test]
+ public void TestKeyEqualWithMatchingModSettings()
+ {
+ var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
+ var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
+
+ Assert.That(key1, Is.EqualTo(key2));
+ Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
+ }
+
+ [TestCase(1.3, DifficultyRating.Easy)]
+ [TestCase(1.993, DifficultyRating.Easy)]
+ [TestCase(1.998, DifficultyRating.Normal)]
+ [TestCase(2.4, DifficultyRating.Normal)]
+ [TestCase(2.693, DifficultyRating.Normal)]
+ [TestCase(2.698, DifficultyRating.Hard)]
+ [TestCase(3.5, DifficultyRating.Hard)]
+ [TestCase(3.993, DifficultyRating.Hard)]
+ [TestCase(3.997, DifficultyRating.Insane)]
+ [TestCase(5.0, DifficultyRating.Insane)]
+ [TestCase(5.292, DifficultyRating.Insane)]
+ [TestCase(5.297, DifficultyRating.Expert)]
+ [TestCase(6.2, DifficultyRating.Expert)]
+ [TestCase(6.493, DifficultyRating.Expert)]
+ [TestCase(6.498, DifficultyRating.ExpertPlus)]
+ [TestCase(8.3, DifficultyRating.ExpertPlus)]
+ public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
+ {
+ var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
+
+ Assert.AreEqual(expectedBracket, actualBracket);
+ }
+
+ private class TestBeatmapDifficultyCache : BeatmapDifficultyCache
+ {
+ public Func ComputeDifficulty { get; set; }
+
+ protected override Task ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default)
+ {
+ return Task.FromResult(ComputeDifficulty?.Invoke(lookup) ?? new StarDifficulty(BASE_STARS, 0));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
index 0ec21a4c7b..5e22101e5c 100644
--- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs
+++ b/osu.Game.Tests/Chat/TestSceneChannelManager.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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -19,6 +20,7 @@ namespace osu.Game.Tests.Chat
{
private ChannelManager channelManager;
private int currentMessageId;
+ private List sentMessages;
[SetUp]
public void Setup() => Schedule(() =>
@@ -34,6 +36,7 @@ namespace osu.Game.Tests.Chat
AddStep("register request handling", () =>
{
currentMessageId = 0;
+ sentMessages = new List();
((DummyAPIAccess)API).HandleRequest = req =>
{
@@ -44,16 +47,11 @@ namespace osu.Game.Tests.Chat
return true;
case PostMessageRequest postMessage:
- postMessage.TriggerSuccess(new Message(++currentMessageId)
- {
- IsAction = postMessage.Message.IsAction,
- ChannelId = postMessage.Message.ChannelId,
- Content = postMessage.Message.Content,
- Links = postMessage.Message.Links,
- Timestamp = postMessage.Message.Timestamp,
- Sender = postMessage.Message.Sender
- });
+ handlePostMessageRequest(postMessage);
+ return true;
+ case MarkChannelAsReadRequest markRead:
+ handleMarkChannelAsReadRequest(markRead);
return true;
}
@@ -83,12 +81,65 @@ namespace osu.Game.Tests.Chat
AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to"));
}
+ [Test]
+ public void TestMarkAsReadIgnoringLocalMessages()
+ {
+ Channel channel = null;
+
+ AddStep("join channel and select it", () =>
+ {
+ channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public));
+ channelManager.CurrentChannel.Value = channel;
+ });
+
+ AddStep("post message", () => channelManager.PostMessage("Something interesting"));
+
+ AddStep("post /help command", () => channelManager.PostCommand("help", channel));
+ AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel));
+ AddStep("post /join command with no channel", () => channelManager.PostCommand("join", channel));
+ AddStep("post /join command with non-existent channel", () => channelManager.PostCommand("join i-dont-exist", channel));
+ AddStep("post non-existent command", () => channelManager.PostCommand("non-existent-cmd arg", channel));
+
+ AddStep("mark channel as read", () => channelManager.MarkChannelAsRead(channel));
+ AddAssert("channel's last read ID is set to the latest message", () => channel.LastReadId == sentMessages.Last().Id);
+ }
+
+ private void handlePostMessageRequest(PostMessageRequest request)
+ {
+ var message = new Message(++currentMessageId)
+ {
+ IsAction = request.Message.IsAction,
+ ChannelId = request.Message.ChannelId,
+ Content = request.Message.Content,
+ Links = request.Message.Links,
+ Timestamp = request.Message.Timestamp,
+ Sender = request.Message.Sender
+ };
+
+ sentMessages.Add(message);
+ request.TriggerSuccess(message);
+ }
+
+ private void handleMarkChannelAsReadRequest(MarkChannelAsReadRequest request)
+ {
+ // only accept messages that were sent through the API
+ if (sentMessages.Contains(request.Message))
+ {
+ request.TriggerSuccess();
+ }
+ else
+ {
+ request.TriggerFailure(new APIException("unknown message!", null));
+ }
+ }
+
private Channel createChannel(int id, ChannelType type) => new Channel(new User())
{
Id = id,
Name = $"Channel {id}",
Topic = $"Topic of channel {id} with type {type}",
Type = type,
+ LastMessageId = 0,
};
private class ChannelManagerContainer : CompositeDrawable
diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
index 8f5ebf53bd..d87ac29d75 100644
--- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
+++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Platform;
+using osu.Framework.Testing;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Collections.IO
@@ -127,7 +128,7 @@ namespace osu.Game.Tests.Collections.IO
[Test]
public async Task TestSaveAndReload()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
+ using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload", bypassCleanup: true))
{
try
{
@@ -148,7 +149,7 @@ namespace osu.Game.Tests.Collections.IO
}
}
- using (HeadlessGameHost host = new HeadlessGameHost("TestSaveAndReload"))
+ using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload"))
{
try
{
diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
index 642ecf00b8..8be74f1a7c 100644
--- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
+++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
@@ -11,6 +11,7 @@ using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Input;
using osu.Game.Input.Bindings;
+using osu.Game.Rulesets;
using Realms;
namespace osu.Game.Tests.Database
@@ -42,7 +43,7 @@ namespace osu.Game.Tests.Database
KeyBindingContainer testContainer = new TestKeyBindingContainer();
- keyBindingStore.Register(testContainer);
+ keyBindingStore.Register(testContainer, Enumerable.Empty());
Assert.That(queryCount(), Is.EqualTo(3));
@@ -66,7 +67,7 @@ namespace osu.Game.Tests.Database
{
KeyBindingContainer testContainer = new TestKeyBindingContainer();
- keyBindingStore.Register(testContainer);
+ keyBindingStore.Register(testContainer, Enumerable.Empty());
using (var primaryUsage = realmContextFactory.GetForRead())
{
diff --git a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
index 41a8f72305..4ab6e5cef6 100644
--- a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
@@ -30,7 +31,7 @@ namespace osu.Game.Tests.Editing.Checks
{
check = new CheckMutedObjects();
- cpi = new ControlPointInfo();
+ cpi = new LegacyControlPointInfo();
cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index aed28f5f84..3bf6aaac7a 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -204,7 +204,7 @@ namespace osu.Game.Tests.Gameplay
this.resources = resources;
}
- protected override ISkin GetSkin() => new TestSkin("test-sample", resources);
+ protected internal override ISkin GetSkin() => new TestSkin("test-sample", resources);
}
private class TestDrawableStoryboardSample : DrawableStoryboardSample
diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs
index 27cece42e8..b612899d79 100644
--- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs
+++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs
@@ -8,7 +8,7 @@ using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Input;
-using osu.Game.Tests.Visual.Navigation;
+using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Input
{
diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
index 7a5789f01a..ce6b3a68a5 100644
--- a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
+++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Game.Online.API;
+using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Mods
@@ -11,26 +12,42 @@ namespace osu.Game.Tests.Mods
public class ModSettingsEqualityComparison
{
[Test]
- public void Test()
+ public void TestAPIMod()
{
+ var apiMod1 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.25 } });
+ var apiMod2 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } });
+ var apiMod3 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } });
+
+ Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
+ Assert.That(apiMod2, Is.EqualTo(apiMod2));
+ Assert.That(apiMod2, Is.EqualTo(apiMod3));
+ Assert.That(apiMod3, Is.EqualTo(apiMod2));
+ }
+
+ [Test]
+ public void TestMod()
+ {
+ var ruleset = new OsuRuleset();
+
var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } };
var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
- var apiMod1 = new APIMod(mod1);
- var apiMod2 = new APIMod(mod2);
- var apiMod3 = new APIMod(mod3);
+
+ var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset);
+ var doulbeConvertedMod2 = new APIMod(mod2).ToMod(ruleset);
+ var doulbeConvertedMod3 = new APIMod(mod3).ToMod(ruleset);
Assert.That(mod1, Is.Not.EqualTo(mod2));
- Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
+ Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doulbeConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod2));
- Assert.That(apiMod2, Is.EqualTo(apiMod2));
+ Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod3));
- Assert.That(apiMod2, Is.EqualTo(apiMod3));
+ Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod3));
Assert.That(mod3, Is.EqualTo(mod2));
- Assert.That(apiMod3, Is.EqualTo(apiMod2));
+ Assert.That(doulbeConvertedMod3, Is.EqualTo(doulbeConvertedMod2));
}
}
}
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index 240ae4a90c..fabb016d5f 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -4,6 +4,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Tests.NonVisual
{
@@ -64,7 +65,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddRedundantSample()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new SampleControlPoint()); // is *not* redundant, special exception for first sample point
cpi.Add(1000, new SampleControlPoint()); // is redundant
@@ -142,7 +143,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestRemoveGroupAlsoRemovedControlPoints()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index 4c44e2ec72..5e14af5c27 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -6,10 +6,10 @@ using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using NUnit.Framework;
-using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Platform;
+using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.IO;
@@ -278,7 +278,7 @@ namespace osu.Game.Tests.NonVisual
private static string getDefaultLocationFor(string testTypeName)
{
- string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName);
+ string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, testTypeName);
if (Directory.Exists(path))
Directory.Delete(path, true);
@@ -288,7 +288,7 @@ namespace osu.Game.Tests.NonVisual
private string prepareCustomPath(string suffix = "")
{
- string path = Path.Combine(RuntimeInfo.StartupDirectory, $"custom-path{suffix}");
+ string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path{suffix}");
if (Directory.Exists(path))
Directory.Delete(path, true);
@@ -308,6 +308,19 @@ namespace osu.Game.Tests.NonVisual
InitialStorage = new DesktopStorage(defaultStorageLocation, this);
InitialStorage.DeleteDirectory(string.Empty);
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ try
+ {
+ // the storage may have changed from the initial location.
+ // this handles cleanup of the initial location.
+ InitialStorage.DeleteDirectory(string.Empty);
+ }
+ catch { }
+ }
}
}
}
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
index e45b8f7dc5..785f31386d 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
@@ -40,10 +40,10 @@ namespace osu.Game.Tests.NonVisual.Skinning
assertPlaybackPosition(0);
AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000);
- assertPlaybackPosition(-1000);
+ assertPlaybackPosition(0);
AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500);
- assertPlaybackPosition(-500);
+ assertPlaybackPosition(0);
}
private void assertPlaybackPosition(double expectedPosition)
diff --git a/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs
new file mode 100644
index 0000000000..5491774e26
--- /dev/null
+++ b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs
@@ -0,0 +1,72 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using MessagePack;
+using NUnit.Framework;
+using osu.Game.Online;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
+
+namespace osu.Game.Tests.Online
+{
+ [TestFixture]
+ public class TestMultiplayerMessagePackSerialization
+ {
+ [Test]
+ public void TestSerialiseRoom()
+ {
+ var room = new MultiplayerRoom(1)
+ {
+ MatchState = new TeamVersusRoomState()
+ };
+
+ var serialized = MessagePackSerializer.Serialize(room);
+
+ var deserialized = MessagePackSerializer.Deserialize(serialized);
+
+ Assert.IsTrue(deserialized.MatchState is TeamVersusRoomState);
+ }
+
+ [Test]
+ public void TestSerialiseUserStateExpected()
+ {
+ var state = new TeamVersusUserState();
+
+ var serialized = MessagePackSerializer.Serialize(typeof(MatchUserState), state);
+ var deserialized = MessagePackSerializer.Deserialize(serialized);
+
+ Assert.IsTrue(deserialized is TeamVersusUserState);
+ }
+
+ [Test]
+ public void TestSerialiseUnionFailsWithSingalR()
+ {
+ var state = new TeamVersusUserState();
+
+ // SignalR serialises using the actual type, rather than a base specification.
+ var serialized = MessagePackSerializer.Serialize(typeof(TeamVersusUserState), state);
+
+ // works with explicit type specified.
+ MessagePackSerializer.Deserialize(serialized);
+
+ // fails with base (union) type.
+ Assert.Throws(() => MessagePackSerializer.Deserialize(serialized));
+ }
+
+ [Test]
+ public void TestSerialiseUnionSucceedsWithWorkaround()
+ {
+ var state = new TeamVersusUserState();
+
+ // SignalR serialises using the actual type, rather than a base specification.
+ var serialized = MessagePackSerializer.Serialize(typeof(TeamVersusUserState), state, SignalRUnionWorkaroundResolver.OPTIONS);
+
+ // works with explicit type specified.
+ MessagePackSerializer.Deserialize(serialized);
+
+ // works with custom resolver.
+ var deserialized = MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
+ Assert.IsTrue(deserialized is TeamVersusUserState);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index cef0532f9d..7170a76b8b 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -9,6 +9,8 @@ namespace osu.Game.Tests.Resources
{
public static class TestResources
{
+ public const double QUICK_BEATMAP_LENGTH = 10000;
+
public static DllResourceStore GetStore() => new DllResourceStore(typeof(TestResources).Assembly);
public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}");
diff --git a/osu.Game.Tests/Resources/countdown-settings.osu b/osu.Game.Tests/Resources/countdown-settings.osu
new file mode 100644
index 0000000000..333e48150d
--- /dev/null
+++ b/osu.Game.Tests/Resources/countdown-settings.osu
@@ -0,0 +1,5 @@
+osu file format v14
+
+[General]
+Countdown: 2
+CountdownOffset: 3
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
index 184a94912a..f0d9ece06f 100644
--- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -36,9 +36,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)]
[TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)]
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
- [TestCase(ScoringMode.Classic, HitResult.Meh, 50)]
- [TestCase(ScoringMode.Classic, HitResult.Ok, 100)]
- [TestCase(ScoringMode.Classic, HitResult.Great, 300)]
+ [TestCase(ScoringMode.Classic, HitResult.Meh, 41)]
+ [TestCase(ScoringMode.Classic, HitResult.Ok, 46)]
+ [TestCase(ScoringMode.Classic, HitResult.Great, 72)]
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
scoreProcessor.Mode.Value = scoringMode;
@@ -85,19 +85,18 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
- [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
- [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
- [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
- [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 594)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
- [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] // (((3 * 300) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
- [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] // (((3 * 350) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
- [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] // (0 * 1 * 300) * (1 + 0 / 25)
- [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] // (((3 * 10) / (4 * 10)) * 1 * 300) * (1 + 0 / 25)
- [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
- [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] // (((3 * 50) / (4 * 50)) * 4 * 300) * (1 + 1 / 25)
- // TODO: The following two cases don't match expectations currently (a single hit is registered in acc portion when it shouldn't be). See https://github.com/ppy/osu/issues/12604.
- [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 330)] // (1 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points)
- [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 450)] // (1 * 1 * 300) * (1 + 0 / 25) + 3 * 50 (bonus points)
+ [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
+ [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 68)]
+ [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 81)]
+ [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 109)]
+ [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 149)]
+ [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 149)]
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 9)]
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 15)]
+ [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
+ [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 149)]
+ [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 18)]
+ [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 18)]
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
{
var minResult = new TestJudgement(hitResult).MinResult;
@@ -129,8 +128,8 @@ namespace osu.Game.Tests.Rulesets.Scoring
///
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
- [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
- [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 69)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 60)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
IEnumerable hitObjects = Enumerable
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index 8124bd4199..7a9fc20426 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -50,10 +50,10 @@ namespace osu.Game.Tests.Skins.IO
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
- Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(1));
+ Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Count, Is.EqualTo(1));
// the first should be overwritten by the second import.
- Assert.That(osu.Dependencies.Get().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
+ Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
}
finally
{
@@ -76,10 +76,10 @@ namespace osu.Game.Tests.Skins.IO
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
- Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(2));
+ Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Count, Is.EqualTo(2));
- Assert.That(osu.Dependencies.Get().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
- Assert.That(osu.Dependencies.Get().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
+ Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
+ Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
}
finally
{
@@ -101,10 +101,10 @@ namespace osu.Game.Tests.Skins.IO
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
- Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(2));
+ Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Count, Is.EqualTo(2));
- Assert.That(osu.Dependencies.Get().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
- Assert.That(osu.Dependencies.Get().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
+ Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
+ Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
}
finally
{
@@ -138,16 +138,42 @@ namespace osu.Game.Tests.Skins.IO
}
}
- private MemoryStream createOsk(string name, string author)
+ [Test]
+ public async Task TestSameMetadataNameDifferentFolderName()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
+ {
+ try
+ {
+ var osu = LoadOsuIntoHost(host);
+
+ var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1", false), "my custom skin 1"));
+ Assert.That(imported.Name, Is.EqualTo("name 1 [my custom skin 1]"));
+ Assert.That(imported.Creator, Is.EqualTo("author 1"));
+
+ var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1", false), "my custom skin 2"));
+ Assert.That(imported2.Name, Is.EqualTo("name 1 [my custom skin 2]"));
+ Assert.That(imported2.Creator, Is.EqualTo("author 1"));
+
+ Assert.That(imported2.Hash, Is.Not.EqualTo(imported.Hash));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ private MemoryStream createOsk(string name, string author, bool makeUnique = true)
{
var zipStream = new MemoryStream();
using var zip = ZipArchive.Create();
- zip.AddEntry("skin.ini", generateSkinIni(name, author));
+ zip.AddEntry("skin.ini", generateSkinIni(name, author, makeUnique));
zip.SaveTo(zipStream);
return zipStream;
}
- private MemoryStream generateSkinIni(string name, string author)
+ private MemoryStream generateSkinIni(string name, string author, bool makeUnique = true)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
@@ -155,8 +181,12 @@ namespace osu.Game.Tests.Skins.IO
writer.WriteLine("[General]");
writer.WriteLine($"Name: {name}");
writer.WriteLine($"Author: {author}");
- writer.WriteLine();
- writer.WriteLine($"# unique {Guid.NewGuid()}");
+
+ if (makeUnique)
+ {
+ writer.WriteLine();
+ writer.WriteLine($"# unique {Guid.NewGuid()}");
+ }
writer.Flush();
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index c15d804a19..aadabec100 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -133,11 +133,12 @@ namespace osu.Game.Tests.Skins
[Test]
public void TestEmptyComboColoursNoFallback()
{
- AddStep("Add custom combo colours to user skin", () => userSource.Configuration.AddComboColours(
+ AddStep("Add custom combo colours to user skin", () => userSource.Configuration.CustomComboColours = new List
+ {
new Color4(100, 150, 200, 255),
new Color4(55, 110, 166, 255),
new Color4(75, 125, 175, 255)
- ));
+ });
AddStep("Disallow default colours fallback in beatmap skin", () => beatmapSource.Configuration.AllowDefaultComboColoursFallback = false);
diff --git a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs
index 2236f85b92..cc8503589d 100644
--- a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs
+++ b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
+using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Online;
using osuTK;
@@ -15,6 +16,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Components
{
+ [HeadlessTest]
public class TestScenePollingComponent : OsuTestScene
{
private Container pollBox;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
index 6f5655006e..4813598c9d 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
@@ -30,7 +31,10 @@ namespace osu.Game.Tests.Visual.Editing
{
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
- Child = new ComposeScreen();
+ Child = new ComposeScreen
+ {
+ State = { Value = Visibility.Visible },
+ };
}
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs
new file mode 100644
index 0000000000..00f2979691
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs
@@ -0,0 +1,100 @@
+// 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.Globalization;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Setup;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneDesignSection : OsuManualInputManagerTestScene
+ {
+ private TestDesignSection designSection;
+ private EditorBeatmap editorBeatmap { get; set; }
+
+ [SetUpSteps]
+ public void SetUp()
+ {
+ AddStep("create blank beatmap", () => editorBeatmap = new EditorBeatmap(new Beatmap()));
+ AddStep("create section", () => Child = new DependencyProvidingContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(EditorBeatmap), editorBeatmap)
+ },
+ Child = designSection = new TestDesignSection()
+ });
+ }
+
+ [Test]
+ public void TestCountdownOff()
+ {
+ AddStep("turn countdown off", () => designSection.EnableCountdown.Current.Value = false);
+
+ AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.None);
+ AddUntilStep("other controls hidden", () => !designSection.CountdownSettings.IsPresent);
+ }
+
+ [Test]
+ public void TestCountdownOn()
+ {
+ AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
+
+ AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal);
+ AddUntilStep("other controls shown", () => designSection.CountdownSettings.IsPresent);
+
+ AddStep("change countdown speed", () => designSection.CountdownSpeed.Current.Value = CountdownType.DoubleSpeed);
+
+ AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.DoubleSpeed);
+ AddUntilStep("other controls still shown", () => designSection.CountdownSettings.IsPresent);
+ }
+
+ [Test]
+ public void TestCountdownOffset()
+ {
+ AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
+
+ AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal);
+
+ checkOffsetAfter("1", 1);
+ checkOffsetAfter(string.Empty, 0);
+ checkOffsetAfter("123", 123);
+ checkOffsetAfter("0", 0);
+ }
+
+ private void checkOffsetAfter(string userInput, int expectedFinalValue)
+ {
+ AddStep("click text box", () =>
+ {
+ var textBox = designSection.CountdownOffset.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(textBox);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddStep("set offset text", () => designSection.CountdownOffset.Current.Value = userInput);
+ AddStep("commit text", () => InputManager.Key(Key.Enter));
+
+ AddAssert($"displayed value is {expectedFinalValue}", () => designSection.CountdownOffset.Current.Value == expectedFinalValue.ToString(CultureInfo.InvariantCulture));
+ AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.BeatmapInfo.CountdownOffset == expectedFinalValue);
+ }
+
+ private class TestDesignSection : DesignSection
+ {
+ public new LabelledSwitchButton EnableCountdown => base.EnableCountdown;
+
+ public new FillFlowContainer CountdownSettings => base.CountdownSettings;
+ public new LabelledEnumDropdown CountdownSpeed => base.CountdownSpeed;
+ public new LabelledNumberBox CountdownOffset => base.CountdownOffset;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
new file mode 100644
index 0000000000..a439555fde
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
@@ -0,0 +1,124 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Overlays.Dialog;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Beatmaps.IO;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneDifficultySwitching : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override bool IsolateSavingFromDatabase => false;
+
+ [Resolved]
+ private OsuGameBase game { get; set; }
+
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; }
+
+ private BeatmapSetInfo importedBeatmapSet;
+
+ public override void SetUpSteps()
+ {
+ AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result);
+ base.SetUpSteps();
+ }
+
+ protected override void LoadEditor()
+ {
+ Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First());
+ base.LoadEditor();
+ }
+
+ [Test]
+ public void TestBasicSwitch()
+ {
+ BeatmapInfo targetDifficulty = null;
+
+ AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
+ switchToDifficulty(() => targetDifficulty);
+ confirmEditingBeatmap(() => targetDifficulty);
+
+ AddStep("exit editor", () => Stack.Exit());
+ // ensure editor loader didn't resume.
+ AddAssert("stack empty", () => Stack.CurrentScreen == null);
+ }
+
+ [Test]
+ public void TestPreventSwitchDueToUnsavedChanges()
+ {
+ BeatmapInfo targetDifficulty = null;
+ PromptForSaveDialog saveDialog = null;
+
+ AddStep("remove first hitobject", () => EditorBeatmap.RemoveAt(0));
+
+ AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
+ switchToDifficulty(() => targetDifficulty);
+
+ AddUntilStep("prompt for save dialog shown", () =>
+ {
+ saveDialog = this.ChildrenOfType().Single();
+ return saveDialog != null;
+ });
+ AddStep("continue editing", () =>
+ {
+ var continueButton = saveDialog.ChildrenOfType().Last();
+ continueButton.TriggerClick();
+ });
+
+ confirmEditingBeatmap(() => importedBeatmapSet.Beatmaps.First());
+
+ AddRepeatStep("exit editor forcefully", () => Stack.Exit(), 2);
+ // ensure editor loader didn't resume.
+ AddAssert("stack empty", () => Stack.CurrentScreen == null);
+ }
+
+ [Test]
+ public void TestAllowSwitchAfterDiscardingUnsavedChanges()
+ {
+ BeatmapInfo targetDifficulty = null;
+ PromptForSaveDialog saveDialog = null;
+
+ AddStep("remove first hitobject", () => EditorBeatmap.RemoveAt(0));
+
+ AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
+ switchToDifficulty(() => targetDifficulty);
+
+ AddUntilStep("prompt for save dialog shown", () =>
+ {
+ saveDialog = this.ChildrenOfType().Single();
+ return saveDialog != null;
+ });
+ AddStep("discard changes", () =>
+ {
+ var continueButton = saveDialog.ChildrenOfType().Single();
+ continueButton.TriggerClick();
+ });
+
+ confirmEditingBeatmap(() => targetDifficulty);
+
+ AddStep("exit editor forcefully", () => Stack.Exit());
+ // ensure editor loader didn't resume.
+ AddAssert("stack empty", () => Stack.CurrentScreen == null);
+ }
+
+ private void switchToDifficulty(Func difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke()));
+
+ private void confirmEditingBeatmap(Func targetDifficulty)
+ {
+ AddUntilStep("current beatmap is correct", () => Beatmap.Value.BeatmapInfo.Equals(targetDifficulty.Invoke()));
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index b6ae91844a..440d66ff9f 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Tests.Resources;
using SharpCompress.Archives;
@@ -55,6 +56,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestExitWithoutSave()
{
+ EditorBeatmap editorBeatmap = null;
+
+ AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
AddStep("exit without save", () =>
{
Editor.Exit();
@@ -62,7 +66,7 @@ namespace osu.Game.Tests.Visual.Editing
});
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
- AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
+ AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
index 0b1617b6a6..0abf0c47f8 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
@@ -78,6 +78,24 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
}
+ [Test]
+ public void TestClampWhenSeekOutsideBeatmapBounds()
+ {
+ AddStep("stop clock", Clock.Stop);
+
+ AddStep("seek before start time", () => Clock.Seek(-1000));
+ AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0);
+
+ AddStep("seek beyond track length", () => Clock.Seek(Clock.TrackLength + 1000));
+ AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength);
+
+ AddStep("seek smoothly before start time", () => Clock.SeekSmoothlyTo(-1000));
+ AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0);
+
+ AddStep("seek smoothly beyond track length", () => Clock.SeekSmoothlyTo(Clock.TrackLength + 1000));
+ AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength);
+ }
+
protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs
new file mode 100644
index 0000000000..98d8a41674
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Components.Menus;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneEditorScreenModes : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ [Test]
+ public void TestSwitchScreensInstantaneously()
+ {
+ AddStep("switch between all screens at once", () =>
+ {
+ foreach (var screen in Enum.GetValues(typeof(EditorScreenMode)).Cast())
+ Editor.ChildrenOfType().Single().Mode.Value = screen;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs
index 96ce418851..ff741a8ed5 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Editing
beatmap.BeatmapInfo.BeatDivisor = 1;
- beatmap.ControlPointInfo = new ControlPointInfo();
+ beatmap.ControlPointInfo.Clear();
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
beatmap.ControlPointInfo.Add(2000, new TimingControlPoint { BeatLength = 500 });
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs
index 62e12158ab..c3c803ff23 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs
@@ -3,8 +3,14 @@
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
@@ -22,11 +28,31 @@ namespace osu.Game.Tests.Visual.Editing
editorBeatmap = new EditorBeatmap(new OsuBeatmap());
}
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestOsu() => runForRuleset(new OsuRuleset().RulesetInfo);
+
+ [Test]
+ public void TestTaiko() => runForRuleset(new TaikoRuleset().RulesetInfo);
+
+ [Test]
+ public void TestCatch() => runForRuleset(new CatchRuleset().RulesetInfo);
+
+ [Test]
+ public void TestMania() => runForRuleset(new ManiaRuleset().RulesetInfo);
+
+ private void runForRuleset(RulesetInfo rulesetInfo)
{
- Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
- Child = new SetupScreen();
+ AddStep("create screen", () =>
+ {
+ editorBeatmap.BeatmapInfo.Ruleset = rulesetInfo;
+
+ Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
+
+ Child = new SetupScreen
+ {
+ State = { Value = Visibility.Visible },
+ };
+ });
}
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
index b82e776164..f961fff1e5 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
@@ -30,7 +31,10 @@ namespace osu.Game.Tests.Visual.Editing
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Beatmap.Disabled = true;
- Child = new TimingScreen();
+ Child = new TimingScreen
+ {
+ State = { Value = Visibility.Visible },
+ };
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
index b7dcad3825..00b5c38e20 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Configuration;
@@ -71,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var working = CreateWorkingBeatmap(rulesetInfo);
Beatmap.Value = working;
- SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) };
+ SelectedMods.Value = new[] { ruleset.CreateMod() };
Player = CreatePlayer(ruleset);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
index 13e84e335d..e560c81fb2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
@@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Gameplay
this.beatmapSkin = beatmapSkin;
}
- protected override ISkin GetSkin() => beatmapSkin;
+ protected internal override ISkin GetSkin() => beatmapSkin;
}
private class TestOsuRuleset : OsuRuleset
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
index 17fe09f2c6..0441c5641e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
@@ -40,6 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddStep("add local player", () => createLeaderboardScore(playerScore, new User { Username = "You", Id = 3 }, true));
+ AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
}
@@ -83,19 +84,38 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("add frenzibyte", () => createRandomScore(new User { Username = "frenzibyte", Id = 14210502 }));
}
+ [Test]
+ public void TestMaxHeight()
+ {
+ int playerNumber = 1;
+ AddRepeatStep("add 3 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 3);
+ checkHeight(4);
+
+ AddRepeatStep("add 4 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 4);
+ checkHeight(8);
+
+ AddRepeatStep("add 4 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 4);
+ checkHeight(8);
+
+ void checkHeight(int panelCount)
+ => AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
+ }
+
private void createRandomScore(User user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user);
private void createLeaderboardScore(BindableDouble score, User user, bool isTracked = false)
{
- var leaderboardScore = leaderboard.AddPlayer(user, isTracked);
+ var leaderboardScore = leaderboard.Add(user, isTracked);
leaderboardScore.TotalScore.BindTo(score);
}
private class TestGameplayLeaderboard : GameplayLeaderboard
{
+ public float Spacing => Flow.Spacing.Y;
+
public bool CheckPositionByUsername(string username, int? expectedPosition)
{
- var scoreItem = this.FirstOrDefault(i => i.User?.Username == username);
+ var scoreItem = Flow.FirstOrDefault(i => i.User?.Username == username);
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
new file mode 100644
index 0000000000..fccc1a377c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
@@ -0,0 +1,135 @@
+// 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.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.UI;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneGameplaySampleTriggerSource : PlayerTestScene
+ {
+ private TestGameplaySampleTriggerSource sampleTriggerSource;
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+
+ private Beatmap beatmap;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BaseDifficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
+ Ruleset = ruleset
+ }
+ };
+
+ const double start_offset = 8000;
+ const double spacing = 2000;
+
+ double t = start_offset;
+ beatmap.HitObjects.AddRange(new[]
+ {
+ new HitCircle
+ {
+ // intentionally start objects a bit late so we can test the case of no alive objects.
+ StartTime = t += spacing,
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ },
+ new HitCircle
+ {
+ StartTime = t += spacing,
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }
+ },
+ new HitCircle
+ {
+ StartTime = t += spacing,
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
+ SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
+ },
+ new HitCircle
+ {
+ StartTime = t + spacing,
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) },
+ SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
+ },
+ });
+
+ return beatmap;
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("Add trigger source", () => Player.HUDOverlay.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer)));
+ }
+
+ [Test]
+ public void TestCorrectHitObject()
+ {
+ HitObjectLifetimeEntry nextObjectEntry = null;
+
+ AddAssert("no alive objects", () => getNextAliveObject() == null);
+
+ AddAssert("check initially correct object", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[0]);
+
+ AddUntilStep("get next object", () =>
+ {
+ var nextDrawableObject = getNextAliveObject();
+
+ if (nextDrawableObject != null)
+ {
+ nextObjectEntry = nextDrawableObject.Entry;
+ InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
+ return true;
+ }
+
+ return false;
+ });
+
+ AddUntilStep("hit first hitobject", () =>
+ {
+ InputManager.Click(MouseButton.Left);
+ return nextObjectEntry.Result.HasResult;
+ });
+
+ AddAssert("check correct object after hit", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[1]);
+
+ AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[2]);
+ AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
+
+ AddUntilStep("no alive objects", () => getNextAliveObject() == null);
+ AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
+ }
+
+ private DrawableHitObject getNextAliveObject() =>
+ Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault();
+
+ [Test]
+ public void TestSampleTriggering()
+ {
+ AddRepeatStep("trigger sample", () => sampleTriggerSource.Play(), 10);
+ }
+
+ public class TestGameplaySampleTriggerSource : GameplaySampleTriggerSource
+ {
+ public TestGameplaySampleTriggerSource(HitObjectContainer hitObjectContainer)
+ : base(hitObjectContainer)
+ {
+ }
+
+ public new HitObject GetMostValidObject() => base.GetMostValidObject();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index b7e92a79a0..4b54cd3510 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -12,12 +12,15 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Skinning;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
{
+ private OsuConfigManager localConfig;
+
private HUDOverlay hudOverlay;
[Cached]
@@ -30,8 +33,14 @@ namespace osu.Game.Tests.Visual.Gameplay
private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
- [Resolved]
- private OsuConfigManager config { get; set; }
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
+ }
+
+ [SetUp]
+ public void SetUp() => Schedule(() => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always));
[Test]
public void TestComboCounterIncrementing()
@@ -84,11 +93,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
createNew();
- HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay;
-
- AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode));
-
- AddStep("set hud to never show", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
+ AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
@@ -97,37 +102,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft));
AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
-
- AddStep("set original config value", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
}
[Test]
public void TestExternalHideDoesntAffectConfig()
{
- HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay;
-
createNew();
- AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode));
-
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
- AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.HUDVisibilityMode));
+ AddAssert("config unchanged", () => localConfig.GetBindable(OsuSetting.HUDVisibilityMode).IsDefault);
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
- AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.HUDVisibilityMode));
+ AddAssert("config unchanged", () => localConfig.GetBindable(OsuSetting.HUDVisibilityMode).IsDefault);
}
[Test]
public void TestChangeHUDVisibilityOnHiddenKeyCounter()
{
- bool keyCounterVisibleValue = false;
-
createNew();
- AddStep("save keycounter visible value", () => keyCounterVisibleValue = config.Get(OsuSetting.KeyOverlay));
- AddStep("set keycounter visible false", () =>
+ AddStep("hide key overlay", () =>
{
- config.SetValue(OsuSetting.KeyOverlay, false);
+ localConfig.SetValue(OsuSetting.KeyOverlay, false);
hudOverlay.KeyCounter.AlwaysVisible.Value = false;
});
@@ -138,8 +134,20 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
+ }
- AddStep("return value", () => config.SetValue(OsuSetting.KeyOverlay, keyCounterVisibleValue));
+ [Test]
+ public void TestHiddenHUDDoesntBlockSkinnableComponentsLoad()
+ {
+ AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
+
+ createNew();
+
+ AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
+ AddUntilStep("wait for components to be hidden", () => !hudOverlay.ChildrenOfType().Single().IsPresent);
+
+ AddStep("reload components", () => hudOverlay.ChildrenOfType().Single().Reload());
+ AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType().Single().ComponentsLoaded);
}
private void createNew(Action action = null)
@@ -158,5 +166,11 @@ namespace osu.Game.Tests.Visual.Gameplay
Child = hudOverlay;
});
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ localConfig?.Dispose();
+ base.Dispose(isDisposing);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs
index 4fa4c00981..b308f3d7d8 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs
@@ -3,7 +3,6 @@
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Bindables;
using osu.Game.Overlays;
using osu.Game.Rulesets;
@@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.Gameplay
protected class OverlayTestPlayer : TestPlayer
{
public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value;
- public new Bindable LocalUserPlaying => base.LocalUserPlaying;
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
index 606395c289..9750838433 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
@@ -74,14 +74,14 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestAddControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100))));
- AddStep("add point", () => path.ControlPoints.Add(new PathControlPoint { Position = { Value = new Vector2(100) } }));
+ AddStep("add point", () => path.ControlPoints.Add(new PathControlPoint { Position = new Vector2(100) }));
}
[Test]
public void TestInsertControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100))));
- AddStep("insert point", () => path.ControlPoints.Insert(1, new PathControlPoint { Position = { Value = new Vector2(0, 100) } }));
+ AddStep("insert point", () => path.ControlPoints.Insert(1, new PathControlPoint { Position = new Vector2(0, 100) }));
}
[Test]
@@ -95,14 +95,14 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestChangePathType()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
- AddStep("change type to bezier", () => path.ControlPoints[0].Type.Value = PathType.Bezier);
+ AddStep("change type to bezier", () => path.ControlPoints[0].Type = PathType.Bezier);
}
[Test]
public void TestAddSegmentByChangingType()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))));
- AddStep("change second point type to bezier", () => path.ControlPoints[1].Type.Value = PathType.Bezier);
+ AddStep("change second point type to bezier", () => path.ControlPoints[1].Type = PathType.Bezier);
}
[Test]
@@ -111,10 +111,10 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
- path.ControlPoints[1].Type.Value = PathType.Bezier;
+ path.ControlPoints[1].Type = PathType.Bezier;
});
- AddStep("change second point type to null", () => path.ControlPoints[1].Type.Value = null);
+ AddStep("change second point type to null", () => path.ControlPoints[1].Type = null);
}
[Test]
@@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
- path.ControlPoints[1].Type.Value = PathType.Bezier;
+ path.ControlPoints[1].Type = PathType.Bezier;
});
AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
@@ -185,8 +185,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private List createSegment(PathType type, params Vector2[] controlPoints)
{
- var points = controlPoints.Select(p => new PathControlPoint { Position = { Value = p } }).ToList();
- points[0].Type.Value = type;
+ var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();
+ points[0].Type = type;
return points;
}
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
index aaf3323432..9037338e23 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
@@ -10,7 +10,6 @@ using osu.Game.Database;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Tests.Resources;
-using osu.Game.Tests.Visual.Navigation;
namespace osu.Game.Tests.Visual.Menus
{
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs
new file mode 100644
index 0000000000..e34ec6c46a
--- /dev/null
+++ b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs
@@ -0,0 +1,72 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osu.Game.Overlays;
+
+namespace osu.Game.Tests.Visual.Menus
+{
+ public class TestSceneSideOverlays : OsuGameTestScene
+ {
+ [SetUpSteps]
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddAssert("no screen offset applied", () => Game.ScreenOffsetContainer.X == 0f);
+ AddUntilStep("wait for overlays", () => Game.Settings.IsLoaded && Game.Notifications.IsLoaded);
+ }
+
+ [Test]
+ public void TestScreenOffsettingOnSettingsOverlay()
+ {
+ foreach (var scalingMode in Enum.GetValues(typeof(ScalingMode)).Cast())
+ {
+ AddStep($"set scaling mode to {scalingMode}", () =>
+ {
+ Game.LocalConfig.SetValue(OsuSetting.Scaling, scalingMode);
+
+ if (scalingMode != ScalingMode.Off)
+ {
+ Game.LocalConfig.SetValue(OsuSetting.ScalingSizeX, 0.5f);
+ Game.LocalConfig.SetValue(OsuSetting.ScalingSizeY, 0.5f);
+ }
+ });
+
+ AddStep("open settings", () => Game.Settings.Show());
+ AddUntilStep("right screen offset applied", () => Precision.AlmostEquals(Game.ScreenOffsetContainer.X, SettingsPanel.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO));
+
+ AddStep("hide settings", () => Game.Settings.Hide());
+ AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f);
+ }
+ }
+
+ [Test]
+ public void TestScreenOffsettingOnNotificationOverlay()
+ {
+ foreach (var scalingMode in Enum.GetValues(typeof(ScalingMode)).Cast())
+ {
+ if (scalingMode != ScalingMode.Off)
+ {
+ AddStep($"set scaling mode to {scalingMode}", () =>
+ {
+ Game.LocalConfig.SetValue(OsuSetting.Scaling, scalingMode);
+ Game.LocalConfig.SetValue(OsuSetting.ScalingSizeX, 0.5f);
+ Game.LocalConfig.SetValue(OsuSetting.ScalingSizeY, 0.5f);
+ });
+ }
+
+ AddStep("open notifications", () => Game.Notifications.Show());
+ AddUntilStep("right screen offset applied", () => Precision.AlmostEquals(Game.ScreenOffsetContainer.X, -NotificationOverlay.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO));
+
+ AddStep("hide notifications", () => Game.Notifications.Hide());
+ AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs
new file mode 100644
index 0000000000..af874cec91
--- /dev/null
+++ b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Tests.Visual.Mods
+{
+ public class TestSceneModFailCondition : ModTestScene
+ {
+ private bool restartRequested;
+
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+
+ protected override TestPlayer CreateModPlayer(Ruleset ruleset)
+ {
+ var player = base.CreateModPlayer(ruleset);
+ player.RestartRequested = () => restartRequested = true;
+ return player;
+ }
+
+ protected override bool AllowFail => true;
+
+ [SetUpSteps]
+ public void SetUp()
+ {
+ AddStep("reset flag", () => restartRequested = false);
+ }
+
+ [Test]
+ public void TestRestartOnFailDisabled() => CreateModTest(new ModTestData
+ {
+ Autoplay = false,
+ Mod = new OsuModSuddenDeath(),
+ PassCondition = () => !restartRequested && Player.ChildrenOfType().Single().State.Value == Visibility.Visible
+ });
+
+ [Test]
+ public void TestRestartOnFailEnabled() => CreateModTest(new ModTestData
+ {
+ Autoplay = false,
+ Mod = new OsuModSuddenDeath
+ {
+ Restart = { Value = true }
+ },
+ PassCondition = () => restartRequested && Player.ChildrenOfType().Single().State.Value == Visibility.Hidden
+ });
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
new file mode 100644
index 0000000000..b1f5781f6f
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
@@ -0,0 +1,164 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Online.Rooms;
+using osu.Game.Online.Rooms.RoomStatuses;
+using osu.Game.Overlays;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.OnlinePlay.Lounge;
+using osu.Game.Screens.OnlinePlay.Lounge.Components;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Users;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneDrawableRoom : OsuTestScene
+ {
+ [Cached]
+ protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
+
+ private readonly Bindable selectedRoom = new Bindable();
+
+ [Test]
+ public void TestMultipleStatuses()
+ {
+ AddStep("create rooms", () =>
+ {
+ Child = new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.9f),
+ Spacing = new Vector2(10),
+ Children = new Drawable[]
+ {
+ createDrawableRoom(new Room
+ {
+ Name = { Value = "Flyte's Trash Playlist" },
+ Status = { Value = new RoomStatusOpen() },
+ EndDate = { Value = DateTimeOffset.Now.AddDays(1) },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap =
+ {
+ Value = new TestBeatmap(new OsuRuleset().RulesetInfo)
+ {
+ BeatmapInfo =
+ {
+ StarDifficulty = 2.5
+ }
+ }.BeatmapInfo,
+ }
+ }
+ }
+ }),
+ createDrawableRoom(new Room
+ {
+ Name = { Value = "Room 2" },
+ Status = { Value = new RoomStatusPlaying() },
+ EndDate = { Value = DateTimeOffset.Now.AddDays(1) },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap =
+ {
+ Value = new TestBeatmap(new OsuRuleset().RulesetInfo)
+ {
+ BeatmapInfo =
+ {
+ StarDifficulty = 2.5
+ }
+ }.BeatmapInfo,
+ }
+ },
+ new PlaylistItem
+ {
+ Beatmap =
+ {
+ Value = new TestBeatmap(new OsuRuleset().RulesetInfo)
+ {
+ BeatmapInfo =
+ {
+ StarDifficulty = 4.5
+ }
+ }.BeatmapInfo,
+ }
+ }
+ }
+ }),
+ createDrawableRoom(new Room
+ {
+ Name = { Value = "Room 3" },
+ Status = { Value = new RoomStatusEnded() },
+ EndDate = { Value = DateTimeOffset.Now },
+ }),
+ createDrawableRoom(new Room
+ {
+ Name = { Value = "Room 4 (spotlight)" },
+ Status = { Value = new RoomStatusOpen() },
+ Category = { Value = RoomCategory.Spotlight },
+ }),
+ }
+ };
+ });
+ }
+
+ [Test]
+ public void TestEnableAndDisablePassword()
+ {
+ DrawableRoom drawableRoom = null;
+ Room room = null;
+
+ AddStep("create room", () => Child = drawableRoom = createDrawableRoom(room = new Room
+ {
+ Name = { Value = "Room with password" },
+ Status = { Value = new RoomStatusOpen() },
+ Type = { Value = MatchType.HeadToHead },
+ }));
+
+ AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType().Any());
+
+ AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha));
+
+ AddStep("set password", () => room.Password.Value = "password");
+ AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType().Single().Alpha));
+
+ AddStep("unset password", () => room.Password.Value = string.Empty);
+ AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha));
+ }
+
+ private DrawableRoom createDrawableRoom(Room room)
+ {
+ room.Host.Value ??= new User { Username = "peppy", Id = 2 };
+
+ if (room.RecentParticipants.Count == 0)
+ {
+ room.RecentParticipants.AddRange(Enumerable.Range(0, 20).Select(i => new User
+ {
+ Id = i,
+ Username = $"User {i}"
+ }));
+ }
+
+ return new DrawableLoungeRoom(room)
+ {
+ MatchingFilter = true,
+ SelectedRoom = { BindTarget = selectedRoom }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
new file mode 100644
index 0000000000..a3a1cacb0d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
@@ -0,0 +1,131 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Testing;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Screens.Play;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneGameplayChatDisplay : MultiplayerTestScene
+ {
+ private GameplayChatDisplay chatDisplay;
+
+ [Cached(typeof(ILocalUserPlayInfo))]
+ private ILocalUserPlayInfo localUserInfo;
+
+ private readonly Bindable localUserPlaying = new Bindable();
+
+ private TextBox textBox => chatDisplay.ChildrenOfType().First();
+
+ public TestSceneGameplayChatDisplay()
+ {
+ var mockLocalUserInfo = new Mock();
+ mockLocalUserInfo.SetupGet(i => i.IsPlaying).Returns(localUserPlaying);
+
+ localUserInfo = mockLocalUserInfo.Object;
+ }
+
+ [SetUpSteps]
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay(SelectedRoom.Value)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 0.5f,
+ });
+
+ AddStep("expand", () => chatDisplay.Expanded.Value = true);
+ }
+
+ [Test]
+ public void TestCantClickWhenPlaying()
+ {
+ setLocalUserPlaying(true);
+
+ AddStep("attempt focus chat", () =>
+ {
+ InputManager.MoveMouseTo(textBox);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ assertChatFocused(false);
+ }
+
+ [Test]
+ public void TestFocusDroppedWhenPlaying()
+ {
+ assertChatFocused(false);
+
+ AddStep("focus chat", () =>
+ {
+ InputManager.MoveMouseTo(textBox);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ setLocalUserPlaying(true);
+ assertChatFocused(false);
+
+ // should still stay non-focused even after entering a new break section.
+ setLocalUserPlaying(false);
+ assertChatFocused(false);
+ }
+
+ [Test]
+ public void TestFocusOnTabKeyWhenExpanded()
+ {
+ setLocalUserPlaying(true);
+
+ assertChatFocused(false);
+ AddStep("press tab", () => InputManager.Key(Key.Tab));
+ assertChatFocused(true);
+ }
+
+ [Test]
+ public void TestFocusOnTabKeyWhenNotExpanded()
+ {
+ AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
+ AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
+
+ AddStep("press tab", () => InputManager.Key(Key.Tab));
+ assertChatFocused(true);
+ AddUntilStep("is visible", () => chatDisplay.IsPresent);
+
+ AddStep("press enter", () => InputManager.Key(Key.Enter));
+ assertChatFocused(false);
+ AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
+ }
+
+ [Test]
+ public void TestFocusToggleViaAction()
+ {
+ AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
+ AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
+
+ AddStep("press tab", () => InputManager.Key(Key.Tab));
+ assertChatFocused(true);
+ AddUntilStep("is visible", () => chatDisplay.IsPresent);
+
+ AddStep("press tab", () => InputManager.Key(Key.Tab));
+ assertChatFocused(false);
+ AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
+ }
+
+ private void assertChatFocused(bool isFocused) =>
+ AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused);
+
+ private void setLocalUserPlaying(bool playing) =>
+ AddStep($"local user {(playing ? "playing" : "not playing")}", () => localUserPlaying.Value = playing);
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
deleted file mode 100644
index 471d0b6c98..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using NUnit.Framework;
-using osu.Framework.Graphics;
-using osu.Game.Online.Rooms;
-using osu.Game.Online.Rooms.RoomStatuses;
-using osu.Game.Screens.OnlinePlay.Lounge.Components;
-using osu.Game.Tests.Visual.OnlinePlay;
-using osu.Game.Users;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public class TestSceneLoungeRoomInfo : OnlinePlayTestScene
- {
- [SetUp]
- public new void Setup() => Schedule(() =>
- {
- SelectedRoom.Value = new Room();
-
- Child = new RoomInfo
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Width = 500
- };
- });
-
- [Test]
- public void TestNonSelectedRoom()
- {
- AddStep("set null room", () => SelectedRoom.Value.RoomID.Value = null);
- }
-
- [Test]
- public void TestOpenRoom()
- {
- AddStep("set open room", () =>
- {
- SelectedRoom.Value.RoomID.Value = 0;
- SelectedRoom.Value.Name.Value = "Room 0";
- SelectedRoom.Value.Host.Value = new User { Username = "peppy", Id = 2 };
- SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
- SelectedRoom.Value.Status.Value = new RoomStatusOpen();
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index bcbdcd2a4f..99b530c2a2 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -9,6 +9,7 @@ using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
@@ -17,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene
{
- protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager;
+ protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager;
private RoomsContainer container;
@@ -29,6 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
+ SelectedRoom = { BindTarget = SelectedRoom }
};
});
@@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add rooms", () => RoomManager.AddRooms(3));
AddAssert("has 3 rooms", () => container.Rooms.Count == 3);
- AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault()));
+ AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.FirstOrDefault()));
AddAssert("has 2 rooms", () => container.Rooms.Count == 2);
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
@@ -74,7 +76,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var room = RoomManager.Rooms[1];
RoomManager.RemoveRoom(room);
- RoomManager.AddRoom(room);
+ RoomManager.AddOrUpdateRoom(room);
});
AddAssert("no selection", () => checkRoomSelected(null));
@@ -115,11 +117,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
- AddStep("filter one room", () => container.Filter(new FilterCriteria { SearchString = "1" }));
+ AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" });
AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1);
- AddStep("remove filter", () => container.Filter(null));
+ AddStep("remove filter", () => container.Filter.Value = null);
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
}
@@ -131,13 +133,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo));
// Todo: What even is this case...?
- AddStep("set empty filter criteria", () => container.Filter(null));
+ AddStep("set empty filter criteria", () => container.Filter.Value = null);
AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
- AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo }));
+ AddStep("filter osu! rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo });
AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
- AddStep("filter catch rooms", () => container.Filter(new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo }));
+ AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo });
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
@@ -150,6 +152,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private bool checkRoomSelected(Room room) => SelectedRoom.Value == room;
private Room getRoomInFlow(int index) =>
- (container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room;
+ (container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room;
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
deleted file mode 100644
index 71ba5db481..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
+++ /dev/null
@@ -1,55 +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 NUnit.Framework;
-using osu.Game.Beatmaps;
-using osu.Game.Online.Rooms;
-using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Screens.OnlinePlay.Match.Components;
-using osu.Game.Tests.Visual.OnlinePlay;
-using osu.Game.Users;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public class TestSceneMatchHeader : OnlinePlayTestScene
- {
- [SetUp]
- public new void Setup() => Schedule(() =>
- {
- SelectedRoom.Value = new Room
- {
- Name = { Value = "A very awesome room" },
- Host = { Value = new User { Id = 2, Username = "peppy" } },
- Playlist =
- {
- new PlaylistItem
- {
- Beatmap =
- {
- Value = new BeatmapInfo
- {
- Metadata = new BeatmapMetadata
- {
- Title = "Title",
- Artist = "Artist",
- AuthorString = "Author",
- },
- Version = "Version",
- Ruleset = new OsuRuleset().RulesetInfo
- }
- },
- RequiredMods =
- {
- new OsuModDoubleTime(),
- new OsuModNoFail(),
- new OsuModRelax(),
- }
- }
- }
- };
-
- Child = new Header();
- });
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
index e14df62af1..ade24b8740 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
@@ -6,9 +6,11 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Timing;
+using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD;
+using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -31,7 +33,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
foreach (var (userId, _) in clocks)
+ {
SpectatorClient.StartPlay(userId, 0);
+ OnlinePlayDependencies.Client.AddUser(new User { Id = userId });
+ }
});
AddStep("create leaderboard", () =>
@@ -41,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable);
- LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add);
+ LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { Expanded = { Value = true } }, Add);
});
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index e9fae32335..bfcb55ce33 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -5,14 +5,22 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Users;
+using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -21,12 +29,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Resolved]
private OsuGameBase game { get; set; }
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
[Resolved]
private BeatmapManager beatmapManager { get; set; }
private MultiSpectatorScreen spectatorScreen;
- private readonly List playingUserIds = new List();
+ private readonly List playingUsers = new List();
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
@@ -41,7 +52,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[SetUp]
- public new void Setup() => Schedule(() => playingUserIds.Clear());
+ public new void Setup() => Schedule(() => playingUsers.Clear());
[Test]
public void TestDelayedStart()
@@ -51,8 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true);
OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true);
- playingUserIds.Add(PLAYER_1_ID);
- playingUserIds.Add(PLAYER_2_ID);
+ playingUsers.Add(new MultiplayerRoomUser(PLAYER_1_ID));
+ playingUsers.Add(new MultiplayerRoomUser(PLAYER_2_ID));
});
loadSpectateScreen(false);
@@ -69,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestGeneral()
{
- int[] userIds = Enumerable.Range(0, 4).Select(i => PLAYER_1_ID + i).ToArray();
+ int[] userIds = getPlayerIds(4);
start(userIds);
loadSpectateScreen();
@@ -78,6 +89,64 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 20);
}
+ [Test]
+ public void TestSpectatorPlayerInteractiveElementsHidden()
+ {
+ HUDVisibilityMode originalConfigValue = default;
+
+ AddStep("get original config hud visibility", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode));
+ AddStep("set config hud visibility to always", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always));
+
+ start(new[] { PLAYER_1_ID, PLAYER_2_ID });
+ loadSpectateScreen(false);
+
+ AddUntilStep("wait for player loaders", () => this.ChildrenOfType().Count() == 2);
+ AddAssert("all player loader settings hidden", () => this.ChildrenOfType().All(l => !l.ChildrenOfType>().Any()));
+
+ AddUntilStep("wait for players to load", () => spectatorScreen.AllPlayersLoaded);
+
+ // components wrapped in skinnable target containers load asynchronously, potentially taking more than one frame to load.
+ // therefore use until step rather than direct assert to account for that.
+ AddUntilStep("all interactive elements removed", () => this.ChildrenOfType().All(p =>
+ !p.ChildrenOfType().Any() &&
+ !p.ChildrenOfType().Any() &&
+ p.ChildrenOfType().SingleOrDefault()?.ShowHandle == false));
+
+ AddStep("restore config hud visibility", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
+ }
+
+ [Test]
+ public void TestTeamDisplay()
+ {
+ AddStep("start players", () =>
+ {
+ var player1 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true);
+ player1.MatchState = new TeamVersusUserState
+ {
+ TeamID = 0,
+ };
+
+ var player2 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true);
+ player2.MatchState = new TeamVersusUserState
+ {
+ TeamID = 1,
+ };
+
+ SpectatorClient.StartPlay(player1.UserID, importedBeatmapId);
+ SpectatorClient.StartPlay(player2.UserID, importedBeatmapId);
+
+ playingUsers.Add(player1);
+ playingUsers.Add(player2);
+ });
+
+ loadSpectateScreen();
+
+ sendFrames(PLAYER_1_ID, 1000);
+ sendFrames(PLAYER_2_ID, 1000);
+
+ AddWaitStep("wait a bit", 20);
+ }
+
[Test]
public void TestTimeDoesNotProgressWhileAllPlayersPaused()
{
@@ -247,6 +316,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType().Single().FrameStableClock.CurrentTime > 30000);
}
+ [Test]
+ public void TestPlayersLeaveWhileSpectating()
+ {
+ start(getPlayerIds(4));
+ sendFrames(getPlayerIds(4), 300);
+
+ loadSpectateScreen();
+
+ for (int count = 3; count >= 0; count--)
+ {
+ var id = PLAYER_1_ID + count;
+
+ end(id);
+ AddUntilStep($"{id} area grayed", () => getInstance(id).Colour != Color4.White);
+ AddUntilStep($"{id} score quit set", () => getLeaderboardScore(id).HasQuit.Value);
+ sendFrames(getPlayerIds(count), 300);
+ }
+
+ Player player = null;
+
+ AddStep($"get {PLAYER_1_ID} player instance", () => player = getInstance(PLAYER_1_ID).ChildrenOfType().Single());
+
+ start(new[] { PLAYER_1_ID });
+ sendFrames(PLAYER_1_ID, 300);
+
+ AddAssert($"{PLAYER_1_ID} player instance still same", () => getInstance(PLAYER_1_ID).ChildrenOfType().Single() == player);
+ AddAssert($"{PLAYER_1_ID} area still grayed", () => getInstance(PLAYER_1_ID).Colour != Color4.White);
+ AddAssert($"{PLAYER_1_ID} score quit still set", () => getLeaderboardScore(PLAYER_1_ID).HasQuit.Value);
+ }
+
private void loadSpectateScreen(bool waitForPlayerLoad = true)
{
AddStep("load screen", () =>
@@ -254,7 +353,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap);
Ruleset.Value = importedBeatmap.Ruleset;
- LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUserIds.ToArray()));
+ LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUsers.ToArray()));
});
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
@@ -266,14 +365,32 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
foreach (int id in userIds)
{
- OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true);
+ var user = new MultiplayerRoomUser(id)
+ {
+ User = new User { Id = id },
+ };
+ OnlinePlayDependencies.Client.AddUser(user.User, true);
SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
- playingUserIds.Add(id);
+
+ playingUsers.Add(user);
}
});
}
+ private void end(int userId)
+ {
+ AddStep($"end play for {userId}", () =>
+ {
+ var user = playingUsers.Single(u => u.UserID == userId);
+
+ OnlinePlayDependencies.Client.RemoveUser(user.User.AsNonNull());
+ SpectatorClient.EndPlay(userId);
+
+ playingUsers.Remove(user);
+ });
+ }
+
private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count);
private void sendFrames(int[] userIds, int count = 10)
@@ -307,5 +424,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single();
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId);
+
+ private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.Id == userId);
+
+ private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index cd3c50cf14..0b70703870 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -12,6 +13,7 @@ using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
@@ -25,9 +27,11 @@ using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
-using osu.Game.Screens.OnlinePlay.Match.Components;
+using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
+using osu.Game.Screens.Play;
+using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK.Input;
@@ -44,6 +48,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
private TestMultiplayer multiplayerScreen;
private TestMultiplayerClient client;
+ private TestRequestHandlingMultiplayerRoomManager roomManager => multiplayerScreen.RoomManager;
+
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
@@ -68,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("load dependencies", () =>
{
- client = new TestMultiplayerClient(multiplayerScreen.RoomManager);
+ client = new TestMultiplayerClient(roomManager);
// The screen gets suspended so it stops receiving updates.
Child = client;
@@ -87,6 +93,121 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestEmpty()
{
// used to test the flow of multiplayer from visual tests.
+ AddStep("empty step", () => { });
+ }
+
+ [Test]
+ public void TestLobbyEvents()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddRepeatStep("random stuff happens", performRandomAction, 30);
+
+ // ensure we have a handful of players so the ready-up sounds good :9
+ AddRepeatStep("player joins", addRandomPlayer, 5);
+
+ // all ready
+ AddUntilStep("all players ready", () =>
+ {
+ var nextUnready = client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
+ if (nextUnready != null)
+ client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
+
+ return client.Room?.Users.All(u => u.State == MultiplayerUserState.Ready) == true;
+ });
+
+ AddStep("unready all players at once", () =>
+ {
+ Debug.Assert(client.Room != null);
+
+ foreach (var u in client.Room.Users) client.ChangeUserState(u.UserID, MultiplayerUserState.Idle);
+ });
+
+ AddStep("ready all players at once", () =>
+ {
+ Debug.Assert(client.Room != null);
+
+ foreach (var u in client.Room.Users) client.ChangeUserState(u.UserID, MultiplayerUserState.Ready);
+ });
+ }
+
+ private void addRandomPlayer()
+ {
+ int randomUser = RNG.Next(200000, 500000);
+ client.AddUser(new User { Id = randomUser, Username = $"user {randomUser}" });
+ }
+
+ private void removeLastUser()
+ {
+ User lastUser = client.Room?.Users.Last().User;
+
+ if (lastUser == null || lastUser == client.LocalUser?.User)
+ return;
+
+ client.RemoveUser(lastUser);
+ }
+
+ private void kickLastUser()
+ {
+ User lastUser = client.Room?.Users.Last().User;
+
+ if (lastUser == null || lastUser == client.LocalUser?.User)
+ return;
+
+ client.KickUser(lastUser.Id);
+ }
+
+ private void markNextPlayerReady()
+ {
+ var nextUnready = client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
+ if (nextUnready != null)
+ client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
+ }
+
+ private void markNextPlayerIdle()
+ {
+ var nextUnready = client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Ready);
+ if (nextUnready != null)
+ client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Idle);
+ }
+
+ private void performRandomAction()
+ {
+ int eventToPerform = RNG.Next(1, 6);
+
+ switch (eventToPerform)
+ {
+ case 1:
+ addRandomPlayer();
+ break;
+
+ case 2:
+ removeLastUser();
+ break;
+
+ case 3:
+ kickLastUser();
+ break;
+
+ case 4:
+ markNextPlayerReady();
+ break;
+
+ case 5:
+ markNextPlayerIdle();
+ break;
+ }
}
[Test]
@@ -131,39 +252,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestExitMidJoin()
{
- Room room = null;
-
AddStep("create room", () =>
{
- room = new Room
- {
- Name = { Value = "Test Room" },
- Playlist =
- {
- new PlaylistItem
- {
- Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
- Ruleset = { Value = new OsuRuleset().RulesetInfo },
- }
- }
- };
- });
-
- AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
- AddStep("select room", () => InputManager.Key(Key.Down));
- AddStep("join room and immediately exit", () =>
- {
- multiplayerScreen.ChildrenOfType().Single().Open(room);
- Schedule(() => Stack.CurrentScreen.Exit());
- });
- }
-
- [Test]
- public void TestJoinRoomWithoutPassword()
- {
- AddStep("create room", () =>
- {
- multiplayerScreen.RoomManager.AddRoom(new Room
+ roomManager.AddServerSideRoom(new Room
{
Name = { Value = "Test Room" },
Playlist =
@@ -177,7 +268,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
});
- AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
+ AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter());
+ AddUntilStep("wait for room", () => this.ChildrenOfType().Any());
+
+ AddStep("select room", () => InputManager.Key(Key.Down));
+ AddStep("join room and immediately exit select", () =>
+ {
+ InputManager.Key(Key.Enter);
+ Schedule(() => Stack.CurrentScreen.Exit());
+ });
+ }
+
+ [Test]
+ public void TestJoinRoomWithoutPassword()
+ {
+ AddStep("create room", () =>
+ {
+ roomManager.AddServerSideRoom(new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+ });
+
+ AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter());
+ AddUntilStep("wait for room", () => this.ChildrenOfType().Any());
+
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
@@ -210,7 +333,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room", () =>
{
- multiplayerScreen.RoomManager.AddRoom(new Room
+ roomManager.AddServerSideRoom(new Room
{
Name = { Value = "Test Room" },
Password = { Value = "password" },
@@ -225,12 +348,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
});
- AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
+ AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter());
+ AddUntilStep("wait for room", () => this.ChildrenOfType().Any());
+
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
- DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
- AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
+ DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;
+ AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick());
@@ -312,6 +437,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
+ AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
+
AddStep("start match externally", () => client.StartMatch());
AddAssert("play not started", () => multiplayerScreen.IsCurrentScreen());
@@ -348,6 +475,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
+ AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
+
AddStep("start match externally", () => client.StartMatch());
AddStep("restore beatmap", () =>
@@ -396,16 +525,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddStep("open mod overlay", () => this.ChildrenOfType().ElementAt(2).TriggerClick());
+ AddStep("open mod overlay", () => this.ChildrenOfType().Single().TriggerClick());
AddStep("invoke on back button", () => multiplayerScreen.OnBackButton());
- AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
+ AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
- testLeave("lounge tab item", () => this.ChildrenOfType.BreadcrumbTabItem>().First().TriggerClick());
-
testLeave("back button", () => multiplayerScreen.OnBackButton());
// mimics home button and OS window close
@@ -421,12 +548,44 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
}
+ [Test]
+ public void TestGameplayFlow()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddRepeatStep("click spectate button", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ }, 2);
+
+ AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
+
+ // Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out.
+ for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000)
+ {
+ var time = i;
+ AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType().SingleOrDefault()?.GameplayClock.CurrentTime > time);
+ }
+
+ AddUntilStep("wait for results", () => Stack.CurrentScreen is ResultsScreen);
+ }
+
private void createRoom(Func room)
{
- AddStep("open room", () =>
- {
- multiplayerScreen.OpenNewRoom(room());
- });
+ AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+ AddStep("open room", () => multiplayerScreen.ChildrenOfType().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index 8121492a0b..3317ddc767 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Online.API;
+using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring;
@@ -51,12 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
OsuScoreProcessor scoreProcessor;
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
- var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
+ var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
+ var multiplayerUsers = new List();
foreach (var user in users)
{
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
- OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true);
+ multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true));
}
Children = new Drawable[]
@@ -64,9 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor = new OsuScoreProcessor(),
};
- scoreProcessor.ApplyBeatmap(playable);
+ scoreProcessor.ApplyBeatmap(playableBeatmap);
- LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, users.ToArray())
+ LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
new file mode 100644
index 0000000000..dfaf2f1dc3
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
@@ -0,0 +1,121 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Online.API;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
+using osu.Game.Online.Rooms;
+using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Tests.Visual.OnlinePlay;
+using osu.Game.Tests.Visual.Spectator;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerTestScene
+ {
+ private static IEnumerable users => Enumerable.Range(0, 16);
+
+ public new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient SpectatorClient =>
+ (TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient;
+
+ protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
+
+ protected class TestDependencies : MultiplayerTestSceneDependencies
+ {
+ protected override TestSpectatorClient CreateSpectatorClient() => new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient();
+ }
+
+ private MultiplayerGameplayLeaderboard leaderboard;
+ private GameplayMatchScoreDisplay gameplayScoreDisplay;
+
+ protected override Room CreateRoom()
+ {
+ var room = base.CreateRoom();
+ room.Type.Value = MatchType.TeamVersus;
+ return room;
+ }
+
+ [SetUpSteps]
+ public override void SetUpSteps()
+ {
+ AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result);
+
+ AddStep("create leaderboard", () =>
+ {
+ leaderboard?.Expire();
+
+ OsuScoreProcessor scoreProcessor;
+ Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
+
+ var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
+ var multiplayerUsers = new List();
+
+ foreach (var user in users)
+ {
+ SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
+ var roomUser = OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true);
+
+ roomUser.MatchState = new TeamVersusUserState
+ {
+ TeamID = RNG.Next(0, 2)
+ };
+
+ multiplayerUsers.Add(roomUser);
+ }
+
+ Children = new Drawable[]
+ {
+ scoreProcessor = new OsuScoreProcessor(),
+ };
+
+ scoreProcessor.ApplyBeatmap(playableBeatmap);
+
+ LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }, gameplayLeaderboard =>
+ {
+ LoadComponentAsync(new MatchScoreDisplay
+ {
+ Team1Score = { BindTarget = leaderboard.TeamScores[0] },
+ Team2Score = { BindTarget = leaderboard.TeamScores[1] }
+ }, Add);
+
+ LoadComponentAsync(gameplayScoreDisplay = new GameplayMatchScoreDisplay
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Team1Score = { BindTarget = leaderboard.TeamScores[0] },
+ Team2Score = { BindTarget = leaderboard.TeamScores[1] }
+ }, Add);
+
+ Add(gameplayLeaderboard);
+ });
+ });
+
+ AddUntilStep("wait for load", () => leaderboard.IsLoaded);
+ AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0);
+ }
+
+ [Test]
+ public void TestScoreUpdates()
+ {
+ AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100);
+ AddToggleStep("switch compact mode", expanded =>
+ {
+ leaderboard.Expanded.Value = expanded;
+ gameplayScoreDisplay.Expanded.Value = expanded;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
index c66d5429d6..61565c88f4 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
@@ -9,7 +9,6 @@ using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
-using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
@@ -18,7 +17,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene
{
- protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager;
+ protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager;
private LoungeSubScreen loungeScreen;
@@ -52,6 +51,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("room join password correct", () => lastJoinedPassword == null);
}
+ [Test]
+ public void TestPopoverHidesOnBackButton()
+ {
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ AddStep("select room", () => InputManager.Key(Key.Down));
+ AddStep("attempt join room", () => InputManager.Key(Key.Enter));
+
+ AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any());
+
+ AddAssert("textbox has focus", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
+
+ AddStep("hit escape", () => InputManager.Key(Key.Escape));
+ AddAssert("textbox lost focus", () => InputManager.FocusedDrawable is SearchTextBox);
+
+ AddStep("hit escape", () => InputManager.Key(Key.Escape));
+ AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any());
+ }
+
[Test]
public void TestPopoverHidesOnLeavingScreen()
{
@@ -59,20 +76,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
- AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any());
+ AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any());
AddStep("exit screen", () => Stack.Exit());
- AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any());
+ AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any());
}
[Test]
public void TestJoinRoomWithPassword()
{
- DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
+ DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
- AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
+ AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick());
@@ -83,12 +100,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithPasswordViaKeyboardOnly()
{
- DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
+ DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
- AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
+ AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password");
AddStep("press enter", () => InputManager.Key(Key.Enter));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 8bcb9cebbc..0d6b428cce 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -15,6 +15,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
@@ -89,7 +90,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
SelectedMods.SetDefault();
});
- AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect()));
+ AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value)));
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
}
@@ -168,6 +169,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new Bindable> FreeMods => base.FreeMods;
public new BeatmapCarousel Carousel => base.Carousel;
+
+ public TestMultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
+ : base(room, beatmap, ruleset)
+ {
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 955be6ca21..21364fe154 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -59,23 +59,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
}
- [Test]
- public void TestSettingValidity()
- {
- AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value);
-
- AddStep("set playlist", () =>
- {
- SelectedRoom.Value.Playlist.Add(new PlaylistItem
- {
- Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
- Ruleset = { Value = new OsuRuleset().RulesetInfo },
- });
- });
-
- AddAssert("create button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
- }
-
[Test]
public void TestCreatedRoom()
{
@@ -97,6 +80,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => Client.Room != null);
}
+ [Test]
+ public void TestSettingValidity()
+ {
+ AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("set playlist", () =>
+ {
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ });
+ });
+
+ AddAssert("create button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
+ }
+
[Test]
public void TestStartMatchWhileSpectating()
{
@@ -129,6 +129,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
+ AddUntilStep("wait for spectating user state", () => Client.LocalUser?.State == MultiplayerUserState.Spectating);
+
AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Enabled.Value);
AddStep("click ready button", () =>
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index 6526f7eea7..c4ebc13245 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
@@ -48,9 +49,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1);
- AddStep("add non-resolvable user", () => Client.AddNullUser(-3));
+ AddStep("add non-resolvable user", () => Client.AddNullUser());
+ AddAssert("null user added", () => Client.Room.AsNonNull().Users.Count(u => u.User == null) == 1);
AddUntilStep("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2);
+
+ AddStep("kick null user", () => this.ChildrenOfType().Single(p => p.User.User == null)
+ .ChildrenOfType().Single().TriggerClick());
+
+ AddAssert("null user kicked", () => Client.Room.AsNonNull().Users.Count == 1);
}
[Test]
@@ -155,6 +162,42 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1);
}
+ [Test]
+ public void TestKickButtonOnlyPresentWhenHost()
+ {
+ AddStep("add user", () => Client.AddUser(new User
+ {
+ Id = 3,
+ Username = "Second",
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }));
+
+ AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1);
+
+ AddStep("make second user host", () => Client.TransferHost(3));
+
+ AddUntilStep("kick buttons not visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 0);
+
+ AddStep("make local user host again", () => Client.TransferHost(API.LocalUser.Value.Id));
+
+ AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1);
+ }
+
+ [Test]
+ public void TestKickButtonKicks()
+ {
+ AddStep("add user", () => Client.AddUser(new User
+ {
+ Id = 3,
+ Username = "Second",
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }));
+
+ AddStep("kick second user", () => this.ChildrenOfType().Single(d => d.IsPresent).TriggerClick());
+
+ AddAssert("second user kicked", () => Client.Room?.Users.Single().UserID == API.LocalUser.Value.Id);
+ }
+
[Test]
public void TestManyUsers()
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
new file mode 100644
index 0000000000..a1543f99e1
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
@@ -0,0 +1,38 @@
+// 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.Osu;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerPlayer : MultiplayerTestScene
+ {
+ private MultiplayerPlayer player;
+
+ [SetUpSteps]
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("set beatmap", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ });
+
+ AddStep("initialise gameplay", () =>
+ {
+ Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, Client.CurrentMatchPlayingItem.Value, Client.Room?.Users.ToArray()));
+ });
+ }
+
+ [Test]
+ public void TestGameplay()
+ {
+ AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs
new file mode 100644
index 0000000000..ff06d4d9c7
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Game.Online.Rooms;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerResults : ScreenTestScene
+ {
+ [Test]
+ public void TestDisplayResults()
+ {
+ MultiplayerResultsScreen screen = null;
+
+ AddStep("show results screen", () =>
+ {
+ var rulesetInfo = new OsuRuleset().RulesetInfo;
+ var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
+
+ var score = new ScoreInfo
+ {
+ Rank = ScoreRank.B,
+ TotalScore = 987654,
+ Accuracy = 0.8,
+ MaxCombo = 500,
+ Combo = 250,
+ Beatmap = beatmapInfo,
+ User = new User { Username = "Test user" },
+ Date = DateTimeOffset.Now,
+ OnlineScoreID = 12345,
+ Ruleset = rulesetInfo,
+ };
+
+ PlaylistItem playlistItem = new PlaylistItem
+ {
+ BeatmapID = beatmapInfo.ID,
+ };
+
+ Stack.Push(screen = new MultiplayerResultsScreen(score, 1, playlistItem));
+ });
+
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
deleted file mode 100644
index b17427a30b..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
+++ /dev/null
@@ -1,157 +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 NUnit.Framework;
-using osu.Framework.Testing;
-using osu.Game.Online.Rooms;
-using osu.Game.Screens.OnlinePlay.Components;
-using osu.Game.Tests.Beatmaps;
-using osu.Game.Tests.Visual.OnlinePlay;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- [HeadlessTest]
- public class TestSceneMultiplayerRoomManager : MultiplayerTestScene
- {
- protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
-
- public TestSceneMultiplayerRoomManager()
- : base(false)
- {
- }
-
- [Test]
- public void TestPollsInitially()
- {
- AddStep("create room manager with a few rooms", () =>
- {
- RoomManager.CreateRoom(createRoom(r => r.Name.Value = "1"));
- RoomManager.PartRoom();
- RoomManager.CreateRoom(createRoom(r => r.Name.Value = "2"));
- RoomManager.PartRoom();
- RoomManager.ClearRooms();
- });
-
- AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2);
- AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value);
- }
-
- [Test]
- public void TestRoomsClearedOnDisconnection()
- {
- AddStep("create room manager with a few rooms", () =>
- {
- RoomManager.CreateRoom(createRoom());
- RoomManager.PartRoom();
- RoomManager.CreateRoom(createRoom());
- RoomManager.PartRoom();
- });
-
- AddStep("disconnect", () => Client.Disconnect());
-
- AddAssert("rooms cleared", () => ((RoomManager)RoomManager).Rooms.Count == 0);
- AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value);
- }
-
- [Test]
- public void TestRoomsPolledOnReconnect()
- {
- AddStep("create room manager with a few rooms", () =>
- {
- RoomManager.CreateRoom(createRoom());
- RoomManager.PartRoom();
- RoomManager.CreateRoom(createRoom());
- RoomManager.PartRoom();
- });
-
- AddStep("disconnect", () => Client.Disconnect());
- AddStep("connect", () => Client.Connect());
-
- AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2);
- AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value);
- }
-
- [Test]
- public void TestRoomsNotPolledWhenJoined()
- {
- AddStep("create room manager with a room", () =>
- {
- RoomManager.CreateRoom(createRoom());
- RoomManager.ClearRooms();
- });
-
- AddAssert("manager not polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 0);
- AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value);
- }
-
- [Test]
- public void TestMultiplayerRoomJoinedWhenCreated()
- {
- AddStep("create room manager with a room", () =>
- {
- RoomManager.CreateRoom(createRoom());
- });
-
- AddUntilStep("multiplayer room joined", () => Client.Room != null);
- }
-
- [Test]
- public void TestMultiplayerRoomPartedWhenAPIRoomParted()
- {
- AddStep("create room manager with a room", () =>
- {
- RoomManager.CreateRoom(createRoom());
- RoomManager.PartRoom();
- });
-
- AddAssert("multiplayer room parted", () => Client.Room == null);
- }
-
- [Test]
- public void TestMultiplayerRoomJoinedWhenAPIRoomJoined()
- {
- AddStep("create room manager with a room", () =>
- {
- var r = createRoom();
- RoomManager.CreateRoom(r);
- RoomManager.PartRoom();
- RoomManager.JoinRoom(r);
- });
-
- AddUntilStep("multiplayer room joined", () => Client.Room != null);
- }
-
- private Room createRoom(Action initFunc = null)
- {
- var room = new Room
- {
- Name =
- {
- Value = "test room"
- },
- Playlist =
- {
- new PlaylistItem
- {
- Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
- Ruleset = { Value = Ruleset.Value }
- }
- }
- };
-
- initFunc?.Invoke(room);
- return room;
- }
-
- private class TestDependencies : MultiplayerTestSceneDependencies
- {
- public TestDependencies()
- {
- // Need to set these values as early as possible.
- RoomManager.TimeBetweenListingPolls.Value = 1;
- RoomManager.TimeBetweenSelectionPolls.Value = 1;
- }
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
new file mode 100644
index 0000000000..0a8bda7ec0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Game.Online.Rooms;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerTeamResults : ScreenTestScene
+ {
+ [TestCase(7483253, 1048576)]
+ [TestCase(1048576, 7483253)]
+ [TestCase(1048576, 1048576)]
+ public void TestDisplayTeamResults(int team1Score, int team2Score)
+ {
+ MultiplayerResultsScreen screen = null;
+
+ AddStep("show results screen", () =>
+ {
+ var rulesetInfo = new OsuRuleset().RulesetInfo;
+ var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
+
+ var score = new ScoreInfo
+ {
+ Rank = ScoreRank.B,
+ TotalScore = 987654,
+ Accuracy = 0.8,
+ MaxCombo = 500,
+ Combo = 250,
+ Beatmap = beatmapInfo,
+ User = new User { Username = "Test user" },
+ Date = DateTimeOffset.Now,
+ OnlineScoreID = 12345,
+ Ruleset = rulesetInfo,
+ };
+
+ PlaylistItem playlistItem = new PlaylistItem
+ {
+ BeatmapID = beatmapInfo.ID,
+ };
+
+ SortedDictionary teamScores = new SortedDictionary
+ {
+ { 0, new BindableInt(team1Score) },
+ { 1, new BindableInt(team2Score) }
+ };
+
+ Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, playlistItem, teamScores));
+ });
+
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
index e4bf9b36ed..ba30315663 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
@@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
SelectedMods.Value = Array.Empty();
});
- AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect()));
+ AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(SelectedRoom.Value)));
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
}
@@ -183,6 +183,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
private class TestPlaylistsSongSelect : PlaylistsSongSelect
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
+
+ public TestPlaylistsSongSelect(Room room)
+ : base(room)
+ {
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs
new file mode 100644
index 0000000000..9e03743e8d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs
@@ -0,0 +1,95 @@
+// 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.Screens.OnlinePlay.Lounge.Components;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneRankRangePill : MultiplayerTestScene
+ {
+ [SetUp]
+ public new void Setup() => Schedule(() =>
+ {
+ Child = new RankRangePill
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ };
+ });
+
+ [Test]
+ public void TestSingleUser()
+ {
+ AddStep("add user", () =>
+ {
+ Client.AddUser(new User
+ {
+ Id = 2,
+ Statistics = { GlobalRank = 1234 }
+ });
+
+ // Remove the local user so only the one above is displayed.
+ Client.RemoveUser(API.LocalUser.Value);
+ });
+ }
+
+ [Test]
+ public void TestMultipleUsers()
+ {
+ AddStep("add users", () =>
+ {
+ Client.AddUser(new User
+ {
+ Id = 2,
+ Statistics = { GlobalRank = 1234 }
+ });
+
+ Client.AddUser(new User
+ {
+ Id = 3,
+ Statistics = { GlobalRank = 3333 }
+ });
+
+ Client.AddUser(new User
+ {
+ Id = 4,
+ Statistics = { GlobalRank = 4321 }
+ });
+
+ // Remove the local user so only the ones above are displayed.
+ Client.RemoveUser(API.LocalUser.Value);
+ });
+ }
+
+ [TestCase(1, 10)]
+ [TestCase(10, 100)]
+ [TestCase(100, 1000)]
+ [TestCase(1000, 10000)]
+ [TestCase(10000, 100000)]
+ [TestCase(100000, 1000000)]
+ [TestCase(1000000, 10000000)]
+ public void TestRange(int min, int max)
+ {
+ AddStep("add users", () =>
+ {
+ Client.AddUser(new User
+ {
+ Id = 2,
+ Statistics = { GlobalRank = min }
+ });
+
+ Client.AddUser(new User
+ {
+ Id = 3,
+ Statistics = { GlobalRank = max }
+ });
+
+ // Remove the local user so only the ones above are displayed.
+ Client.RemoveUser(API.LocalUser.Value);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs
new file mode 100644
index 0000000000..50ec2bf3ac
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs
@@ -0,0 +1,143 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Online.Rooms;
+using osu.Game.Screens.OnlinePlay.Lounge.Components;
+using osu.Game.Tests.Visual.OnlinePlay;
+using osu.Game.Users;
+using osu.Game.Users.Drawables;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneRecentParticipantsList : OnlinePlayTestScene
+ {
+ private RecentParticipantsList list;
+
+ [SetUp]
+ public new void Setup() => Schedule(() =>
+ {
+ SelectedRoom.Value = new Room { Name = { Value = "test room" } };
+
+ Child = list = new RecentParticipantsList
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ NumberOfCircles = 4
+ };
+ });
+
+ [Test]
+ public void TestCircleCountNearLimit()
+ {
+ AddStep("add 8 users", () =>
+ {
+ for (int i = 0; i < 8; i++)
+ addUser(i);
+ });
+
+ AddStep("set 8 circles", () => list.NumberOfCircles = 8);
+ AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0);
+
+ AddStep("add one more user", () => addUser(9));
+ AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2);
+
+ AddStep("remove first user", () => removeUserAt(0));
+ AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0);
+
+ AddStep("add one more user", () => addUser(9));
+ AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2);
+
+ AddStep("remove last user", () => removeUserAt(8));
+ AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0);
+ }
+
+ [Test]
+ public void TestHiddenUsersBecomeDisplayed()
+ {
+ AddStep("add 8 users", () =>
+ {
+ for (int i = 0; i < 8; i++)
+ addUser(i);
+ });
+
+ AddStep("set 3 circles", () => list.NumberOfCircles = 3);
+
+ for (int i = 0; i < 8; i++)
+ {
+ AddStep("remove user", () => removeUserAt(0));
+ int remainingUsers = 7 - i;
+
+ int displayedUsers = remainingUsers > 3 ? 2 : remainingUsers;
+ AddAssert($"{displayedUsers} avatars displayed", () => list.ChildrenOfType().Count() == displayedUsers);
+ }
+ }
+
+ [Test]
+ public void TestCircleCount()
+ {
+ AddStep("add 50 users", () =>
+ {
+ for (int i = 0; i < 50; i++)
+ addUser(i);
+ });
+
+ AddStep("set 3 circles", () => list.NumberOfCircles = 3);
+ AddAssert("2 users displayed", () => list.ChildrenOfType().Count() == 2);
+ AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48);
+
+ AddStep("set 10 circles", () => list.NumberOfCircles = 10);
+ AddAssert("9 users displayed", () => list.ChildrenOfType().Count() == 9);
+ AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41);
+ }
+
+ [Test]
+ public void TestAddAndRemoveUsers()
+ {
+ AddStep("add 50 users", () =>
+ {
+ for (int i = 0; i < 50; i++)
+ addUser(i);
+ });
+
+ AddStep("remove from start", () => removeUserAt(0));
+ AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3);
+ AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46);
+
+ AddStep("remove from end", () => removeUserAt(SelectedRoom.Value.RecentParticipants.Count - 1));
+ AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3);
+ AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45);
+
+ AddRepeatStep("remove 45 users", () => removeUserAt(0), 45);
+ AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3);
+ AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0);
+ AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f);
+
+ AddStep("remove another user", () => removeUserAt(0));
+ AddAssert("2 circles displayed", () => list.ChildrenOfType().Count() == 2);
+ AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0);
+
+ AddRepeatStep("remove the remaining two users", () => removeUserAt(0), 2);
+ AddAssert("0 circles displayed", () => !list.ChildrenOfType().Any());
+ }
+
+ private void addUser(int id)
+ {
+ SelectedRoom.Value.RecentParticipants.Add(new User
+ {
+ Id = id,
+ Username = $"User {id}"
+ });
+ SelectedRoom.Value.ParticipantCount.Value++;
+ }
+
+ private void removeUserAt(int index)
+ {
+ SelectedRoom.Value.RecentParticipants.RemoveAt(index);
+ SelectedRoom.Value.ParticipantCount.Value--;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs
deleted file mode 100644
index 8c4133418c..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs
+++ /dev/null
@@ -1,81 +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 System.Linq;
-using NUnit.Framework;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Testing;
-using osu.Framework.Utils;
-using osu.Game.Online.Rooms;
-using osu.Game.Online.Rooms.RoomStatuses;
-using osu.Game.Screens.OnlinePlay.Lounge.Components;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public class TestSceneRoomStatus : OsuTestScene
- {
- [Test]
- public void TestMultipleStatuses()
- {
- AddStep("create rooms", () =>
- {
- Child = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
- Children = new Drawable[]
- {
- new DrawableRoom(new Room
- {
- Name = { Value = "Open - ending in 1 day" },
- Status = { Value = new RoomStatusOpen() },
- EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
- }) { MatchingFilter = true },
- new DrawableRoom(new Room
- {
- Name = { Value = "Playing - ending in 1 day" },
- Status = { Value = new RoomStatusPlaying() },
- EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
- }) { MatchingFilter = true },
- new DrawableRoom(new Room
- {
- Name = { Value = "Ended" },
- Status = { Value = new RoomStatusEnded() },
- EndDate = { Value = DateTimeOffset.Now }
- }) { MatchingFilter = true },
- new DrawableRoom(new Room
- {
- Name = { Value = "Open" },
- Status = { Value = new RoomStatusOpen() },
- Category = { Value = RoomCategory.Realtime }
- }) { MatchingFilter = true },
- }
- };
- });
- }
-
- [Test]
- public void TestEnableAndDisablePassword()
- {
- DrawableRoom drawableRoom = null;
- Room room = null;
-
- AddStep("create room", () => Child = drawableRoom = new DrawableRoom(room = new Room
- {
- Name = { Value = "Room with password" },
- Status = { Value = new RoomStatusOpen() },
- Category = { Value = RoomCategory.Realtime },
- }) { MatchingFilter = true });
-
- AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha));
-
- AddStep("set password", () => room.Password.Value = "password");
- AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType().Single().Alpha));
-
- AddStep("unset password", () => room.Password.Value = string.Empty);
- AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha));
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
index e19665497d..a8fda19c60 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
@@ -17,6 +17,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
+using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
@@ -150,10 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createRoom(Func