1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-23 23:32:15 +08:00

Merge branch 'master' into fix_strict_tracking_scoring

This commit is contained in:
Dan Balasescu
2025-07-02 18:14:15 +09:00
Unverified
188 changed files with 3837 additions and 3592 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.604.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.625.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+90 -113
View File
@@ -2,21 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game;
using osu.Game.Configuration;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Velopack;
using Velopack.Sources;
using UpdateManager = osu.Game.Updater.UpdateManager;
namespace osu.Desktop.Updater
{
public partial class VelopackUpdateManager : Game.Updater.UpdateManager
public partial class VelopackUpdateManager : UpdateManager
{
[Resolved]
private INotificationOverlay notificationOverlay { get; set; } = null!;
@@ -27,143 +28,119 @@ namespace osu.Desktop.Updater
[Resolved]
private ILocalUserPlayInfo? localUserInfo { get; set; }
[Resolved]
private OsuConfigManager osuConfigManager { get; set; } = null!;
private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying;
private readonly Bindable<ReleaseStream> releaseStream = new Bindable<ReleaseStream>();
private UpdateManager? updateManager;
private UpdateInfo? pendingUpdate;
private ScheduledDelegate? scheduledBackgroundCheck;
protected override void LoadComplete()
private void scheduleNextUpdateCheck()
{
// Used by the base implementation.
osuConfigManager.BindWith(OsuSetting.ReleaseStream, releaseStream);
releaseStream.BindValueChanged(_ => onReleaseStreamChanged(), true);
base.LoadComplete();
scheduledBackgroundCheck?.Cancel();
scheduledBackgroundCheck = Scheduler.AddDelayed(() =>
{
log("Running scheduled background update check...");
CheckForUpdate();
}, 60000 * 30);
}
private void onReleaseStreamChanged()
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
{
updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, releaseStream.Value == ReleaseStream.Tachyon), new UpdateOptions
scheduledBackgroundCheck?.Cancel();
if (isInGameplay)
{
AllowVersionDowngrade = true,
log("Update check cancelled - user is in gameplay");
scheduleNextUpdateCheck();
return false;
}
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
{
AllowVersionDowngrade = true
});
Schedule(() => Task.Run(CheckForUpdateAsync));
}
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task<bool> checkForUpdateAsync()
{
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
bool scheduleRecheck = false;
try
if (cancellationToken.IsCancellationRequested)
{
// Avoid any kind of update checking while gameplay is running.
if (isInGameplay)
{
scheduleRecheck = true;
return true;
}
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
// Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975).
if (pendingUpdate != null)
{
// If there is an update pending restart, show the notification to restart again.
notificationOverlay.Post(new UpdateApplicationCompleteNotification
{
Activated = () =>
{
Task.Run(restartToApplyUpdate);
return true;
}
});
return true;
}
if (updateManager == null)
{
scheduleRecheck = true;
return false;
}
pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
// No update is available. We'll check again later.
if (pendingUpdate == null)
{
scheduleRecheck = true;
return false;
}
// An update is found, let's notify the user and start downloading it.
UpdateProgressNotification notification = new UpdateProgressNotification
{
CompletionClickAction = () =>
{
Task.Run(restartToApplyUpdate);
return true;
},
};
runOutsideOfGameplay(() => notificationOverlay.Post(notification));
notification.StartDownload();
try
{
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
runOutsideOfGameplay(() => notification.State = ProgressNotificationState.Completed);
}
catch (Exception e)
{
// In the case of an error, a separate notification will be displayed.
scheduleRecheck = true;
notification.FailDownload();
Logger.Error(e, @"update failed!");
}
}
catch (Exception e)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
Logger.Log($@"update check failed ({e.Message})");
}
finally
{
if (scheduleRecheck)
{
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
log("Update check cancelled");
scheduleNextUpdateCheck();
return true;
}
if (update == null)
{
// No update is available.
log("No update found");
scheduleNextUpdateCheck();
return false;
}
// Download update in the background while notifying awaiters of the update being available.
log($"New update available: {update.TargetFullRelease.Version}");
downloadUpdate(updateManager, update, cancellationToken);
return true;
}
private void runOutsideOfGameplay(Action action)
private void downloadUpdate(Velopack.UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () =>
{
log($"Beginning download of update {update.TargetFullRelease.Version}...");
UpdateDownloadProgressNotification progressNotification = new UpdateDownloadProgressNotification(cancellationToken)
{
CompletionClickAction = () =>
{
restartToApplyUpdate(updateManager, update);
return true;
}
};
try
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(progressNotification.CancellationToken, cancellationToken))
{
progressNotification.StartDownload();
runOutsideOfGameplay(() => notificationOverlay.Post(progressNotification), cts.Token);
await updateManager.DownloadUpdatesAsync(update, p => progressNotification.Progress = p / 100f, cts.Token).ConfigureAwait(false);
runOutsideOfGameplay(() => progressNotification.State = ProgressNotificationState.Completed, cts.Token);
}
}
catch (OperationCanceledException)
{
progressNotification.FailDownload();
log(@"Update cancelled");
}
catch (Exception e)
{
// In the case of an error, a separate notification will be displayed.
progressNotification.FailDownload();
Logger.Error(e, @"Update failed!");
}
return true;
}, cancellationToken);
private void runOutsideOfGameplay(Action action, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return;
if (isInGameplay)
{
Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000);
Scheduler.AddDelayed(() => runOutsideOfGameplay(action, cancellationToken), 1000);
return;
}
action();
}
private async Task restartToApplyUpdate()
private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update) => Task.Run(async () =>
{
if (updateManager == null)
return;
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
await updateManager.WaitExitThenApplyUpdatesAsync(update.TargetFullRelease).ConfigureAwait(false);
Schedule(() => game.AttemptExit());
}
});
private static void log(string text) => Logger.Log($"VelopackUpdateManager: {text}");
}
}
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
@@ -23,21 +24,21 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
new BeatmapStatistic
{
Name = @"Fruits",
Name = BeatmapStatisticStrings.Fruits,
Content = fruits.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
BarDisplayLength = fruits / (float)sum,
},
new BeatmapStatistic
{
Name = @"Juice Streams",
Name = BeatmapStatisticStrings.JuiceStreams,
Content = juiceStreams.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
BarDisplayLength = juiceStreams / (float)sum,
},
new BeatmapStatistic
{
Name = @"Banana Showers",
Name = BeatmapStatisticStrings.BananaShowers,
Content = bananaShowers.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
BarDisplayLength = Math.Min(bananaShowers / 10f, 1),
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -219,5 +220,40 @@ namespace osu.Game.Rulesets.Catch.Edit
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
}
#region Clipboard handling
public override string ConvertSelectionToString()
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
// 1,2,3,4 ...
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
public override void SelectFromTimestamp(double timestamp, string objectDescription)
{
if (!selection_regex.IsMatch(objectDescription))
return;
List<CatchHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<CatchHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] splitDescription = objectDescription.Split(',');
for (int i = 0; i < splitDescription.Length; i++)
{
if (!int.TryParse(splitDescription[i], out int combo) || combo < 1)
continue;
CatchHitObject? current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo);
if (current == null)
continue;
EditorBeatmap.SelectedHitObjects.Add(current);
if (i < splitDescription.Length - 1)
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
#endregion
}
}
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
get
{
if (UserAdjustedSettingsCount != 1)
if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate))
return string.Empty;
if (!CircleSize.IsDefault) return format("CS", CircleSize);
@@ -19,7 +19,6 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
{
protected override Ruleset CreateRuleset() => new ManiaRuleset();
@@ -72,13 +71,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -137d, HitResult.Miss },
new object[] { 5f, -138d, HitResult.Miss },
new object[] { 5f, 111d, HitResult.Ok },
new object[] { 5f, 112d, HitResult.Miss },
new object[] { 5f, 113d, HitResult.Miss },
new object[] { 5f, 114d, HitResult.Miss },
new object[] { 5f, 135d, HitResult.Miss },
new object[] { 5f, 136d, HitResult.Miss },
new object[] { 5f, 137d, HitResult.Miss },
new object[] { 5f, 138d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 112d, HitResult.Miss },
// new object[] { 5f, 113d, HitResult.Miss },
// new object[] { 5f, 114d, HitResult.Miss },
// new object[] { 5f, 135d, HitResult.Miss },
// new object[] { 5f, 136d, HitResult.Miss },
// new object[] { 5f, 137d, HitResult.Miss },
// new object[] { 5f, 138d, HitResult.Miss },
// OD = 9.3 test cases.
// PERFECT hit window is [ -14ms, 14ms]
@@ -99,13 +99,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 9.3f, 70d, HitResult.Ok },
new object[] { 9.3f, 71d, HitResult.Ok },
new object[] { 9.3f, 98d, HitResult.Ok },
new object[] { 9.3f, 99d, HitResult.Miss },
new object[] { 9.3f, 100d, HitResult.Miss },
new object[] { 9.3f, 101d, HitResult.Miss },
new object[] { 9.3f, 122d, HitResult.Miss },
new object[] { 9.3f, 123d, HitResult.Miss },
new object[] { 9.3f, 124d, HitResult.Miss },
new object[] { 9.3f, 125d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 9.3f, 99d, HitResult.Miss },
// new object[] { 9.3f, 100d, HitResult.Miss },
// new object[] { 9.3f, 101d, HitResult.Miss },
// new object[] { 9.3f, 122d, HitResult.Miss },
// new object[] { 9.3f, 123d, HitResult.Miss },
// new object[] { 9.3f, 124d, HitResult.Miss },
// new object[] { 9.3f, 125d, HitResult.Miss },
new object[] { 9.3f, -98d, HitResult.Ok },
new object[] { 9.3f, -99d, HitResult.Ok },
new object[] { 9.3f, -100d, HitResult.Meh },
@@ -145,13 +146,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -137d, HitResult.Miss },
new object[] { 5f, -138d, HitResult.Miss },
new object[] { 5f, 111d, HitResult.Ok },
new object[] { 5f, 112d, HitResult.Miss },
new object[] { 5f, 113d, HitResult.Miss },
new object[] { 5f, 114d, HitResult.Miss },
new object[] { 5f, 135d, HitResult.Miss },
new object[] { 5f, 136d, HitResult.Miss },
new object[] { 5f, 137d, HitResult.Miss },
new object[] { 5f, 138d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 112d, HitResult.Miss },
// new object[] { 5f, 113d, HitResult.Miss },
// new object[] { 5f, 114d, HitResult.Miss },
// new object[] { 5f, 135d, HitResult.Miss },
// new object[] { 5f, 136d, HitResult.Miss },
// new object[] { 5f, 137d, HitResult.Miss },
// new object[] { 5f, 138d, HitResult.Miss },
// OD = 9.3 test cases.
// PERFECT hit window is [ -16ms, 16ms]
@@ -172,13 +174,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 9.3f, 70d, HitResult.Ok },
new object[] { 9.3f, 71d, HitResult.Ok },
new object[] { 9.3f, 98d, HitResult.Ok },
new object[] { 9.3f, 99d, HitResult.Miss },
new object[] { 9.3f, 100d, HitResult.Miss },
new object[] { 9.3f, 101d, HitResult.Miss },
new object[] { 9.3f, 122d, HitResult.Miss },
new object[] { 9.3f, 123d, HitResult.Miss },
new object[] { 9.3f, 124d, HitResult.Miss },
new object[] { 9.3f, 125d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 9.3f, 99d, HitResult.Miss },
// new object[] { 9.3f, 100d, HitResult.Miss },
// new object[] { 9.3f, 101d, HitResult.Miss },
// new object[] { 9.3f, 122d, HitResult.Miss },
// new object[] { 9.3f, 123d, HitResult.Miss },
// new object[] { 9.3f, 124d, HitResult.Miss },
// new object[] { 9.3f, 125d, HitResult.Miss },
new object[] { 9.3f, -98d, HitResult.Ok },
new object[] { 9.3f, -99d, HitResult.Ok },
new object[] { 9.3f, -100d, HitResult.Meh },
@@ -207,13 +210,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 3.1f, 88d, HitResult.Ok },
new object[] { 3.1f, 89d, HitResult.Ok },
new object[] { 3.1f, 116d, HitResult.Ok },
new object[] { 3.1f, 117d, HitResult.Miss },
new object[] { 3.1f, 118d, HitResult.Miss },
new object[] { 3.1f, 119d, HitResult.Miss },
new object[] { 3.1f, 140d, HitResult.Miss },
new object[] { 3.1f, 141d, HitResult.Miss },
new object[] { 3.1f, 142d, HitResult.Miss },
new object[] { 3.1f, 143d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 3.1f, 117d, HitResult.Miss },
// new object[] { 3.1f, 118d, HitResult.Miss },
// new object[] { 3.1f, 119d, HitResult.Miss },
// new object[] { 3.1f, 140d, HitResult.Miss },
// new object[] { 3.1f, 141d, HitResult.Miss },
// new object[] { 3.1f, 142d, HitResult.Miss },
// new object[] { 3.1f, 143d, HitResult.Miss },
new object[] { 3.1f, -116d, HitResult.Ok },
new object[] { 3.1f, -117d, HitResult.Ok },
new object[] { 3.1f, -118d, HitResult.Meh },
@@ -253,13 +257,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -122d, HitResult.Miss },
new object[] { 5f, -123d, HitResult.Miss },
new object[] { 5f, 96d, HitResult.Ok },
new object[] { 5f, 97d, HitResult.Miss },
new object[] { 5f, 98d, HitResult.Miss },
new object[] { 5f, 99d, HitResult.Miss },
new object[] { 5f, 120d, HitResult.Miss },
new object[] { 5f, 121d, HitResult.Miss },
new object[] { 5f, 122d, HitResult.Miss },
new object[] { 5f, 123d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 97d, HitResult.Miss },
// new object[] { 5f, 98d, HitResult.Miss },
// new object[] { 5f, 99d, HitResult.Miss },
// new object[] { 5f, 120d, HitResult.Miss },
// new object[] { 5f, 121d, HitResult.Miss },
// new object[] { 5f, 122d, HitResult.Miss },
// new object[] { 5f, 123d, HitResult.Miss },
// OD = 3.1 test cases.
// PERFECT hit window is [ -16ms, 16ms]
@@ -280,13 +285,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 3.1f, 78d, HitResult.Ok },
new object[] { 3.1f, 79d, HitResult.Ok },
new object[] { 3.1f, 96d, HitResult.Ok },
new object[] { 3.1f, 97d, HitResult.Miss },
new object[] { 3.1f, 98d, HitResult.Miss },
new object[] { 3.1f, 99d, HitResult.Miss },
new object[] { 3.1f, 120d, HitResult.Miss },
new object[] { 3.1f, 121d, HitResult.Miss },
new object[] { 3.1f, 122d, HitResult.Miss },
new object[] { 3.1f, 123d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 3.1f, 97d, HitResult.Miss },
// new object[] { 3.1f, 98d, HitResult.Miss },
// new object[] { 3.1f, 99d, HitResult.Miss },
// new object[] { 3.1f, 120d, HitResult.Miss },
// new object[] { 3.1f, 121d, HitResult.Miss },
// new object[] { 3.1f, 122d, HitResult.Miss },
// new object[] { 3.1f, 123d, HitResult.Miss },
new object[] { 3.1f, -96d, HitResult.Ok },
new object[] { 3.1f, -97d, HitResult.Ok },
new object[] { 3.1f, -98d, HitResult.Meh },
@@ -327,13 +333,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -98d, HitResult.Miss },
new object[] { 5f, -99d, HitResult.Miss },
new object[] { 5f, 79d, HitResult.Ok },
new object[] { 5f, 80d, HitResult.Miss },
new object[] { 5f, 81d, HitResult.Miss },
new object[] { 5f, 82d, HitResult.Miss },
new object[] { 5f, 96d, HitResult.Miss },
new object[] { 5f, 97d, HitResult.Miss },
new object[] { 5f, 98d, HitResult.Miss },
new object[] { 5f, 99d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 80d, HitResult.Miss },
// new object[] { 5f, 81d, HitResult.Miss },
// new object[] { 5f, 82d, HitResult.Miss },
// new object[] { 5f, 96d, HitResult.Miss },
// new object[] { 5f, 97d, HitResult.Miss },
// new object[] { 5f, 98d, HitResult.Miss },
// new object[] { 5f, 99d, HitResult.Miss },
// OD = 9.3 test cases.
// This leads to "effective" OD of 13.02.
@@ -356,13 +363,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 9.3f, 50d, HitResult.Ok },
new object[] { 9.3f, 51d, HitResult.Ok },
new object[] { 9.3f, 69d, HitResult.Ok },
new object[] { 9.3f, 70d, HitResult.Miss },
new object[] { 9.3f, 71d, HitResult.Miss },
new object[] { 9.3f, 72d, HitResult.Miss },
new object[] { 9.3f, 86d, HitResult.Miss },
new object[] { 9.3f, 87d, HitResult.Miss },
new object[] { 9.3f, 88d, HitResult.Miss },
new object[] { 9.3f, 89d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 9.3f, 70d, HitResult.Miss },
// new object[] { 9.3f, 71d, HitResult.Miss },
// new object[] { 9.3f, 72d, HitResult.Miss },
// new object[] { 9.3f, 86d, HitResult.Miss },
// new object[] { 9.3f, 87d, HitResult.Miss },
// new object[] { 9.3f, 88d, HitResult.Miss },
// new object[] { 9.3f, 89d, HitResult.Miss },
new object[] { 9.3f, -69d, HitResult.Ok },
new object[] { 9.3f, -70d, HitResult.Ok },
new object[] { 9.3f, -71d, HitResult.Meh },
@@ -402,13 +410,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -191d, HitResult.Miss },
new object[] { 5f, -192d, HitResult.Miss },
new object[] { 5f, 155d, HitResult.Ok },
new object[] { 5f, 156d, HitResult.Miss },
new object[] { 5f, 157d, HitResult.Miss },
new object[] { 5f, 158d, HitResult.Miss },
new object[] { 5f, 189d, HitResult.Miss },
new object[] { 5f, 190d, HitResult.Miss },
new object[] { 5f, 191d, HitResult.Miss },
new object[] { 5f, 192d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 156d, HitResult.Miss },
// new object[] { 5f, 157d, HitResult.Miss },
// new object[] { 5f, 158d, HitResult.Miss },
// new object[] { 5f, 189d, HitResult.Miss },
// new object[] { 5f, 190d, HitResult.Miss },
// new object[] { 5f, 191d, HitResult.Miss },
// new object[] { 5f, 192d, HitResult.Miss },
};
private static readonly object[][] score_v1_non_convert_double_time_test_cases =
@@ -440,13 +449,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -205d, HitResult.Miss },
new object[] { 5f, -206d, HitResult.Miss },
new object[] { 5f, 167d, HitResult.Ok },
new object[] { 5f, 168d, HitResult.Miss },
new object[] { 5f, 169d, HitResult.Miss },
new object[] { 5f, 170d, HitResult.Miss },
new object[] { 5f, 203d, HitResult.Miss },
new object[] { 5f, 204d, HitResult.Miss },
new object[] { 5f, 205d, HitResult.Miss },
new object[] { 5f, 206d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 168d, HitResult.Miss },
// new object[] { 5f, 169d, HitResult.Miss },
// new object[] { 5f, 170d, HitResult.Miss },
// new object[] { 5f, 203d, HitResult.Miss },
// new object[] { 5f, 204d, HitResult.Miss },
// new object[] { 5f, 205d, HitResult.Miss },
// new object[] { 5f, 206d, HitResult.Miss },
};
private static readonly object[][] score_v1_non_convert_half_time_test_cases =
@@ -478,13 +488,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -103d, HitResult.Miss },
new object[] { 5f, -104d, HitResult.Miss },
new object[] { 5f, 83d, HitResult.Ok },
new object[] { 5f, 84d, HitResult.Miss },
new object[] { 5f, 85d, HitResult.Miss },
new object[] { 5f, 86d, HitResult.Miss },
new object[] { 5f, 101d, HitResult.Miss },
new object[] { 5f, 102d, HitResult.Miss },
new object[] { 5f, 103d, HitResult.Miss },
new object[] { 5f, 104d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 84d, HitResult.Miss },
// new object[] { 5f, 85d, HitResult.Miss },
// new object[] { 5f, 86d, HitResult.Miss },
// new object[] { 5f, 101d, HitResult.Miss },
// new object[] { 5f, 102d, HitResult.Miss },
// new object[] { 5f, 103d, HitResult.Miss },
// new object[] { 5f, 104d, HitResult.Miss },
};
private const double note_time = 300;
@@ -517,6 +528,7 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV2 single note @ OD{overallDifficulty}", beatmap, $@"SV2 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_test_cases))]
public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
@@ -544,6 +556,7 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1 single note @ OD{overallDifficulty}", beatmap, $@"SV1 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_convert_test_cases))]
public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
@@ -572,6 +585,7 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_hard_rock_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndHardRockNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
@@ -600,6 +614,7 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1+HR single note @ OD{overallDifficulty}", beatmap, $@"SV1+HR {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_easy_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndEasyNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
@@ -628,6 +643,7 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1+EZ single note @ OD{overallDifficulty}", beatmap, $@"SV1+EZ {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_double_time_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndDoubleTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
@@ -656,6 +672,7 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1+DT single note @ OD{overallDifficulty}", beatmap, $@"SV1+DT {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_half_time_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndHalfTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
@@ -12,7 +12,6 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneReplayStability : ReplayStabilityTestScene
{
private static readonly object[][] test_cases =
@@ -22,87 +21,79 @@ namespace osu.Game.Rulesets.Mania.Tests
// while round brackets `()` represent *open* or *exclusive* bounds.
// OD = 5 test cases.
// PERFECT hit window is [ -19.4ms, 19.4ms]
// GREAT hit window is [ -49.0ms, 49.0ms]
// GOOD hit window is [ -82.0ms, 82.0ms]
// OK hit window is [-112.0ms, 112.0ms]
// MEH hit window is [-136.0ms, 136.0ms]
// MISS hit window is [-173.0ms, 173.0ms]
// PERFECT hit window is [ -19.5ms, 19.5ms]
// GREAT hit window is [ -49.5ms, 49.5ms]
// GOOD hit window is [ -82.5ms, 82.5ms]
// OK hit window is [-112.5ms, 112.5ms]
// MEH hit window is [-136.5ms, 136.5ms]
// MISS hit window is [-173.5ms, 173.5ms]
new object[] { 5f, -19d, HitResult.Perfect },
new object[] { 5f, -19.2d, HitResult.Perfect },
new object[] { 5f, -19.38d, HitResult.Perfect },
// new object[] { 5f, -19.4d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
new object[] { 5f, -19.44d, HitResult.Great },
new object[] { 5f, -19.7d, HitResult.Great },
new object[] { 5f, -20d, HitResult.Great },
new object[] { 5f, -48d, HitResult.Great },
new object[] { 5f, -48.4d, HitResult.Great },
new object[] { 5f, -48.7d, HitResult.Great },
new object[] { 5f, -49d, HitResult.Great },
new object[] { 5f, -49.2d, HitResult.Good },
new object[] { 5f, -49.2d, HitResult.Great },
new object[] { 5f, -49.7d, HitResult.Good },
new object[] { 5f, -50d, HitResult.Good },
new object[] { 5f, -81d, HitResult.Good },
new object[] { 5f, -81.2d, HitResult.Good },
new object[] { 5f, -81.7d, HitResult.Good },
new object[] { 5f, -82d, HitResult.Good },
new object[] { 5f, -82.2d, HitResult.Ok },
new object[] { 5f, -82.2d, HitResult.Good },
new object[] { 5f, -82.7d, HitResult.Ok },
new object[] { 5f, -83d, HitResult.Ok },
new object[] { 5f, -111d, HitResult.Ok },
new object[] { 5f, -111.2d, HitResult.Ok },
new object[] { 5f, -111.7d, HitResult.Ok },
new object[] { 5f, -112d, HitResult.Ok },
new object[] { 5f, -112.2d, HitResult.Meh },
new object[] { 5f, -112.2d, HitResult.Ok },
new object[] { 5f, -112.7d, HitResult.Meh },
new object[] { 5f, -113d, HitResult.Meh },
new object[] { 5f, -135d, HitResult.Meh },
new object[] { 5f, -135.2d, HitResult.Meh },
new object[] { 5f, -135.8d, HitResult.Meh },
new object[] { 5f, -136d, HitResult.Meh },
new object[] { 5f, -136.2d, HitResult.Miss },
new object[] { 5f, -136.2d, HitResult.Meh },
new object[] { 5f, -136.7d, HitResult.Miss },
new object[] { 5f, -137d, HitResult.Miss },
// OD = 9.3 test cases.
// PERFECT hit window is [ -14.67ms, 14.67ms]
// GREAT hit window is [ -36.10ms, 36.10ms]
// GOOD hit window is [ -69.10ms, 69.10ms]
// OK hit window is [ -99.10ms, 99.10ms]
// MEH hit window is [-123.10ms, 123.10ms]
// MISS hit window is [-160.10ms, 160.10ms]
// PERFECT hit window is [ -14.5ms, 14.5ms]
// GREAT hit window is [ -36.5ms, 36.5ms]
// GOOD hit window is [ -69.5ms, 69.5ms]
// OK hit window is [ -99.5ms, 99.5ms]
// MEH hit window is [-123.5ms, 123.5ms]
// MISS hit window is [-160.5ms, 160.5ms]
new object[] { 9.3f, 14d, HitResult.Perfect },
new object[] { 9.3f, 14.2d, HitResult.Perfect },
new object[] { 9.3f, 14.6d, HitResult.Perfect },
// new object[] { 9.3f, 14.67d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
new object[] { 9.3f, 14.7d, HitResult.Great },
new object[] { 9.3f, 15d, HitResult.Great },
new object[] { 9.3f, 35d, HitResult.Great },
new object[] { 9.3f, 35.3d, HitResult.Great },
new object[] { 9.3f, 35.8d, HitResult.Great },
new object[] { 9.3f, 36.05d, HitResult.Great },
new object[] { 9.3f, 36.3d, HitResult.Good },
new object[] { 9.3f, 36.3d, HitResult.Great },
new object[] { 9.3f, 36.7d, HitResult.Good },
new object[] { 9.3f, 37d, HitResult.Good },
new object[] { 9.3f, 68d, HitResult.Good },
new object[] { 9.3f, 68.4d, HitResult.Good },
new object[] { 9.3f, 68.9d, HitResult.Good },
new object[] { 9.3f, 69.07d, HitResult.Good },
new object[] { 9.3f, 69.25d, HitResult.Ok },
new object[] { 9.3f, 69.25d, HitResult.Good },
new object[] { 9.3f, 69.85d, HitResult.Ok },
new object[] { 9.3f, 70d, HitResult.Ok },
new object[] { 9.3f, 98d, HitResult.Ok },
new object[] { 9.3f, 98.3d, HitResult.Ok },
new object[] { 9.3f, 98.6d, HitResult.Ok },
new object[] { 9.3f, 99d, HitResult.Ok },
new object[] { 9.3f, 99.3d, HitResult.Meh },
new object[] { 9.3f, 99.3d, HitResult.Ok },
new object[] { 9.3f, 99.7d, HitResult.Meh },
new object[] { 9.3f, 100d, HitResult.Meh },
new object[] { 9.3f, 122d, HitResult.Meh },
new object[] { 9.3f, 122.34d, HitResult.Meh },
new object[] { 9.3f, 122.57d, HitResult.Meh },
new object[] { 9.3f, 123.04d, HitResult.Meh },
new object[] { 9.3f, 123.45d, HitResult.Miss },
new object[] { 9.3f, 123.45d, HitResult.Meh },
new object[] { 9.3f, 123.95d, HitResult.Miss },
new object[] { 9.3f, 124d, HitResult.Miss },
};
@@ -110,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCaseSource(nameof(test_cases))]
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double note_time = 100;
const double note_time = 300;
var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
@@ -42,14 +43,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
new BeatmapStatistic
{
Name = @"Notes",
Name = BeatmapStatisticStrings.Notes,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = notes.ToString(),
BarDisplayLength = notes / (float)sum,
},
new BeatmapStatistic
{
Name = @"Hold Notes",
Name = BeatmapStatisticStrings.HoldNotes,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = holdNotes.ToString(),
BarDisplayLength = holdNotes / (float)sum,
@@ -197,6 +197,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public override void OnKilled()
{
base.OnKilled();
// flush the final state of holding on kill.
// this matters because some skin implementations like legacy skin
// insert drawables in the hierarchy that are not a child of this DHO
// (see `LegacyBodyPiece` and related machinations with `lightContainer` being added at column level)
isHolding.Value = Result.IsHolding(Time.Current);
(bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
}
@@ -1,15 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring
{
public class ManiaHitWindows : HitWindows
{
private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D);
private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34);
private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67);
private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97);
private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121);
private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158);
private readonly double multiplier;
private double perfect;
private double great;
private double good;
private double ok;
private double meh;
private double miss;
public ManiaHitWindows()
: this(1)
{
@@ -36,11 +51,41 @@ namespace osu.Game.Rulesets.Mania.Scoring
return false;
}
protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r =>
new DifficultyRange(
r.Result,
r.Min * multiplier,
r.Average * multiplier,
r.Max * multiplier)).ToArray();
public override void SetDifficulty(double difficulty)
{
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) * multiplier) + 0.5;
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range) * multiplier) + 0.5;
good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range) * multiplier) + 0.5;
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range) * multiplier) + 0.5;
meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range) * multiplier) + 0.5;
miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range) * multiplier) + 0.5;
}
public override double WindowFor(HitResult result)
{
switch (result)
{
case HitResult.Perfect:
return perfect;
case HitResult.Great:
return great;
case HitResult.Good:
return good;
case HitResult.Ok:
return ok;
case HitResult.Meh:
return meh;
case HitResult.Miss:
return miss;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
}
}
@@ -13,7 +13,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK;
@@ -22,21 +21,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModRelax : OsuModTestScene
{
private readonly HitCircle hitObject;
private readonly HitWindows hitWindows = new OsuHitWindows();
public TestSceneOsuModRelax()
{
hitWindows.SetDifficulty(9);
hitObject = new HitCircle
{
StartTime = 1000,
Position = new Vector2(100, 100),
HitWindows = hitWindows
};
}
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModRelaxTestPlayer(CurrentTestData, AllowFail);
[Test]
@@ -46,12 +30,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject> { hitObject }
Difficulty = { OverallDifficulty = 9 },
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100, 100),
HitWindows = new OsuHitWindows()
}
}
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2()),
new OsuReplayFrame(hitObject.StartTime, hitObject.Position),
new OsuReplayFrame(100, new Vector2(100)),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
});
@@ -63,13 +56,22 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject> { hitObject }
Difficulty = { OverallDifficulty = 9 },
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100, 100),
HitWindows = new OsuHitWindows()
}
}
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2(hitObject.X - 22, hitObject.Y - 22)), // must be an edge hit for the cursor to not stay on the object for too long
new OsuReplayFrame(hitObject.StartTime - OsuModRelax.RELAX_LENIENCY, new Vector2(hitObject.X - 22, hitObject.Y - 22)),
new OsuReplayFrame(hitObject.StartTime, new Vector2(0)),
new OsuReplayFrame(0, new Vector2(78, 78)), // must be an edge hit for the cursor to not stay on the object for too long
new OsuReplayFrame(1000 - OsuModRelax.RELAX_LENIENCY, new Vector2(78, 78)),
new OsuReplayFrame(1000, new Vector2(0)),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
});
@@ -110,23 +110,23 @@ namespace osu.Game.Rulesets.Osu.Tests
new Spinner
{
StartTime = 0,
Duration = 1000,
Duration = 3000,
Position = OsuPlayfield.BASE_SIZE / 2,
},
new Slider
{
StartTime = 2500,
StartTime = 4500,
RepeatCount = 0,
Position = OsuPlayfield.BASE_SIZE / 2,
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero),
new PathControlPoint(new Vector2(100, 0)),
new PathControlPoint(new Vector2(200, 0)),
})
},
new HitCircle
{
StartTime = 4500,
StartTime = 10000,
Position = OsuPlayfield.BASE_SIZE / 2,
},
},
@@ -17,7 +17,6 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
{
protected override Ruleset CreateRuleset() => new OsuRuleset();
@@ -14,26 +14,27 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override IResourceStore<byte[]> RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneOsuHitObjectSamples)));
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
[TestCase("normal-hitnormal2", "normal-hitnormal")]
[TestCase("hitnormal", "hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string beatmapSkinSampleName, string userSkinSampleName)
{
SetupSkins(expectedSample, expectedSample);
SetupSkins(beatmapSkinSampleName, userSkinSampleName);
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expectedSample);
AssertBeatmapLookup(beatmapSkinSampleName);
}
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
[TestCase("", "normal-hitnormal")]
[TestCase("normal-hitnormal", "normal-hitnormal")]
[TestCase("", "hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string beatmapSkinSampleName, string userSkinSampleName)
{
SetupSkins(string.Empty, expectedSample);
SetupSkins(beatmapSkinSampleName, userSkinSampleName);
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
AssertUserLookup(expectedSample);
AssertUserLookup(userSkinSampleName);
}
[TestCase("normal-hitnormal2")]
@@ -13,7 +13,6 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneReplayStability : ReplayStabilityTestScene
{
private static readonly object[][] test_cases =
@@ -23,53 +22,49 @@ namespace osu.Game.Rulesets.Osu.Tests
// while round brackets `()` represent *open* or *exclusive* bounds.
// OD = 5 test cases.
// GREAT hit window is [ -50ms, 50ms]
// OK hit window is [-100ms, 100ms]
// MEH hit window is [-150ms, 150ms]
// MISS hit window is [-400ms, 400ms]
// GREAT hit window is [ -49.5ms, 49.5ms]
// OK hit window is [ -99.5ms, 99.5ms]
// MEH hit window is [-149.5ms, 149.5ms]
new object[] { 5f, 49d, HitResult.Great },
new object[] { 5f, 49.2d, HitResult.Great },
new object[] { 5f, 49.7d, HitResult.Great },
new object[] { 5f, 50d, HitResult.Great },
new object[] { 5f, 49.7d, HitResult.Ok },
new object[] { 5f, 50d, HitResult.Ok },
new object[] { 5f, 50.4d, HitResult.Ok },
new object[] { 5f, 50.9d, HitResult.Ok },
new object[] { 5f, 51d, HitResult.Ok },
new object[] { 5f, 99d, HitResult.Ok },
new object[] { 5f, 99.2d, HitResult.Ok },
new object[] { 5f, 99.7d, HitResult.Ok },
new object[] { 5f, 100d, HitResult.Ok },
new object[] { 5f, 99.7d, HitResult.Meh },
new object[] { 5f, 100d, HitResult.Meh },
new object[] { 5f, 100.4d, HitResult.Meh },
new object[] { 5f, 100.9d, HitResult.Meh },
new object[] { 5f, 101d, HitResult.Meh },
new object[] { 5f, 149d, HitResult.Meh },
new object[] { 5f, 149.2d, HitResult.Meh },
new object[] { 5f, 149.7d, HitResult.Meh },
new object[] { 5f, 150d, HitResult.Meh },
new object[] { 5f, 149.7d, HitResult.Miss },
new object[] { 5f, 150d, HitResult.Miss },
new object[] { 5f, 150.4d, HitResult.Miss },
new object[] { 5f, 150.9d, HitResult.Miss },
new object[] { 5f, 151d, HitResult.Miss },
// OD = 5.7 test cases.
// GREAT hit window is [ -45.8ms, 45.8ms]
// OK hit window is [ -94.4ms, 94.4ms]
// MEH hit window is [-143.0ms, 143.0ms]
// MISS hit window is [-400.0ms, 400.0ms]
new object[] { 5.7f, 45d, HitResult.Great },
new object[] { 5.7f, 45.2d, HitResult.Great },
new object[] { 5.7f, 45.8d, HitResult.Great },
new object[] { 5.7f, 45.9d, HitResult.Ok },
new object[] { 5.7f, 46d, HitResult.Ok },
new object[] { 5.7f, 46.4d, HitResult.Ok },
new object[] { 5.7f, 94d, HitResult.Ok },
new object[] { 5.7f, 94.2d, HitResult.Ok },
new object[] { 5.7f, 94.4d, HitResult.Ok },
new object[] { 5.7f, 94.48d, HitResult.Ok },
new object[] { 5.7f, 94.9d, HitResult.Meh },
new object[] { 5.7f, 95d, HitResult.Meh },
new object[] { 5.7f, 95.4d, HitResult.Meh },
// GREAT hit window is [ -44.5ms, 44.5ms]
// OK hit window is [ -93.5ms, 93.5ms]
// MEH hit window is [-142.5ms, 142.5ms]
new object[] { 5.7f, 44d, HitResult.Great },
new object[] { 5.7f, 44.2d, HitResult.Great },
new object[] { 5.7f, 44.8d, HitResult.Ok },
new object[] { 5.7f, 45d, HitResult.Ok },
new object[] { 5.7f, 45.4d, HitResult.Ok },
new object[] { 5.7f, 93d, HitResult.Ok },
new object[] { 5.7f, 93.4d, HitResult.Ok },
new object[] { 5.7f, 93.9d, HitResult.Meh },
new object[] { 5.7f, 94d, HitResult.Meh },
new object[] { 5.7f, 94.4d, HitResult.Meh },
new object[] { 5.7f, 142d, HitResult.Meh },
new object[] { 5.7f, 142.7d, HitResult.Meh },
new object[] { 5.7f, 143d, HitResult.Meh },
new object[] { 5.7f, 142.2d, HitResult.Meh },
new object[] { 5.7f, 142.7d, HitResult.Miss },
new object[] { 5.7f, 143d, HitResult.Miss },
new object[] { 5.7f, 143.4d, HitResult.Miss },
new object[] { 5.7f, 143.9d, HitResult.Miss },
new object[] { 5.7f, 144d, HitResult.Miss },
@@ -484,6 +484,47 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
}
/// <summary>
/// Sliders are common to by 1/2 or 1/4 beat length in order to place the circle on the next beat.
/// This tests a user pressing the next circle in the window between the last tick and the end of the slider (<see cref="SliderEventGenerator.TAIL_LENIENCY"/>).
/// </summary>
[Test]
public void TestHitNextCircleDuringTailLeniency()
{
const double bpm = 240;
const double beat_length = 60000 / bpm;
const double slider_start = time_slider_start;
const double slider_end = slider_start + beat_length;
const double last_tick_time = slider_end + SliderEventGenerator.TAIL_LENIENCY;
const double next_circle_time = slider_end + beat_length / 4;
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(140, 0), Actions = { OsuAction.RightButton }, Time = last_tick_time + 20 },
},
[
new Slider
{
StartTime = slider_start,
Position = new Vector2(0, 0),
TickDistanceMultiplier = 10, // no ticks
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(100, 0),
}, 100),
},
new HitCircle
{
StartTime = next_circle_time,
Position = new Vector2(140, 0)
}
], bpm: bpm);
AddAssert("all judgements are hit", () => judgementResults.All(j => j.Type.IsHit()));
}
private void assertAllMaxJudgements()
{
AddAssert("All judgements max", () =>
@@ -522,6 +563,11 @@ namespace osu.Game.Rulesets.Osu.Tests
}, slider_path_length),
};
performTest(frames, [slider], bpm, tickRate);
}
private void performTest(List<ReplayFrame> frames, List<OsuHitObject> objects, double? bpm = null, int? tickRate = null)
{
AddStep("load player", () =>
{
var cpi = new ControlPointInfo();
@@ -531,7 +577,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{
HitObjects = { slider },
HitObjects = objects,
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty
@@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 99, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 99, slider_end_position, OsuAction.LeftButton),
});
assertHeadJudgement(HitResult.Ok);
@@ -70,8 +70,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 99, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 99, slider_end_position, OsuAction.LeftButton),
}, s =>
{
s.SliderVelocityMultiplier = 2;
@@ -91,8 +91,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 149, slider_end_position, OsuAction.LeftButton),
}, s =>
{
s.TickDistanceMultiplier = 0.2f;
@@ -116,8 +116,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 149, slider_end_position, OsuAction.LeftButton),
}, s =>
{
s.SliderVelocityMultiplier = 2;
@@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.LINEAR, new[]
@@ -195,8 +195,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.LINEAR, new[]
@@ -224,8 +224,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
@@ -259,8 +259,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
@@ -289,8 +289,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
@@ -320,8 +320,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start + 149, slider_start_position - new Vector2(20), OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 149, slider_start_position - new Vector2(20), OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
@@ -476,15 +476,24 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestHitWindows : HitWindows
{
private static readonly DifficultyRange[] ranges =
{
new DifficultyRange(HitResult.Great, 500, 500, 500),
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
};
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
protected override DifficultyRange[] GetRanges() => ranges;
public override void SetDifficulty(double difficulty) { }
public override double WindowFor(HitResult result)
{
switch (result)
{
case HitResult.Great:
return 500;
case HitResult.Miss:
return early_miss_window;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
}
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
+4 -3
View File
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
@@ -22,21 +23,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
new BeatmapStatistic
{
Name = "Circles",
Name = BeatmapStatisticStrings.Circles,
Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
BarDisplayLength = circles / (float)sum,
},
new BeatmapStatistic
{
Name = "Sliders",
Name = BeatmapStatisticStrings.Sliders,
Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
BarDisplayLength = sliders / (float)sum,
},
new BeatmapStatistic
{
Name = @"Spinners",
Name = BeatmapStatisticStrings.Spinners,
Content = spinners.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
BarDisplayLength = Math.Min(spinners / 10f, 1),
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
get
{
if (UserAdjustedSettingsCount != 1)
if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate))
return string.Empty;
if (!CircleSize.IsDefault) return format("CS", CircleSize);
@@ -19,7 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Magnetised";
public override string Acronym => "MG";
@@ -115,10 +115,6 @@ namespace osu.Game.Rulesets.Osu.Mods
#region Reduce AR (IApplicableToDifficulty)
public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
{
}
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
// Decrease AR to increase preempt time
+2 -2
View File
@@ -21,12 +21,12 @@ namespace osu.Game.Rulesets.Osu.Objects
/// <summary>
/// The RPM required to clear the spinner at ODs [ 0, 5, 10 ].
/// </summary>
private static readonly (int min, int mid, int max) clear_rpm_range = (90, 150, 225);
private static readonly DifficultyRange clear_rpm_range = new DifficultyRange(90, 150, 225);
/// <summary>
/// The RPM required to complete the spinner and receive full score at ODs [ 0, 5, 10 ].
/// </summary>
private static readonly (int min, int mid, int max) complete_rpm_range = (250, 380, 430);
private static readonly DifficultyRange complete_rpm_range = new DifficultyRange(250, 380, 430);
public double EndTime
{
+2 -3
View File
@@ -373,10 +373,9 @@ namespace osu.Game.Rulesets.Osu
preempt /= rate;
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, OsuHitWindows.GREAT_WINDOW_RANGE);
greatHitWindow /= rate;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, OsuHitWindows.GREAT_WINDOW_RANGE);
return adjustedDifficulty;
}
+36 -8
View File
@@ -1,24 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuHitWindows : HitWindows
{
public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(80, 50, 20);
public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(140, 100, 60);
public static readonly DifficultyRange MEH_WINDOW_RANGE = new DifficultyRange(200, 150, 100);
/// <summary>
/// osu! ruleset has a fixed miss window regardless of difficulty settings.
/// </summary>
public const double MISS_WINDOW = 400;
internal static readonly DifficultyRange[] OSU_RANGES =
{
new DifficultyRange(HitResult.Great, 80, 50, 20),
new DifficultyRange(HitResult.Ok, 140, 100, 60),
new DifficultyRange(HitResult.Meh, 200, 150, 100),
new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW),
};
private double great;
private double ok;
private double meh;
public override bool IsHitResultAllowed(HitResult result)
{
@@ -34,6 +36,32 @@ namespace osu.Game.Rulesets.Osu.Scoring
return false;
}
protected override DifficultyRange[] GetRanges() => OSU_RANGES;
public override void SetDifficulty(double difficulty)
{
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE)) - 0.5;
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE)) - 0.5;
meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, MEH_WINDOW_RANGE)) - 0.5;
}
public override double WindowFor(HitResult result)
{
switch (result)
{
case HitResult.Great:
return great;
case HitResult.Ok:
return ok;
case HitResult.Meh:
return meh;
case HitResult.Miss:
return MISS_WINDOW;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
}
}
@@ -171,13 +171,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 6 });
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 10 });
var hitWindows = new HitWindows();
var hitWindows = new DefaultHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
PerformTest(new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time - hitWindows.WindowFor(HitResult.Great), TaikoAction.LeftCentre),
new TaikoReplayFrame(hit_time - (hitWindows.WindowFor(HitResult.Great) + 0.1), TaikoAction.LeftCentre),
}, beatmap);
AssertJudgementCount(1);
@@ -15,7 +15,6 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
{
protected override string? ExportLocation => null;
@@ -177,7 +176,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new TaikoModHardRock()]
Mods = [new TaikoModEasy()]
}
};
@@ -12,7 +12,6 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneReplayStability : ReplayStabilityTestScene
{
private static readonly object[][] test_cases =
@@ -22,40 +21,38 @@ namespace osu.Game.Rulesets.Taiko.Tests
// while round brackets `()` represent *open* or *exclusive* bounds.
// OD = 5 test cases.
// GREAT hit window is [-35ms, 35ms]
// OK hit window is [-80ms, 80ms]
// MISS hit window is [-95ms, 95ms]
// GREAT hit window is [-34.5ms, 34.5ms]
// OK hit window is [-79.5ms, 79.5ms]
// MISS hit window is [-94.5ms, 94.5ms]
new object[] { 5f, -34d, HitResult.Great },
new object[] { 5f, -34.2d, HitResult.Great },
new object[] { 5f, -34.7d, HitResult.Great },
new object[] { 5f, -35d, HitResult.Great },
new object[] { 5f, -34.7d, HitResult.Ok },
new object[] { 5f, -35d, HitResult.Ok },
new object[] { 5f, -35.2d, HitResult.Ok },
new object[] { 5f, -35.8d, HitResult.Ok },
new object[] { 5f, -36d, HitResult.Ok },
new object[] { 5f, -79d, HitResult.Ok },
new object[] { 5f, -79.3d, HitResult.Ok },
new object[] { 5f, -79.7d, HitResult.Ok },
new object[] { 5f, -80d, HitResult.Ok },
new object[] { 5f, -79.7d, HitResult.Miss },
new object[] { 5f, -80d, HitResult.Miss },
new object[] { 5f, -80.2d, HitResult.Miss },
new object[] { 5f, -80.8d, HitResult.Miss },
new object[] { 5f, -81d, HitResult.Miss },
// OD = 7.8 test cases.
// GREAT hit window is [-26.6ms, 26.6ms]
// OK hit window is [-63.2ms, 63.2ms]
// MISS hit window is [-81.0ms, 81.0ms]
new object[] { 7.8f, -26d, HitResult.Great },
new object[] { 7.8f, -26.4d, HitResult.Great },
new object[] { 7.8f, -26.59d, HitResult.Great },
new object[] { 7.8f, -26.8d, HitResult.Ok },
new object[] { 7.8f, -27d, HitResult.Ok },
new object[] { 7.8f, -27.1d, HitResult.Ok },
new object[] { 7.8f, -63d, HitResult.Ok },
new object[] { 7.8f, -63.18d, HitResult.Ok },
new object[] { 7.8f, -63.4d, HitResult.Ok },
new object[] { 7.8f, -63.7d, HitResult.Miss },
new object[] { 7.8f, -64d, HitResult.Miss },
new object[] { 7.8f, -64.2d, HitResult.Miss },
// GREAT hit window is [-25.5ms, 25.5ms]
// OK hit window is [-62.5ms, 62.5ms]
// MISS hit window is [-80.5ms, 80.5ms]
new object[] { 7.8f, -25d, HitResult.Great },
new object[] { 7.8f, -25.4d, HitResult.Great },
new object[] { 7.8f, -25.8d, HitResult.Ok },
new object[] { 7.8f, -26d, HitResult.Ok },
new object[] { 7.8f, -26.1d, HitResult.Ok },
new object[] { 7.8f, -62d, HitResult.Ok },
new object[] { 7.8f, -62.4d, HitResult.Ok },
new object[] { 7.8f, -62.7d, HitResult.Miss },
new object[] { 7.8f, -63d, HitResult.Miss },
new object[] { 7.8f, -63.2d, HitResult.Miss },
};
[TestCaseSource(nameof(test_cases))]
@@ -14,26 +14,27 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected override IResourceStore<byte[]> RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples)));
[TestCase("taiko-normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
[TestCase("taiko-normal-hitnormal2", "taiko-normal-hitnormal")]
[TestCase("hitnormal", "hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string beatmapSkinSampleName, string userSkinSampleName)
{
SetupSkins(expectedSample, expectedSample);
SetupSkins(beatmapSkinSampleName, userSkinSampleName);
CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expectedSample);
AssertBeatmapLookup(beatmapSkinSampleName);
}
[TestCase("taiko-normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
[TestCase("", "taiko-normal-hitnormal")]
[TestCase("taiko-normal-hitnormal", "taiko-normal-hitnormal")]
[TestCase("", "hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string beatmapSkinSampleName, string userSkinSampleName)
{
SetupSkins(string.Empty, expectedSample);
SetupSkins(beatmapSkinSampleName, userSkinSampleName);
CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
AssertUserLookup(expectedSample);
AssertUserLookup(userSkinSampleName);
}
[TestCase("taiko-normal-hitnormal2")]
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Beatmaps
@@ -22,21 +23,21 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
new BeatmapStatistic
{
Name = @"Hits",
Name = BeatmapStatisticStrings.Hits,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = hits.ToString(),
BarDisplayLength = hits / (float)sum,
},
new BeatmapStatistic
{
Name = @"Drumrolls",
Name = BeatmapStatisticStrings.Drumrolls,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = drumRolls.ToString(),
BarDisplayLength = drumRolls / (float)sum,
},
new BeatmapStatistic
{
Name = @"Swells",
Name = BeatmapStatisticStrings.Swells,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
Content = swells.ToString(),
BarDisplayLength = Math.Min(swells / 10f, 1),
@@ -25,16 +25,16 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
get
{
if (UserAdjustedSettingsCount != 1)
if (!IsExactlyOneSettingChanged(ScrollSpeed, OverallDifficulty, DrainRate))
return string.Empty;
if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed);
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
if (!DrainRate.IsDefault) return format("HP", DrainRate);
if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed, 2);
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty, 1);
if (!DrainRate.IsDefault) return format("HP", DrainRate, 1);
return string.Empty;
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
string format(string acronym, DifficultyBindable bindable, int digits) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(digits)}";
}
}
@@ -1,18 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Scoring
{
public class TaikoHitWindows : HitWindows
{
internal static readonly DifficultyRange[] TAIKO_RANGES =
{
new DifficultyRange(HitResult.Great, 50, 35, 20),
new DifficultyRange(HitResult.Ok, 120, 80, 50),
new DifficultyRange(HitResult.Miss, 135, 95, 70),
};
public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(50, 35, 20);
public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(120, 80, 50);
public static readonly DifficultyRange MISS_WINDOW_RANGE = new DifficultyRange(135, 95, 70);
private double great;
private double ok;
private double miss;
public override bool IsHitResultAllowed(HitResult result)
{
@@ -27,6 +30,29 @@ namespace osu.Game.Rulesets.Taiko.Scoring
return false;
}
protected override DifficultyRange[] GetRanges() => TAIKO_RANGES;
public override void SetDifficulty(double difficulty)
{
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE)) - 0.5;
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE)) - 0.5;
miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, MISS_WINDOW_RANGE)) - 0.5;
}
public override double WindowFor(HitResult result)
{
switch (result)
{
case HitResult.Great:
return great;
case HitResult.Ok:
return ok;
case HitResult.Miss:
return miss;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
}
}
+2 -3
View File
@@ -274,10 +274,9 @@ namespace osu.Game.Rulesets.Taiko
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
var greatHitWindowRange = TaikoHitWindows.TAIKO_RANGES.Single(range => range.Result == HitResult.Great);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE);
greatHitWindow /= rate;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, TaikoHitWindows.GREAT_WINDOW_RANGE);
return adjustedDifficulty;
}
@@ -211,6 +211,31 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8));
}
[Test]
public void TestEncodeStabilityOfSliderWithFractionalCoordinates()
{
Slider originalSlider = new Slider
{
Position = new Vector2(0.6f),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(25.6f, 78.4f)),
new PathControlPoint(new Vector2(55.8f, 34.2f)),
})
};
var beatmap = new Beatmap
{
HitObjects = { originalSlider }
};
var encoded = encodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
var decodedAfterEncode = decodeFromLegacy(encoded, string.Empty, version: LegacyBeatmapEncoder.FIRST_LAZER_VERSION);
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
Assert.That(decodedSlider.Path.ControlPoints.Select(p => p.Position),
Is.EquivalentTo(originalSlider.Path.ControlPoints.Select(p => p.Position)));
}
private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
{
// equal to null, no need to SequenceEqual
@@ -233,11 +258,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name)
private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name, int version = LegacyDecoder<Beatmap>.LATEST_VERSION)
{
using (var reader = new LineBufferedReader(stream))
{
var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader);
var beatmap = new LegacyBeatmapDecoder(version) { ApplyOffsets = false }.Decode(reader);
var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name);
stream.Seek(0, SeekOrigin.Begin);
beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader);
@@ -359,7 +359,7 @@ namespace osu.Game.Tests.Gameplay
}
public override Judgement CreateJudgement() => new TestJudgement(maxResult);
protected override HitWindows CreateHitWindows() => new HitWindows();
protected override HitWindows CreateHitWindows() => new DefaultHitWindows();
private class TestJudgement : Judgement
{
@@ -64,11 +64,9 @@ namespace osu.Game.Tests.Gameplay
/// <summary>
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
/// normal-hitnormal2
/// normal-hitnormal
/// hitnormal
/// </summary>
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
{
@@ -162,7 +160,6 @@ namespace osu.Game.Tests.Gameplay
/// Tests that a control point that provides a custom sample of 2 causes <see cref="TestDefaultCustomSampleFromBeatmap"/>.
/// </summary>
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestControlPointCustomSampleFromBeatmap(string sampleName)
{
@@ -166,11 +166,7 @@ namespace osu.Game.Tests.Mods
/// </summary>
private BeatmapDifficulty applyDifficulty(BeatmapDifficulty difficulty)
{
// ensure that ReadFromDifficulty doesn't pollute the values.
var newDifficulty = difficulty.Clone();
testMod.ReadFromDifficulty(difficulty);
testMod.ApplyToDifficulty(newDifficulty);
return newDifficulty;
}
@@ -85,16 +85,6 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
}
/*
* The following tests have been written a bit strangely (they don't check exact
* bound equality with what the filter says).
* This is to account for floating-point arithmetic issues.
* For example, specifying a bpm<140 filter would previously match beatmaps with BPM
* of 139.99999, which would be displayed in the UI as 140.
* Due to this the tests check the last tick inside the range and the first tick
* outside of the range.
*/
[TestCase("star")]
[TestCase("stars")]
public void TestApplyStarQueries(string variant)
@@ -105,11 +95,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.IsNotNull(filterCriteria.StarDifficulty.Max);
Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d);
Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d);
Assert.AreEqual(filterCriteria.StarDifficulty.Max, 4.00d);
Assert.IsNull(filterCriteria.StarDifficulty.Min);
}
[Test]
public void TestStarQueriesInclusive()
{
const string query = "stars>=6";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(filterCriteria.StarDifficulty.Min, 6.00d);
Assert.True(filterCriteria.StarDifficulty.IsLowerInclusive);
Assert.IsNull(filterCriteria.StarDifficulty.Max);
}
/*
* The following tests have been written a bit strangely (they don't check exact
* bound equality with what the filter says).
* This is to account for floating-point arithmetic issues.
* For example, specifying a bpm<140 filter would previously match beatmaps with BPM
* of 139.99999, which would be displayed in the UI as 140.
* Due to this the tests check the last tick inside the range and the first tick
* outside of the range.
*/
[Test]
public void TestApplyApproachRateQueries()
{
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.NonVisual
public void TestResultIfOnlyParentHitWindowIsEmpty()
{
var testObject = new TestHitObject(HitWindows.Empty);
HitObject nested = new TestHitObject(new HitWindows());
HitObject nested = new TestHitObject(new DefaultHitWindows());
testObject.AddNested(nested);
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
@@ -43,8 +43,8 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestResultIfParentHitWindowsIsNotEmpty()
{
var testObject = new TestHitObject(new HitWindows());
HitObject nested = new TestHitObject(new HitWindows());
var testObject = new TestHitObject(new DefaultHitWindows());
HitObject nested = new TestHitObject(new DefaultHitWindows());
testObject.AddNested(nested);
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.NonVisual
HitObject nested = new TestHitObject(HitWindows.Empty);
firstObject.AddNested(nested);
var secondObject = new TestHitObject(new HitWindows());
var secondObject = new TestHitObject(new DefaultHitWindows());
testDrawableRuleset.HitObjects = new List<HitObject> { firstObject, secondObject };
Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, secondObject.HitWindows);
@@ -21,5 +21,18 @@ namespace osu.Game.Tests.NonVisual
{
Assert.AreEqual(expectedOutput, input.FormatAccuracy().ToString());
}
[TestCase(3, "3.00")]
[TestCase(3.3, "3.30")]
[TestCase(3.55, "3.55")]
[TestCase(3.553, "3.55")]
[TestCase(3.557, "3.55")]
[TestCase(3.9999, "3.99")]
[TestCase(3.999999, "3.99")]
[TestCase(4, "4.00")]
public void TestStarRatingFormatting(double input, string expectedOutput)
{
Assert.AreEqual(expectedOutput, input.FormatStarRating().ToString());
}
}
}
@@ -0,0 +1,187 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Visual;
using osu.Game.Updater;
namespace osu.Game.Tests.NonVisual
{
[HeadlessTest]
public partial class TestSceneUpdateManager : OsuTestScene
{
[Cached(typeof(INotificationOverlay))]
private readonly INotificationOverlay notifications = new TestNotificationOverlay();
private TestUpdateManager manager = null!;
private OsuConfigManager config = null!;
[SetUpSteps]
public void SetupSteps()
{
AddStep("add manager", () =>
{
config = new OsuConfigManager(LocalStorage);
config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
Child = new DependencyProvidingContainer
{
CachedDependencies = [(typeof(OsuConfigManager), config)],
Child = manager = new TestUpdateManager()
};
});
// Updates should be checked when the object is loaded for the first time.
AddUntilStep("check pending", () => manager.IsPending);
AddStep("complete check", () => manager.Complete());
AddUntilStep("1 check completed", () => manager.Completions, () => Is.EqualTo(1));
AddUntilStep("no check pending", () => !manager.IsPending);
}
/// <summary>
/// Updates should be checked when the release stream is changed.
/// </summary>
[Test]
public void TestReleaseStreamChanged()
{
AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon));
AddUntilStep("check pending", () => manager.IsPending);
AddStep("complete check", () => manager.Complete());
AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2));
AddUntilStep("no check pending", () => !manager.IsPending);
AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer));
AddUntilStep("check pending", () => manager.IsPending);
AddStep("complete check", () => manager.Complete());
AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3));
AddUntilStep("no check pending", () => !manager.IsPending);
}
/// <summary>
/// Changing the release stream should start a new invocation and cancel the existing one.
/// </summary>
[Test]
public void TestNewInvocationOnReleaseStreamChanged()
{
AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon));
AddUntilStep("check pending", () => manager.IsPending);
AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer));
AddUntilStep("3 invocations", () => manager.Invocations, () => Is.EqualTo(3));
AddStep("complete check", () => manager.Complete());
AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2));
AddUntilStep("no check pending", () => !manager.IsPending);
}
/// <summary>
/// Updates should be checked when the user requests them to.
/// </summary>
[Test]
public void TestUserRequest()
{
AddStep("request check", () => manager.CheckForUpdate());
AddUntilStep("check pending", () => manager.IsPending);
AddStep("complete check", () => manager.Complete());
AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2));
AddUntilStep("no check pending", () => !manager.IsPending);
AddStep("request check", () => manager.CheckForUpdate());
AddUntilStep("check pending", () => manager.IsPending);
AddStep("complete check", () => manager.Complete());
AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3));
AddUntilStep("no check pending", () => !manager.IsPending);
}
/// <summary>
/// User requests should start a new invocation and cancel the existing one.
/// </summary>
[Test]
public void TestUserRequestOverridesExistingCheck()
{
// This part covering double user input is not really possible because the settings button is disabled during the check,
// but it's kept here for sanity in-case the update manager is used as a standalone object elsewhere.
AddStep("request check", () => manager.CheckForUpdate());
AddUntilStep("check pending", () => manager.IsPending);
AddStep("request check", () => manager.CheckForUpdate());
AddUntilStep("3 invocations", () => manager.Invocations, () => Is.EqualTo(3));
AddStep("complete check", () => manager.Complete());
AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2));
AddUntilStep("no check pending", () => !manager.IsPending);
// This next part tests for the user requesting an update during a background check, and is possible to occur in practice.
AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon));
AddUntilStep("check pending", () => manager.IsPending);
AddStep("request check", () => manager.CheckForUpdate());
AddUntilStep("5 invocations", () => manager.Invocations, () => Is.EqualTo(5));
AddStep("complete check", () => manager.Complete());
AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3));
AddUntilStep("no check pending", () => !manager.IsPending);
}
private partial class TestUpdateManager : UpdateManager
{
public bool IsPending { get; private set; }
public int Invocations { get; private set; }
public int Completions { get; private set; }
private TaskCompletionSource<bool>? pendingCheck;
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
{
Invocations++;
var check = pendingCheck = new TaskCompletionSource<bool>();
IsPending = true;
try
{
bool result = await check.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
Completions++;
return result;
}
finally
{
IsPending = false;
}
}
public void Complete()
{
pendingCheck?.SetResult(true);
}
}
private partial class TestNotificationOverlay : INotificationOverlay
{
public void Post(Notification notification)
{
}
public void Hide()
{
}
public IBindable<int> UnreadCount { get; } = new Bindable<int>();
public IEnumerable<Notification> AllNotifications { get; } = Enumerable.Empty<Notification>();
}
}
}
@@ -32,7 +32,7 @@ using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osu.Game.Storyboards.Drawables;
using osu.Game.Tests.Resources;
using osuTK;
@@ -325,7 +325,7 @@ namespace osu.Game.Tests.Visual.Background
private void setupUserSettings()
{
AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen());
AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmapInfo != null);
AddUntilStep("Song select has selection", () => songSelect.Carousel?.CurrentSelection != null);
AddStep("Set default user settings", () =>
{
SelectedMods.Value = new[] { new OsuModNoFail() };
@@ -340,7 +340,7 @@ namespace osu.Game.Tests.Visual.Background
rulesets?.Dispose();
}
private partial class DummySongSelect : PlaySongSelect
private partial class DummySongSelect : SoloSongSelect
{
private FadeAccessibleBackground background;
@@ -355,7 +355,7 @@ namespace osu.Game.Tests.Visual.Background
public readonly Bindable<double> DimLevel = new BindableDouble();
public readonly Bindable<double> BlurLevel = new BindableDouble();
public new BeatmapCarousel Carousel => base.Carousel;
public BeatmapCarousel Carousel => this.ChildrenOfType<BeatmapCarousel>().SingleOrDefault();
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
@@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim);
public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White;
public bool IsBackgroundUndimmed() => background.CurrentColour == new Color4(0.9f, 0.9f, 0.9f, 1f);
public bool IsUserBlurApplied() => Precision.AlmostEquals(background.CurrentBlur, new Vector2((float)BlurLevel.Value * BackgroundScreenBeatmap.USER_BLUR_FACTOR), 0.1f);
@@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
public bool IsBackgroundBlur() => Precision.AlmostEquals(background.CurrentBlur, new Vector2(BACKGROUND_BLUR), 0.1f);
public bool IsBackgroundBlur() => Precision.AlmostBigger(background.CurrentBlur.X, 0, 0.1f);
public bool CheckBackgroundBlur(Vector2 expected) => Precision.AlmostEquals(background.CurrentBlur, expected, 0.1f);
@@ -15,7 +15,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
@@ -190,7 +190,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Set tags again", () => EditorBeatmap.BeatmapInfo.Metadata.Tags = tags_to_discard);
AddStep("Exit editor", () => Editor.Exit());
AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save);
}
@@ -14,7 +14,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Editing
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Editing
() => Is.EqualTo(1));
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
addStepClickLink("00:00:000 (1)", waitForSeek: false);
AddUntilStep("received 'must be in edit'",
@@ -151,12 +151,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("Wait for song select", () =>
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.BeatmapSetsLoaded
&& Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect
&& songSelect.CarouselItemsPresented
);
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
AddStep("Open editor for ruleset", () =>
((PlaySongSelect)Game.ScreenStack.CurrentScreen)
((SoloSongSelect)Game.ScreenStack.CurrentScreen)
.Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
);
AddUntilStep("Wait for editor open", () => editor?.ReadyForUse == true);
@@ -9,7 +9,6 @@ using osu.Game.Audio;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
@@ -74,8 +73,8 @@ namespace osu.Game.Tests.Visual.Gameplay
//
// We want to keep seeking while asserting various test conditions, so
// continue to seek until we unset the flag.
var gameplayClockContainer = Player.ChildrenOfType<GameplayClockContainer>().First();
gameplayClockContainer.Seek(gameplayClockContainer.CurrentTime > 30000 ? 0 : 60000);
var gameplayClockContainer = Player?.GameplayClockContainer;
gameplayClockContainer?.Seek(gameplayClockContainer.CurrentTime > 30000 ? 0 : 60000);
}
}
@@ -65,30 +65,30 @@ namespace osu.Game.Tests.Visual.Gameplay
{
new HitCircle
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
},
new HitCircle
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }
},
new HitCircle
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
},
new HitCircle
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
},
new Slider
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
@@ -6,11 +6,16 @@ using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -157,6 +162,51 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500);
}
[Test]
public void TestReplayDoesNotFailUntilRunningOutOfFrames()
{
var score = new Score
{
ScoreInfo = TestResources.CreateTestScoreInfo(Beatmap.Value.BeatmapInfo),
Replay = new Replay
{
Frames =
{
new OsuReplayFrame(0, Vector2.Zero),
new OsuReplayFrame(10000, Vector2.Zero),
}
}
};
score.ScoreInfo.Mods = [];
score.ScoreInfo.Rank = ScoreRank.F;
AddStep("set global state", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
Ruleset.Value = Beatmap.Value.BeatmapInfo.Ruleset;
SelectedMods.Value = score.ScoreInfo.Mods;
});
AddStep("create player", () => Player = new TestReplayPlayer(score, showResults: false));
AddStep("load player", () => LoadScreen(Player));
AddUntilStep("wait for loaded", () => Player.IsCurrentScreen());
AddStep("seek to 8000", () => Player.Seek(8000));
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddAssert("player failed after 10000", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(10000));
}
[Test]
public void TestPlayerLoaderSettingsHover()
{
loadPlayerWithBeatmap();
AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False);
AddStep("move mouse to right of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopRight));
AddUntilStep("wait for settings overlay visible", () => settingsOverlay().Expanded.Value, () => Is.True);
AddStep("move mouse to centre of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False);
PlayerSettingsOverlay settingsOverlay() => Player.ChildrenOfType<PlayerSettingsOverlay>().Single();
}
private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
{
AddStep("create player", () =>
@@ -150,6 +150,24 @@ namespace osu.Game.Tests.Visual.Menus
});
});
// cross-reference: `TestSceneOverallRanking.TestRoundingTreatment()`.
AddStep("Test rounding treatment", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
GlobalRank = 111_111,
PP = 5071.495M
},
new UserStatistics
{
GlobalRank = 111_111,
PP = 5072.99M
});
});
AddStep("No change 1", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
@@ -308,7 +308,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => RoomJoined);
ClickButtonWhenEnabled<UserModSelectButton>();
AddAssert("mod select shows unranked", () => this.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
AddUntilStep("mod select shows unranked", () => this.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
AddStep("select flashlight", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick());
@@ -165,23 +165,75 @@ namespace osu.Game.Tests.Visual.Multiplayer
Name = "A host-only room",
QueueMode = QueueMode.HostOnly,
Type = MatchType.HeadToHead,
RoomID = 1337,
}),
new MultiplayerRoomPanel(new Room
{
Name = "An all-players, team-versus room",
QueueMode = QueueMode.AllPlayers,
Type = MatchType.TeamVersus
Type = MatchType.TeamVersus,
RoomID = 1338,
}),
new MultiplayerRoomPanel(new Room
{
Name = "A round-robin room",
QueueMode = QueueMode.AllPlayersRoundRobin,
Type = MatchType.HeadToHead
Type = MatchType.HeadToHead,
RoomID = 1339,
}),
}
});
}
[Test]
public void TestRoomWithLongTitle()
{
AddStep("create rooms", () => Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new[]
{
new MultiplayerRoomPanel(new Room
{
Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.",
QueueMode = QueueMode.HostOnly,
Type = MatchType.HeadToHead,
RoomID = 1337,
}),
}
});
}
[Test]
public void TestRoomWithUpdatedRoomID()
{
Room room = null!;
AddStep("create rooms", () => Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new[]
{
new MultiplayerRoomPanel(room = new Room
{
Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.",
QueueMode = QueueMode.HostOnly,
Type = MatchType.HeadToHead,
}),
}
});
AddWaitStep("wait", 3);
AddStep("set room ID", () => room.RoomID = 1337);
AddWaitStep("wait", 3);
AddStep("clear room ID", () => room.RoomID = null);
}
private RoomPanel createLoungeRoom(Room room)
{
room.Host ??= new APIUser { Username = "peppy", Id = 2 };
@@ -26,8 +26,8 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osuTK.Input;
@@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("exit", () => getEditor().Exit());
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect
&& songSelect.Beatmap.Value is DummyWorkingBeatmap);
}
@@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("switch ruleset at song select", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddStep("open editor", () => ((SoloSongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddAssert("editor ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
@@ -187,8 +187,8 @@ namespace osu.Game.Tests.Visual.Navigation
});
AddAssert("gameplay ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(SoloSongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
AddAssert("previous ruleset restored", () => Game.Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo));
}
@@ -289,8 +289,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true));
AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying);
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(SoloSongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying);
AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true));
@@ -352,13 +352,13 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("wait for song select",
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.BeatmapSetsLoaded);
&& Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect
&& songSelect.CarouselItemsPresented);
}
private void openEditor()
{
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddStep("open editor", () => ((SoloSongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
}
@@ -5,7 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Navigation
InputManager.Key(Key.P);
});
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
}
[Test]
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("state is play", () => buttons.State == ButtonSystemState.Play);
AddStep("press P", () => InputManager.Key(Key.P));
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
}
}
}
@@ -15,7 +15,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
@@ -54,10 +54,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
PushAndConfirm(() => new PlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault);
AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for carousel load", () => songSelect.CarouselItemsPresented);
AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
@@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Navigation
.AsEnumerable()
.First(k => k.RulesetName == "osu" && k.ActionInt == 0);
private Screens.Select.SongSelect songSelect => Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect;
private SoloSongSelect songSelect => Game.ScreenStack.CurrentScreen as SoloSongSelect;
private Player player => Game.ScreenStack.CurrentScreen as Player;
@@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Game.Configuration;
using osu.Game.Screens.Play;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
@@ -83,9 +84,9 @@ namespace osu.Game.Tests.Visual.Navigation
private void loadToPlayerNonBreakTime()
{
Player? player = null;
Screens.Select.SongSelect songSelect = null!;
PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
SoloSongSelect songSelect = null!;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
@@ -17,9 +17,9 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPerformAtSongSelect()
{
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(SoloSongSelect) }));
AddAssert("did perform", () => actionPerformed);
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
}
[Test]
public void TestPerformAtMenuFromSongSelect()
{
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("returned to menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("Press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(SoloSongSelect) }));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
AddAssert("did perform", () => actionPerformed);
}
@@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Navigation
private void importAndWaitForSongSelect()
{
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddUntilStep("beatmap updated", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID == 241526);
}
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -81,11 +81,9 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(osuImport);
var maniaImport = importBeatmap(2, new ManiaRuleset().RulesetInfo);
confirmBeatmapInSongSelect(maniaImport);
presentAndConfirm(maniaImport);
var catchImport = importBeatmap(3, new CatchRuleset().RulesetInfo);
confirmBeatmapInSongSelect(catchImport);
presentAndConfirm(catchImport);
// Ruleset is always changed.
@@ -103,11 +101,9 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(osuImport);
var maniaImport = importBeatmap(2, new ManiaRuleset().RulesetInfo);
confirmBeatmapInSongSelect(maniaImport);
presentAndConfirm(maniaImport);
var catchImport = importBeatmap(3, new CatchRuleset().RulesetInfo);
confirmBeatmapInSongSelect(catchImport);
presentAndConfirm(catchImport);
// force ruleset to osu!mania
@@ -178,14 +174,14 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddUntilStep("wait for carousel loaded", () =>
{
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().SingleOrDefault()?.IsLoaded == true;
});
AddUntilStep("beatmap in song select", () =>
{
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().Single().BeatmapSets.Any(b => b.MatchesOnlineID(getImport()));
var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetInfo bsi && bsi.MatchesOnlineID(getImport()));
});
}
@@ -193,7 +189,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset));
}
@@ -203,7 +199,7 @@ namespace osu.Game.Tests.Visual.Navigation
Predicate<BeatmapInfo> pred = b => b.OnlineID == importedID * 1024 + 2;
AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 1024 + 2));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.OnlineID, () => Is.EqualTo(expectedRulesetOnlineID ?? getImport().Beatmaps.First().Ruleset.OnlineID));
}
@@ -18,7 +18,8 @@ using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using FilterControl = osu.Game.Screens.SelectV2.FilterControl;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -96,9 +97,9 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestFromSongSelectWithFilter([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq");
AddStep("filter to nothing", () => ((SoloSongSelect)Game.ScreenStack.CurrentScreen).ChildrenOfType<FilterControl>().Single().Search("fdsajkl;fgewq"));
AddUntilStep("wait for no results", () => Beatmap.IsDefault);
var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
@@ -109,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
@@ -121,7 +122,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestFromSongSelect([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
var firstImport = importScore(1);
presentAndConfirm(firstImport, type);
@@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
var firstImport = importScore(1);
presentAndConfirm(firstImport, type);
@@ -147,7 +148,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestPresentTwoImportsWithSameOnlineIDButDifferentHashes([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
var firstImport = importScore(1);
presentAndConfirm(firstImport, type);
@@ -160,7 +161,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestScoreRefetchIgnoresEmptyHash()
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
importScore(-1, hash: string.Empty);
importScore(3, hash: @"deadbeef");
@@ -26,7 +26,6 @@ using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Overlays;
@@ -49,20 +48,13 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Resources;
using osu.Game.Utils;
using osuTK;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.Select.BeatmapCarousel;
using CollectionDropdown = osu.Game.Collections.CollectionDropdown;
using FilterControl = osu.Game.Screens.Select.FilterControl;
using FooterButtonRandom = osu.Game.Screens.Select.FooterButtonRandom;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -146,62 +138,70 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitSongSelectWithEscape()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
ModSelectOverlay modSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddStep("Show mods overlay", () =>
{
modSelect = songSelect!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect.Show();
});
AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible);
pushEscape();
AddAssert("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
AddAssert("Overlay was hidden", () => modSelect.State.Value == Visibility.Hidden);
exitViaEscapeAndConfirm();
}
[Test]
public void TestEnterGameplayWhileFilteringToNoSelection()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("force selection", () =>
AddStep("force selection and change filter immediately", () =>
{
songSelect.FinaliseSelection();
songSelect.FilterControl.CurrentTextSearch.Value = "test";
InputManager.Key(Key.Enter);
songSelect.ChildrenOfType<FilterControl>().Single().Search("test");
});
AddUntilStep("wait for player", () => !songSelect.IsCurrentScreen());
AddStep("return to song select", () => songSelect.MakeCurrent());
AddUntilStep("wait for selection lost", () => songSelect.Beatmap.IsDefault);
AddUntilStep("selection not lost", () => !songSelect.Beatmap.IsDefault);
AddUntilStep("placeholder visible", () => songSelect.ChildrenOfType<NoResultsPlaceholder>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestSongSelectBackActionHandling()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for filter control", () => filterControlTextBox().IsLoaded);
AddStep("set filter", () => filterControlTextBox().Current.Value = "test");
AddStep("press back", () => InputManager.Click(MouseButton.Button1));
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen, () => Is.EqualTo(songSelect));
AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value));
AddStep("set filter again", () => filterControlTextBox().Current.Value = "test");
AddStep("open collections dropdown", () =>
{
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionDropdown>().Single());
InputManager.MoveMouseTo(songSelect.ChildrenOfType<Screens.SelectV2.CollectionDropdown>().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("press back once", () => InputManager.Click(MouseButton.Button1));
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
AddAssert("collections dropdown closed", () => songSelect
.ChildrenOfType<CollectionDropdown>().Single()
.ChildrenOfType<Screens.SelectV2.CollectionDropdown>().Single()
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1));
@@ -210,17 +210,17 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back a third time", () => InputManager.Click(MouseButton.Button1));
ConfirmAtMainMenu();
TextBox filterControlTextBox() => songSelect.ChildrenOfType<FilterControl.FilterControlTextBox>().Single();
FilterControl.SongSelectSearchTextBox filterControlTextBox() => songSelect.ChildrenOfType<FilterControl.SongSelectSearchTextBox>().Single();
}
[Test]
public void TestSongSelectRandomRewindButton()
{
Guid? originalSelection = null;
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("Add two beatmaps", () =>
{
@@ -247,41 +247,6 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestSongSelectScrollHandling()
{
TestPlaySongSelect songSelect = null;
double scrollPosition = 0;
AddStep("set game volume to max", () => Game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.VolumeUniversal, 1d));
AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType<VolumeOverlay>().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden));
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("store scroll position", () => scrollPosition = getCarouselScrollPosition());
AddStep("move to left side", () => InputManager.MoveMouseTo(
songSelect.ChildrenOfType<Screens.Select.SongSelect.LeftSideInteractionContainer>().Single().ScreenSpaceDrawQuad.TopLeft + new Vector2(1)));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition));
AddRepeatStep("alt-scroll down", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.ScrollVerticalBy(-1);
InputManager.ReleaseKey(Key.AltLeft);
}, 5);
AddAssert("game volume decreased", () => Game.Dependencies.Get<FrameworkConfigManager>().Get<double>(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1));
AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType<BeatmapCarousel>().Single()));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition));
double getCarouselScrollPosition() => Game.ChildrenOfType<UserTrackingScrollContainer<DrawableCarouselItem>>().Single().Current;
}
[Test]
public void TestNewSongSelectScrollHandling()
{
SoloSongSelect songSelect = null;
double scrollPosition = 0;
@@ -293,6 +258,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for beatmap", () => Game.ChildrenOfType<PanelBeatmapSet>().Any());
// TODO: this logic can likely be removed when we fix https://github.com/ppy/osu/issues/33379
// It should be probably be immediate in this case.
AddWaitStep("wait for scroll", 10);
AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition());
@@ -325,7 +292,7 @@ namespace osu.Game.Tests.Visual.Navigation
}, 5);
AddAssert("game volume decreased", () => Game.Dependencies.Get<FrameworkConfigManager>().Get<double>(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1));
AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType<Screens.SelectV2.BeatmapCarousel>().Single()));
AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType<BeatmapCarousel>().Single()));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition));
@@ -339,21 +306,21 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestOpenModSelectOverlayUsingAction()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddStep("Show mods overlay", () => InputManager.Key(Key.F1));
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddAssert("Overlay was shown", () => songSelect!.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
}
[Test]
public void TestAttemptPlayBeatmapWrongHashFails()
{
Screens.Select.SongSelect songSelect = null;
Screens.SelectV2.SongSelect songSelect = null;
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
@@ -384,11 +351,11 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestAttemptPlayBeatmapMissingFails()
{
Screens.Select.SongSelect songSelect = null;
Screens.SelectV2.SongSelect songSelect = null;
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
@@ -418,9 +385,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -461,9 +428,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely());
@@ -515,9 +482,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely());
@@ -558,9 +525,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -663,7 +630,7 @@ namespace osu.Game.Tests.Visual.Navigation
playToResults();
ScoreInfo score = null;
LeaderboardScore scorePanel = null;
BeatmapLeaderboardScore scorePanel = null;
AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score);
@@ -672,18 +639,11 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
() => Game.ChildrenOfType<Dropdown<BeatmapLeaderboardScope>>().First().Current.Value = BeatmapLeaderboardScope.Local);
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<BeatmapLeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
AddStep("open options", () => InputManager.Key(Key.F3));
AddStep("choose clear all scores", () => InputManager.Key(Key.Number4));
AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog == null);
AddStep("Clear all scores", () => Game.Dependencies.Get<ScoreManager>().Delete());
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
@@ -696,7 +656,7 @@ namespace osu.Game.Tests.Visual.Navigation
playToResults();
ScoreInfo score = null;
LeaderboardScore scorePanel = null;
BeatmapLeaderboardScore scorePanel = null;
AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score);
@@ -705,9 +665,9 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
() => Game.ChildrenOfType<Dropdown<BeatmapLeaderboardScope>>().First().Current.Value = BeatmapLeaderboardScope.Local);
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<BeatmapLeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
AddStep("right click panel", () =>
{
@@ -718,7 +678,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("click delete", () =>
{
var dropdownItem = Game
.ChildrenOfType<PlayBeatmapDetailArea>().First()
.ChildrenOfType<BeatmapLeaderboardWedge>().First()
.ChildrenOfType<OsuContextMenu>().First()
.ChildrenOfType<DrawableOsuMenuItem>().First(i => i.Item.Text.ToString() == "Delete");
@@ -744,9 +704,9 @@ namespace osu.Game.Tests.Visual.Navigation
IWorkingBeatmap beatmap() => Game.Beatmap.Value;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
@@ -777,9 +737,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestMenuMakesMusic()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice);
@@ -791,7 +751,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPushSongSelectAndPressBackButtonImmediately()
{
AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect()));
AddStep("push song select", () => Game.ScreenStack.Push(new SoloSongSelect()));
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
ConfirmAtMainMenu();
@@ -800,18 +760,23 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitSongSelectWithClick()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
ModSelectOverlay modSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddStep("Show mods overlay", () =>
{
modSelect = songSelect!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect.Show();
});
AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible);
AddStep("Move mouse to dimmed area", () => InputManager.MoveMouseTo(new Vector2(
songSelect.ScreenSpaceDrawQuad.TopLeft.X + 1,
songSelect.ScreenSpaceDrawQuad.TopLeft.Y + songSelect.ScreenSpaceDrawQuad.Height / 2)));
AddStep("Click left mouse button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
AddUntilStep("Overlay was hidden", () => modSelect.State.Value == Visibility.Hidden);
exitViaBackButtonAndConfirm();
}
@@ -876,10 +841,18 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded);
TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
SoloSongSelect songSelect = null;
ModSelectOverlay modSelect = null;
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddStep("Show mods overlay", () =>
{
modSelect = songSelect!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect.Show();
});
AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible);
AddStep("Show mods overlay", () => modSelect.Show());
AddStep("Change ruleset to osu!taiko", () =>
{
@@ -890,7 +863,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType<ToolbarRulesetSelector>().Single().Current.Value.OnlineID == 1);
AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddAssert("Mods overlay still visible", () => modSelect.State.Value == Visibility.Visible);
}
[Test]
@@ -900,10 +873,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show());
AddStep("Show options overlay", () => InputManager.Key(Key.F3));
AddUntilStep("Options overlay visible", () => this.ChildrenOfType<FooterButtonOptions.Popover>().SingleOrDefault()?.State.Value == Visibility.Visible);
AddStep("Change ruleset to osu!taiko", () =>
{
@@ -914,7 +889,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType<ToolbarRulesetSelector>().Single().Current.Value.OnlineID == 1);
AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible);
AddAssert("Options overlay still visible", () => this.ChildrenOfType<FooterButtonOptions.Popover>().Single().State.Value == Visibility.Visible);
}
[Test]
@@ -1186,7 +1161,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitGameFromSongSelect()
{
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
exitViaEscapeAndConfirm();
pushEscape(); // returns to osu! logo
@@ -1201,6 +1176,8 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitWithHoldDisabled()
{
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0));
AddStep("press escape twice rapidly", () =>
@@ -1256,10 +1233,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("close settings sidebar", () => InputManager.Key(Key.Escape));
Screens.Select.SongSelect songSelect = null;
Screens.SelectV2.SongSelect songSelect = null;
AddRepeatStep("go to solo", () => InputManager.Key(Key.P), 3);
AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect) != null);
AddUntilStep("wait for beatmap sets loaded", () => songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.SelectV2.SongSelect) != null);
AddUntilStep("wait for beatmap sets loaded", () => songSelect.CarouselItemsPresented);
AddStep("switch to osu! ruleset", () =>
{
@@ -1269,7 +1246,7 @@ namespace osu.Game.Tests.Visual.Navigation
});
AddStep("touch beatmap wedge", () =>
{
var wedge = Game.ChildrenOfType<BeatmapInfoWedge>().Single();
var wedge = Game.ChildrenOfType<BeatmapTitleWedge>().Single();
var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
@@ -1285,7 +1262,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
AddStep("touch beatmap wedge", () =>
{
var wedge = Game.ChildrenOfType<BeatmapInfoWedge>().Single();
var wedge = Game.ChildrenOfType<BeatmapTitleWedge>().Single();
var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
@@ -1302,7 +1279,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("click beatmap wedge", () =>
{
InputManager.MoveMouseTo(Game.ChildrenOfType<BeatmapInfoWedge>().Single());
InputManager.MoveMouseTo(Game.ChildrenOfType<BeatmapTitleWedge>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
@@ -1313,7 +1290,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
BeatmapSetInfo beatmapSet = null;
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
AddUntilStep("wait for selected", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet));
AddStep("select", () => InputManager.Key(Key.Enter));
@@ -1343,9 +1320,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitSongSelectAndImmediatelyClickLogo()
{
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -1374,9 +1351,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
BeatmapSetInfo beatmap = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
@@ -1405,9 +1382,9 @@ namespace osu.Game.Tests.Visual.Navigation
IWorkingBeatmap beatmap() => Game.Beatmap.Value;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -1445,12 +1422,5 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("Click back button", () => InputManager.Click(MouseButton.Left));
ConfirmAtMainMenu();
}
public partial class TestPlaySongSelect : PlaySongSelect
{
public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
}
}
}
@@ -17,6 +17,7 @@ using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Online.API;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets.Mods;
@@ -26,17 +27,19 @@ using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Screens.SelectV2;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps.IO;
using osuTK;
using osuTK.Input;
using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
namespace osu.Game.Tests.Visual.Navigation
{
public partial class TestSceneSkinEditorNavigation : OsuGameTestScene
{
private TestPlaySongSelect songSelect;
private SoloSongSelect songSelect;
private ModSelectOverlay modSelect => songSelect.ChildrenOfType<ModSelectOverlay>().First();
private SkinEditor skinEditor => Game.ChildrenOfType<SkinEditor>().FirstOrDefault();
[Test]
@@ -331,10 +334,10 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestModOverlayClosesOnOpeningSkinEditor()
{
advanceToSongSelect();
AddStep("open mod overlay", () => songSelect.ModSelectOverlay.Show());
AddStep("open mod overlay", () => modSelect.Show());
openSkinEditor();
AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
AddUntilStep("mod overlay closed", () => modSelect.State.Value == Visibility.Hidden);
}
[Test]
@@ -448,8 +451,8 @@ namespace osu.Game.Tests.Visual.Navigation
private void advanceToSongSelect()
{
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
}
private void openSkinEditor()
@@ -9,7 +9,9 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
@@ -126,6 +128,71 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime);
}
[Test]
public void TestAutoplayShortcutReturnsInitialModsOnExit()
{
PushAndConfirm(() => new SoloSongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault);
AddStep("open mod select", () => InputManager.Key(Key.F1));
AddStep("search magnetised", () => this.ChildrenOfType<ModSelectOverlay>().Single().SearchTerm = "MG");
AddStep("select", () => InputManager.Key(Key.Enter));
AddAssert("magnetised selected", () => Game.SelectedMods.Value.Single(), Is.TypeOf<OsuModMagnetised>);
AddStep("configure mod", () => ((OsuModMagnetised)Game.SelectedMods.Value.Single()).AttractionStrength.Value = 1.0f);
pushEscape();
pushEscape();
AddStep("press ctrl+enter", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Enter);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
return Game.ScreenStack.CurrentScreen is Player;
});
AddAssert("only autoplay selected", () => Game.SelectedMods.Value.Single(), Is.TypeOf<OsuModAutoplay>);
pushEscape();
waitForScreen<SoloSongSelect>();
AddAssert("magnetised selected", () => Game.SelectedMods.Value.Single(), Is.TypeOf<OsuModMagnetised>);
AddAssert("mod configured", () => ((OsuModMagnetised)Game.SelectedMods.Value.Single()).AttractionStrength.Value, () => Is.EqualTo(1.0f));
}
[Test]
public void TestLeaderboardCorrectInPlayer()
{
IWorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new SoloSongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("switch to next difficulty and immediately press enter", () =>
{
InputManager.Key(Key.Down);
Schedule(() => InputManager.Key(Key.Enter));
});
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
return Game.ScreenStack.CurrentScreen is Player;
});
AddAssert("leaderboard matches gameplay beatmap", () => Game.ChildrenOfType<LeaderboardManager>().Single().CurrentCriteria?.Beatmap, () => Is.EqualTo(beatmap().BeatmapInfo));
}
private Func<Player> playToResults()
{
var player = playToCompletion();
@@ -243,6 +243,30 @@ namespace osu.Game.Tests.Visual.Online
});
}
[Test]
public void TestBeatmapSetHasVideoOrStoryboard()
{
AddStep("show beatmapset with video", () =>
{
var beatmapSet = getBeatmapSet();
beatmapSet.HasVideo = true;
overlay.ShowBeatmapSet(beatmapSet);
});
AddStep("show beatmapset with storyboard", () =>
{
var beatmapSet = getBeatmapSet();
beatmapSet.HasStoryboard = true;
overlay.ShowBeatmapSet(beatmapSet);
});
AddStep("show beatmapset with video and storyboard", () =>
{
var beatmapSet = getBeatmapSet();
beatmapSet.HasVideo = true;
beatmapSet.HasStoryboard = true;
overlay.ShowBeatmapSet(beatmapSet);
});
}
[Test]
public void TestSelectedModsDontAffectStatistics()
{
@@ -46,6 +46,32 @@ namespace osu.Game.Tests.Visual.Ranking
});
}
// cross-reference: `TestSceneToolbarUserButton.TestTransientUserStatisticsDisplay()`, "Test rounding treatment" step.
[Test]
public void TestRoundingTreatment()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_071.495M
},
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072.99M
});
}
[Test]
public void TestAllDecreased()
{
@@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -14,10 +16,12 @@ using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Ranking;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneUserTagControl : OsuTestScene
public partial class TestSceneUserTagControl : OsuManualInputManagerTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
@@ -63,6 +67,8 @@ namespace osu.Game.Tests.Visual.Ranking
beatmapSet.Beatmaps.Single().TopTags =
[
new APIBeatmapTag { TagId = 3, VoteCount = 9 },
new APIBeatmapTag { TagId = 2, VoteCount = 8 },
new APIBeatmapTag { TagId = 0, VoteCount = 7 },
];
Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500);
return true;
@@ -79,6 +85,11 @@ namespace osu.Game.Tests.Visual.Ranking
return false;
};
});
}
[Test]
public void TestRulesetSupport()
{
AddStep("show for osu! beatmap", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
@@ -86,6 +97,7 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap.Value = working;
recreateControl();
});
AddStep("show for taiko beatmap", () =>
{
var working = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo);
@@ -95,6 +107,47 @@ namespace osu.Game.Tests.Visual.Ranking
});
}
[Test]
public void TestTagsDoNotMoveUntilMouseMovesAway()
{
AddStep("show", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.BeatmapInfo.OnlineID = 42;
Beatmap.Value = working;
recreateControl();
});
AddUntilStep("wait for ready", () => getTagFlow().Count, () => Is.EqualTo(4));
AddAssert("tag 2 is second", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(1));
AddStep("vote for tag 2", () =>
{
InputManager.MoveMouseTo(getDrawableTagById(2));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(9));
AddStep("remove vote for tag 2", () =>
{
InputManager.MoveMouseTo(getDrawableTagById(2));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("tag 2 not voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(8));
AddAssert("tag 2 is still second", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(1));
AddStep("vote for tag 2", () =>
{
InputManager.MoveMouseTo(getDrawableTagById(2));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(9));
AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero));
AddAssert("tag 2 reordered to first", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(0));
FillFlowContainer<UserTagControl.DrawableUserTag> getTagFlow() => this.ChildrenOfType<FillFlowContainer<UserTagControl.DrawableUserTag>>().Single();
UserTagControl.DrawableUserTag getDrawableTagById(long id) => getTagFlow().Single(t => t.UserTag.Id == id);
}
private void recreateControl()
{
Child = new PopoverContainer
@@ -119,7 +119,6 @@ namespace osu.Game.Tests.Visual.SongSelect
{
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
});
@@ -140,7 +139,6 @@ namespace osu.Game.Tests.Visual.SongSelect
var difficultyAdjustMod = ruleset.CreateMod<OsuModDifficultyAdjust>().AsNonNull();
var originalDifficulty = advancedStats.BeatmapInfo.Difficulty;
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
@@ -1,548 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Audio;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneBeatmapLeaderboard : OsuManualInputManagerTestScene
{
private readonly FailableLeaderboard leaderboard;
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay;
private ScoreManager scoreManager = null!;
private RulesetStore rulesetStore = null!;
private BeatmapManager beatmapManager = null!;
private PlaySongSelect songSelect = null!;
private LeaderboardManager leaderboardManager = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
dependencies.CacheAs<Screens.Select.SongSelect>(songSelect = new PlaySongSelect());
dependencies.Cache(leaderboardManager = new LeaderboardManager());
Dependencies.Cache(Realm);
return dependencies;
}
[BackgroundDependencyLoader]
private void load()
{
LoadComponent(songSelect);
LoadComponent(leaderboardManager);
}
public TestSceneBeatmapLeaderboard()
{
Add(new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
dialogOverlay = new DialogOverlay
{
Depth = -1
},
leaderboard = new FailableLeaderboard
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = BeatmapLeaderboardScope.Global,
}
}
});
}
[Test]
public void TestLocalScoresDisplay()
{
BeatmapInfo beatmapInfo = null!;
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
AddStep(@"Set beatmap", () =>
{
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
leaderboard.BeatmapInfo = beatmapInfo;
});
clearScores();
checkDisplayedCount(0);
importMoreScores(() => beatmapInfo);
checkDisplayedCount(10);
importMoreScores(() => beatmapInfo);
checkDisplayedCount(20);
clearScores();
checkDisplayedCount(0);
}
[Test]
public void TestLocalScoresDisplayWorksWhenStartingOffline()
{
BeatmapInfo beatmapInfo = null!;
AddStep("Log out", () => API.Logout());
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
AddStep(@"Set beatmap", () =>
{
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
leaderboard.BeatmapInfo = beatmapInfo;
});
clearScores();
importMoreScores(() => beatmapInfo);
checkDisplayedCount(10);
}
[Test]
public void TestLocalScoresDisplayOnBeatmapEdit()
{
BeatmapInfo beatmapInfo = null!;
string originalHash = string.Empty;
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
AddStep(@"Import beatmap", () =>
{
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
leaderboard.BeatmapInfo = beatmapInfo;
});
clearScores();
checkDisplayedCount(0);
AddStep(@"Perform initial save to guarantee stable hash", () =>
{
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
beatmapManager.Save(beatmapInfo, beatmap);
originalHash = beatmapInfo.Hash;
});
importMoreScores(() => beatmapInfo);
checkDisplayedCount(10);
checkStoredCount(10);
AddStep(@"Save with changes", () =>
{
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
beatmap.Difficulty.ApproachRate = 12;
beatmapManager.Save(beatmapInfo, beatmap);
});
AddAssert("Hash changed", () => beatmapInfo.Hash, () => Is.Not.EqualTo(originalHash));
checkDisplayedCount(0);
checkStoredCount(10);
importMoreScores(() => beatmapInfo);
importMoreScores(() => beatmapInfo);
checkDisplayedCount(20);
checkStoredCount(30);
AddStep(@"Revert changes", () =>
{
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
beatmap.Difficulty.ApproachRate = 8;
beatmapManager.Save(beatmapInfo, beatmap);
});
AddAssert("Hash restored", () => beatmapInfo.Hash, () => Is.EqualTo(originalHash));
checkDisplayedCount(10);
checkStoredCount(30);
clearScores();
checkDisplayedCount(0);
checkStoredCount(0);
}
[Test]
public void TestGlobalScoresDisplay()
{
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global);
AddStep(@"New Scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo())));
AddStep(@"New Scores with teams", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()).Select(s =>
{
s.User.Team = new APITeam();
return s;
})));
}
[Test]
public void TestPersonalBest()
{
AddStep(@"Show personal best", showPersonalBest);
AddStep("null personal best position", showPersonalBestWithNullPosition);
}
[Test]
public void TestPlaceholderStates()
{
AddStep("ensure no scores displayed", () => leaderboard.SetScores(null));
AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure));
AddStep(@"No team", () => leaderboard.SetErrorState(LeaderboardState.NoTeam));
AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter));
AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn));
AddStep(@"Ruleset unavailable", () => leaderboard.SetErrorState(LeaderboardState.RulesetUnavailable));
AddStep(@"Beatmap unavailable", () => leaderboard.SetErrorState(LeaderboardState.BeatmapUnavailable));
AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected));
}
[Test]
public void TestUseTheseModsDoesNotCopySystemMods()
{
AddStep(@"set scores", () => leaderboard.SetScores(leaderboard.Scores, new ScoreInfo
{
Position = 999,
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Ruleset = new OsuRuleset().RulesetInfo,
Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), },
User = new APIUser
{
Id = 6602580,
Username = @"waaiiru",
CountryCode = CountryCode.ES,
}
}));
AddUntilStep("wait for scores", () => this.ChildrenOfType<LeaderboardScore>().Count(), () => Is.GreaterThan(0));
AddStep("right click panel", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<LeaderboardScore>().Single());
InputManager.Click(MouseButton.Right);
});
AddStep("click use these mods", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableOsuMenuItem>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("song select received HD", () => songSelect.Mods.Value.Any(m => m is OsuModHidden));
AddAssert("song select did not receive SV2", () => !songSelect.Mods.Value.Any(m => m is ModScoreV2));
}
private void showPersonalBestWithNullPosition()
{
leaderboard.SetScores(leaderboard.Scores, new ScoreInfo
{
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 6602580,
Username = @"waaiiru",
CountryCode = CountryCode.ES,
},
});
}
private void showPersonalBest()
{
leaderboard.SetScores(leaderboard.Scores, new ScoreInfo
{
Position = 999,
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Ruleset = new OsuRuleset().RulesetInfo,
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
User = new APIUser
{
Id = 6602580,
Username = @"waaiiru",
CountryCode = CountryCode.ES,
}
});
}
private void importMoreScores(Func<BeatmapInfo> beatmapInfo)
{
AddStep(@"Import new scores", () =>
{
foreach (var score in GenerateSampleScores(beatmapInfo()))
scoreManager.Import(score);
});
}
private void clearScores()
{
AddStep("Clear all scores", () => scoreManager.Delete());
}
private void checkDisplayedCount(int expected) =>
AddUntilStep($"{expected} scores displayed", () => leaderboard.ChildrenOfType<LeaderboardScore>().Count(), () => Is.EqualTo(expected));
private void checkStoredCount(int expected) =>
AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All<ScoreInfo>().Count(s => !s.DeletePending)), () => Is.EqualTo(expected));
public static ScoreInfo[] GenerateSampleScores(BeatmapInfo beatmapInfo)
{
return new[]
{
new ScoreInfo
{
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now,
Mods = new Mod[]
{
new OsuModHidden(),
new OsuModFlashlight
{
FollowDelay = { Value = 200 },
SizeMultiplier = { Value = 5 },
},
new OsuModDifficultyAdjust
{
CircleSize = { Value = 11 },
ApproachRate = { Value = 10 },
OverallDifficulty = { Value = 10 },
DrainRate = { Value = 10 },
ExtendedLimits = { Value = true }
}
},
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
User = new APIUser
{
Id = 6602580,
Username = @"waaiiru",
CountryCode = CountryCode.ES,
},
},
new ScoreInfo
{
Rank = ScoreRank.X,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddSeconds(-30),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 4608074,
Username = @"Skycries",
CountryCode = CountryCode.BR,
},
},
new ScoreInfo
{
Rank = ScoreRank.SH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddSeconds(-70),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 1014222,
Username = @"eLy",
CountryCode = CountryCode.JP,
},
},
new ScoreInfo
{
Rank = ScoreRank.S,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddMinutes(-40),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 1541390,
Username = @"Toukai",
CountryCode = CountryCode.CA,
},
},
new ScoreInfo
{
Rank = ScoreRank.A,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddHours(-2),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 2243452,
Username = @"Satoruu",
CountryCode = CountryCode.VE,
},
},
new ScoreInfo
{
Rank = ScoreRank.B,
Accuracy = 0.9826,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddHours(-25),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 2705430,
Username = @"Mooha",
CountryCode = CountryCode.FR,
},
},
new ScoreInfo
{
Rank = ScoreRank.C,
Accuracy = 0.9654,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddHours(-50),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 7151382,
Username = @"Mayuri Hana",
CountryCode = CountryCode.TH,
},
},
new ScoreInfo
{
Rank = ScoreRank.D,
Accuracy = 0.6025,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddHours(-72),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 2051389,
Username = @"FunOrange",
CountryCode = CountryCode.CA,
},
},
new ScoreInfo
{
Rank = ScoreRank.D,
Accuracy = 0.5140,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddMonths(-10),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 6169483,
Username = @"-Hebel-",
CountryCode = CountryCode.MX,
},
},
new ScoreInfo
{
Rank = ScoreRank.D,
Accuracy = 0.4222,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddYears(-2),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 6702666,
Username = @"prhtnsm",
CountryCode = CountryCode.DE,
},
},
};
}
private partial class FailableLeaderboard : BeatmapLeaderboard
{
public new void SetErrorState(LeaderboardState state) => base.SetErrorState(state);
public new void SetScores(IEnumerable<ScoreInfo>? scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore);
}
}
}
@@ -23,6 +23,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osu.Game.Utils;
@@ -64,16 +65,16 @@ namespace osu.Game.Tests.Visual.SongSelect
switch (rulesetID)
{
case 0:
return 336; // recommended star rating of 2
return 337; // recommended star rating of 2
case 1:
return 973; // SR 3
case 2:
return 1905; // SR 4
return 1906; // SR 4
case 3:
return 3329; // SR 5
return 3330; // SR 5
default:
return 0;
@@ -248,7 +249,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect select && select.CarouselItemsPresented);
AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(getImport().Beatmaps[expectedDiff - 1].OnlineID));
}
File diff suppressed because it is too large Load Diff
@@ -263,8 +263,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
var results = await runGrouping(GroupMode.Difficulty, beatmapSets);
assertGroup(results, 0, "Below 1 Star", new[] { beatmapBelow1 }, ref total);
assertGroup(results, 1, "1 Star", new[] { beatmapAbove1 }, ref total);
assertGroup(results, 2, "2 Stars", new[] { beatmapAlmost2, beatmap2, beatmapAbove2 }, ref total);
assertGroup(results, 1, "1 Star", new[] { beatmapAbove1, beatmapAlmost2 }, ref total);
assertGroup(results, 2, "2 Stars", new[] { beatmap2, beatmapAbove2 }, ref total);
assertGroup(results, 3, "7 Stars", new[] { beatmap7 }, ref total);
assertTotal(results, total);
}
@@ -36,6 +36,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public abstract partial class BeatmapCarouselTestScene : OsuManualInputManagerTestScene
{
protected readonly Stack<BeatmapSetInfo> BeatmapSetRequestedSelections = new Stack<BeatmapSetInfo>();
protected readonly Stack<BeatmapInfo> BeatmapRequestedSelections = new Stack<BeatmapInfo>();
protected readonly BindableList<BeatmapSetInfo> BeatmapSets = new BindableList<BeatmapSetInfo>();
@@ -73,6 +74,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
AddStep("create components", () =>
{
BeatmapRequestedSelections.Clear();
BeatmapSetRequestedSelections.Clear();
BeatmapRecommendationFunction = null;
NewItemsPresentedInvocationCount = 0;
@@ -113,6 +115,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
NewItemsPresented = _ => NewItemsPresentedInvocationCount++,
RequestSelection = b =>
{
BeatmapRequestedSelections.Push(b);
Carousel.CurrentSelection = b;
},
RequestRecommendedSelection = beatmaps =>
@@ -184,6 +187,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False);
protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target));
protected void ToggleGroupCollapse() => AddStep("toggle group collapse", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Enter);
InputManager.ReleaseKey(Key.ShiftLeft);
});
protected void SelectNextGroup() => AddStep("select next group", () =>
{
InputManager.PressKey(Key.ShiftLeft);
@@ -263,9 +273,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group);
GroupDefinition? groupDefinition = groupingFilter.GroupItems.Keys.ElementAtOrDefault(group);
if (groupDefinition == null)
return false;
// offset by one because the group itself is included in the items list.
CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel + 1);
CarouselItem item = groupingFilter.GroupItems[groupDefinition].ElementAt(panel + 1);
return (Carousel.CurrentSelection as BeatmapInfo)?
.Equals(item.Model as BeatmapInfo) == true;
@@ -114,17 +114,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SelectPrevPanel();
SelectPrevPanel();
ICarouselPanel? groupPanel = null;
AddStep("get group panel", () => groupPanel = GetKeyboardSelectedPanel());
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, () => Is.EqualTo(groupPanel));
SelectPrevSet();
WaitForBeatmapSelection(0, 1);
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, () => Is.EqualTo(groupPanel));
SelectPrevSet();
WaitForBeatmapSelection(0, 1);
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
// Expanding a group will move keyboard selection to the selected beatmap if contained.
AddAssert("keyboard selected panel is expanded", () => groupPanel?.Expanded.Value, () => Is.True);
AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf<BeatmapInfo>);
}
[Test]
@@ -102,19 +102,116 @@ namespace osu.Game.Tests.Visual.SongSelectV2
WaitForBeatmapSelection(0, 0);
SelectPrevPanel();
ICarouselPanel? groupPanel = null;
AddStep("get group panel", () => groupPanel = GetKeyboardSelectedPanel());
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, () => Is.EqualTo(groupPanel));
SelectPrevSet();
WaitForBeatmapSelection(0, 0);
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, () => Is.EqualTo(groupPanel));
SelectPrevSet();
WaitForBeatmapSelection(0, 0);
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
// Expanding a group will move keyboard selection to the selected beatmap if contained.
AddAssert("keyboard selected panel is expanded", () => groupPanel?.Expanded.Value, () => Is.True);
AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf<BeatmapInfo>);
}
[Test]
public void TestKeyboardGroupToggleCollapse_SelectionContained()
{
SelectNextSet();
WaitForBeatmapSelection(0, 0);
checkBeatmapIsKeyboardSelected();
ToggleGroupCollapse();
checkGroupKeyboardSelected(0);
ToggleGroupCollapse();
checkBeatmapIsKeyboardSelected();
}
[Test]
public void TestKeyboardGroupToggleCollapse_SelectionNotContained()
{
SelectNextSet();
WaitForBeatmapSelection(0, 0);
checkBeatmapIsKeyboardSelected();
SelectNextGroup();
checkGroupKeyboardSelected(1);
ToggleGroupCollapse();
checkGroupKeyboardSelected(1);
ToggleGroupCollapse();
checkGroupKeyboardSelected(1);
}
[Test]
public void TestKeyboardGroupTraversalSingleGroup()
{
RemoveAllBeatmaps();
AddBeatmaps(1, 1);
WaitForBeatmapSelection(0, 0);
SelectNextGroup();
checkBeatmapIsKeyboardSelected();
SelectPrevGroup();
checkBeatmapIsKeyboardSelected();
}
[Test]
public void TestKeyboardGroupTraversal()
{
SelectNextSet();
WaitForBeatmapSelection(0, 0);
checkBeatmapIsKeyboardSelected();
SelectNextGroup();
WaitForBeatmapSelection(0, 0);
WaitForExpandedGroup(1);
checkGroupKeyboardSelected(1);
SelectNextGroup();
WaitForBeatmapSelection(0, 0);
WaitForExpandedGroup(2);
checkGroupKeyboardSelected(2);
SelectNextGroup();
WaitForBeatmapSelection(0, 0);
WaitForExpandedGroup(0);
checkBeatmapIsKeyboardSelected();
SelectPrevGroup();
WaitForBeatmapSelection(0, 0);
WaitForExpandedGroup(2);
checkGroupKeyboardSelected(2);
}
private void checkBeatmapIsKeyboardSelected() =>
AddUntilStep("check keyboard selected group is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(Carousel.CurrentSelection));
private void checkGroupKeyboardSelected(int index) => AddUntilStep($"check keyboard selected group is {index}", () => GetKeyboardSelectedPanel()?.Item?.Model, () =>
{
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(index);
// offset by one because the group itself is included in the items list.
CarouselItem item = groupingFilter.GroupItems[g].ElementAt(0);
return Is.EqualTo(item.Model);
});
[Test]
public void TestGroupSelectionOnHeaderMouse()
{
@@ -129,9 +226,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroupStarDifficulty>);
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
// Expanding a group will move keyboard selection to the selected beatmap if contained.
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelBeatmapStandalone>);
AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf<PanelBeatmapStandalone>);
}
@@ -313,9 +313,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
CheckDisplayedBeatmapsCount(4);
SelectNextSet();
WaitForSetSelection(0, 1);
SelectPrevSet();
WaitForSetSelection(1, 1);
SelectPrevSet();
WaitForSetSelection(0, 1);
}
[Test]
@@ -155,6 +155,38 @@ namespace osu.Game.Tests.Visual.SongSelectV2
WaitForSetSelection(1, 0);
}
[Test]
public void TestMultipleKeyboardOperationsPerFrame()
{
AddBeatmaps(10, 3);
WaitForDrawablePanels();
SelectNextSet();
WaitForSetSelection(0, 0);
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
AddStep("Press two keys at once", () =>
{
InputManager.Key(Key.Down);
InputManager.Key(Key.Right);
});
// Second key is respected, so only set selection changes.
WaitForSetSelection(1, 0);
AddStep("Press two keys at once", () =>
{
InputManager.Key(Key.Left);
InputManager.Key(Key.Up);
});
// Second key is respected, so only keyboard selection changes.
WaitForSetSelection(1, 0);
}
[Test]
public void TestKeyboardSelection()
{
@@ -195,16 +227,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SelectNextSet();
WaitForSetSelection(0, 0);
// In the case of a grouped beatmap set, the header gets activated and re-selects the recommended difficulty.
// This is probably fine.
CheckActivationCount(1);
// We don't want it to request present though, which would start gameplay.
CheckActivationCount(0);
CheckRequestPresentCount(0);
SelectPrevSet();
WaitForSetSelection(0, 0);
CheckActivationCount(1);
CheckActivationCount(0);
CheckRequestPresentCount(0);
}
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
@@ -36,6 +37,49 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}
}
[Test]
public void TestGroupingModeChangeStillWorks()
{
BeatmapInfo originalSelected = null!;
GroupDefinition? expanded = null;
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
AddBeatmaps(10, 3, true);
WaitForDrawablePanels();
nextRandom();
ensureRandomDidNotRepeat();
AddStep("store selection", () => originalSelected = (BeatmapInfo)Carousel.CurrentSelection!);
SortAndGroupBy(SortMode.Artist, GroupMode.Difficulty);
WaitForFiltering();
AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(originalSelected));
storeExpandedGroup();
for (int i = 0; i < 5; i++)
{
nextRandom();
ensureRandomDidNotRepeat();
checkExpandedGroupUnchanged();
}
SortAndGroupBy(SortMode.Artist, GroupMode.None);
WaitForFiltering();
for (int i = 0; i < 5; i++)
{
nextRandom();
ensureRandomDidNotRepeat();
}
void storeExpandedGroup() => AddStep("store open group", () => expanded = Carousel.ExpandedGroup);
void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded));
}
/// <summary>
/// Test random non-repeating algorithm
/// </summary>
@@ -47,57 +91,139 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddBeatmaps(10, 3, true);
WaitForDrawablePanels();
nextRandom();
ensureRandomDidNotRepeat();
nextRandom();
ensureRandomDidNotRepeat();
nextRandom();
ensureRandomDidNotRepeat();
GroupDefinition? expanded = null;
prevRandom();
for (int i = 0; i < 2; i++)
{
nextRandom();
expanded ??= storeExpandedGroup();
ensureSetRandomDidNotRepeat();
checkExpandedGroupUnchanged();
}
nextRandom();
ensureSetRandomDidRepeat();
checkExpandedGroupUnchanged();
prevRandomSet();
checkRewindCorrectSet();
prevRandom();
checkExpandedGroupUnchanged();
prevRandomSet();
checkRewindCorrectSet();
checkExpandedGroupUnchanged();
nextRandom();
ensureRandomDidNotRepeat();
ensureSetRandomDidNotRepeat();
checkExpandedGroupUnchanged();
nextRandom();
ensureRandomDidNotRepeat();
ensureSetRandomDidRepeat();
checkExpandedGroupUnchanged();
nextRandom();
AddAssert("ensure repeat", () => BeatmapSetRequestedSelections.Contains(Carousel.SelectedBeatmapSet!));
GroupDefinition? storeExpandedGroup()
{
AddStep("store open group", () => expanded = Carousel.ExpandedGroup);
return null;
}
void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded));
}
/// <summary>
/// Test random non-repeating algorithm
/// </summary>
[Test]
public void TestRandomDifficultyGrouping()
public void TestRandomDifficultyGroupingRewindsCorrectly()
{
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
AddBeatmaps(10, 3, true);
AddBeatmaps(3, 3, true);
WaitForDrawablePanels();
nextRandom();
ensureRandomDidNotRepeat();
nextRandom();
ensureRandomDidNotRepeat();
nextRandom();
ensureRandomDidNotRepeat();
GroupDefinition? expanded = null;
prevRandom();
checkRewindCorrectSet();
prevRandom();
checkRewindCorrectSet();
for (int i = 0; i < 3; i++)
{
nextRandom();
expanded ??= storeExpandedGroup();
ensureRandomDidNotRepeat();
checkExpandedGroupUnchanged();
}
for (int i = 0; i < 2; i++)
{
prevRandom();
checkRewindCorrect();
checkExpandedGroupUnchanged();
}
for (int i = 0; i < 2; i++)
{
nextRandom();
ensureRandomDidNotRepeat();
checkExpandedGroupUnchanged();
}
nextRandom();
ensureRandomDidNotRepeat();
nextRandom();
ensureRandomDidNotRepeat();
ensureRandomDidRepeat();
checkExpandedGroupUnchanged();
GroupDefinition? storeExpandedGroup()
{
AddStep("store open group", () => expanded = Carousel.ExpandedGroup);
return null;
}
void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded));
}
/// <summary>
/// Test random non-repeating algorithm
/// </summary>
[Test]
public void TestRandomDifficultyGroupingRepeatsWhenExhausted()
{
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
AddBeatmaps(3, 3, true);
WaitForDrawablePanels();
GroupDefinition? expanded = null;
for (int i = 0; i < 3; i++)
{
nextRandom();
expanded ??= storeExpandedGroup();
ensureRandomDidNotRepeat();
checkExpandedGroupUnchanged();
}
for (int i = 0; i < 3; i++)
{
nextRandom();
ensureRandomDidRepeat();
}
for (int i = 0; i < 5; i++)
{
prevRandom();
checkRewindCorrect();
checkExpandedGroupUnchanged();
}
nextRandom();
AddAssert("ensure repeat", () => BeatmapSetRequestedSelections.Contains(Carousel.SelectedBeatmapSet!));
checkExpandedGroupUnchanged();
// can't assert repeat or otherwise as we went through multiple permutations.
GroupDefinition? storeExpandedGroup()
{
AddStep("store open group", () => expanded = Carousel.ExpandedGroup);
return null;
}
void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded));
}
[Test]
@@ -116,7 +242,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
for (int i = 0; i < random_select_count; i++)
{
prevRandom();
prevRandomSet();
checkRewindCorrectSet();
}
}
@@ -167,20 +293,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(postRandomSelection));
prevRandom();
prevRandomSet();
AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(postRandomSelection));
}
private void nextRandom() =>
AddStep("select random next", () => Carousel.NextRandom());
private void ensureRandomDidRepeat() =>
AddAssert("did repeat", () => BeatmapRequestedSelections.Distinct().Count(), () => Is.LessThan(BeatmapRequestedSelections.Count));
private void ensureRandomDidNotRepeat() =>
AddAssert("no repeats", () => BeatmapRequestedSelections.Distinct().Count(), () => Is.EqualTo(BeatmapRequestedSelections.Count));
private void ensureSetRandomDidRepeat() =>
AddAssert("did repeat", () => BeatmapSetRequestedSelections.Distinct().Count(), () => Is.LessThan(BeatmapSetRequestedSelections.Count));
private void ensureSetRandomDidNotRepeat() =>
AddAssert("no repeats", () => BeatmapSetRequestedSelections.Distinct().Count(), () => Is.EqualTo(BeatmapSetRequestedSelections.Count));
private void checkRewindCorrect() =>
AddAssert("rewind matched expected beatmap", () => BeatmapRequestedSelections.Peek(), () => Is.EqualTo(Carousel.SelectedBeatmapInfo));
private void checkRewindCorrectSet() =>
AddAssert("rewind matched expected set", () => BeatmapSetRequestedSelections.Peek(), () => Is.EqualTo(Carousel.SelectedBeatmapSet));
private void prevRandom() => AddStep("select random last", () =>
private void prevRandom() => AddStep("select last random", () =>
{
Carousel.PreviousRandom();
BeatmapRequestedSelections.Pop();
// Pop twice because the PreviousRandom call also requests selection.
BeatmapRequestedSelections.Pop();
});
private void prevRandomSet() => AddStep("select last random set", () =>
{
Carousel.PreviousRandom();
BeatmapSetRequestedSelections.Pop();
@@ -53,6 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
fillFlow = new FillFlowContainer
{
X = 100,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
@@ -281,7 +282,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
},
new ScoreInfo
{
Position = 110000,
Position = 2233,
Rank = ScoreRank.D,
Accuracy = 1,
MaxCombo = 244,
@@ -28,7 +28,6 @@ using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.SongSelect;
using osu.Game.Users;
using osuTK.Input;
@@ -118,8 +117,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
setScope(BeatmapLeaderboardScope.Global);
AddStep(@"New Scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo())));
AddStep(@"New Scores with teams", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()).Select(s =>
AddStep(@"New Scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo())));
AddStep(@"New Scores with teams", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()).Select(s =>
{
s.User.Team = new APITeam();
return s;
@@ -150,7 +149,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Test]
public void TestUseTheseModsDoesNotCopySystemMods()
{
AddStep(@"set scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
AddStep(@"set scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
{
OnlineID = 1337,
Position = 999,
@@ -297,7 +296,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
private void showPersonalBestWithNullPosition()
{
leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
{
OnlineID = 1337,
Rank = ScoreRank.XH,
@@ -318,7 +317,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
private void showPersonalBest()
{
leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
{
OnlineID = 1337,
Position = 999,
@@ -347,7 +346,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
AddStep(@"Import new scores", () =>
{
foreach (var score in TestSceneBeatmapLeaderboard.GenerateSampleScores(beatmapInfo()))
foreach (var score in GenerateSampleScores(beatmapInfo()))
scoreManager.Import(score);
});
}
@@ -368,5 +367,216 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public new void SetState(LeaderboardState state) => base.SetState(state);
public new void SetScores(IEnumerable<ScoreInfo> scores, ScoreInfo? userScore = null, int? totalCount = null) => base.SetScores(scores, userScore, totalCount);
}
public static ScoreInfo[] GenerateSampleScores(BeatmapInfo beatmapInfo)
{
return new[]
{
new ScoreInfo
{
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now,
Mods = new Mod[]
{
new OsuModHidden(),
new OsuModFlashlight
{
FollowDelay = { Value = 200 },
SizeMultiplier = { Value = 5 },
},
new OsuModDifficultyAdjust
{
CircleSize = { Value = 11 },
ApproachRate = { Value = 10 },
OverallDifficulty = { Value = 10 },
DrainRate = { Value = 10 },
ExtendedLimits = { Value = true }
}
},
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
User = new APIUser
{
Id = 6602580,
Username = @"waaiiru",
CountryCode = CountryCode.ES,
},
},
new ScoreInfo
{
Rank = ScoreRank.X,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddSeconds(-30),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 4608074,
Username = @"Skycries",
CountryCode = CountryCode.BR,
},
},
new ScoreInfo
{
Rank = ScoreRank.SH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddSeconds(-70),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 1014222,
Username = @"eLy",
CountryCode = CountryCode.JP,
},
},
new ScoreInfo
{
Rank = ScoreRank.S,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddMinutes(-40),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 1541390,
Username = @"Toukai",
CountryCode = CountryCode.CA,
},
},
new ScoreInfo
{
Rank = ScoreRank.A,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddHours(-2),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 2243452,
Username = @"Satoruu",
CountryCode = CountryCode.VE,
},
},
new ScoreInfo
{
Rank = ScoreRank.B,
Accuracy = 0.9826,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddHours(-25),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 2705430,
Username = @"Mooha",
CountryCode = CountryCode.FR,
},
},
new ScoreInfo
{
Rank = ScoreRank.C,
Accuracy = 0.9654,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddHours(-50),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 7151382,
Username = @"Mayuri Hana",
CountryCode = CountryCode.TH,
},
},
new ScoreInfo
{
Rank = ScoreRank.D,
Accuracy = 0.6025,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddHours(-72),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 2051389,
Username = @"FunOrange",
CountryCode = CountryCode.CA,
},
},
new ScoreInfo
{
Rank = ScoreRank.D,
Accuracy = 0.5140,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddMonths(-10),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 6169483,
Username = @"-Hebel-",
CountryCode = CountryCode.MX,
},
},
new ScoreInfo
{
Rank = ScoreRank.D,
Accuracy = 0.4222,
MaxCombo = 244,
TotalScore = 1707827,
Date = DateTime.Now.AddYears(-2),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
Id = 6702666,
Username = @"prhtnsm",
CountryCode = CountryCode.DE,
},
},
};
}
}
}
@@ -114,11 +114,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddWaitStep("wait for transition", 3);
AddStep("show overlay", () => externalOverlay.Show());
AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
contentDisplayed();
AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("hide overlay", () => externalOverlay.Hide());
AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
contentHidden();
AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
@@ -133,11 +133,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
AddStep("show external overlay", () => externalOverlay.Show());
AddAssert("footer shown", () => screenFooter.State.Value == Visibility.Visible);
AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
contentDisplayed();
AddStep("hide external overlay", () => externalOverlay.Hide());
AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden);
AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
contentHidden();
AddStep("show footer", () => screenFooter.Show());
AddAssert("content still hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
@@ -216,17 +216,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddWaitStep("wait for transition", 3);
AddStep("show overlay", () => externalOverlay.Show());
AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
contentDisplayed();
AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("resize active button", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(240, 300, Easing.OutQuint));
AddStep("resize active button back", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(116, 300, Easing.OutQuint));
AddStep("hide overlay", () => externalOverlay.Hide());
AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
contentHidden();
AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
private void contentHidden()
{
AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
}
private void contentDisplayed()
{
AddUntilStep("content displayed in footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
}
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{
public TestShearedOverlayContainer()
@@ -261,7 +271,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
@@ -456,7 +457,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
applyUpdate = false;
var updateNotification = new UpdateManager.UpdateProgressNotification
var updateNotification = new UpdateManager.UpdateDownloadProgressNotification(CancellationToken.None)
{
CompletionClickAction = () => applyUpdate = true
};
@@ -468,9 +469,9 @@ namespace osu.Game.Tests.Visual.UserInterface
checkProgressingCount(1);
waitForCompletion();
UpdateManager.UpdateApplicationCompleteNotification? completionNotification = null;
UpdateManager.UpdateReadyNotification? completionNotification = null;
AddUntilStep("wait for completion notification",
() => (completionNotification = notificationOverlay.ChildrenOfType<UpdateManager.UpdateApplicationCompleteNotification>().SingleOrDefault()) != null);
() => (completionNotification = notificationOverlay.ChildrenOfType<UpdateManager.UpdateReadyNotification>().SingleOrDefault()) != null);
AddStep("click notification", () => completionNotification?.TriggerClick());
AddUntilStep("wait for update applied", () => applyUpdate);
@@ -20,6 +20,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards
set => iconContainer.Size = value;
}
public MarginPadding IconPadding
{
get => iconContainer.Padding;
set => iconContainer.Padding = value;
}
private readonly Container iconContainer;
protected IconPill(IconUsage icon)
@@ -16,6 +16,8 @@ namespace osu.Game.Beatmaps.Formats
public abstract class LegacyDecoder<T> : Decoder<T>
where T : new()
{
// If this is updated, a new release of `osu-server-beatmap-submission` is required with updated packages.
// See usage at https://github.com/ppy/osu-server-beatmap-submission/blob/master/osu.Server.BeatmapSubmission/Services/BeatmapPackageParser.cs#L96-L97.
public const int LATEST_VERSION = 14;
public const int MAX_COMBO_COLOUR_COUNT = 8;
+20 -2
View File
@@ -92,8 +92,8 @@ namespace osu.Game.Beatmaps
/// </list>
/// </param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range)
=> DifficultyRange(difficulty, range.od0, range.od5, range.od10);
static double DifficultyRange(double difficulty, DifficultyRange range)
=> DifficultyRange(difficulty, range.Min, range.Mid, range.Max);
/// <summary>
/// Inverse function to <see cref="DifficultyRange(double,double,double,double)"/>.
@@ -110,5 +110,23 @@ namespace osu.Game.Beatmaps
? (difficultyValue - diff5) / (diff10 - diff5) * 5 + 5
: (difficultyValue - diff5) / (diff5 - diff0) * 5 + 5;
}
/// <summary>
/// Inverse function to <see cref="DifficultyRange(double,osu.Game.Beatmaps.DifficultyRange)"/>.
/// Maps a value returned by the function above back to the difficulty that produced it.
/// </summary>
/// <param name="difficultyValue">The difficulty-dependent value to be unmapped.</param>
/// <param name="range">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
static double InverseDifficultyRange(double difficultyValue, DifficultyRange range)
=> InverseDifficultyRange(difficultyValue, range.Min, range.Mid, range.Max);
}
/// <summary>
/// Represents a piecewise-linear difficulty curve for a given gameplay quantity.
/// </summary>
/// <param name="Min">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
/// <param name="Mid">Midpoint of the resulting range which will be achieved by a difficulty value of 5.</param>
/// <param name="Max">Maximum of the resulting range which will be achieved by a difficulty value of 10.</param>
public record struct DifficultyRange(double Min, double Mid, double Max);
}
+46 -1
View File
@@ -98,8 +98,9 @@ namespace osu.Game.Database
/// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯.
/// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions.
/// 48 2025-03-19 Clear online status for all qualified beatmaps (some were stuck in a qualified state due to local caching issues).
/// 49 2025-06-10 Reset the LegacyOnlineID to -1 for all scores that have it set to 0 (which is semantically the same) for consistency of handling with OnlineID.
/// </summary>
private const int schema_version = 48;
private const int schema_version = 49;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@@ -542,6 +543,44 @@ namespace osu.Game.Database
return writeTask;
}
/// <summary>
/// Write changes to realm asynchronously, guaranteeing order of execution.
/// </summary>
/// <param name="action">The work to run.</param>
public Task<T> WriteAsync<T>(Func<Realm, T> action)
{
ObjectDisposedException.ThrowIf(isDisposed, this);
// Required to ensure the write is tracked and accounted for before disposal.
// Can potentially be avoided if we have a need to do so in the future.
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread.");
// CountdownEvent will fail if already at zero.
if (!pendingAsyncWrites.TryAddCount())
pendingAsyncWrites.Reset(1);
// Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval.
// Adding a forced Task.Run resolves this.
var writeTask = Task.Run(async () =>
{
T result;
total_writes_async.Value++;
// Not attempting to use Realm.GetInstanceAsync as there's seemingly no benefit to us (for now) and it adds complexity due to locking
// concerns in getRealmInstance(). On a quick check, it looks to be more suited to cases where realm is connecting to an online sync
// server, which we don't use. May want to report upstream or revisit in the future.
using (var realm = getRealmInstance())
// ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]).
result = await realm.WriteAsync(() => action(realm)).ConfigureAwait(false);
pendingAsyncWrites.Signal();
return result;
});
return writeTask;
}
/// <summary>
/// Subscribe to a realm collection and begin watching for asynchronous changes.
/// </summary>
@@ -1255,6 +1294,12 @@ namespace osu.Game.Database
foreach (var beatmap in beatmaps)
beatmap.ResetOnlineInfo(resetOnlineId: false);
break;
case 49:
foreach (var score in migration.NewRealm.All<ScoreInfo>().Where(s => s.LegacyOnlineID == 0))
score.LegacyOnlineID = -1;
break;
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
@@ -14,6 +14,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Skinning;
using Realms;
namespace osu.Game.Database
@@ -177,6 +178,7 @@ namespace osu.Game.Database
c.CreateMap<RealmUser, RealmUser>();
c.CreateMap<RealmFile, RealmFile>();
c.CreateMap<RealmNamedFileUsage, RealmNamedFileUsage>();
c.CreateMap<SkinInfo, SkinInfo>();
}
/// <summary>
+101 -20
View File
@@ -233,6 +233,13 @@ namespace osu.Game.Graphics.Carousel
protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) =>
Scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item);
/// <summary>
/// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target.
/// </summary>
/// <param name="item">The candidate item.</param>
/// <returns>Whether the provided item is a valid group target. If <c>false</c>, more panels will be checked in the user's requested direction until a valid target is found.</returns>
protected virtual bool CheckValidForGroupSelection(CarouselItem item) => false;
/// <summary>
/// When a user is traversing the carousel via set selection keys, assert whether the item provided is a valid target.
/// </summary>
@@ -300,7 +307,7 @@ namespace osu.Game.Graphics.Carousel
/// <summary>
/// Retrieve a list of all <see cref="CarouselItem"/>s currently displayed.
/// </summary>
protected IReadOnlyCollection<CarouselItem>? GetCarouselItems() => carouselItems;
public IReadOnlyCollection<CarouselItem>? GetCarouselItems() => carouselItems;
private List<CarouselItem>? carouselItems;
@@ -451,27 +458,88 @@ namespace osu.Game.Graphics.Carousel
// if the selection is changed more than once during an update frame,
// which can happen if repeat inputs are enqueued for processing at a rate faster than the update refresh rate.
// `refreshAfterSelection()` is the method responsible for updating the index of the selected item here which runs once per frame.
case GlobalAction.SelectPrevious:
Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Keyboard, -1));
return true;
case GlobalAction.SelectNext:
Scheduler.AddOnce(traverseKeyboardSelection, 1);
return true;
case GlobalAction.SelectPrevious:
Scheduler.AddOnce(traverseKeyboardSelection, -1);
return true;
case GlobalAction.ActivateNextSet:
Scheduler.AddOnce(traverseSetSelection, 1);
Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Keyboard, 1));
return true;
case GlobalAction.ActivatePreviousSet:
Scheduler.AddOnce(traverseSetSelection, -1);
Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Set, -1));
return true;
case GlobalAction.ActivateNextSet:
Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Set, 1));
return true;
case GlobalAction.ExpandPreviousGroup:
Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Group, -1));
return true;
case GlobalAction.ExpandNextGroup:
Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Group, 1));
return true;
case GlobalAction.ToggleCurrentGroup:
if (carouselItems == null || carouselItems.Count == 0)
return true;
if (currentKeyboardSelection.CarouselItem == null || currentKeyboardSelection.Index == null)
return true;
if (CheckValidForGroupSelection(currentKeyboardSelection.CarouselItem))
{
// If keyboard selection is a group, toggle group and then change keyboard selection to actual selection.
Activate(currentKeyboardSelection.CarouselItem);
}
else
{
// If current keyboard selection is not a group, toggle the closest group and move keyboard selection to that group.
for (int i = currentKeyboardSelection.Index.Value; i >= 0; i--)
{
var newItem = carouselItems[i];
if (CheckValidForGroupSelection(newItem))
{
Activate(newItem);
return true;
}
}
}
return true;
}
return false;
void traverseFromKey(TraversalOperation traversal)
{
switch (traversal.Type)
{
case TraversalType.Keyboard:
traverseKeyboardSelection(traversal.Direction);
break;
case TraversalType.Set:
traverseSetSelection(traversal.Direction);
break;
case TraversalType.Group:
traverseGroupSelection(traversal.Direction);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
private enum TraversalType { Keyboard, Set, Group }
private record TraversalOperation(TraversalType Type, int Direction);
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
@@ -520,23 +588,38 @@ namespace osu.Game.Graphics.Carousel
}
/// <summary>
/// Select the next valid selection relative to a current selection.
/// Select the next valid group selection relative to a current selection.
/// This is generally for keyboard based traversal.
/// </summary>
/// <param name="direction">Positive for downwards, negative for upwards.</param>
/// <returns>Whether selection was possible.</returns>
private void traverseGroupSelection(int direction) => traverseSelection(direction, CheckValidForGroupSelection);
/// <summary>
/// Select the next valid set selection relative to a current selection.
/// This is generally for keyboard based traversal.
/// </summary>
/// <param name="direction">Positive for downwards, negative for upwards.</param>
/// <returns>Whether selection was possible.</returns>
private void traverseSetSelection(int direction)
{
if (carouselItems == null || carouselItems.Count == 0) return;
// If the user has a different keyboard selection and requests
// set selection, first transfer the keyboard selection to actual selection.
//
// It is assumed that selecting a set will immediately change selection to one of its children.
if (currentKeyboardSelection.CarouselItem != null && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
{
Activate(currentKeyboardSelection.CarouselItem);
return;
}
traverseSelection(direction, CheckValidForSetSelection);
}
private void traverseSelection(int direction, Func<CarouselItem, bool> predicate)
{
if (carouselItems == null || carouselItems.Count == 0) return;
int originalIndex;
int newIndex;
@@ -553,7 +636,7 @@ namespace osu.Game.Graphics.Carousel
// make sure to go back to the set header this item belongs to, so that the block below doesn't find it and stop too early.
if (direction < 0)
{
while (newIndex > 0 && !CheckValidForSetSelection(carouselItems[newIndex]))
while (newIndex > 0 && !predicate(carouselItems[newIndex]))
newIndex--;
}
}
@@ -569,9 +652,9 @@ namespace osu.Game.Graphics.Carousel
var newItem = carouselItems[newIndex];
if (CheckValidForSetSelection(newItem))
if (!newItem.IsExpanded && predicate(newItem))
{
HandleItemActivated(newItem);
Activate(newItem);
return;
}
} while (true);
@@ -732,9 +815,7 @@ namespace osu.Game.Graphics.Carousel
if (range != displayedRange)
{
Logger.Log($"Updating displayed range of carousel from {displayedRange} to {range}");
displayedRange = range;
updateDisplayedRange(range);
}
@@ -947,7 +1028,7 @@ namespace osu.Game.Graphics.Carousel
/// Implementation of scroll container which handles very large vertical lists by internally using <c>double</c> precision
/// for pre-display Y values.
/// </summary>
protected partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler<GlobalAction>
public partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler<GlobalAction>
{
public readonly Container Panels;
@@ -40,21 +40,24 @@ namespace osu.Game.Graphics.Containers
RelativeSizeAxes = Axes.Y;
Width = contractedWidth;
InternalChild = new OsuScrollContainer
InternalChild = CreateScrollContainer().With(s =>
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = FillFlow = new FillFlowContainer
s.RelativeSizeAxes = Axes.Both;
s.ScrollbarVisible = false;
}).WithChild(
FillFlow = new FillFlowContainer
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
},
};
}
);
}
protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer();
private ScheduledDelegate? hoverExpandEvent;
protected override void LoadComplete()
@@ -18,6 +18,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@@ -440,6 +441,11 @@ namespace osu.Game.Graphics.UserInterface
private partial class DropdownSearchTextBox : OsuTextBox
{
public DropdownSearchTextBox()
{
PlaceholderText = HomeStrings.SearchPlaceholder;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider? colourProvider)
{
@@ -179,7 +179,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnMouseDown(MouseDownEvent e)
{
Content.ScaleTo(0.9f, 2000, Easing.OutQuint);
return base.OnMouseDown(e);
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
@@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void load(OverlayColourProvider overlayColourProvider)
{
DefaultBackgroundColour = overlayColourProvider.Colour3;
triangleGradientSecondColour ??= overlayColourProvider.Colour1;
triangleGradientSecondColour ??= DefaultBackgroundColour.Lighten(0.2f);
if (Text == default)
{
@@ -54,7 +54,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
// Many buttons have local colours, but this provides a sane default for all other cases.
DefaultBackgroundColour = overlayColourProvider?.Colour3 ?? colours.Blue3;
triangleGradientSecondColour ??= overlayColourProvider?.Colour1 ?? colours.Blue3.Lighten(0.2f);
triangleGradientSecondColour ??= DefaultBackgroundColour.Lighten(0.2f);
}
protected override void LoadComplete()
@@ -199,6 +199,11 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.Left, GlobalAction.ActivatePreviousSet),
new KeyBinding(InputKey.Right, GlobalAction.ActivateNextSet),
new KeyBinding(new[] { InputKey.Shift, InputKey.Left }, GlobalAction.ExpandPreviousGroup),
new KeyBinding(new[] { InputKey.Shift, InputKey.Right }, GlobalAction.ExpandNextGroup),
new KeyBinding(new[] { InputKey.Shift, InputKey.Enter }, GlobalAction.ToggleCurrentGroup),
new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection),
new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom),
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom),
@@ -506,6 +511,15 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges))]
EditorDiscardUnsavedChanges,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExpandPreviousGroup))]
ExpandPreviousGroup,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExpandNextGroup))]
ExpandNextGroup,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleCurrentGroup))]
ToggleCurrentGroup,
}
public enum GlobalActionCategory
@@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public class BeatmapStatisticStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.BeatmapStatisticStrings";
/// <summary>
/// "Circles"
/// </summary>
public static LocalisableString Circles => new TranslatableString(getKey(@"circles"), @"Circles");
/// <summary>
/// "Sliders"
/// </summary>
public static LocalisableString Sliders => new TranslatableString(getKey(@"sliders"), @"Sliders");
/// <summary>
/// "Spinners"
/// </summary>
public static LocalisableString Spinners => new TranslatableString(getKey(@"spinners"), @"Spinners");
/// <summary>
/// "Hits"
/// </summary>
public static LocalisableString Hits => new TranslatableString(getKey(@"hits"), @"Hits");
/// <summary>
/// "Drumrolls"
/// </summary>
public static LocalisableString Drumrolls => new TranslatableString(getKey(@"drumrolls"), @"Drumrolls");
/// <summary>
/// "Swells"
/// </summary>
public static LocalisableString Swells => new TranslatableString(getKey(@"swells"), @"Swells");
/// <summary>
/// "Fruits"
/// </summary>
public static LocalisableString Fruits => new TranslatableString(getKey(@"fruits"), @"Fruits");
/// <summary>
/// "Juice Streams"
/// </summary>
public static LocalisableString JuiceStreams => new TranslatableString(getKey(@"juice_streams"), @"Juice Streams");
/// <summary>
/// "Banana Showers"
/// </summary>
public static LocalisableString BananaShowers => new TranslatableString(getKey(@"banana_showers"), @"Banana Showers");
/// <summary>
/// "Notes"
/// </summary>
public static LocalisableString Notes => new TranslatableString(getKey(@"notes"), @"Notes");
/// <summary>
/// "Hold Notes"
/// </summary>
public static LocalisableString HoldNotes => new TranslatableString(getKey(@"hold_notes"), @"Hold Notes");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
+5
View File
@@ -154,6 +154,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString TimelineShowTimingChanges => new TranslatableString(getKey(@"timeline_show_timing_changes"), @"Show timing changes");
/// <summary>
/// "Finish editing and import changes"
/// </summary>
public static LocalisableString FinishEditingExternally => new TranslatableString(getKey(@"Finish editing and import changes"), @"Finish editing and import changes");
/// <summary>
/// "Show breaks"
/// </summary>
@@ -79,23 +79,26 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString LearnMoreAboutLazerTooltip => new TranslatableString(getKey(@"check_out_the_feature_comparison"), @"Check out the feature comparison and FAQ");
/// <summary>
/// "Check with your package manager / provider for other release streams."
/// </summary>
public static LocalisableString ChangeReleaseStreamPackageManagerWarning => new TranslatableString(getKey(@"change_release_stream_package_warning"), @"Check with your package manager / provider for other release streams.");
/// <summary>
/// "Are you sure you want to run a potentially unstable version of the game?"
/// </summary>
public static LocalisableString ChangeReleaseStreamConfirmation => new TranslatableString(getKey(@"change_release stream_confirmation"),
@"Are you sure you want to run a potentially unstable version of the game?");
public static LocalisableString ChangeReleaseStreamConfirmation => new TranslatableString(getKey(@"change_release_stream_confirmation"), @"Are you sure you want to run a potentially unstable version of the game?");
/// <summary>
/// "If you run into issues starting the game, you can usually run the installer from the official site to recover."
/// </summary>
public static LocalisableString ChangeReleaseStreamConfirmationInfo => new TranslatableString(getKey(@"change_release stream_confirmation_info"),
@"If you run into issues starting the game, you can usually run the installer from the official site to recover.");
public static LocalisableString ChangeReleaseStreamConfirmationInfo => new TranslatableString(getKey(@"change_release_stream_confirmation_info"), @"If you run into issues starting the game, you can usually run the installer from the official site to recover.");
/// <summary>
/// "You are running the latest release ({0})"
/// </summary>
public static LocalisableString RunningLatestRelease(string version) => new TranslatableString(getKey(@"running_latest_release"), @"You are running the latest release ({0})", version);
private static string getKey(string key) => $"{prefix}:{key}";
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -139,6 +139,21 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ActivateNextSet => new TranslatableString(getKey(@"activate_next_set"), @"Activate next set");
/// <summary>
/// "Expand previous group"
/// </summary>
public static LocalisableString ExpandPreviousGroup => new TranslatableString(getKey(@"expand_previous_group"), @"Expand previous group");
/// <summary>
/// "Expand next group"
/// </summary>
public static LocalisableString ExpandNextGroup => new TranslatableString(getKey(@"expand_next_group"), @"Expand next group");
/// <summary>
/// "Toggle expansion of current group"
/// </summary>
public static LocalisableString ToggleCurrentGroup => new TranslatableString(getKey(@"toggle_current_group"), @"Toggle expansion of current group");
/// <summary>
/// "Home"
/// </summary>
@@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString NoPPForUnrankedMods => new TranslatableString(getKey(@"no_pp_for_unranked_mods"), @"Performance points are not granted for this score because of unranked mods.");
/// <summary>
/// "Performance points are not granted for failed scores."
/// </summary>
public static LocalisableString NoPPForFailedScores => new TranslatableString(getKey(@"no_pp_for_failed_scores"), @"Performance points are not granted for failed scores.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -14,11 +14,6 @@ namespace osu.Game.Localisation.SkinComponents
/// </summary>
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute");
/// <summary>
/// "The attribute to be displayed."
/// </summary>
public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), @"The attribute to be displayed.");
/// <summary>
/// "Template"
/// </summary>
@@ -14,31 +14,16 @@ namespace osu.Game.Localisation.SkinComponents
/// </summary>
public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), @"Sprite name");
/// <summary>
/// "The filename of the sprite"
/// </summary>
public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), @"The filename of the sprite");
/// <summary>
/// "Font"
/// </summary>
public static LocalisableString Font => new TranslatableString(getKey(@"font"), @"Font");
/// <summary>
/// "The font to use."
/// </summary>
public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), @"The font to use.");
/// <summary>
/// "Text"
/// </summary>
public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), @"Text");
/// <summary>
/// "The text to be displayed."
/// </summary>
public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), @"The text to be displayed.");
/// <summary>
/// "Corner radius"
/// </summary>
@@ -54,30 +39,20 @@ namespace osu.Game.Localisation.SkinComponents
/// </summary>
public static LocalisableString ShowLabel => new TranslatableString(getKey(@"show_label"), @"Show label");
/// <summary>
/// "Whether the component&#39;s label should be shown."
/// </summary>
public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown.");
/// <summary>
/// "Colour"
/// </summary>
public static LocalisableString Colour => new TranslatableString(getKey(@"colour"), @"Colour");
/// <summary>
/// "The colour of the component."
/// </summary>
public static LocalisableString ColourDescription => new TranslatableString(getKey(@"colour_description"), @"The colour of the component.");
/// <summary>
/// "Text colour"
/// </summary>
public static LocalisableString TextColour => new TranslatableString(getKey(@"text_colour"), @"Text colour");
/// <summary>
/// "The colour of the text."
/// "Text weight"
/// </summary>
public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text.");
public static LocalisableString TextWeight => new TranslatableString(getKey(@"text_weight"), @"Text weight");
/// <summary>
/// "Use relative size"
@@ -59,6 +59,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Stars(LocalisableString value) => new TranslatableString(getKey(@"stars"), @"{0} stars", value);
/// <summary>
/// "Submitted"
/// </summary>
public static LocalisableString Submitted => new TranslatableString(getKey(@"submitted"), @"Submitted");
/// <summary>
/// "Ranked"
/// </summary>
public static LocalisableString Ranked => new TranslatableString(getKey(@"ranked"), @"Ranked");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -80,11 +80,22 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("artist_unicode")]
public string ArtistUnicode { get; set; } = string.Empty;
/// <summary>
/// The creator of this beatmap set.
/// </summary>
/// <remarks>
/// This is not included when the set is retrieved via <see cref="SearchBeatmapSetsRequest"/>,
/// but the creator's ID and username will be filled in this property from the <see cref="AuthorID"/> and <see cref="AuthorString"/> properties.
/// </remarks>
[JsonProperty(@"user")]
public APIUser Author = new APIUser();
/// <summary>
/// Helper property to deserialize a username to <see cref="APIUser"/>.
/// The ID of the beatmap set's creator.
/// </summary>
/// <remarks>
/// Helper property to deserialize the ID to <see cref="Author"/>.
/// </remarks>
[JsonProperty(@"user_id")]
public int AuthorID
{
@@ -93,8 +104,11 @@ namespace osu.Game.Online.API.Requests.Responses
}
/// <summary>
/// Helper property to deserialize a username to <see cref="APIUser"/>.
/// The username of the beatmap set's creator.
/// </summary>
/// <remarks>
/// Helper property to deserialize the username to <see cref="Author"/>.
/// </remarks>
[JsonProperty(@"creator")]
public string AuthorString
{
+21
View File
@@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Online.API;
namespace osu.Game.Online.Rooms
{
public static class RoomExtensions
{
/// <summary>
/// Get the room page URL, or <c>null</c> if unavailable.
/// </summary>
public static string? GetOnlineURL(this Room room, IAPIProvider api)
{
if (!room.RoomID.HasValue)
return null;
return $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{room.RoomID.Value}";
}
}
}

Some files were not shown because too many files have changed in this diff Show More