diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 4177c402aa..5a3eadf607 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -9,7 +9,7 @@
]
},
"jetbrains.resharper.globaltools": {
- "version": "2020.3.2",
+ "version": "2022.1.0-eap10",
"commands": [
"jb"
]
@@ -27,7 +27,7 @@
]
},
"ppy.localisationanalyser.tools": {
- "version": "2022.320.0",
+ "version": "2022.417.0",
"commands": [
"localisation"
]
diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml
index 5b19c3732c..91ca622f55 100644
--- a/.github/ISSUE_TEMPLATE/bug-issue.yml
+++ b/.github/ISSUE_TEMPLATE/bug-issue.yml
@@ -9,7 +9,7 @@ body:
Important to note that your issue may have already been reported before. Please check:
- Pinned issues, at the top of https://github.com/ppy/osu/issues.
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- - And most importantly, search for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful.
+ - And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful.
- type: dropdown
attributes:
@@ -48,20 +48,28 @@ body:
Attaching log files is required for every reported bug. See instructions below on how to find them.
+ **Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
+
+ ### Desktop platforms
+
If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there
- **Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
-
- The default places to find the logs are as follows:
+ The default places to find the logs on desktop platforms are as follows:
- `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux & macOS*
- - `Android/data/sh.ppy.osulazer/files/logs` *on Android*
- - *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
If you have selected a custom location for the game files, you can find the `logs` folder there.
+ ### Mobile platforms
+
+ The places to find the logs on mobile platforms are as follows:
+ - *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
+ - *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
+
+ ---
+
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ec3816d541..f2066f27de 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,6 +2,60 @@ on: [push, pull_request]
name: Continuous Integration
jobs:
+ inspect-code:
+ name: Code Quality
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ # FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
+ # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
+ - name: Install .NET 3.1.x LTS
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "3.1.x"
+
+ - name: Install .NET 6.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "6.0.x"
+
+ - name: Restore Tools
+ run: dotnet tool restore
+
+ - name: Restore Packages
+ run: dotnet restore
+
+ - name: Restore inspectcode cache
+ uses: actions/cache@v3
+ with:
+ path: ${{ github.workspace }}/inspectcode
+ key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }}
+
+ - name: CodeFileSanity
+ run: |
+ # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
+ # FIXME: Suppress warnings from templates project
+ exit_code=0
+ while read -r line; do
+ if [[ ! -z "$line" ]]; then
+ echo "::error::$line"
+ exit_code=1
+ fi
+ done <<< $(dotnet codefilesanity)
+ exit $exit_code
+
+ # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
+ # - name: .NET Format (Dry Run)
+ # run: dotnet format --dry-run --check
+
+ - name: InspectCode
+ run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+
+ - name: NVika
+ run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
+
test:
name: Test
runs-on: ${{matrix.os.fullname}}
@@ -93,52 +147,4 @@ jobs:
# cannot accept .sln(f) files as arguments.
# Build just the main game for now.
- name: Build
- run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
-
- inspect-code:
- name: Code Quality
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
-
- # FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
- # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- - name: Install .NET 3.1.x LTS
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: "3.1.x"
-
- - name: Install .NET 6.0.x
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: "6.0.x"
-
- - name: Restore Tools
- run: dotnet tool restore
-
- - name: Restore Packages
- run: dotnet restore
-
- - name: CodeFileSanity
- run: |
- # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
- # FIXME: Suppress warnings from templates project
- exit_code=0
- while read -r line; do
- if [[ ! -z "$line" ]]; then
- echo "::error::$line"
- exit_code=1
- fi
- done <<< $(dotnet codefilesanity)
- exit $exit_code
-
- # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
- # - name: .NET Format (Dry Run)
- # run: dotnet format --dry-run --check
-
- - name: InspectCode
- run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
-
- - name: NVika
- run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
+ run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/misc.xml b/.idea/.idea.osu.Desktop/.idea/misc.xml
index 1d8c84d0af..4e1d56f4dd 100644
--- a/.idea/.idea.osu.Desktop/.idea/misc.xml
+++ b/.idea/.idea.osu.Desktop/.idea/misc.xml
@@ -1,5 +1,10 @@
+
+
+
diff --git a/Directory.Build.props b/Directory.Build.props
index 5bdf12218c..709545bf1d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -26,14 +26,6 @@
true
$(NoWarn);CS1591
-
-
- $(NoWarn);NU1701
-
false
ppy Pty Ltd
@@ -42,7 +34,7 @@
https://github.com/ppy/osu
Automated release.
ppy Pty Ltd
- Copyright (c) 2021 ppy Pty Ltd
+ Copyright (c) 2022 ppy Pty Ltd
osu game
diff --git a/InspectCode.ps1 b/InspectCode.ps1
index 8316f48ff3..df0d73ea43 100644
--- a/InspectCode.ps1
+++ b/InspectCode.ps1
@@ -5,7 +5,7 @@ dotnet tool restore
# - cmd: dotnet format --dry-run --check
dotnet CodeFileSanity
-dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
exit $LASTEXITCODE
diff --git a/InspectCode.sh b/InspectCode.sh
index cf2bc18175..65b55e0da0 100755
--- a/InspectCode.sh
+++ b/InspectCode.sh
@@ -2,5 +2,5 @@
dotnet tool restore
dotnet CodeFileSanity
-dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
diff --git a/LICENCE b/LICENCE
index b5962ad3b2..d3e7537cef 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,4 +1,4 @@
-Copyright (c) 2021 ppy Pty Ltd .
+Copyright (c) 2022 ppy Pty Ltd .
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
index fae3784f5e..312d3d5e9a 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty();
}
}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
index ca64636076..f6addab279 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty();
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
index 63a8b48b3c..a4dc1762d5 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty();
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
index ca64636076..f6addab279 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty();
}
}
diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj
index 4624d3d771..b8c3ad373a 100644
--- a/Templates/osu.Game.Templates.csproj
+++ b/Templates/osu.Game.Templates.csproj
@@ -8,7 +8,7 @@
https://github.com/ppy/osu/blob/master/Templates
https://github.com/ppy/osu
Automated release.
- Copyright (c) 2021 ppy Pty Ltd
+ Copyright (c) 2022 ppy Pty Ltd
Templates to use when creating a ruleset for consumption in osu!.
dotnet-new;templates;osu
netstandard2.1
diff --git a/osu.Android.props b/osu.Android.props
index 6a3b113fa2..82dec74855 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index e317a44bc3..eb9045d9ce 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -123,7 +123,12 @@ namespace osu.Desktop
tools.RemoveUninstallerRegistryEntry();
}, onEveryRun: (version, tools, firstRun) =>
{
- tools.SetProcessAppUserModelId();
+ // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
+ // causes the right-click context menu to function incorrectly.
+ //
+ // This may turn out to be non-required after an alternative solution is implemented.
+ // see https://github.com/clowd/Clowd.Squirrel/issues/24
+ // tools.SetProcessAppUserModelId();
});
}
diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
index 8f3ad853dc..ba37a14442 100644
--- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
+++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
@@ -19,7 +19,7 @@ namespace osu.Desktop.Security
public class ElevatedPrivilegesChecker : Component
{
[Resolved]
- private NotificationOverlay notifications { get; set; }
+ private INotificationOverlay notifications { get; set; }
private bool elevated;
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index b307146b10..c09cce1235 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -25,7 +25,7 @@ namespace osu.Desktop.Updater
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{
private UpdateManager updateManager;
- private NotificationOverlay notificationOverlay;
+ private INotificationOverlay notificationOverlay;
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
@@ -39,9 +39,9 @@ namespace osu.Desktop.Updater
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
[BackgroundDependencyLoader]
- private void load(NotificationOverlay notification)
+ private void load(INotificationOverlay notifications)
{
- notificationOverlay = notification;
+ notificationOverlay = notifications;
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index a06484214b..a4f309c6ac 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,8 +24,7 @@
-
-
+
@@ -33,7 +32,6 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index 1757fd7c73..dc1ec17e2c 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -11,7 +11,7 @@
false
A free-to-win rhythm game. Rhythm is just a *click* away!
testing
- Copyright (c) 2021 ppy Pty Ltd
+ Copyright (c) 2022 ppy Pty Ltd
en-AU
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index 7e8d567fbe..48d46636df 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
- [TestCase(4.0505463516206195d, "diffcalc-test")]
- public void Test(double expected, string name)
- => base.Test(expected, name);
+ [TestCase(4.0505463516206195d, 127, "diffcalc-test")]
+ public void Test(double expectedStarRating, int expectedMaxCombo, string name)
+ => base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(5.1696411260785498d, "diffcalc-test")]
- public void TestClockRateAdjusted(double expected, string name)
- => Test(expected, name, new CatchModDoubleTime());
+ [TestCase(5.1696411260785498d, 127, "diffcalc-test")]
+ public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap);
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index b2a555f89d..04b522b404 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -90,6 +90,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new LegacyHitExplosion();
return null;
+
+ default:
+ throw new UnsupportedSkinComponentException(component);
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
index 6ec49d7634..715614a201 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
- [TestCase(2.3449735700206298d, "diffcalc-test")]
- public void Test(double expected, string name)
- => base.Test(expected, name);
+ [TestCase(2.3449735700206298d, 151, "diffcalc-test")]
+ public void Test(double expectedStarRating, int expectedMaxCombo, string name)
+ => base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(2.7879104989252959d, "diffcalc-test")]
- public void TestClockRateAdjusted(double expected, string name)
- => Test(expected, name, new ManiaModDoubleTime());
+ [TestCase(2.7879104989252959d, 151, "diffcalc-test")]
+ public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap);
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
index ab6bd78ece..31550a8105 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
+ private const double release_threshold = 24;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
@@ -37,31 +38,43 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
var maniaCurrent = (ManiaDifficultyHitObject)current;
double endTime = maniaCurrent.EndTime;
int column = maniaCurrent.BaseObject.Column;
+ double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.StartTime); // Lowest value we can assume with the current information
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
+ bool isOverlapping = false;
// Fill up the holdEndTimes array
for (int i = 0; i < holdEndTimes.Length; ++i)
{
- // If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
- if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
- holdAddition = 1.0;
-
- // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
- if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
- holdAddition = 0;
+ // The current note is overlapped if a previous note or end is overlapping the current note body
+ isOverlapping |= Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1);
// We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
holdFactor = 1.25;
+ closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i]));
+
// Decay individual strains
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
}
holdEndTimes[column] = endTime;
+ // The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
+ // Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
+ // holdAddition
+ // ^
+ // 1.0 + - - - - - -+-----------
+ // | /
+ // 0.5 + - - - - -/ Sigmoid Curve
+ // | /|
+ // 0.0 +--------+-+---------------> Release Difference / ms
+ // release_threshold
+ if (isOverlapping)
+ holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
+
// Increase individual strain in own column
individualStrains[column] += 2.0 * holdFactor;
individualStrain = individualStrains[column];
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index 431bd77402..315b4444c2 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -116,9 +116,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground();
- }
- break;
+ default:
+ throw new UnsupportedSkinComponentException(component);
+ }
}
return base.GetDrawableComponent(component);
diff --git a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs
new file mode 100644
index 0000000000..d8c10b814d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs
@@ -0,0 +1,108 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Osu.Skinning.Legacy;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [HeadlessTest]
+ public class LegacyMainCirclePieceTest : OsuTestScene
+ {
+ private static readonly object?[][] texture_priority_cases =
+ {
+ // default priority lookup
+ new object?[]
+ {
+ // available textures
+ new[] { @"hitcircle", @"hitcircleoverlay" },
+ // priority lookup prefix
+ null,
+ // expected circle and overlay
+ @"hitcircle", @"hitcircleoverlay",
+ },
+ // custom priority lookup
+ new object?[]
+ {
+ new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle", @"sliderstartcircleoverlay" },
+ @"sliderstartcircle",
+ @"sliderstartcircle", @"sliderstartcircleoverlay",
+ },
+ // when no sprites are available for the specified prefix, fall back to "hitcircle"/"hitcircleoverlay".
+ new object?[]
+ {
+ new[] { @"hitcircle", @"hitcircleoverlay" },
+ @"sliderstartcircle",
+ @"hitcircle", @"hitcircleoverlay",
+ },
+ // when a circle is available for the specified prefix but no overlay exists, no overlay is displayed.
+ new object?[]
+ {
+ new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle" },
+ @"sliderstartcircle",
+ @"sliderstartcircle", null
+ },
+ // when no circle is available for the specified prefix but an overlay exists, the overlay is ignored.
+ new object?[]
+ {
+ new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircleoverlay" },
+ @"sliderstartcircle",
+ @"hitcircle", @"hitcircleoverlay",
+ }
+ };
+
+ [TestCaseSource(nameof(texture_priority_cases))]
+ public void TestTexturePriorities(string[] textureFilenames, string priorityLookup, string? expectedCircle, string? expectedOverlay)
+ {
+ TestLegacyMainCirclePiece piece = null!;
+
+ AddStep("load circle piece", () =>
+ {
+ var skin = new Mock();
+
+ // shouldn't be required as GetTexture(string) calls GetTexture(string, WrapMode, WrapMode) by default,
+ // but moq doesn't handle that well, therefore explicitly requiring to use `CallBase`:
+ // https://github.com/moq/moq4/issues/972
+ skin.Setup(s => s.GetTexture(It.IsAny())).CallBase();
+
+ skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny(), It.IsAny()))
+ .Returns((string componentName, WrapMode _, WrapMode __) => new Texture(1, 1) { AssetName = componentName });
+
+ Child = new DependencyProvidingContainer
+ {
+ CachedDependencies = new (Type, object)[] { (typeof(ISkinSource), skin.Object) },
+ Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
+ };
+
+ var sprites = this.ChildrenOfType().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray();
+ Debug.Assert(sprites.Length <= 2);
+ });
+
+ AddAssert("check circle sprite", () => piece.CircleSprite?.Texture?.AssetName == expectedCircle);
+ AddAssert("check overlay sprite", () => piece.OverlaySprite?.Texture?.AssetName == expectedOverlay);
+ }
+
+ private class TestLegacyMainCirclePiece : LegacyMainCirclePiece
+ {
+ public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
+ public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
+
+ public TestLegacyMainCirclePiece(string? priorityLookupPrefix)
+ : base(priorityLookupPrefix, false)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs
similarity index 71%
rename from osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs
rename to osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs
index b8310bc4e7..9b49e60363 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs
@@ -6,18 +6,18 @@ using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
- public class TestSceneOsuModAimAssist : OsuModTestScene
+ public class TestSceneOsuModMagnetised : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
- public void TestAimAssist(float strength)
+ public void TestMagnetised(float strength)
{
CreateModTest(new ModTestData
{
- Mod = new OsuModAimAssist
+ Mod = new OsuModMagnetised
{
- AssistStrength = { Value = strength },
+ AttractionStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index b7984e6995..df577ea8d3 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,15 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.6972307565739273d, "diffcalc-test")]
- [TestCase(1.4484754139145539d, "zero-length-sliders")]
- public void Test(double expected, string name)
- => base.Test(expected, name);
+ [TestCase(6.6972307565739273d, 206, "diffcalc-test")]
+ [TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
+ public void Test(double expectedStarRating, int expectedMaxCombo, string name)
+ => base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(8.9382559208689809d, "diffcalc-test")]
- [TestCase(1.7548875851757628d, "zero-length-sliders")]
- public void TestClockRateAdjusted(double expected, string name)
- => Test(expected, name, new OsuModDoubleTime());
+ [TestCase(8.9382559208689809d, 206, "diffcalc-test")]
+ [TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
+ public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
+
+ [TestCase(6.6972307218715166d, 239, "diffcalc-test")]
+ [TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
+ public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
index d673b7a6ac..a40ae611d8 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
@@ -70,7 +70,9 @@ namespace osu.Game.Rulesets.Osu.Tests
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
- Child = new SkinProvidingContainer(tintingSkin)
+ var provider = Ruleset.Value.CreateInstance().CreateLegacySkinProvider(tintingSkin, Beatmap.Value.Beatmap);
+
+ Child = new SkinProvidingContainer(provider)
{
RelativeSizeAxes = Axes.Both,
Child = dho = new DrawableSlider(prepareObject(new Slider
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
index 2d3cc3c103..a5282877ee 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
+using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
@@ -20,13 +21,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
new BeatmapStatistic
{
- Name = @"Circle Count",
+ Name = BeatmapsetsStrings.ShowStatsCountCircles,
Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
},
new BeatmapStatistic
{
- Name = @"Slider Count",
+ Name = BeatmapsetsStrings.ShowStatsCountSliders,
Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
},
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index c5b1baaad1..df6fd19d36 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -61,10 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
-
- int maxCombo = beatmap.HitObjects.Count;
- // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
- maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1);
+ int maxCombo = beatmap.GetMaxCombo();
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 983964d639..aaf455e95f 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
public bool PerformFail() => false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index 31179cdf4a..b31ef5d2fd 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index ad4c5dfd5d..7567c96b50 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1.12;
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
+
private DrawableOsuBlinds blinds;
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
index d677ab43d0..5b42772358 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index 38c84be295..44d72fae61 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
@@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModFlashlight : ModFlashlight, IApplicableToDrawableHitObject
{
public override double ScoreMultiplier => 1.12;
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
private const double default_follow_delay = 120;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
similarity index 83%
rename from osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs
rename to osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index 1abbd67d8f..ca6e9cfb1d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -16,20 +16,20 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
- internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset
+ internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset
{
- public override string Name => "Aim Assist";
- public override string Acronym => "AA";
- public override IconUsage? Icon => FontAwesome.Solid.MousePointer;
+ public override string Name => "Magnetised";
+ public override string Acronym => "MG";
+ public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun;
- public override string Description => "No need to chase the circle – the circle chases you!";
+ public override string Description => "No need to chase the circles – your cursor is a magnet!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
private IFrameStableClock gameplayClock;
- [SettingSource("Assist strength", "How much this mod will assist you.", 0)]
- public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f)
+ [SettingSource("Attraction strength", "How strong the pull is.", 0)]
+ public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
{
Precision = 0.05f,
MinValue = 0.05f,
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void easeTo(DrawableHitObject hitObject, Vector2 destination)
{
- double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value);
+ double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
index 778447e444..70c075276f 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
- public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
+ public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 7479c3120a..fea9246035 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -4,19 +4,14 @@
#nullable enable
using System;
-using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
-using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -28,12 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "It never gets boring!";
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
- private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
-
- ///
- /// Number of previous hitobjects to be shifted together when another object is being moved.
- ///
- private const int preceding_hitobjects_to_shift = 10;
private Random? rng;
@@ -42,330 +31,33 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!(beatmap is OsuBeatmap osuBeatmap))
return;
- var hitObjects = osuBeatmap.HitObjects;
-
Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value);
- var randomObjects = randomiseObjects(hitObjects);
+ var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects);
- applyRandomisation(hitObjects, randomObjects);
- }
-
- ///
- /// Randomise the position of each hit object and return a list of s describing how each hit object should be placed.
- ///
- /// A list of s to have their positions randomised.
- /// A list of s describing how each hit object should be placed.
- private List randomiseObjects(IEnumerable hitObjects)
- {
- Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects");
-
- var randomObjects = new List();
- RandomObjectInfo? previous = null;
float rateOfChangeMultiplier = 0;
- foreach (OsuHitObject hitObject in hitObjects)
+ foreach (var positionInfo in positionInfos)
{
- var current = new RandomObjectInfo(hitObject);
- randomObjects.Add(current);
-
// rateOfChangeMultiplier only changes every 5 iterations in a combo
// to prevent shaky-line-shaped streams
- if (hitObject.IndexInCurrentCombo % 5 == 0)
+ if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
- if (previous == null)
+ if (positionInfo == positionInfos.First())
{
- current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
- current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
+ positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
+ positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
}
else
{
- current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
-
- // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
- // is proportional to the distance between the last and the current hit object
- // to allow jumps and prevent too sharp turns during streams.
-
- // Allow maximum jump angle when jump distance is more than half of playfield diagonal length
- current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f));
+ positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f));
}
-
- previous = current;
}
- return randomObjects;
- }
-
- ///
- /// Reposition the hit objects according to the information in .
- ///
- /// The hit objects to be repositioned.
- /// A list of describing how each hit object should be placed.
- private void applyRandomisation(IReadOnlyList hitObjects, IReadOnlyList randomObjects)
- {
- RandomObjectInfo? previous = null;
-
- for (int i = 0; i < hitObjects.Count; i++)
- {
- var hitObject = hitObjects[i];
-
- var current = randomObjects[i];
-
- if (hitObject is Spinner)
- {
- previous = null;
- continue;
- }
-
- computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null);
-
- // Move hit objects back into the playfield if they are outside of it
- Vector2 shift = Vector2.Zero;
-
- switch (hitObject)
- {
- case HitCircle circle:
- shift = clampHitCircleToPlayfield(circle, current);
- break;
-
- case Slider slider:
- shift = clampSliderToPlayfield(slider, current);
- break;
- }
-
- if (shift != Vector2.Zero)
- {
- var toBeShifted = new List();
-
- for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
- {
- // only shift hit circles
- if (!(hitObjects[j] is HitCircle)) break;
-
- toBeShifted.Add(hitObjects[j]);
- }
-
- if (toBeShifted.Count > 0)
- applyDecreasingShift(toBeShifted, shift);
- }
-
- previous = current;
- }
- }
-
- ///
- /// Compute the randomised position of a hit object while attempting to keep it inside the playfield.
- ///
- /// The representing the hit object to have the randomised position computed for.
- /// The representing the hit object immediately preceding the current one.
- /// The representing the hit object immediately preceding the one.
- private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious)
- {
- float previousAbsoluteAngle = 0f;
-
- if (previous != null)
- {
- Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
- Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
- previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
- }
-
- float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle;
-
- var posRelativeToPrev = new Vector2(
- current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
- current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
- );
-
- Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre;
-
- posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
-
- current.PositionRandomised = lastEndPosition + posRelativeToPrev;
- }
-
- ///
- /// Move the randomised position of a hit circle so that it fits inside the playfield.
- ///
- /// The deviation from the original randomised position in order to fit within the playfield.
- private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo)
- {
- var previousPosition = objectInfo.PositionRandomised;
- objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding(
- objectInfo.PositionRandomised,
- (float)circle.Radius
- );
-
- circle.Position = objectInfo.PositionRandomised;
-
- return objectInfo.PositionRandomised - previousPosition;
- }
-
- ///
- /// Moves the and all necessary nested s into the if they aren't already.
- ///
- /// The deviation from the original randomised position in order to fit within the playfield.
- private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo)
- {
- var possibleMovementBounds = calculatePossibleMovementBounds(slider);
-
- var previousPosition = objectInfo.PositionRandomised;
-
- // Clamp slider position to the placement area
- // If the slider is larger than the playfield, force it to stay at the original position
- float newX = possibleMovementBounds.Width < 0
- ? objectInfo.PositionOriginal.X
- : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
-
- float newY = possibleMovementBounds.Height < 0
- ? objectInfo.PositionOriginal.Y
- : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
-
- slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY);
- objectInfo.EndPositionRandomised = slider.EndPosition;
-
- shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal);
-
- return objectInfo.PositionRandomised - previousPosition;
- }
-
- ///
- /// Decreasingly shift a list of s by a specified amount.
- /// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
- ///
- /// The list of hit objects to be shifted.
- /// The amount to be shifted.
- private void applyDecreasingShift(IList hitObjects, Vector2 shift)
- {
- for (int i = 0; i < hitObjects.Count; i++)
- {
- var hitObject = hitObjects[i];
- // The first object is shifted by a vector slightly smaller than shift
- // The last object is shifted by a vector slightly larger than zero
- Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
-
- hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
- }
- }
-
- ///
- /// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates)
- /// such that the entire slider is inside the playfield.
- ///
- ///
- /// If the slider is larger than the playfield, the returned may have negative width/height.
- ///
- private RectangleF calculatePossibleMovementBounds(Slider slider)
- {
- var pathPositions = new List();
- slider.Path.GetPathToProgress(pathPositions, 0, 1);
-
- float minX = float.PositiveInfinity;
- float maxX = float.NegativeInfinity;
-
- float minY = float.PositiveInfinity;
- float maxY = float.NegativeInfinity;
-
- // Compute the bounding box of the slider.
- foreach (var pos in pathPositions)
- {
- minX = MathF.Min(minX, pos.X);
- maxX = MathF.Max(maxX, pos.X);
-
- minY = MathF.Min(minY, pos.Y);
- maxY = MathF.Max(maxY, pos.Y);
- }
-
- // Take the circle radius into account.
- float radius = (float)slider.Radius;
-
- minX -= radius;
- minY -= radius;
-
- maxX += radius;
- maxY += radius;
-
- // Given the bounding box of the slider (via min/max X/Y),
- // the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
- // and the amount that it can move to the right is WIDTH - maxX.
- // Same calculation applies for the Y axis.
- float left = -minX;
- float right = OsuPlayfield.BASE_SIZE.X - maxX;
- float top = -minY;
- float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
-
- return new RectangleF(left, top, right - left, bottom - top);
- }
-
- ///
- /// Shifts all nested s and s by the specified shift.
- ///
- /// whose nested s and s should be shifted
- /// The the 's nested s and s should be shifted by
- private void shiftNestedObjects(Slider slider, Vector2 shift)
- {
- foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
- {
- if (!(hitObject is OsuHitObject osuHitObject))
- continue;
-
- osuHitObject.Position += shift;
- }
- }
-
- ///
- /// Clamp a position to playfield, keeping a specified distance from the edges.
- ///
- /// The position to be clamped.
- /// The minimum distance allowed from playfield edges.
- /// The clamped position.
- private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
- {
- return new Vector2(
- Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
- Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
- );
- }
-
- private class RandomObjectInfo
- {
- ///
- /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
- ///
- ///
- /// of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
- ///
- ///
- /// If is 0, the player's cursor doesn't need to change its direction of movement when passing
- /// the previous object to reach this one.
- ///
- public float RelativeAngle { get; set; }
-
- ///
- /// The jump distance from the previous hit object to this one.
- ///
- ///
- /// of the first hit object in a beatmap is relative to the playfield center.
- ///
- public float DistanceFromPrevious { get; set; }
-
- public Vector2 PositionOriginal { get; }
- public Vector2 PositionRandomised { get; set; }
-
- public Vector2 EndPositionOriginal { get; }
- public Vector2 EndPositionRandomised { get; set; }
-
- public OsuHitObject HitObject { get; }
-
- public RandomObjectInfo(OsuHitObject hitObject)
- {
- PositionRandomised = PositionOriginal = hitObject.Position;
- EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
- HitObject = hitObject;
- }
+ osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 9719de441e..6b81efdca6 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModAimAssist) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray();
///
/// How early before a hitobject's start time to trigger a hit.
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs
index 0403e81229..429fe30fc5 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs
@@ -9,6 +9,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSuddenDeath : ModSuddenDeath
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
+ {
+ typeof(OsuModAutopilot),
+ typeof(OsuModTarget),
+ }).ToArray();
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
index 5285380097..4fab9b6a5a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"Practice keeping up with the beat of the song.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSuddenDeath) };
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
public Bindable Seed { get; } = new Bindable
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index 28c3b069b6..45ce4d555a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) };
private float theta;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index 40a05400ea..693a5bee0b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 628d95dff4..fa2d2ba38c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -6,10 +6,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
-using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@@ -21,10 +21,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public readonly IBindable ScaleBindable = new BindableFloat();
public readonly IBindable IndexInCurrentComboBindable = new Bindable();
- // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects.
+ // Must be set to update IsHovered as it's used in relax mod to detect osu hit objects.
public override bool HandlePositionalInput => true;
- protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
+ protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
///
/// Whether this can be hit, given a time value.
@@ -89,6 +89,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
+ private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent.ScreenSpaceDrawQuad.AABBFloat;
+
+ ///
+ /// Calculates the position of the given relative to the playfield area.
+ ///
+ /// The drawable to calculate its relative position.
+ protected float CalculateDrawableRelativePosition(Drawable drawable) => (drawable.ScreenSpaceDrawQuad.Centre.X - parentScreenSpaceRectangle.X) / parentScreenSpaceRectangle.Width;
+
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 1447f131c6..c48ab998ba 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -13,7 +13,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
-using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
@@ -208,7 +207,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position
- slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X);
+ slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 47a2618ddd..207e7a4ab0 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
- new OsuModAimAssist(),
+ new OsuModMagnetised(),
new ModAdaptiveSpeed()
};
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index c6007885be..391147648f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
@@ -3,10 +3,10 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
@@ -16,63 +16,61 @@ using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
+#nullable enable
+
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class LegacyMainCirclePiece : CompositeDrawable
{
public override bool RemoveCompletedTransforms => false;
- private readonly string priorityLookup;
+ ///
+ /// A prioritised prefix to perform texture lookups with.
+ ///
+ private readonly string? priorityLookupPrefix;
+
private readonly bool hasNumber;
- public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
+ protected Drawable CircleSprite = null!;
+ protected Drawable OverlaySprite = null!;
+
+ protected Container OverlayLayer { get; private set; } = null!;
+
+ private SkinnableSpriteText hitCircleText = null!;
+
+ private readonly Bindable accentColour = new Bindable();
+ private readonly IBindable indexInCurrentCombo = new Bindable();
+
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? drawableObject { get; set; }
+
+ [Resolved]
+ private ISkinSource skin { get; set; } = null!;
+
+ public LegacyMainCirclePiece(string? priorityLookupPrefix = null, bool hasNumber = true)
{
- this.priorityLookup = priorityLookup;
+ this.priorityLookupPrefix = priorityLookupPrefix;
this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
- private Drawable hitCircleSprite;
-
- protected Container OverlayLayer { get; private set; }
-
- private Drawable hitCircleOverlay;
- private SkinnableSpriteText hitCircleText;
-
- private readonly Bindable accentColour = new Bindable();
- private readonly IBindable indexInCurrentCombo = new Bindable();
-
- [Resolved]
- private DrawableHitObject drawableObject { get; set; }
-
- [Resolved]
- private ISkinSource skin { get; set; }
-
[BackgroundDependencyLoader]
private void load()
{
- var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
+ var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
- bool allowFallback = false;
-
- // attempt lookup using priority specification
- Texture baseTexture = getTextureWithFallback(string.Empty);
-
- // if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
- if (baseTexture == null)
- {
- allowFallback = true;
- baseTexture = getTextureWithFallback(string.Empty);
- }
+ // if a base texture for the specified prefix exists, continue using it for subsequent lookups.
+ // otherwise fall back to the default prefix "hitcircle".
+ string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle";
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
- // the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
- // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
+ // the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
+ // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
InternalChildren = new[]
{
- hitCircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = baseTexture })
+ CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -81,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Child = hitCircleOverlay = new KiaiFlashingDrawable(() => getAnimationWithFallback(@"overlay", 1000 / 2d))
+ Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -105,39 +103,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
if (overlayAboveNumber)
- OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue);
+ OverlayLayer.ChangeChildDepth(OverlaySprite, float.MinValue);
- accentColour.BindTo(drawableObject.AccentColour);
- indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
-
- Texture getTextureWithFallback(string name)
+ if (drawableOsuObject != null)
{
- Texture tex = null;
-
- if (!string.IsNullOrEmpty(priorityLookup))
- {
- tex = skin.GetTexture($"{priorityLookup}{name}");
-
- if (!allowFallback)
- return tex;
- }
-
- return tex ?? skin.GetTexture($"hitcircle{name}");
- }
-
- Drawable getAnimationWithFallback(string name, double frameLength)
- {
- Drawable animation = null;
-
- if (!string.IsNullOrEmpty(priorityLookup))
- {
- animation = skin.GetAnimation($"{priorityLookup}{name}", true, true, frameLength: frameLength);
-
- if (!allowFallback)
- return animation;
- }
-
- return animation ?? skin.GetAnimation($"hitcircle{name}", true, true, frameLength: frameLength);
+ accentColour.BindTo(drawableOsuObject.AccentColour);
+ indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
}
}
@@ -145,28 +116,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
base.LoadComplete();
- accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
+ accentColour.BindValueChanged(colour => CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
- drawableObject.ApplyCustomUpdateState += updateStateTransforms;
- updateStateTransforms(drawableObject, drawableObject.State.Value);
+ if (drawableObject != null)
+ {
+ drawableObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableObject, drawableObject.State.Value);
+ }
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
const double legacy_fade_duration = 240;
- using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ using (BeginAbsoluteSequence(drawableObject.AsNonNull().HitStateUpdateTime))
{
switch (state)
{
case ArmedState.Hit:
- hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
- hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ CircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
+ CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
- hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
- hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ OverlaySprite.FadeOut(legacy_fade_duration, Easing.Out);
+ OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber)
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index 900ad6f6d3..572185e6e1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -35,6 +35,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinComponents.FollowPoint:
return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false);
+ case OsuSkinComponents.SliderScorePoint:
+ return this.GetAnimation(component.LookupName, false, false);
+
case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
if (followCircle != null)
@@ -123,6 +126,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinComponents.ApproachCircle:
return new LegacyApproachCircle();
+
+ default:
+ throw new UnsupportedSkinComponentException(component);
}
}
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
index 97a4b14a62..da73c2addb 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Utils
{
- public static class OsuHitObjectGenerationUtils
+ public static partial class OsuHitObjectGenerationUtils
{
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
new file mode 100644
index 0000000000..d1bc3b45df
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -0,0 +1,340 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osuTK;
+
+#nullable enable
+
+namespace osu.Game.Rulesets.Osu.Utils
+{
+ public static partial class OsuHitObjectGenerationUtils
+ {
+ ///
+ /// Number of previous hitobjects to be shifted together when an object is being moved.
+ ///
+ private const int preceding_hitobjects_to_shift = 10;
+
+ private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
+
+ ///
+ /// Generate a list of s containing information for how the given list of
+ /// s are positioned.
+ ///
+ /// A list of s to process.
+ /// A list of s describing how each hit object is positioned relative to the previous one.
+ public static List GeneratePositionInfos(IEnumerable hitObjects)
+ {
+ var positionInfos = new List();
+ Vector2 previousPosition = playfield_centre;
+ float previousAngle = 0;
+
+ foreach (OsuHitObject hitObject in hitObjects)
+ {
+ Vector2 relativePosition = hitObject.Position - previousPosition;
+ float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ float relativeAngle = absoluteAngle - previousAngle;
+
+ positionInfos.Add(new ObjectPositionInfo(hitObject)
+ {
+ RelativeAngle = relativeAngle,
+ DistanceFromPrevious = relativePosition.Length
+ });
+
+ previousPosition = hitObject.EndPosition;
+ previousAngle = absoluteAngle;
+ }
+
+ return positionInfos;
+ }
+
+ ///
+ /// Reposition the hit objects according to the information in .
+ ///
+ /// Position information for each hit object.
+ /// The repositioned hit objects.
+ public static List RepositionHitObjects(IEnumerable objectPositionInfos)
+ {
+ List workingObjects = objectPositionInfos.Select(o => new WorkingObject(o)).ToList();
+ WorkingObject? previous = null;
+
+ for (int i = 0; i < workingObjects.Count; i++)
+ {
+ var current = workingObjects[i];
+ var hitObject = current.HitObject;
+
+ if (hitObject is Spinner)
+ {
+ previous = null;
+ continue;
+ }
+
+ computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null);
+
+ // Move hit objects back into the playfield if they are outside of it
+ Vector2 shift = Vector2.Zero;
+
+ switch (hitObject)
+ {
+ case HitCircle _:
+ shift = clampHitCircleToPlayfield(current);
+ break;
+
+ case Slider _:
+ shift = clampSliderToPlayfield(current);
+ break;
+ }
+
+ if (shift != Vector2.Zero)
+ {
+ var toBeShifted = new List();
+
+ for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
+ {
+ // only shift hit circles
+ if (!(workingObjects[j].HitObject is HitCircle)) break;
+
+ toBeShifted.Add(workingObjects[j].HitObject);
+ }
+
+ if (toBeShifted.Count > 0)
+ applyDecreasingShift(toBeShifted, shift);
+ }
+
+ previous = current;
+ }
+
+ return workingObjects.Select(p => p.HitObject).ToList();
+ }
+
+ ///
+ /// Compute the modified position of a hit object while attempting to keep it inside the playfield.
+ ///
+ /// The representing the hit object to have the modified position computed for.
+ /// The representing the hit object immediately preceding the current one.
+ /// The representing the hit object immediately preceding the one.
+ private static void computeModifiedPosition(WorkingObject current, WorkingObject? previous, WorkingObject? beforePrevious)
+ {
+ float previousAbsoluteAngle = 0f;
+
+ if (previous != null)
+ {
+ Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
+ Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
+ previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ }
+
+ float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
+
+ var posRelativeToPrev = new Vector2(
+ current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
+ current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
+ );
+
+ Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
+
+ posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
+
+ current.PositionModified = lastEndPosition + posRelativeToPrev;
+ }
+
+ ///
+ /// Move the modified position of a so that it fits inside the playfield.
+ ///
+ /// The deviation from the original modified position in order to fit within the playfield.
+ private static Vector2 clampHitCircleToPlayfield(WorkingObject workingObject)
+ {
+ var previousPosition = workingObject.PositionModified;
+ workingObject.EndPositionModified = workingObject.PositionModified = clampToPlayfieldWithPadding(
+ workingObject.PositionModified,
+ (float)workingObject.HitObject.Radius
+ );
+
+ workingObject.HitObject.Position = workingObject.PositionModified;
+
+ return workingObject.PositionModified - previousPosition;
+ }
+
+ ///
+ /// Moves the and all necessary nested s into the if they aren't already.
+ ///
+ /// The deviation from the original modified position in order to fit within the playfield.
+ private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
+ {
+ var slider = (Slider)workingObject.HitObject;
+ var possibleMovementBounds = calculatePossibleMovementBounds(slider);
+
+ var previousPosition = workingObject.PositionModified;
+
+ // Clamp slider position to the placement area
+ // If the slider is larger than the playfield, force it to stay at the original position
+ float newX = possibleMovementBounds.Width < 0
+ ? workingObject.PositionOriginal.X
+ : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
+
+ float newY = possibleMovementBounds.Height < 0
+ ? workingObject.PositionOriginal.Y
+ : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
+
+ slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
+ workingObject.EndPositionModified = slider.EndPosition;
+
+ shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
+
+ return workingObject.PositionModified - previousPosition;
+ }
+
+ ///
+ /// Decreasingly shift a list of s by a specified amount.
+ /// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
+ ///
+ /// The list of hit objects to be shifted.
+ /// The amount to be shifted.
+ private static void applyDecreasingShift(IList hitObjects, Vector2 shift)
+ {
+ for (int i = 0; i < hitObjects.Count; i++)
+ {
+ var hitObject = hitObjects[i];
+ // The first object is shifted by a vector slightly smaller than shift
+ // The last object is shifted by a vector slightly larger than zero
+ Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
+
+ hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
+ }
+ }
+
+ ///
+ /// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates)
+ /// such that the entire slider is inside the playfield.
+ ///
+ ///
+ /// If the slider is larger than the playfield, the returned may have negative width/height.
+ ///
+ private static RectangleF calculatePossibleMovementBounds(Slider slider)
+ {
+ var pathPositions = new List();
+ slider.Path.GetPathToProgress(pathPositions, 0, 1);
+
+ float minX = float.PositiveInfinity;
+ float maxX = float.NegativeInfinity;
+
+ float minY = float.PositiveInfinity;
+ float maxY = float.NegativeInfinity;
+
+ // Compute the bounding box of the slider.
+ foreach (var pos in pathPositions)
+ {
+ minX = MathF.Min(minX, pos.X);
+ maxX = MathF.Max(maxX, pos.X);
+
+ minY = MathF.Min(minY, pos.Y);
+ maxY = MathF.Max(maxY, pos.Y);
+ }
+
+ // Take the circle radius into account.
+ float radius = (float)slider.Radius;
+
+ minX -= radius;
+ minY -= radius;
+
+ maxX += radius;
+ maxY += radius;
+
+ // Given the bounding box of the slider (via min/max X/Y),
+ // the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
+ // and the amount that it can move to the right is WIDTH - maxX.
+ // Same calculation applies for the Y axis.
+ float left = -minX;
+ float right = OsuPlayfield.BASE_SIZE.X - maxX;
+ float top = -minY;
+ float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
+
+ return new RectangleF(left, top, right - left, bottom - top);
+ }
+
+ ///
+ /// Shifts all nested s and s by the specified shift.
+ ///
+ /// whose nested s and s should be shifted
+ /// The the 's nested s and s should be shifted by
+ private static void shiftNestedObjects(Slider slider, Vector2 shift)
+ {
+ foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
+ {
+ if (!(hitObject is OsuHitObject osuHitObject))
+ continue;
+
+ osuHitObject.Position += shift;
+ }
+ }
+
+ ///
+ /// Clamp a position to playfield, keeping a specified distance from the edges.
+ ///
+ /// The position to be clamped.
+ /// The minimum distance allowed from playfield edges.
+ /// The clamped position.
+ private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
+ {
+ return new Vector2(
+ Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
+ Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
+ );
+ }
+
+ public class ObjectPositionInfo
+ {
+ ///
+ /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
+ ///
+ ///
+ /// of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
+ ///
+ ///
+ /// If is 0, the player's cursor doesn't need to change its direction of movement when passing
+ /// the previous object to reach this one.
+ ///
+ public float RelativeAngle { get; set; }
+
+ ///
+ /// The jump distance from the previous hit object to this one.
+ ///
+ ///
+ /// of the first hit object in a beatmap is relative to the playfield center.
+ ///
+ public float DistanceFromPrevious { get; set; }
+
+ ///
+ /// The hit object associated with this .
+ ///
+ public OsuHitObject HitObject { get; }
+
+ public ObjectPositionInfo(OsuHitObject hitObject)
+ {
+ HitObject = hitObject;
+ }
+ }
+
+ private class WorkingObject
+ {
+ public Vector2 PositionOriginal { get; }
+ public Vector2 PositionModified { get; set; }
+ public Vector2 EndPositionModified { get; set; }
+
+ public ObjectPositionInfo PositionInfo { get; }
+ public OsuHitObject HitObject => PositionInfo.HitObject;
+
+ public WorkingObject(ObjectPositionInfo positionInfo)
+ {
+ PositionInfo = positionInfo;
+ PositionModified = PositionOriginal = HitObject.Position;
+ EndPositionModified = HitObject.EndPosition;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index 2b1cbc580e..226da7df09 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(2.2420075288523802d, "diffcalc-test")]
- [TestCase(2.2420075288523802d, "diffcalc-test-strong")]
- public void Test(double expected, string name)
- => base.Test(expected, name);
+ [TestCase(2.2420075288523802d, 200, "diffcalc-test")]
+ [TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
+ public void Test(double expectedStarRating, int expectedMaxCombo, string name)
+ => base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(3.134084469440479d, "diffcalc-test")]
- [TestCase(3.134084469440479d, "diffcalc-test-strong")]
- public void TestClockRateAdjusted(double expected, string name)
- => Test(expected, name, new TaikoModDoubleTime());
+ [TestCase(3.134084469440479d, 200, "diffcalc-test")]
+ [TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
+ public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
+ => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap);
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
index 63854e7ead..5c7e3954e8 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
@@ -28,9 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
// flying hits all land in one common scrolling container (and stay there for rewind purposes),
// so we need to manually get the latest one.
- flyingHit = this.ChildrenOfType()
- .OrderByDescending(h => h.HitObject.StartTime)
- .FirstOrDefault();
+ flyingHit = this.ChildrenOfType().MaxBy(h => h.HitObject.StartTime);
});
AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType);
diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
index f047c03f4b..1a1fde1990 100644
--- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
///
/// Default size of a drawable taiko hit object.
///
- public const float DEFAULT_SIZE = 0.45f;
+ public const float DEFAULT_SIZE = 0.475f;
public override Judgement CreateJudgement() => new TaikoJudgement();
diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
index 6c17573b50..6e0f6a3109 100644
--- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
///
/// Scale multiplier for a strong drawable taiko hit object.
///
- public const float STRONG_SCALE = 1.4f;
+ public const float STRONG_SCALE = 1 / 0.65f;
///
/// Default size of a strong drawable taiko hit object.
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
index a106c4f629..f2452ad88c 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
@@ -11,6 +11,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Default
@@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
///
public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
{
- public const float SYMBOL_SIZE = 0.45f;
+ public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_SIZE;
public const float SYMBOL_BORDER = 8;
+
private const double pre_beat_transition_time = 80;
private Color4 accentColour;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
index 9feb2054da..c4657fcc49 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("approachcircle"),
- Scale = new Vector2(0.73f),
+ Scale = new Vector2(0.83f),
Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("taikobigcircle"),
- Scale = new Vector2(0.7f),
+ Scale = new Vector2(0.8f),
Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index bbc8f0abea..af5921b0fb 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -57,6 +57,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.DrumRollTick:
return this.GetAnimation("sliderscorepoint", false, false);
+ case TaikoSkinComponents.Swell:
+ // todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601).
+ return null;
+
case TaikoSkinComponents.HitTarget:
if (GetTexture("taikobigcircle") != null)
return new TaikoLegacyHitTarget();
@@ -119,6 +123,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.Mascot:
return new DrawableTaikoMascot();
+
+ default:
+ throw new UnsupportedSkinComponentException(component);
}
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
index 296c5cef76..a354464a8e 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
@@ -160,6 +160,40 @@ namespace osu.Game.Tests.Gameplay
assertHealthNotEqualTo(1);
}
+ [Test]
+ public void TestFailConditions()
+ {
+ var beatmap = createBeatmap(0, 1000);
+ createProcessor(beatmap);
+
+ AddStep("setup fail conditions", () => processor.FailConditions += ((_, result) => result.Type == HitResult.Miss));
+
+ AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
+ AddAssert("not failed", () => !processor.HasFailed);
+ AddStep("apply miss hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Miss }));
+ AddAssert("failed", () => processor.HasFailed);
+ }
+
+ [TestCase(HitResult.Miss)]
+ [TestCase(HitResult.Meh)]
+ public void TestMultipleFailConditions(HitResult resultApplied)
+ {
+ var beatmap = createBeatmap(0, 1000);
+ createProcessor(beatmap);
+
+ AddStep("setup multiple fail conditions", () =>
+ {
+ processor.FailConditions += ((_, result) => result.Type == HitResult.Miss);
+ processor.FailConditions += ((_, result) => result.Type == HitResult.Meh);
+ });
+
+ AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
+ AddAssert("not failed", () => !processor.HasFailed);
+
+ AddStep($"apply {resultApplied.ToString().ToLower()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
+ AddAssert("failed", () => processor.HasFailed);
+ }
+
[Test]
public void TestBonusObjectsExcludedFromDrain()
{
diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
index 77b402ad3c..5c04ac88a7 100644
--- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
@@ -26,6 +26,12 @@ namespace osu.Game.Tests.Gameplay
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
}
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("reset audio offset", () => localConfig.SetValue(OsuSetting.AudioOffset, 0.0));
+ }
+
[Test]
public void TestStartThenElapsedTime()
{
@@ -36,7 +42,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
- Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
+ Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
});
AddStep("start clock", () => gameplayClockContainer.Start());
@@ -53,7 +59,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
- Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
+ Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
});
AddStep("start clock", () => gameplayClockContainer.Start());
@@ -73,26 +79,29 @@ namespace osu.Game.Tests.Gameplay
public void TestSeekPerformsInGameplayTime(
[Values(1.0, 0.5, 2.0)] double clockRate,
[Values(0.0, 200.0, -200.0)] double userOffset,
- [Values(false, true)] bool whileStopped)
+ [Values(false, true)] bool whileStopped,
+ [Values(false, true)] bool setAudioOffsetBeforeConstruction)
{
ClockBackedTestWorkingBeatmap working = null;
GameplayClockContainer gameplayClockContainer = null;
+ if (setAudioOffsetBeforeConstruction)
+ AddStep($"preset audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
+
AddStep("create container", () =>
{
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
working.LoadTrack();
- Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
+ Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
- if (whileStopped)
- gameplayClockContainer.Stop();
-
- gameplayClockContainer.Reset();
+ gameplayClockContainer.Reset(startClock: !whileStopped);
});
AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate)));
- AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
+
+ if (!setAudioOffsetBeforeConstruction)
+ AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 76ec35d87d..e0a497cf24 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -88,7 +88,7 @@ namespace osu.Game.Tests.Gameplay
[Test]
public void TestSampleHasLifetimeEndWithInitialClockTime()
{
- GameplayClockContainer gameplayContainer = null;
+ MasterGameplayClockContainer gameplayContainer = null;
DrawableStoryboardSample sample = null;
AddStep("create container", () =>
@@ -96,8 +96,11 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
- Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true)
+ const double start_time = 1000;
+
+ Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time)
{
+ StartTime = start_time,
IsPaused = { Value = true },
Child = new FrameStabilityContainer
{
diff --git a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
new file mode 100644
index 0000000000..312b939315
--- /dev/null
+++ b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
+using osu.Game.Utils;
+
+namespace osu.Game.Tests.Mods
+{
+ [TestFixture]
+ public class MultiModIncompatibilityTest
+ {
+ ///
+ /// Ensures that all mods grouped into s, as declared by the default rulesets, are pairwise incompatible with each other.
+ ///
+ [TestCase(typeof(OsuRuleset))]
+ [TestCase(typeof(TaikoRuleset))]
+ [TestCase(typeof(CatchRuleset))]
+ [TestCase(typeof(ManiaRuleset))]
+ public void TestAllMultiModsFromRulesetAreIncompatible(Type rulesetType)
+ {
+ var ruleset = (Ruleset)Activator.CreateInstance(rulesetType);
+ Assert.That(ruleset, Is.Not.Null);
+
+ var allMultiMods = getMultiMods(ruleset);
+
+ Assert.Multiple(() =>
+ {
+ foreach (var multiMod in allMultiMods)
+ {
+ int modCount = multiMod.Mods.Length;
+
+ for (int i = 0; i < modCount; ++i)
+ {
+ // indexing from i + 1 ensures that only pairs of different mods are checked, and are checked only once
+ // (indexing from 0 would check each pair twice, and also check each mod against itself).
+ for (int j = i + 1; j < modCount; ++j)
+ {
+ var firstMod = multiMod.Mods[i];
+ var secondMod = multiMod.Mods[j];
+
+ Assert.That(
+ ModUtils.CheckCompatibleSet(new[] { firstMod, secondMod }), Is.False,
+ $"{firstMod.Name} ({firstMod.Acronym}) and {secondMod.Name} ({secondMod.Acronym}) should be incompatible.");
+ }
+ }
+ }
+ });
+ }
+
+ ///
+ /// This local helper is used rather than , because the aforementioned method flattens multi mods.
+ /// >
+ private static IEnumerable getMultiMods(Ruleset ruleset)
+ => Enum.GetValues(typeof(ModType)).Cast().SelectMany(ruleset.GetModsFor).OfType();
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
index 7516e7500b..76c49edf78 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
@@ -69,6 +69,34 @@ namespace osu.Game.Tests.NonVisual.Skinning
"Gameplay/osu/followpoint",
"followpoint", 1
},
+ new object[]
+ {
+ // Looking up a filename with extension specified should work.
+ new[] { "followpoint.png" },
+ "followpoint.png",
+ "followpoint.png", 1
+ },
+ new object[]
+ {
+ // Looking up a filename with extension specified should also work with @2x sprites.
+ new[] { "followpoint@2x.png" },
+ "followpoint.png",
+ "followpoint@2x.png", 2
+ },
+ new object[]
+ {
+ // Looking up a path with extension specified should work.
+ new[] { "Gameplay/osu/followpoint.png" },
+ "Gameplay/osu/followpoint.png",
+ "Gameplay/osu/followpoint.png", 1
+ },
+ new object[]
+ {
+ // Looking up a path with extension specified should also work with @2x sprites.
+ new[] { "Gameplay/osu/followpoint@2x.png" },
+ "Gameplay/osu/followpoint.png",
+ "Gameplay/osu/followpoint@2x.png", 2
+ },
};
[TestCaseSource(nameof(fallbackTestCases))]
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index e98ea98bb2..0622514783 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -33,9 +33,10 @@ namespace osu.Game.Tests.Online
var converted = deserialized?.ToMod(new TestRuleset());
+ Assert.NotNull(converted);
Assert.That(converted, Is.TypeOf(typeof(UnknownMod)));
- Assert.That(converted?.Type, Is.EqualTo(ModType.System));
- Assert.That(converted?.Acronym, Is.EqualTo("WNG??"));
+ Assert.That(converted.Type, Is.EqualTo(ModType.System));
+ Assert.That(converted.Acronym, Is.EqualTo("WNG??"));
}
[Test]
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index 9b0facd625..dde8715764 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Game.Database;
+using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Skinning;
@@ -110,6 +111,27 @@ namespace osu.Game.Tests.Skins.IO
assertImportedOnce(import1, import2);
});
+ [Test]
+ public Task TestImportExportedSkinFilename() => runSkinTest(async osu =>
+ {
+ MemoryStream exportStream = new MemoryStream();
+
+ var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "custom.osk"));
+ assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu);
+
+ import1.PerformRead(s =>
+ {
+ new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream);
+ });
+
+ string exportFilename = import1.GetDisplayString();
+
+ var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(exportStream, $"{exportFilename}.osk"));
+ assertCorrectMetadata(import2, "name 1 [custom]", "author 1", osu);
+
+ assertImportedOnce(import1, import2);
+ });
+
[Test]
public Task TestSameMetadataNameSameFolderName() => runSkinTest(async osu =>
{
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 9f708ace70..f7140537ee 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -359,9 +359,9 @@ namespace osu.Game.Tests.Visual.Background
protected override BackgroundScreen CreateBackground() =>
new FadeAccessibleBackground(Beatmap.Value);
- public override void OnEntering(IScreen last)
+ public override void OnEntering(ScreenTransitionEvent e)
{
- base.OnEntering(last);
+ base.OnEntering(e);
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
}
diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
index e40dd58663..51ca55f37f 100644
--- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
+++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Collections
});
Dependencies.Cache(manager);
- Dependencies.Cache(dialogOverlay);
+ Dependencies.CacheAs(dialogOverlay);
}
[SetUp]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
index d100fba8d6..30c8539d85 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
@@ -1,44 +1,71 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
+using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene
{
- [Cached(typeof(EditorBeatmap))]
- [Cached(typeof(IBeatSnapProvider))]
- private readonly EditorBeatmap editorBeatmap =
- new EditorBeatmap(new OsuBeatmap
- {
- BeatmapInfo =
- {
- Ruleset = new OsuRuleset().RulesetInfo
- }
- });
+ private EditorBeatmap editorBeatmap;
[Cached]
private EditorClipboard clipboard = new EditorClipboard();
- protected override void LoadComplete()
+ [SetUpSteps]
+ public void SetUpSteps()
{
- base.LoadComplete();
-
- Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
-
- Child = new ComposeScreen
+ AddStep("setup compose screen", () =>
{
- State = { Value = Visibility.Visible },
- };
+ var beatmap = new OsuBeatmap
+ {
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
+ };
+
+ editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null));
+
+ Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
+
+ Child = new DependencyProvidingContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(EditorBeatmap), editorBeatmap),
+ (typeof(IBeatSnapProvider), editorBeatmap),
+ },
+ Child = new ComposeScreen { State = { Value = Visibility.Visible } },
+ };
+ });
+
+ AddUntilStep("wait for composer", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+ }
+
+ ///
+ /// Ensures that the skin of the edited beatmap is properly wrapped in a .
+ ///
+ [Test]
+ public void TestLegacyBeatmapSkinHasTransformer()
+ {
+ AddAssert("legacy beatmap skin has transformer", () =>
+ {
+ var sources = this.ChildrenOfType().First().AllSources;
+ return sources.OfType().Count(t => t.Skin == editorBeatmap.BeatmapSkin.AsNonNull().Skin) == 1;
+ });
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs
index 34e6d1996d..b2f4fa2738 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs
@@ -7,11 +7,10 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
-using osu.Framework.Utils;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
-using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables;
using osuTK;
@@ -36,7 +35,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
- assertSpritesFromSkin(false);
+ AddAssert("sprite didn't find texture", () =>
+ sprites.All(sprite => sprite.ChildrenOfType().All(s => s.Texture == null)));
}
[Test]
@@ -48,9 +48,12 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
- assertSpritesFromSkin(true);
+ // Only checking for at least one sprite that succeeded, as not all skins in this test provide the hitcircleoverlay texture.
+ AddAssert("sprite found texture", () =>
+ sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Texture != null)));
- AddAssert("skinnable sprite has correct size", () => sprites.Any(s => Precision.AlmostEquals(s.ChildrenOfType().Single().Size, new Vector2(128, 128))));
+ AddAssert("skinnable sprite has correct size", () =>
+ sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Size == new Vector2(128))));
}
[Test]
@@ -104,9 +107,5 @@ namespace osu.Game.Tests.Visual.Gameplay
s.LifetimeStart = double.MinValue;
s.LifetimeEnd = double.MaxValue;
});
-
- private void assertSpritesFromSkin(bool fromSkin) =>
- AddAssert($"sprites are {(fromSkin ? "from skin" : "from storyboard")}",
- () => sprites.All(sprite => sprite.ChildrenOfType().Any() == fromSkin));
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
index 744227c55e..83d7d769df 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
@@ -56,10 +56,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private double lastFrequency = double.MaxValue;
- protected override void Update()
+ protected override void UpdateAfterChildren()
{
- base.Update();
+ base.UpdateAfterChildren();
+ // This must be done in UpdateAfterChildren to allow the gameplay clock to have updated before checking values.
double freq = Beatmap.Value.Track.AggregateFrequency.Value;
FrequencyIncreased |= freq > lastFrequency;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
index 79d7bb366d..bf491db45a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
@@ -21,7 +21,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps()
{
+ AddUntilStep("player is playing", () => Player.LocalUserPlaying.Value);
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
+ AddAssert("player is not playing", () => !Player.LocalUserPlaying.Value);
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
AddAssert("total number of results == 1", () =>
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
index 6b3fc304e0..ae2bc60fc6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("get variables", () =>
{
sampleDisabler = Player;
- slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).FirstOrDefault();
+ slider = Player.ChildrenOfType().MinBy(s => s.HitObject.StartTime);
samples = slider?.ChildrenOfType().ToArray();
return slider != null;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
index 5a1fc1b1e5..b90bd93002 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
@@ -1,12 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Timing;
-using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu;
@@ -36,10 +34,10 @@ namespace osu.Game.Tests.Visual.Gameplay
BeatmapInfo = { AudioLeadIn = leadIn }
});
- AddAssert($"first frame is {expectedStartTime}", () =>
+ AddStep("check first frame time", () =>
{
- Debug.Assert(player.FirstFrameClockTime != null);
- return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
+ Assert.That(player.FirstFrameClockTime, Is.Not.Null);
+ Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
});
}
@@ -59,10 +57,10 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
- AddAssert($"first frame is {expectedStartTime}", () =>
+ AddStep("check first frame time", () =>
{
- Debug.Assert(player.FirstFrameClockTime != null);
- return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
+ Assert.That(player.FirstFrameClockTime, Is.Not.Null);
+ Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
});
}
@@ -97,10 +95,10 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
- AddAssert($"first frame is {expectedStartTime}", () =>
+ AddStep("check first frame time", () =>
{
- Debug.Assert(player.FirstFrameClockTime != null);
- return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
+ Assert.That(player.FirstFrameClockTime, Is.Not.Null);
+ Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
});
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index ea0255ab76..ab5d766609 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -85,7 +85,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
pauseAndConfirm();
+ AddAssert("player not playing", () => !Player.LocalUserPlaying.Value);
+
resumeAndConfirm();
+ AddUntilStep("player playing", () => Player.LocalUserPlaying.Value);
}
[Test]
@@ -389,9 +392,9 @@ namespace osu.Game.Tests.Visual.Gameplay
public void ExitViaQuickExit() => PerformExit(false);
- public override void OnEntering(IScreen last)
+ public override void OnEntering(ScreenTransitionEvent e)
{
- base.OnEntering(last);
+ base.OnEntering(e);
GameplayClockContainer.Stop();
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 958d617d63..950c755cc1 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private SessionStatics sessionStatics { get; set; }
- [Cached]
+ [Cached(typeof(INotificationOverlay))]
private readonly NotificationOverlay notificationOverlay;
[Cached]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index 8b7e1c4e58..e89350de1a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -26,6 +26,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture]
public class TestSceneReplayDownloadButton : OsuManualInputManagerTestScene
{
+ private const long online_score_id = 2553163309;
+
[Resolved]
private RulesetStore rulesets { get; set; }
@@ -43,6 +45,15 @@ namespace osu.Game.Tests.Visual.Gameplay
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
}
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("delete previous imports", () =>
+ {
+ scoreManager.Delete(s => s.OnlineID == online_score_id);
+ });
+ }
+
[Test]
public void TestDisplayStates()
{
@@ -150,10 +161,12 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true)));
AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable);
+ AddAssert("button is enabled", () => downloadButton.ChildrenOfType().First().Enabled.Value);
AddStep("delete score", () => scoreManager.Delete(imported.Value));
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
+ AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
[Test]
@@ -178,7 +191,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
return new APIScore
{
- OnlineID = 2553163309,
+ OnlineID = online_score_id,
RulesetID = 0,
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
HasReplay = replayAvailable,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index ccf13e1e8f..64afe1235b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestSkinSourceContainer skinSource;
private PausableSkinnableSound skinnableSound;
- [SetUp]
+ [SetUpSteps]
public void SetUpSteps()
{
AddStep("setup hierarchy", () =>
diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
index 82accceb23..c68cd39c65 100644
--- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
+++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Menus
private IntroScreen intro;
- [Cached]
+ [Cached(typeof(INotificationOverlay))]
private NotificationOverlay notifications;
private ScheduledDelegate trackResetDelegate;
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index c65595d82e..dbc7e54b5e 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -2,6 +2,7 @@
// 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;
@@ -22,6 +23,17 @@ namespace osu.Game.Tests.Visual.Menus
[Resolved]
private IRulesetStore rulesets { get; set; }
+ private readonly Mock notifications = new Mock();
+
+ private readonly BindableInt unreadNotificationCount = new BindableInt();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Dependencies.CacheAs(notifications.Object);
+ notifications.SetupGet(n => n.UnreadCount).Returns(unreadNotificationCount);
+ }
+
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -31,10 +43,6 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
public void TestNotificationCounter()
{
- ToolbarNotificationButton notificationButton = null;
-
- AddStep("retrieve notification button", () => notificationButton = toolbar.ChildrenOfType().Single());
-
setNotifications(1);
setNotifications(2);
setNotifications(3);
@@ -43,7 +51,7 @@ namespace osu.Game.Tests.Visual.Menus
void setNotifications(int count)
=> AddStep($"set notification count to {count}",
- () => notificationButton.NotificationCount.Value = count);
+ () => unreadNotificationCount.Value = count);
}
[TestCase(false)]
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs
index 064d6f82fd..87d836687f 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs
@@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
+using osu.Game.Configuration;
using osu.Game.Overlays.Toolbar;
using osuTK;
using osuTK.Graphics;
@@ -15,7 +18,10 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public class TestSceneToolbarClock : OsuManualInputManagerTestScene
{
+ private Bindable clockDisplayMode;
+
private readonly Container mainContainer;
+ private readonly ToolbarClock toolbarClock;
public TestSceneToolbarClock()
{
@@ -49,7 +55,7 @@ namespace osu.Game.Tests.Visual.Menus
RelativeSizeAxes = Axes.Y,
Width = 2,
},
- new ToolbarClock(),
+ toolbarClock = new ToolbarClock(),
new Box
{
Colour = Color4.DarkRed,
@@ -65,6 +71,12 @@ namespace osu.Game.Tests.Visual.Menus
AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale));
}
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ clockDisplayMode = config.GetBindable(OsuSetting.ToolbarClockDisplayMode);
+ }
+
[Test]
public void TestRealGameTime()
{
@@ -76,5 +88,20 @@ namespace osu.Game.Tests.Visual.Menus
{
AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 });
}
+
+ [Test]
+ public void TestDisplayModeChange()
+ {
+ AddStep("Set clock display mode", () => clockDisplayMode.Value = ToolbarClockDisplayMode.Full);
+
+ AddStep("Trigger click", () => toolbarClock.TriggerClick());
+ AddAssert("State is digital with runtime", () => clockDisplayMode.Value == ToolbarClockDisplayMode.DigitalWithRuntime);
+ AddStep("Trigger click", () => toolbarClock.TriggerClick());
+ AddAssert("State is digital", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Digital);
+ AddStep("Trigger click", () => toolbarClock.TriggerClick());
+ AddAssert("State is analog", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Analog);
+ AddStep("Trigger click", () => toolbarClock.TriggerClick());
+ AddAssert("State is full", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Full);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
new file mode 100644
index 0000000000..4e6342868a
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
@@ -0,0 +1,210 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Spectator;
+using osu.Game.Replays.Legacy;
+using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play.HUD;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public abstract class MultiplayerGameplayLeaderboardTestScene : OsuTestScene
+ {
+ private const int total_users = 16;
+
+ protected readonly BindableList MultiplayerUsers = new BindableList();
+
+ protected MultiplayerGameplayLeaderboard Leaderboard { get; private set; }
+
+ protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId);
+
+ protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor);
+
+ private readonly BindableList multiplayerUserIds = new BindableList();
+
+ private OsuConfigManager config;
+
+ private readonly Mock spectatorClient = new Mock();
+ private readonly Mock multiplayerClient = new Mock();
+
+ private readonly Dictionary lastHeaders = new Dictionary();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
+ Dependencies.CacheAs(spectatorClient.Object);
+ Dependencies.CacheAs(multiplayerClient.Object);
+
+ // To emulate `MultiplayerClient.CurrentMatchPlayingUserIds` we need a bindable list of *only IDs*.
+ // This tracks the list of users 1:1.
+ MultiplayerUsers.BindCollectionChanged((c, e) =>
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ Debug.Assert(e.NewItems != null);
+
+ foreach (var user in e.NewItems.OfType())
+ multiplayerUserIds.Add(user.UserID);
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(e.OldItems != null);
+
+ foreach (var user in e.OldItems.OfType())
+ multiplayerUserIds.Remove(user.UserID);
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ multiplayerUserIds.Clear();
+ break;
+ }
+ });
+
+ multiplayerClient.SetupGet(c => c.CurrentMatchPlayingUserIds)
+ .Returns(() => multiplayerUserIds);
+ }
+
+ [SetUpSteps]
+ public virtual void SetUpSteps()
+ {
+ AddStep("reset counts", () =>
+ {
+ spectatorClient.Invocations.Clear();
+ lastHeaders.Clear();
+ });
+
+ AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = new APIUser
+ {
+ Id = 1,
+ });
+
+ AddStep("populate users", () =>
+ {
+ MultiplayerUsers.Clear();
+ for (int i = 0; i < total_users; i++)
+ MultiplayerUsers.Add(CreateUser(i));
+ });
+
+ AddStep("create leaderboard", () =>
+ {
+ Leaderboard?.Expire();
+
+ Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
+ var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
+ OsuScoreProcessor scoreProcessor = new OsuScoreProcessor();
+ scoreProcessor.ApplyBeatmap(playableBeatmap);
+
+ Child = scoreProcessor;
+
+ LoadComponentAsync(Leaderboard = CreateLeaderboard(scoreProcessor), Add);
+ });
+
+ AddUntilStep("wait for load", () => Leaderboard.IsLoaded);
+
+ AddStep("check watch requests were sent", () =>
+ {
+ foreach (var user in MultiplayerUsers)
+ spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once);
+ });
+ }
+
+ [Test]
+ public void TestScoreUpdates()
+ {
+ AddRepeatStep("update state", UpdateUserStatesRandomly, 100);
+ AddToggleStep("switch compact mode", expanded => Leaderboard.Expanded.Value = expanded);
+ }
+
+ [Test]
+ public void TestUserQuit()
+ {
+ AddUntilStep("mark users quit", () =>
+ {
+ if (MultiplayerUsers.Count == 0)
+ return true;
+
+ MultiplayerUsers.RemoveAt(0);
+ return false;
+ });
+
+ AddStep("check stop watching requests were sent", () =>
+ {
+ foreach (var user in MultiplayerUsers)
+ spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once);
+ });
+ }
+
+ [Test]
+ public void TestChangeScoringMode()
+ {
+ AddRepeatStep("update state", UpdateUserStatesRandomly, 5);
+ AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
+ AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
+ }
+
+ protected void UpdateUserStatesRandomly()
+ {
+ foreach (var user in MultiplayerUsers)
+ {
+ if (RNG.NextBool())
+ continue;
+
+ int userId = user.UserID;
+
+ if (!lastHeaders.TryGetValue(userId, out var header))
+ {
+ lastHeaders[userId] = header = new FrameHeader(new ScoreInfo
+ {
+ Statistics = new Dictionary
+ {
+ [HitResult.Miss] = 0,
+ [HitResult.Meh] = 0,
+ [HitResult.Great] = 0
+ }
+ });
+ }
+
+ switch (RNG.Next(0, 3))
+ {
+ case 0:
+ header.Combo = 0;
+ header.Statistics[HitResult.Miss]++;
+ break;
+
+ case 1:
+ header.Combo++;
+ header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
+ header.Statistics[HitResult.Meh]++;
+ break;
+
+ default:
+ header.Combo++;
+ header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
+ header.Statistics[HitResult.Great]++;
+ break;
+ }
+
+ spectatorClient.Raise(s => s.OnNewFrames -= null, userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) }));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
index d8ec0ad1f0..7d010592ae 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@@ -34,9 +35,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestMultipleStatuses()
{
+ FillFlowContainer rooms = null;
+
AddStep("create rooms", () =>
{
- Child = new FillFlowContainer
+ Child = rooms = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -124,6 +127,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
};
});
+
+ AddUntilStep("wait for panel load", () => rooms.Count == 5);
+ AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2);
+ AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 3);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs
new file mode 100644
index 0000000000..b5f901e51d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Overlays.Mods;
+using osu.Game.Screens.OnlinePlay;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneFreeModSelectScreen : MultiplayerTestScene
+ {
+ [Test]
+ public void TestFreeModSelect()
+ {
+ FreeModSelectScreen freeModSelectScreen = null;
+
+ AddStep("create free mod select screen", () => Child = freeModSelectScreen = new FreeModSelectScreen
+ {
+ State = { Value = Visibility.Visible }
+ });
+ AddUntilStep("all column content loaded",
+ () => freeModSelectScreen.ChildrenOfType().Any()
+ && freeModSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded));
+
+ AddUntilStep("all visible mods are playable",
+ () => this.ChildrenOfType()
+ .Where(panel => panel.IsPresent)
+ .All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
+
+ AddToggleStep("toggle visibility", visible =>
+ {
+ if (freeModSelectScreen != null)
+ freeModSelectScreen.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
index 512d206a06..c3487751b9 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
@@ -9,13 +9,14 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
+using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneGameplayChatDisplay : MultiplayerTestScene
+ public class TestSceneGameplayChatDisplay : OsuManualInputManagerTestScene
{
private GameplayChatDisplay chatDisplay;
@@ -35,11 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[SetUpSteps]
- public override void SetUpSteps()
+ public void SetUpSteps()
{
- base.SetUpSteps();
-
- AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay(SelectedRoom.Value)
+ AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay(new Room())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
index 5c2fd26857..ff6c02c4e5 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
@@ -3,80 +3,155 @@
using System;
using System.Linq;
+using Moq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Platform;
+using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Framework.Utils;
-using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
-using osu.Game.Rulesets;
+using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
-using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMatchStartControl : MultiplayerTestScene
+ public class TestSceneMatchStartControl : OsuManualInputManagerTestScene
{
+ private readonly Mock multiplayerClient = new Mock();
+ private readonly Mock availabilityTracker = new Mock();
+
+ private readonly Bindable beatmapAvailability = new Bindable();
+ private readonly Bindable room = new Bindable();
+
+ private MultiplayerRoom multiplayerRoom;
+ private MultiplayerRoomUser localUser;
+ private OngoingOperationTracker ongoingOperationTracker;
+
+ private PopoverContainer content;
private MatchStartControl control;
- private BeatmapSetInfo importedSet;
- private readonly Bindable selectedItem = new Bindable();
+ private OsuButton readyButton => control.ChildrenOfType().Single();
- private BeatmapManager beatmaps;
- private RulesetStore rulesets;
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
+ new CachedModelDependencyContainer(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } };
[BackgroundDependencyLoader]
- private void load(GameHost host, AudioManager audio)
+ private void load()
{
- Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
- Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
- Dependencies.Cache(Realm);
+ Dependencies.CacheAs(multiplayerClient.Object);
+ Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker());
+ Dependencies.CacheAs(availabilityTracker.Object);
+
+ availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
+
+ multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
+ multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
+
+ // By default, the local user is to be the host.
+ multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
+
+ // Assume all state changes are accepted by the server.
+ multiplayerClient.Setup(m => m.ChangeState(It.IsAny()))
+ .Callback((MultiplayerUserState r) =>
+ {
+ Logger.Log($"Changing local user state from {localUser.State} to {r}");
+ localUser.State = r;
+ raiseRoomUpdated();
+ });
+
+ multiplayerClient.Setup(m => m.StartMatch())
+ .Callback(() =>
+ {
+ multiplayerClient.Raise(m => m.LoadRequested -= null);
+
+ // immediately "end" gameplay, as we don't care about that part of the process.
+ changeUserState(localUser.UserID, MultiplayerUserState.Idle);
+ });
+
+ multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny()))
+ .Callback((MatchUserRequest request) =>
+ {
+ switch (request)
+ {
+ case StartMatchCountdownRequest countdownStart:
+ setRoomCountdown(countdownStart.Duration);
+ break;
+
+ case StopCountdownRequest _:
+ multiplayerRoom.Countdown = null;
+ raiseRoomUpdated();
+ break;
+ }
+ });
+
+ Children = new Drawable[]
+ {
+ ongoingOperationTracker,
+ content = new PopoverContainer { RelativeSizeAxes = Axes.Both }
+ };
}
- [SetUp]
- public new void Setup() => Schedule(() =>
+ [SetUpSteps]
+ public void SetUpSteps()
{
- AvailabilityTracker.SelectedItem.BindTo(selectedItem);
-
- beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
- importedSet = beatmaps.GetAllUsableBeatmapSets().First();
- Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
-
- selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
+ AddStep("reset state", () =>
{
- RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
- };
+ multiplayerClient.Invocations.Clear();
- Child = new PopoverContainer
+ beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
+
+ var playlistItem = new PlaylistItem(Beatmap.Value.BeatmapInfo)
+ {
+ RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
+ };
+
+ room.Value = new Room
+ {
+ Playlist = { playlistItem },
+ CurrentPlaylistItem = { Value = playlistItem }
+ };
+
+ localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id) { User = API.LocalUser.Value };
+
+ multiplayerRoom = new MultiplayerRoom(0)
+ {
+ Playlist =
+ {
+ new MultiplayerPlaylistItem(playlistItem),
+ },
+ Users = { localUser },
+ Host = localUser,
+ };
+ });
+
+ AddStep("create control", () =>
{
- RelativeSizeAxes = Axes.Both,
- Child = control = new MatchStartControl
+ content.Child = control = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(250, 50),
- }
- };
- });
+ };
+ });
+ }
[Test]
public void TestStartWithCountdown()
{
ClickButtonWhenEnabled();
AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+
ClickButtonWhenEnabled();
AddStep("click the first countdown button", () =>
{
@@ -85,8 +160,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
- AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
- AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
+ AddStep("check request received", () =>
+ {
+ multiplayerClient.Verify(m => m.SendMatchRequest(It.Is(req =>
+ req.Duration == TimeSpan.FromSeconds(10)
+ )), Times.Once);
+ });
}
[Test]
@@ -94,6 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
ClickButtonWhenEnabled();
AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+
ClickButtonWhenEnabled();
AddStep("click the first countdown button", () =>
{
@@ -102,6 +182,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
+ AddStep("check request received", () =>
+ {
+ multiplayerClient.Verify(m => m.SendMatchRequest(It.Is(req =>
+ req.Duration == TimeSpan.FromSeconds(10)
+ )), Times.Once);
+ });
+
ClickButtonWhenEnabled();
AddStep("click the cancel button", () =>
{
@@ -110,41 +197,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
- AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
- AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
+ AddStep("check request received", () =>
+ {
+ multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny()), Times.Once);
+ });
}
[Test]
public void TestReadyAndUnReadyDuringCountdown()
{
- AddStep("add second user as host", () =>
- {
- MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
- MultiplayerClient.TransferHost(2);
- });
+ AddStep("add second user as host", () => addUser(new APIUser { Id = 2, Username = "Another user" }, true));
- AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely());
+ AddStep("start countdown", () => setRoomCountdown(TimeSpan.FromMinutes(1)));
ClickButtonWhenEnabled();
- AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
+ checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled();
- AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
+ checkLocalUserState(MultiplayerUserState.Idle);
}
[Test]
public void TestCountdownWhileSpectating()
{
- AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
- AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
+ AddStep("set spectating", () => changeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
+ checkLocalUserState(MultiplayerUserState.Spectating);
AddAssert("countdown button is visible", () => this.ChildrenOfType().Single().IsPresent);
AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
- AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
+ AddStep("add second user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
- AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
+ AddStep("set second user ready", () => changeUserState(2, MultiplayerUserState.Ready));
AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
}
@@ -153,60 +238,54 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add second user as host", () =>
{
- MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
- MultiplayerClient.TransferHost(2);
+ addUser(new APIUser { Id = 2, Username = "Another user" }, true);
});
- AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
- AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null);
+ AddStep("start countdown", () => multiplayerClient.Object.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
+ AddUntilStep("countdown started", () => multiplayerRoom.Countdown != null);
- AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID));
- AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true);
+ AddStep("transfer host to local user", () => transferHost(localUser));
+ AddUntilStep("local user is host", () => multiplayerRoom.Host?.Equals(multiplayerClient.Object.LocalUser) == true);
ClickButtonWhenEnabled();
- AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
- AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
+ checkLocalUserState(MultiplayerUserState.Ready);
+ AddAssert("countdown still active", () => multiplayerRoom.Countdown != null);
}
[Test]
- public void TestCountdownButtonVisibilityWithAutoStartEnablement()
+ public void TestCountdownButtonVisibilityWithAutoStart()
{
ClickButtonWhenEnabled();
- AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
+ checkLocalUserState(MultiplayerUserState.Ready);
AddUntilStep("countdown button visible", () => this.ChildrenOfType().Single().IsPresent);
- AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely());
+ AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
ClickButtonWhenEnabled();
- AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
+ checkLocalUserState(MultiplayerUserState.Ready);
AddUntilStep("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent);
}
[Test]
public void TestClickingReadyButtonUnReadiesDuringAutoStart()
{
- AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely());
+ AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
ClickButtonWhenEnabled();
- AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
+ checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled();
- AddUntilStep("local user became idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
+ checkLocalUserState(MultiplayerUserState.Idle);
}
[Test]
public void TestDeletedBeatmapDisableReady()
{
- OsuButton readyButton = null;
+ AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
- AddUntilStep("ensure ready button enabled", () =>
- {
- readyButton = control.ChildrenOfType().Single();
- return readyButton.Enabled.Value;
- });
-
- AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
+ AddStep("mark beatmap not available", () => beatmapAvailability.Value = BeatmapAvailability.NotDownloaded());
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
- AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
+
+ AddStep("mark beatmap available", () => beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable());
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
}
@@ -215,31 +294,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add second user as host", () =>
{
- MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
- MultiplayerClient.TransferHost(2);
+ addUser(new APIUser { Id = 2, Username = "Another user" }, true);
});
ClickButtonWhenEnabled();
- AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
+ checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled();
- AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
+ checkLocalUserState(MultiplayerUserState.Idle);
}
[TestCase(true)]
[TestCase(false)]
public void TestToggleStateWhenHost(bool allReady)
{
- AddStep("setup", () =>
- {
- MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
-
- if (!allReady)
- MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
- });
+ if (!allReady)
+ AddStep("add other user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
ClickButtonWhenEnabled();
- AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
+ checkLocalUserState(MultiplayerUserState.Ready);
verifyGameplayStartFlow();
}
@@ -249,12 +322,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add host", () =>
{
- MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
- MultiplayerClient.TransferHost(2);
+ addUser(new APIUser { Id = 2, Username = "Another user" }, true);
});
ClickButtonWhenEnabled();
- AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
+
+ AddStep("make local user host", () => transferHost(localUser));
verifyGameplayStartFlow();
}
@@ -264,18 +337,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("setup", () =>
{
- MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
- MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
+ addUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled();
- AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
+ checkLocalUserState(MultiplayerUserState.Ready);
- AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0));
+ AddStep("transfer host", () => transferHost(multiplayerRoom.Users[1]));
ClickButtonWhenEnabled();
- AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
- AddUntilStep("ready button enabled", () => control.ChildrenOfType().Single().Enabled.Value);
+ checkLocalUserState(MultiplayerUserState.Idle);
+ AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
}
[TestCase(true)]
@@ -283,44 +355,83 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestManyUsersChangingState(bool isHost)
{
const int users = 10;
- AddStep("setup", () =>
- {
- MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
- for (int i = 0; i < users; i++)
- MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
- });
- if (!isHost)
- AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
+ AddStep("add many users", () =>
+ {
+ for (int i = 0; i < users; i++)
+ addUser(new APIUser { Id = i, Username = "Another user" }, !isHost && i == 2);
+ });
ClickButtonWhenEnabled();
AddRepeatStep("change user ready state", () =>
{
- MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
+ changeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
}, 20);
AddRepeatStep("ready all users", () =>
{
- var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
+ var nextUnready = multiplayerRoom.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null)
- MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
+ changeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
}, users);
}
private void verifyGameplayStartFlow()
{
- AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
+ checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled();
- AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
- AddStep("finish gameplay", () =>
- {
- MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
- MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
- });
-
- AddUntilStep("ready button enabled", () => control.ChildrenOfType().Single().Enabled.Value);
+ AddStep("check start request received", () => multiplayerClient.Verify(m => m.StartMatch(), Times.Once));
}
+
+ private void checkLocalUserState(MultiplayerUserState state) =>
+ AddUntilStep($"local user is {state}", () => localUser.State == state);
+
+ private void setRoomCountdown(TimeSpan duration)
+ {
+ multiplayerRoom.Countdown = new MatchStartCountdown { TimeRemaining = duration };
+ raiseRoomUpdated();
+ }
+
+ private void changeUserState(int userId, MultiplayerUserState newState)
+ {
+ multiplayerRoom.Users.Single(u => u.UserID == userId).State = newState;
+ raiseRoomUpdated();
+ }
+
+ private void addUser(APIUser user, bool asHost = false)
+ {
+ var multiplayerRoomUser = new MultiplayerRoomUser(user.Id) { User = user };
+
+ multiplayerRoom.Users.Add(multiplayerRoomUser);
+
+ if (asHost)
+ transferHost(multiplayerRoomUser);
+
+ raiseRoomUpdated();
+ }
+
+ private void transferHost(MultiplayerRoomUser user)
+ {
+ multiplayerRoom.Host = user;
+ raiseRoomUpdated();
+ }
+
+ private void changeRoomSettings(MultiplayerRoomSettings settings)
+ {
+ multiplayerRoom.Settings = settings;
+
+ // Changing settings should reset all user ready statuses.
+ foreach (var user in multiplayerRoom.Users)
+ {
+ if (user.State == MultiplayerUserState.Ready)
+ user.State = MultiplayerUserState.Idle;
+ }
+
+ raiseRoomUpdated();
+ }
+
+ private void raiseRoomUpdated() => multiplayerClient.Raise(m => m.RoomUpdated -= null);
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index e5e3fecd06..703b526e8c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -464,16 +464,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
private class TestMultiSpectatorScreen : MultiSpectatorScreen
{
- private readonly double? gameplayStartTime;
+ private readonly double? startTime;
- public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? gameplayStartTime = null)
+ public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? startTime = null)
: base(room, users)
{
- this.gameplayStartTime = gameplayStartTime;
+ this.startTime = startTime;
}
protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap)
- => new MasterGameplayClockContainer(beatmap, gameplayStartTime ?? 0, gameplayStartTime.HasValue);
+ => new MasterGameplayClockContainer(beatmap, 0) { StartTime = startTime ?? 0 };
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index d0765fc4b3..6a69917fb4 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -495,17 +495,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
- AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
+ AddAssert("Mods match current item",
+ () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() });
- AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
+ AddAssert("Mods don't match current item",
+ () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
- AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
+ AddAssert("Mods match current item",
+ () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
}
[Test]
@@ -665,6 +668,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
}
+ [Test]
+ public void TestGameplayDoesntStartWithNonLoadedUser()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist =
+ {
+ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
+ {
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ }
+ }
+ });
+
+ pressReadyButton();
+
+ AddStep("join other user and ready", () =>
+ {
+ multiplayerClient.AddUser(new APIUser { Id = 1234 });
+ multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
+ });
+
+ AddStep("start match", () =>
+ {
+ multiplayerClient.StartMatch();
+ });
+
+ AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player);
+
+ AddWaitStep("wait some", 20);
+
+ AddAssert("ensure gameplay hasn't started", () => this.ChildrenOfType().SingleOrDefault()?.IsRunning == false);
+ }
+
[Test]
public void TestRoomSettingsReQueriedWhenJoiningRoom()
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index bcd4474876..6e4aa48b0e 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -1,161 +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 System;
-using System.Collections.Generic;
using System.Linq;
-using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions;
-using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
-using osu.Game.Configuration;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Multiplayer;
-using osu.Game.Online.Spectator;
-using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring;
-using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
-using osu.Game.Tests.Visual.OnlinePlay;
-using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene
+ public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerGameplayLeaderboardTestScene
{
- private static IEnumerable users => Enumerable.Range(0, 16);
-
- public new TestMultiplayerSpectatorClient SpectatorClient => (TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient;
-
- private MultiplayerGameplayLeaderboard leaderboard;
- private OsuConfigManager config;
-
- [BackgroundDependencyLoader]
- private void load()
+ protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor)
{
- Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
- }
-
- public override void SetUpSteps()
- {
- base.SetUpSteps();
-
- AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely());
-
- AddStep("create leaderboard", () =>
+ return new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
{
- leaderboard?.Expire();
-
- OsuScoreProcessor scoreProcessor;
- Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
-
- var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
- var multiplayerUsers = new List();
-
- foreach (int user in users)
- {
- SpectatorClient.SendStartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID);
- multiplayerUsers.Add(OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true));
- }
-
- Children = new Drawable[]
- {
- scoreProcessor = new OsuScoreProcessor(),
- };
-
- scoreProcessor.ApplyBeatmap(playableBeatmap);
-
- LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray())
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }, Add);
- });
-
- AddUntilStep("wait for load", () => leaderboard.IsLoaded);
- AddUntilStep("wait for user population", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count > 0);
- }
-
- [Test]
- public void TestScoreUpdates()
- {
- AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100);
- AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
- }
-
- [Test]
- public void TestUserQuit()
- {
- foreach (int user in users)
- AddStep($"mark user {user} quit", () => MultiplayerClient.RemoveUser(UserLookupCache.GetUserAsync(user).GetResultSafely().AsNonNull()));
- }
-
- [Test]
- public void TestChangeScoringMode()
- {
- AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 5);
- AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
- AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
- }
-
- protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
-
- protected class TestDependencies : MultiplayerTestSceneDependencies
- {
- protected override TestSpectatorClient CreateSpectatorClient() => new TestMultiplayerSpectatorClient();
- }
-
- public class TestMultiplayerSpectatorClient : TestSpectatorClient
- {
- private readonly Dictionary lastHeaders = new Dictionary();
-
- public void RandomlyUpdateState()
- {
- foreach ((int userId, _) in WatchedUserStates)
- {
- if (RNG.NextBool())
- continue;
-
- if (!lastHeaders.TryGetValue(userId, out var header))
- {
- lastHeaders[userId] = header = new FrameHeader(new ScoreInfo
- {
- Statistics = new Dictionary
- {
- [HitResult.Miss] = 0,
- [HitResult.Meh] = 0,
- [HitResult.Great] = 0
- }
- });
- }
-
- switch (RNG.Next(0, 3))
- {
- case 0:
- header.Combo = 0;
- header.Statistics[HitResult.Miss]++;
- break;
-
- case 1:
- header.Combo++;
- header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
- header.Statistics[HitResult.Meh]++;
- break;
-
- default:
- header.Combo++;
- header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
- header.Statistics[HitResult.Great]++;
- break;
- }
-
- ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) }));
- }
- }
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
index 7f5aced925..5caab9487e 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
@@ -1,121 +1,57 @@
// 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.Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests.Responses;
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;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerTestScene
+ public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerGameplayLeaderboardTestScene
{
- private static IEnumerable users => Enumerable.Range(0, 16);
+ private int team;
- public new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient SpectatorClient =>
- (TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient;
-
- protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
-
- protected class TestDependencies : MultiplayerTestSceneDependencies
+ protected override MultiplayerRoomUser CreateUser(int userId)
{
- protected override TestSpectatorClient CreateSpectatorClient() => new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient();
+ var user = base.CreateUser(userId);
+ user.MatchState = new TeamVersusUserState
+ {
+ TeamID = team++ % 2
+ };
+ return user;
}
- private MultiplayerGameplayLeaderboard leaderboard;
- private GameplayMatchScoreDisplay gameplayScoreDisplay;
-
- protected override Room CreateRoom()
- {
- var room = base.CreateRoom();
- room.Type.Value = MatchType.TeamVersus;
- return room;
- }
+ protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor) =>
+ new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
public override void SetUpSteps()
{
base.SetUpSteps();
- AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely());
-
- AddStep("create leaderboard", () =>
+ AddStep("Add external display components", () =>
{
- leaderboard?.Expire();
-
- OsuScoreProcessor scoreProcessor;
- Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
-
- var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
- var multiplayerUsers = new List();
-
- foreach (int user in users)
+ LoadComponentAsync(new MatchScoreDisplay
{
- SpectatorClient.SendStartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID);
- var roomUser = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true);
+ Team1Score = { BindTarget = Leaderboard.TeamScores[0] },
+ Team2Score = { BindTarget = Leaderboard.TeamScores[1] }
+ }, Add);
- roomUser.MatchState = new TeamVersusUserState
- {
- TeamID = RNG.Next(0, 2)
- };
-
- multiplayerUsers.Add(roomUser);
- }
-
- Children = new Drawable[]
+ LoadComponentAsync(new GameplayMatchScoreDisplay
{
- scoreProcessor = new OsuScoreProcessor(),
- };
-
- scoreProcessor.ApplyBeatmap(playableBeatmap);
-
- LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, 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", () => MultiplayerClient.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;
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Team1Score = { BindTarget = Leaderboard.TeamScores[0] },
+ Team2Score = { BindTarget = Leaderboard.TeamScores[1] },
+ Expanded = { BindTarget = Leaderboard.Expanded },
+ }, Add);
});
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
index 6536ef2ca1..111f51675d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
@@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
namespace osu.Game.Tests.Visual.Multiplayer
@@ -13,13 +14,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public new void Setup() => Schedule(() =>
{
- Child = new Container
+ Child = new PopoverContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- Height = 50,
- Child = new MultiplayerMatchFooter()
+ RelativeSizeAxes = Axes.Both,
+ Child = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ Height = 50,
+ Child = new MultiplayerMatchFooter()
+ }
};
});
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 381b9b58bd..714951cc42 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -8,13 +8,12 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
-using osu.Framework.Extensions;
using osu.Framework.Extensions.TypeExtensions;
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.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
@@ -27,6 +26,7 @@ using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Select;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -35,10 +35,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager manager;
private RulesetStore rulesets;
- private List beatmaps;
+ private IList beatmaps => importedBeatmapSet?.PerformRead(s => s.Beatmaps) ?? new List();
private TestMultiplayerMatchSongSelect songSelect;
+ private Live importedBeatmapSet;
+
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
@@ -46,44 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
- beatmaps = new List();
-
- var metadata = new BeatmapMetadata
- {
- Artist = "Some Artist",
- Title = "Some Beatmap",
- Author = { Username = "Some Author" },
- };
-
- var beatmapSetInfo = new BeatmapSetInfo
- {
- OnlineID = 10,
- Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
- DateAdded = DateTimeOffset.UtcNow
- };
-
- for (int i = 0; i < 8; ++i)
- {
- int beatmapId = 10 * 10 + i;
-
- int length = RNG.Next(30000, 200000);
- double bpm = RNG.NextSingle(80, 200);
-
- var beatmap = new BeatmapInfo
- {
- Ruleset = rulesets.GetRuleset(i % 4) ?? throw new InvalidOperationException(),
- OnlineID = beatmapId,
- Length = length,
- BPM = bpm,
- Metadata = metadata,
- Difficulty = new BeatmapDifficulty()
- };
-
- beatmaps.Add(beatmap);
- beatmapSetInfo.Beatmaps.Add(beatmap);
- }
-
- manager.Import(beatmapSetInfo);
+ importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()));
}
public override void SetUpSteps()
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
index cbd8b472b8..1231866b36 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
@@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@@ -183,14 +184,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertItemInHistoryListStep(2, 0);
}
+ [Test]
+ public void TestInsertedItemDoesNotRefreshAllOthers()
+ {
+ AddStep("change to round robin queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayersRoundRobin }).WaitSafely());
+
+ // Add a few items for the local user.
+ addItemStep();
+ addItemStep();
+ addItemStep();
+ addItemStep();
+ addItemStep();
+
+ DrawableRoomPlaylistItem[] drawableItems = null;
+ AddStep("get drawable items", () => drawableItems = this.ChildrenOfType().ToArray());
+
+ // Add 1 item for another user.
+ AddStep("join second user", () => MultiplayerClient.AddUser(new APIUser { Id = 10 }));
+ addItemStep(userId: 10);
+
+ // New item inserted towards the top of the list.
+ assertItemInQueueListStep(7, 1);
+ AddAssert("all previous playlist items remained", () => drawableItems.All(this.ChildrenOfType().Contains));
+ }
+
///
/// Adds a step to create a new playlist item.
///
- private void addItemStep(bool expired = false) => AddStep("add item", () => MultiplayerClient.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
+ private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{
- Expired = expired,
- PlayedAt = DateTimeOffset.Now
- })));
+ MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
+ {
+ Expired = expired,
+ PlayedAt = DateTimeOffset.Now
+ })).WaitSafely();
+ });
///
/// Asserts the position of a given playlist item in the queue list.
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
index bdc348b043..bcb36a585f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
@@ -26,10 +26,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
var score = TestResources.CreateTestScoreInfo(beatmapInfo);
- SortedDictionary teamScores = new SortedDictionary
+ SortedDictionary teamScores = new SortedDictionary
{
- { 0, new BindableInt(team1Score) },
- { 1, new BindableInt(team2Score) }
+ { 0, new BindableLong(team1Score) },
+ { 1, new BindableLong(team2Score) }
};
Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, new PlaylistItem(beatmapInfo), teamScores));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs
index f95e73ff3c..b0a977dcbb 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs
@@ -1,67 +1,68 @@
// 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 Moq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneRankRangePill : MultiplayerTestScene
+ public class TestSceneRankRangePill : OsuTestScene
{
- [SetUp]
- public new void Setup() => Schedule(() =>
+ private readonly Mock multiplayerClient = new Mock();
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
+ // not used directly in component, but required due to it inheriting from OnlinePlayComposite.
+ new CachedModelDependencyContainer(base.CreateChildDependencies(parent));
+
+ [BackgroundDependencyLoader]
+ private void load()
{
+ Dependencies.CacheAs(multiplayerClient.Object);
+
Child = new RankRangePill
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
- });
+ }
[Test]
public void TestSingleUser()
{
- AddStep("add user", () =>
+ setupRoomWithUsers(new APIUser
{
- MultiplayerClient.AddUser(new APIUser
- {
- Id = 2,
- Statistics = { GlobalRank = 1234 }
- });
-
- // Remove the local user so only the one above is displayed.
- MultiplayerClient.RemoveUser(API.LocalUser.Value);
+ Id = 2,
+ Statistics = { GlobalRank = 1234 }
});
}
[Test]
public void TestMultipleUsers()
{
- AddStep("add users", () =>
- {
- MultiplayerClient.AddUser(new APIUser
+ setupRoomWithUsers(
+ new APIUser
{
Id = 2,
Statistics = { GlobalRank = 1234 }
- });
-
- MultiplayerClient.AddUser(new APIUser
+ },
+ new APIUser
{
Id = 3,
Statistics = { GlobalRank = 3333 }
- });
-
- MultiplayerClient.AddUser(new APIUser
+ },
+ new APIUser
{
Id = 4,
Statistics = { GlobalRank = 4321 }
});
-
- // Remove the local user so only the ones above are displayed.
- MultiplayerClient.RemoveUser(API.LocalUser.Value);
- });
}
[TestCase(1, 10)]
@@ -73,22 +74,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(1000000, 10000000)]
public void TestRange(int min, int max)
{
- AddStep("add users", () =>
- {
- MultiplayerClient.AddUser(new APIUser
+ setupRoomWithUsers(
+ new APIUser
{
Id = 2,
Statistics = { GlobalRank = min }
- });
-
- MultiplayerClient.AddUser(new APIUser
+ },
+ new APIUser
{
Id = 3,
Statistics = { GlobalRank = max }
});
+ }
- // Remove the local user so only the ones above are displayed.
- MultiplayerClient.RemoveUser(API.LocalUser.Value);
+ private void setupRoomWithUsers(params APIUser[] users)
+ {
+ AddStep("setup room", () =>
+ {
+ multiplayerClient.SetupGet(m => m.Room).Returns(new MultiplayerRoom(0)
+ {
+ Users = new List(users.Select(apiUser => new MultiplayerRoomUser(apiUser.Id) { User = apiUser }))
+ });
+
+ multiplayerClient.Raise(m => m.RoomUpdated -= null);
});
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs
new file mode 100644
index 0000000000..8c96ec699f
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.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 System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Screens.Menu;
+using osu.Game.Screens.Select;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public class TestSceneButtonSystemNavigation : OsuGameTestScene
+ {
+ private ButtonSystem buttons => ((MainMenu)Game.ScreenStack.CurrentScreen).ChildrenOfType().Single();
+
+ [Test]
+ public void TestGlobalActionHasPriority()
+ {
+ AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial);
+
+ // triggering the cookie in the initial state with any key should only happen if no other action is bound to that key.
+ // here, F10 is bound to GlobalAction.ToggleGameplayMouseButtons.
+ AddStep("press F10", () => InputManager.Key(Key.F10));
+ AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial);
+
+ AddStep("press P", () => InputManager.Key(Key.P));
+ AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel);
+ }
+
+ [Test]
+ public void TestShortcutKeys()
+ {
+ AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial);
+
+ AddStep("press P", () => InputManager.Key(Key.P));
+ AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel);
+
+ AddStep("press P", () => InputManager.Key(Key.P));
+ AddAssert("state is play", () => buttons.State == ButtonSystemState.Play);
+
+ AddStep("press P", () => InputManager.Key(Key.P));
+ AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
index 347b4b6c54..b7a74dcd27 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
@@ -59,8 +59,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () =>
{
- // dismiss any notifications that may appear (ie. muted notification).
- clickMouseInCentre();
+ DismissAnyNotifications();
return player != null;
});
@@ -73,12 +72,6 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("key counter did increase", () => keyCounter.CountPresses == 1);
}
- private void clickMouseInCentre()
- {
- InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
- InputManager.Click(MouseButton.Left);
- }
-
private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel
.ChildrenOfType()
.FirstOrDefault(s => s.Ruleset.ShortName == "osu");
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs
index 22a00a3e5a..2662b3930c 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs
@@ -89,18 +89,11 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () =>
{
- // dismiss any notifications that may appear (ie. muted notification).
- clickMouseInCentre();
+ DismissAnyNotifications();
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value);
}
-
- private void clickMouseInCentre()
- {
- InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
- InputManager.Click(MouseButton.Left);
- }
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
index b8d1636ea0..0f8337deb6 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Navigation
typeof(OsuLogo),
typeof(IdleTracker),
typeof(OnScreenDisplay),
- typeof(NotificationOverlay),
+ typeof(INotificationOverlay),
typeof(BeatmapListingOverlay),
typeof(DashboardOverlay),
typeof(NewsOverlay),
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Navigation
typeof(LoginOverlay),
typeof(MusicController),
typeof(AccountCreationOverlay),
- typeof(DialogOverlay),
+ typeof(IDialogOverlay),
typeof(ScreenshotManager)
};
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
index 1ebceed15d..2ce914ba3d 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
@@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
@@ -113,12 +114,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("did not perform", () => !actionPerformed);
AddAssert("only one exit attempt", () => blocker.ExitAttempts == 1);
- AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded);
+ waitForDialogOverlayLoad();
if (confirmed)
{
AddStep("accept dialog", () => InputManager.Key(Key.Number1));
- AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null);
+ AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null);
AddUntilStep("did perform", () => actionPerformed);
}
else
@@ -145,7 +146,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddWaitStep("wait a bit", 10);
- AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded);
+ waitForDialogOverlayLoad();
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == blocker2);
AddAssert("did not perform", () => !actionPerformed);
@@ -171,6 +172,48 @@ namespace osu.Game.Tests.Visual.Navigation
}
}
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestPerformBlockedByDialogSubScreen(bool confirm)
+ {
+ TestScreenWithNestedStack screenWithNestedStack = null;
+
+ PushAndConfirm(() => screenWithNestedStack = new TestScreenWithNestedStack());
+
+ AddAssert("wait for nested screen", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
+
+ AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
+
+ AddUntilStep("wait for dialog", () => screenWithNestedStack.Blocker.ExitAttempts == 1);
+
+ AddWaitStep("wait a bit", 10);
+
+ waitForDialogOverlayLoad();
+
+ AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == screenWithNestedStack);
+ AddAssert("nested screen didn't change", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
+
+ AddAssert("did not perform", () => !actionPerformed);
+
+ AddAssert("only one exit attempt", () => screenWithNestedStack.Blocker.ExitAttempts == 1);
+
+ if (confirm)
+ {
+ AddStep("accept dialog", () => InputManager.Key(Key.Number1));
+ AddAssert("nested screen changed", () => screenWithNestedStack.SubScreenStack.CurrentScreen != screenWithNestedStack.Blocker);
+ AddUntilStep("did perform", () => actionPerformed);
+ }
+ else
+ {
+ AddStep("cancel dialog", () => InputManager.Key(Key.Number2));
+ AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == screenWithNestedStack);
+ AddAssert("nested screen didn't change", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
+ AddAssert("did not perform", () => !actionPerformed);
+ }
+ }
+
+ private void waitForDialogOverlayLoad() => AddUntilStep("wait for dialog overlay loaded", () => ((Drawable)Game.Dependencies.Get()).IsLoaded);
+
private void importAndWaitForSongSelect()
{
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -181,13 +224,13 @@ namespace osu.Game.Tests.Visual.Navigation
public class DialogBlockingScreen : OsuScreen
{
[Resolved]
- private DialogOverlay dialogOverlay { get; set; }
+ private IDialogOverlay dialogOverlay { get; set; }
private int dialogDisplayCount;
public int ExitAttempts { get; private set; }
- public override bool OnExiting(IScreen next)
+ public override bool OnExiting(ScreenExitEvent e)
{
ExitAttempts++;
@@ -197,7 +240,32 @@ namespace osu.Game.Tests.Visual.Navigation
return true;
}
- return base.OnExiting(next);
+ return base.OnExiting(e);
+ }
+ }
+
+ public class TestScreenWithNestedStack : OsuScreen, IHasSubScreenStack
+ {
+ public DialogBlockingScreen Blocker { get; private set; }
+
+ public ScreenStack SubScreenStack { get; } = new ScreenStack();
+
+ public TestScreenWithNestedStack()
+ {
+ AddInternal(SubScreenStack);
+
+ SubScreenStack.Push(Blocker = new DialogBlockingScreen());
+ }
+
+ public override bool OnExiting(ScreenExitEvent e)
+ {
+ if (SubScreenStack.CurrentScreen != null)
+ {
+ SubScreenStack.CurrentScreen.Exit();
+ return true;
+ }
+
+ return base.OnExiting(e);
}
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 394976eb43..a1f41d4caf 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -6,8 +6,8 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
-using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
@@ -23,12 +22,10 @@ using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play;
-using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options;
-using osu.Game.Skinning.Editor;
using osu.Game.Tests.Beatmaps.IO;
using osuTK;
using osuTK.Input;
@@ -70,73 +67,6 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
}
- [Test]
- public void TestEditComponentDuringGameplay()
- {
- Screens.Select.SongSelect songSelect = null;
- PushAndConfirm(() => songSelect = new TestPlaySongSelect());
- AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
-
- AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
-
- AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
-
- SkinEditor skinEditor = null;
-
- AddStep("open skin editor", () =>
- {
- InputManager.PressKey(Key.ControlLeft);
- InputManager.PressKey(Key.ShiftLeft);
- InputManager.Key(Key.S);
- InputManager.ReleaseKey(Key.ControlLeft);
- InputManager.ReleaseKey(Key.ShiftLeft);
- });
-
- AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType().FirstOrDefault()) != null);
-
- AddStep("Click gameplay scene button", () =>
- {
- skinEditor.ChildrenOfType().First(b => b.Text == "Gameplay").TriggerClick();
- });
-
- AddUntilStep("wait for player", () =>
- {
- // dismiss any notifications that may appear (ie. muted notification).
- clickMouseInCentre();
- return Game.ScreenStack.CurrentScreen is Player;
- });
-
- BarHitErrorMeter hitErrorMeter = null;
-
- AddUntilStep("select bar hit error blueprint", () =>
- {
- var blueprint = skinEditor.ChildrenOfType().FirstOrDefault(b => b.Item is BarHitErrorMeter);
-
- if (blueprint == null)
- return false;
-
- hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
- skinEditor.SelectedComponents.Clear();
- skinEditor.SelectedComponents.Add(blueprint.Item);
- return true;
- });
-
- AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
-
- AddStep("hover first slider", () =>
- {
- InputManager.MoveMouseTo(
- skinEditor.ChildrenOfType().First()
- .ChildrenOfType>().First()
- .ChildrenOfType>().First()
- );
- });
-
- AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
-
- AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
- }
-
[Test]
public void TestRetryCountIncrements()
{
@@ -154,8 +84,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () =>
{
- // dismiss any notifications that may appear (ie. muted notification).
- clickMouseInCentre();
+ DismissAnyNotifications();
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
@@ -200,10 +129,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("choose clear all scores", () => InputManager.Key(Key.Number4));
- AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded);
- AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null);
+ AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get()).IsLoaded);
+ AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
- AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null);
+ AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null);
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true));
@@ -246,10 +175,10 @@ namespace osu.Game.Tests.Visual.Navigation
InputManager.Click(MouseButton.Left);
});
- AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded);
- AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null);
+ AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get()).IsLoaded);
+ AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
- AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null);
+ AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null);
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true));
@@ -279,8 +208,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () =>
{
- // dismiss any notifications that may appear (ie. muted notification).
- clickMouseInCentre();
+ DismissAnyNotifications();
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
@@ -595,8 +523,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () =>
{
- // dismiss any notifications that may appear (ie. muted notification).
- clickMouseInCentre();
+ DismissAnyNotifications();
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
@@ -606,12 +533,6 @@ namespace osu.Game.Tests.Visual.Navigation
return () => player;
}
- private void clickMouseInCentre()
- {
- InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
- InputManager.Click(MouseButton.Left);
- }
-
private void pushEscape() =>
AddStep("Press escape", () => InputManager.Key(Key.Escape));
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorSceneLibrary.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorSceneLibrary.cs
new file mode 100644
index 0000000000..d3aeba2c0f
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorSceneLibrary.cs
@@ -0,0 +1,136 @@
+// 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.Extensions;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Testing;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD.HitErrorMeters;
+using osu.Game.Skinning.Editor;
+using osu.Game.Tests.Beatmaps.IO;
+using osuTK.Input;
+using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public class TestSceneSkinEditorSceneLibrary : OsuGameTestScene
+ {
+ private SkinEditor skinEditor;
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ Screens.Select.SongSelect songSelect = null;
+ PushAndConfirm(() => songSelect = new TestPlaySongSelect());
+ AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
+
+ AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
+
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+
+ AddStep("open skin editor", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.PressKey(Key.ShiftLeft);
+ InputManager.Key(Key.S);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ InputManager.ReleaseKey(Key.ShiftLeft);
+ });
+
+ AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType().FirstOrDefault()) != null);
+ }
+
+ [Test]
+ public void TestEditComponentDuringGameplay()
+ {
+ switchToGameplayScene();
+
+ BarHitErrorMeter hitErrorMeter = null;
+
+ AddUntilStep("select bar hit error blueprint", () =>
+ {
+ var blueprint = skinEditor.ChildrenOfType().FirstOrDefault(b => b.Item is BarHitErrorMeter);
+
+ if (blueprint == null)
+ return false;
+
+ hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
+ skinEditor.SelectedComponents.Clear();
+ skinEditor.SelectedComponents.Add(blueprint.Item);
+ return true;
+ });
+
+ AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
+
+ AddStep("hover first slider", () =>
+ {
+ InputManager.MoveMouseTo(
+ skinEditor.ChildrenOfType().First()
+ .ChildrenOfType>().First()
+ .ChildrenOfType>().First()
+ );
+ });
+
+ AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
+
+ AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
+ }
+
+ [Test]
+ public void TestAutoplayCompatibleModsRetainedOnEnteringGameplay()
+ {
+ AddStep("select DT", () => Game.SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
+
+ switchToGameplayScene();
+
+ AddAssert("DT still selected", () => ((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Single() is OsuModDoubleTime);
+ }
+
+ [Test]
+ public void TestAutoplayIncompatibleModsRemovedOnEnteringGameplay()
+ {
+ AddStep("select no fail and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModSpunOut() });
+
+ switchToGameplayScene();
+
+ AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
+ }
+
+ [Test]
+ public void TestDuplicateAutoplayModRemovedOnEnteringGameplay()
+ {
+ AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() });
+
+ switchToGameplayScene();
+
+ AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
+ }
+
+ [Test]
+ public void TestCinemaModRemovedOnEnteringGameplay()
+ {
+ AddStep("select cinema", () => Game.SelectedMods.Value = new Mod[] { new OsuModCinema() });
+
+ switchToGameplayScene();
+
+ AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
+ }
+
+ private void switchToGameplayScene()
+ {
+ AddStep("Click gameplay scene button", () => skinEditor.ChildrenOfType().First(b => b.Text == "Gameplay").TriggerClick());
+
+ AddUntilStep("wait for player", () =>
+ {
+ DismissAnyNotifications();
+ return Game.ScreenStack.CurrentScreen is Player;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index a056e0cd2c..5999125013 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
@@ -5,9 +5,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables.Cards;
+using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@@ -29,6 +31,14 @@ namespace osu.Game.Tests.Visual.Online
private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType().Single();
+ private OsuConfigManager localConfig;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
+ }
+
[SetUpSteps]
public void SetUpSteps()
{
@@ -61,6 +71,8 @@ namespace osu.Game.Tests.Visual.Online
Id = API.LocalUser.Value.Id + 1,
};
});
+
+ AddStep("reset size", () => localConfig.SetValue(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal));
}
[Test]
@@ -121,23 +133,23 @@ namespace osu.Game.Tests.Visual.Online
}
[Test]
- public void TestCardSizeSwitching()
+ public void TestCardSizeSwitching([Values] bool viaConfig)
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
assertAllCardsOfType(100);
- setCardSize(BeatmapCardSize.Extra);
+ setCardSize(BeatmapCardSize.Extra, viaConfig);
assertAllCardsOfType(100);
- setCardSize(BeatmapCardSize.Normal);
+ setCardSize(BeatmapCardSize.Normal, viaConfig);
assertAllCardsOfType(100);
AddStep("fetch for 0 beatmaps", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
- setCardSize(BeatmapCardSize.Extra);
+ setCardSize(BeatmapCardSize.Extra, viaConfig);
AddAssert("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
}
@@ -361,7 +373,13 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("\"no maps found\" placeholder not shown", () => !overlay.ChildrenOfType().Any(d => d.IsPresent));
}
- private void setCardSize(BeatmapCardSize cardSize) => AddStep($"set card size to {cardSize}", () => overlay.ChildrenOfType().Single().Current.Value = cardSize);
+ private void setCardSize(BeatmapCardSize cardSize, bool viaConfig) => AddStep($"set card size to {cardSize}", () =>
+ {
+ if (viaConfig)
+ localConfig.SetValue(OsuSetting.BeatmapListingCardSize, cardSize);
+ else
+ overlay.ChildrenOfType().Single().Current.Value = cardSize;
+ });
private void assertAllCardsOfType(int expectedCount)
where T : BeatmapCard =>
@@ -370,5 +388,11 @@ namespace osu.Game.Tests.Visual.Online
int loadedCorrectCount = this.ChildrenOfType().Count(card => card.IsLoaded && card.GetType() == typeof(T));
return loadedCorrectCount > 0 && loadedCorrectCount == expectedCount;
});
+
+ protected override void Dispose(bool isDisposing)
+ {
+ localConfig?.Dispose();
+ base.Dispose(isDisposing);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs
index be3fc7aff9..82b34c50c2 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs
@@ -71,7 +71,9 @@ namespace osu.Game.Tests.Visual.Online
{
Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(),
- }
+ },
+ PassCount = RNG.Next(0, 999),
+ PlayCount = RNG.Next(1000, 1999),
};
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
new file mode 100644
index 0000000000..a3bfbd47a3
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
@@ -0,0 +1,188 @@
+// 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.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.Graphics.Sprites;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Chat.ChannelList;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [TestFixture]
+ public class TestSceneChannelList : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
+
+ [Cached]
+ private readonly Bindable selected = new Bindable();
+
+ private OsuSpriteText selectorText;
+ private OsuSpriteText selectedText;
+ private OsuSpriteText leaveText;
+ private ChannelList channelList;
+
+ [SetUp]
+ public void SetUp()
+ {
+ Schedule(() =>
+ {
+ Child = new GridContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.7f,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.Absolute, 20),
+ new Dimension(GridSizeMode.Absolute, 20),
+ new Dimension(GridSizeMode.Absolute, 20),
+ new Dimension(),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ selectorText = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ },
+ },
+ new Drawable[]
+ {
+ selectedText = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ },
+ },
+ new Drawable[]
+ {
+ leaveText = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ },
+ },
+ new Drawable[]
+ {
+ channelList = new ChannelList
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Y,
+ Width = 190,
+ },
+ },
+ },
+ };
+
+ channelList.OnRequestSelect += channel =>
+ {
+ channelList.SelectorActive.Value = false;
+ selected.Value = channel;
+ };
+
+ channelList.OnRequestLeave += channel =>
+ {
+ leaveText.Text = $"OnRequestLeave: {channel.Name}";
+ leaveText.FadeOutFromOne(1000, Easing.InQuint);
+ selected.Value = null;
+ channelList.RemoveChannel(channel);
+ };
+
+ channelList.SelectorActive.BindValueChanged(change =>
+ {
+ selectorText.Text = $"Channel Selector Active: {change.NewValue}";
+ selected.Value = null;
+ }, true);
+
+ selected.BindValueChanged(change =>
+ {
+ selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}";
+ }, true);
+ });
+ }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("Add Public Channels", () =>
+ {
+ for (int i = 0; i < 10; i++)
+ channelList.AddChannel(createRandomPublicChannel());
+ });
+
+ AddStep("Add Private Channels", () =>
+ {
+ for (int i = 0; i < 10; i++)
+ channelList.AddChannel(createRandomPrivateChannel());
+ });
+ }
+
+ [Test]
+ public void TestVisual()
+ {
+ AddStep("Unread Selected", () =>
+ {
+ if (selected.Value != null)
+ channelList.GetItem(selected.Value).Unread.Value = true;
+ });
+
+ AddStep("Read Selected", () =>
+ {
+ if (selected.Value != null)
+ channelList.GetItem(selected.Value).Unread.Value = false;
+ });
+
+ AddStep("Add Mention Selected", () =>
+ {
+ if (selected.Value != null)
+ channelList.GetItem(selected.Value).Mentions.Value++;
+ });
+
+ AddStep("Add 98 Mentions Selected", () =>
+ {
+ if (selected.Value != null)
+ channelList.GetItem(selected.Value).Mentions.Value += 98;
+ });
+
+ AddStep("Clear Mentions Selected", () =>
+ {
+ if (selected.Value != null)
+ channelList.GetItem(selected.Value).Mentions.Value = 0;
+ });
+ }
+
+ private Channel createRandomPublicChannel()
+ {
+ int id = RNG.Next(0, 10000);
+ return new Channel
+ {
+ Name = $"#channel-{id}",
+ Type = ChannelType.Public,
+ Id = id,
+ };
+ }
+
+ private Channel createRandomPrivateChannel()
+ {
+ int id = RNG.Next(0, 10000);
+ return new Channel(new APIUser
+ {
+ Id = id,
+ Username = $"test user {id}",
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelListItem.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelListItem.cs
deleted file mode 100644
index af419c8b91..0000000000
--- a/osu.Game.Tests/Visual/Online/TestSceneChannelListItem.cs
+++ /dev/null
@@ -1,163 +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 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.Framework.Graphics.Shapes;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Chat;
-using osu.Game.Overlays;
-using osu.Game.Overlays.Chat.ChannelList;
-using osuTK;
-
-namespace osu.Game.Tests.Visual.Online
-{
- [TestFixture]
- public class TestSceneChannelListItem : OsuTestScene
- {
- [Cached]
- private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
-
- [Cached]
- private readonly Bindable selected = new Bindable();
-
- private static readonly List channels = new List
- {
- createPublicChannel("#public-channel"),
- createPublicChannel("#public-channel-long-name"),
- createPrivateChannel("test user", 2),
- createPrivateChannel("test user long name", 3),
- };
-
- private readonly Dictionary channelMap = new Dictionary();
-
- private FillFlowContainer flow;
- private OsuSpriteText selectedText;
- private OsuSpriteText leaveText;
-
- [SetUp]
- public void SetUp()
- {
- Schedule(() =>
- {
- foreach (var item in channelMap.Values)
- item.Expire();
-
- channelMap.Clear();
-
- Child = new FillFlowContainer
- {
- Direction = FillDirection.Vertical,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Both,
- Spacing = new Vector2(10),
- Children = new Drawable[]
- {
- selectedText = new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- },
- leaveText = new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Height = 16,
- AlwaysPresent = true,
- },
- new Container
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AutoSizeAxes = Axes.Y,
- Width = 190,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = colourProvider.Background6,
- },
- flow = new FillFlowContainer
- {
- Direction = FillDirection.Vertical,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- },
- },
- },
- },
- };
-
- selected.BindValueChanged(change =>
- {
- selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}";
- }, true);
-
- foreach (var channel in channels)
- {
- var item = new ChannelListItem(channel);
- flow.Add(item);
- channelMap.Add(channel, item);
- item.OnRequestSelect += c => selected.Value = c;
- item.OnRequestLeave += leaveChannel;
- }
- });
- }
-
- [Test]
- public void TestVisual()
- {
- AddStep("Select second item", () => selected.Value = channels.Skip(1).First());
-
- AddStep("Unread Selected", () =>
- {
- if (selected.Value != null)
- channelMap[selected.Value].Unread.Value = true;
- });
-
- AddStep("Read Selected", () =>
- {
- if (selected.Value != null)
- channelMap[selected.Value].Unread.Value = false;
- });
-
- AddStep("Add Mention Selected", () =>
- {
- if (selected.Value != null)
- channelMap[selected.Value].Mentions.Value++;
- });
-
- AddStep("Add 98 Mentions Selected", () =>
- {
- if (selected.Value != null)
- channelMap[selected.Value].Mentions.Value += 98;
- });
-
- AddStep("Clear Mentions Selected", () =>
- {
- if (selected.Value != null)
- channelMap[selected.Value].Mentions.Value = 0;
- });
- }
-
- private void leaveChannel(Channel channel)
- {
- leaveText.Text = $"OnRequestLeave: {channel.Name}";
- leaveText.FadeOutFromOne(1000, Easing.InQuint);
- }
-
- private static Channel createPublicChannel(string name) =>
- new Channel { Name = name, Type = ChannelType.Public, Id = 1234 };
-
- private static Channel createPrivateChannel(string username, int id)
- => new Channel(new APIUser { Id = id, Username = username });
- }
-}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
index d077868175..6818147da4 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online
Dependencies.Cache(chatManager);
Dependencies.Cache(new ChatOverlay());
- Dependencies.Cache(dialogOverlay);
+ Dependencies.CacheAs(dialogOverlay);
}
[SetUp]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 80a6698761..4d1dee1650 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -534,11 +534,33 @@ namespace osu.Game.Tests.Visual.Online
});
});
- AddStep("Highlight message and open chat", () =>
+ AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1));
+ }
+
+ [Test]
+ public void TestHighlightWithNullChannel()
+ {
+ Message message = null;
+
+ AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
+
+ AddStep("Send message in channel 1", () =>
{
- chatOverlay.HighlightMessage(message, channel1);
- chatOverlay.Show();
+ channel1.AddNewMessages(message = new Message
+ {
+ ChannelId = channel1.Id,
+ Content = "Message to highlight!",
+ Timestamp = DateTimeOffset.Now,
+ Sender = new APIUser
+ {
+ Id = 2,
+ Username = "Someone",
+ }
+ });
});
+
+ AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null);
+ AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1));
}
private void pressChannelHotkey(int number)
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs
new file mode 100644
index 0000000000..a241aa0517
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs
@@ -0,0 +1,122 @@
+// 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.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Chat;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [TestFixture]
+ public class TestSceneChatTextBox : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
+
+ [Cached]
+ private readonly Bindable currentChannel = new Bindable();
+
+ private OsuSpriteText commitText;
+ private OsuSpriteText searchText;
+ private ChatTextBar bar;
+
+ [SetUp]
+ public void SetUp()
+ {
+ Schedule(() =>
+ {
+ Child = new GridContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.Absolute, 30),
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ commitText = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Font = OsuFont.Default.With(size: 20),
+ },
+ searchText = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Font = OsuFont.Default.With(size: 20),
+ },
+ },
+ },
+ },
+ },
+ new Drawable[]
+ {
+ bar = new ChatTextBar
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Width = 0.99f,
+ },
+ },
+ },
+ };
+
+ bar.OnChatMessageCommitted += text =>
+ {
+ commitText.Text = $"{nameof(bar.OnChatMessageCommitted)}: {text}";
+ commitText.FadeOutFromOne(1000, Easing.InQuint);
+ };
+
+ bar.OnSearchTermsChanged += text =>
+ {
+ searchText.Text = $"{nameof(bar.OnSearchTermsChanged)}: {text}";
+ };
+ });
+ }
+
+ [Test]
+ public void TestVisual()
+ {
+ AddStep("Public Channel", () => currentChannel.Value = createPublicChannel("#osu"));
+ AddStep("Public Channel Long Name", () => currentChannel.Value = createPublicChannel("#public-channel-long-name"));
+ AddStep("Private Channel", () => currentChannel.Value = createPrivateChannel("peppy", 2));
+ AddStep("Private Long Name", () => currentChannel.Value = createPrivateChannel("test user long name", 3));
+
+ AddStep("Chat Mode Channel", () => bar.ShowSearch.Value = false);
+ AddStep("Chat Mode Search", () => bar.ShowSearch.Value = true);
+ }
+
+ private static Channel createPublicChannel(string name)
+ => new Channel { Name = name, Type = ChannelType.Public, Id = 1234 };
+
+ private static Channel createPrivateChannel(string username, int id)
+ => new Channel(new APIUser { Id = id, Username = username });
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs
index 2c253650d5..79f62a16e3 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs
@@ -200,7 +200,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
public ChannelManager ChannelManager { get; } = new ChannelManager();
- [Cached]
+ [Cached(typeof(INotificationOverlay))]
public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay
{
Anchor = Anchor.TopRight,
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index ee9a0e263b..f5fe00458a 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -259,7 +259,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
{
- ID = --highestScoreId,
+ ID = getNextLowestScoreId(),
Accuracy = userScore.Accuracy,
Passed = true,
Rank = userScore.Rank,
@@ -274,7 +274,7 @@ namespace osu.Game.Tests.Visual.Playlists
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
{
- ID = ++lowestScoreId,
+ ID = getNextHighestScoreId(),
Accuracy = userScore.Accuracy,
Passed = true,
Rank = userScore.Rank,
@@ -306,7 +306,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
result.Scores.Add(new MultiplayerScore
{
- ID = sort == "score_asc" ? --highestScoreId : ++lowestScoreId,
+ ID = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId(),
Accuracy = 1,
Passed = true,
Rank = ScoreRank.X,
@@ -327,6 +327,17 @@ namespace osu.Game.Tests.Visual.Playlists
return result;
}
+ ///