1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-04 06:23:37 +08:00

Compare commits

...

579 Commits

381 changed files with 8592 additions and 5356 deletions
+1 -1
View File
@@ -50,7 +50,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright>
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2024 ppy Pty Ltd <contact@ppy.sh>.
Copyright (c) 2025 ppy Pty Ltd <contact@ppy.sh>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
using osuTK;
@@ -17,5 +18,8 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
if (button.HasValue)
Actions.Add(button.Value);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is EmptyFreeformReplayFrame freeformFrame && Time == freeformFrame.Time && Position == freeformFrame.Position && Actions.SequenceEqual(freeformFrame.Actions);
}
}
@@ -9,5 +9,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
public class PippidonReplayFrame : ReplayFrame
{
public Vector2 Position;
public override bool IsEquivalentTo(ReplayFrame other)
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Position == pippidonFrame.Position;
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.EmptyScrolling.Replays
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
if (button.HasValue)
Actions.Add(button.Value);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is EmptyScrollingReplayFrame scrollingFrame && Time == scrollingFrame.Time && Actions.SequenceEqual(scrollingFrame.Actions);
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Pippidon.Replays
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
if (button.HasValue)
Actions.Add(button.Value);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Actions.SequenceEqual(pippidonFrame.Actions);
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
<PackageTags>dotnet-new;templates;osu</PackageTags>
<TargetFramework>netstandard2.1</TargetFramework>
+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.718.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+11 -37
View File
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Android.App;
using Android.Content.PM;
using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Game;
@@ -21,58 +23,30 @@ namespace osu.Android
[Cached]
private readonly OsuGameActivity gameActivity;
private readonly PackageInfo packageInfo;
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
public OsuGameAndroid(OsuGameActivity activity)
: base(null)
{
gameActivity = activity;
packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
}
public override Version AssemblyVersion
public override string Version
{
get
{
var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
if (!IsDeployedBuild)
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
try
{
// We store the osu! build number in the "VersionCode" field to better support google play releases.
// If we were to use the main build number, it would require a new submission each time (similar to TestFlight).
// In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time.
//
// We also need to be aware that older SDK versions store this as a 32bit int.
//
// Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060
// https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
string versionName;
if (OperatingSystem.IsAndroidVersionAtLeast(28))
{
versionName = packageInfo.LongVersionCode.ToString();
// ensure we only read the trailing portion of long (the part we are interested in).
versionName = versionName.Substring(versionName.Length - 9);
}
else
{
#pragma warning disable CS0618 // Type or member is obsolete
// this is required else older SDKs will report missing method exception.
versionName = packageInfo.VersionCode.ToString();
#pragma warning restore CS0618 // Type or member is obsolete
}
// undo play store version garbling (as mentioned above).
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
catch
{
}
return new Version(packageInfo.VersionName.AsNonNull());
return packageInfo.VersionName.AsNonNull();
}
}
public override Version AssemblyVersion => new Version(packageInfo.VersionName.AsNonNull().Split('-').First());
protected override void LoadComplete()
{
base.LoadComplete();
+1 -1
View File
@@ -123,7 +123,7 @@ namespace osu.Desktop
public override bool RestartAppWhenExited()
{
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
return true;
}
+4 -4
View File
@@ -184,7 +184,7 @@ namespace osu.Desktop
var app = VelopackApp.Build();
app.WithFirstRun(_ => isFirstRun = true);
app.OnFirstRun(_ => isFirstRun = true);
if (OperatingSystem.IsWindows())
configureWindows(app);
@@ -195,9 +195,9 @@ namespace osu.Desktop
[SupportedOSPlatform("windows")]
private static void configureWindows(VelopackApp app)
{
app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations());
app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations());
app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
}
}
}
+88 -100
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,130 @@ 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();
}
private void onReleaseStreamChanged()
{
updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, releaseStream.Value == ReleaseStream.Tachyon), new UpdateOptions
scheduledBackgroundCheck?.Cancel();
scheduledBackgroundCheck = Scheduler.AddDelayed(() =>
{
AllowVersionDowngrade = true,
});
Schedule(() => Task.Run(CheckForUpdateAsync));
log("Running scheduled background update check...");
CheckForUpdate();
}, 60000 * 30);
}
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task<bool> checkForUpdateAsync()
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
{
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
bool scheduleRecheck = false;
scheduledBackgroundCheck?.Cancel();
if (isInGameplay)
{
log("Update check cancelled - user is in gameplay");
scheduleNextUpdateCheck();
return false;
}
try
{
// Avoid any kind of update checking while gameplay is running.
if (isInGameplay)
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
{
scheduleRecheck = true;
AllowVersionDowngrade = true
});
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
{
log("Update check cancelled");
scheduleNextUpdateCheck();
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 (update == 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;
// No update is available.
log("No update found");
scheduleNextUpdateCheck();
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!");
}
// 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;
}
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})");
log($"Update check failed with error ({e.Message})");
// we shouldn't crash on a web failure. or any failure for the matter.
scheduleNextUpdateCheck();
return true;
}
finally
}
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)
{
if (scheduleRecheck)
CompletionClickAction = () =>
{
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
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)
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}");
}
}
+1 -1
View File
@@ -26,7 +26,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="9.0.2" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.1053" />
<PackageReference Include="Velopack" Version="0.0.1298" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />
+1 -1
View File
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<language>en-AU</language>
</metadata>
<files>
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModHalfTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
}
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModDoubleTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
}
@@ -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 -1
View File
@@ -33,6 +33,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Catch
@@ -265,9 +266,10 @@ namespace osu.Game.Rulesets.Catch
}
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
preempt /= rate;
@@ -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);
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
@@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!";
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
}
}
}
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -64,5 +65,12 @@ namespace osu.Game.Rulesets.Catch.Replays
return new LegacyReplayFrame(Time, Position, null, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is CatchReplayFrame catchFrame
&& Time == catchFrame.Time
&& Position == catchFrame.Position
&& Dashing == catchFrame.Dashing
&& Actions.SequenceEqual(catchFrame.Actions);
}
}
@@ -5,7 +5,6 @@ using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
@@ -38,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ManiaModScoreV2) } },
};
[TestCaseSource(nameof(mania_mod_mapping))]
@@ -9,7 +9,6 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -19,7 +18,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 +70,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 +98,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 +145,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 +173,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 +209,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 +256,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 +284,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 +332,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 +362,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 +409,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 +448,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 +487,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;
@@ -510,7 +520,7 @@ namespace osu.Game.Rulesets.Mania.Tests
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ModScoreV2()]
Mods = [new ManiaModScoreV2()]
}
};
@@ -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,
+28 -2
View File
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania
yield return new ManiaModMirror();
if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2();
yield return new ManiaModScoreV2();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@@ -296,7 +296,7 @@ namespace osu.Game.Rulesets.Mania
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
new ManiaModScoreV2(),
};
default:
@@ -414,6 +414,32 @@ namespace osu.Game.Rulesets.Mania
}), true)
};
/// <seealso cref="ManiaHitWindows"/>
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
// notably, in mania, hit windows are designed to be independent of track playback rate (see `ManiaHitWindows.SpeedMultiplier`).
// *however*, to not make matters *too* simple, mania Hard Rock and Easy differ from all other rulesets
// in that they apply multipliers *to hit window durations directly* rather than to the Overall Difficulty attribute itself.
// because the duration of hit window durations as a function of OD is not a linear function,
// this means that multiplying the OD is *not* the same thing as multiplying the hit window duration.
// in fact, the second operation is *much* harsher and will produce values much farther outside of normal operating range
// (even negative in the case of Easy).
// stable handles this wrong on song select and just assumes that it can handle mania EZ / HR the same way as all other rulesets.
double perfectHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, ManiaHitWindows.PERFECT_WINDOW_RANGE);
if (mods.Any(m => m is ManiaModHardRock))
perfectHitWindow /= ManiaModHardRock.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
else if (mods.Any(m => m is ManiaModEasy))
perfectHitWindow /= ManiaModEasy.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(perfectHitWindow, ManiaHitWindows.PERFECT_WINDOW_RANGE);
return adjustedDifficulty;
}
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
{
return new ManiaFilterCriteria();
@@ -2,12 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -17,29 +15,21 @@ namespace osu.Game.Rulesets.Mania.Mods
/// <remarks>
/// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same.
/// </remarks>
public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject
public interface IManiaRateAdjustmentMod : IApplicableToHitObject
{
BindableNumber<double> SpeedChange { get; }
HitWindows HitWindows { get; set; }
void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty)
{
HitWindows = new ManiaHitWindows(SpeedChange.Value);
HitWindows.SetDifficulty(difficulty.OverallDifficulty);
}
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Note:
hitObject.HitWindows = HitWindows;
((ManiaHitWindows)hitObject.HitWindows).SpeedMultiplier = SpeedChange.Value;
break;
case HoldNote hold:
hold.Head.HitWindows = HitWindows;
hold.Tail.HitWindows = HitWindows;
((ManiaHitWindows)hold.Head.HitWindows).SpeedMultiplier = SpeedChange.Value;
((ManiaHitWindows)hold.Tail.HitWindows).SpeedMultiplier = SpeedChange.Value;
break;
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => Name;
public abstract int KeyCount { get; }
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
public override double ScoreMultiplier => 0.9;
public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
@@ -1,11 +1,41 @@
// 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.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModClassic : ModClassic
public class ManiaModClassic : ModClassic, IApplicableToBeatmap
{
public void ApplyToBeatmap(IBeatmap beatmap)
{
bool isConvert = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
foreach (var ho in beatmap.HitObjects)
{
switch (ho)
{
case Note note:
{
var hitWindows = (ManiaHitWindows)note.HitWindows;
hitWindows.IsConvert = isConvert;
hitWindows.ClassicModActive = true;
break;
}
case HoldNote hold:
{
var headWindows = (ManiaHitWindows)hold.Head.HitWindows;
var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows;
headWindows.IsConvert = tailWindows.IsConvert = isConvert;
headWindows.ClassicModActive = tailWindows.ClassicModActive = true;
break;
}
}
}
}
}
}
@@ -1,14 +1,11 @@
// 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.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
}
}
@@ -1,16 +1,12 @@
// 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.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
+21 -1
View File
@@ -2,12 +2,32 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModEasy : ModEasyWithExtraLives
public class ManiaModEasy : ModEasyWithExtraLives, IApplicableToHitObject
{
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1 / 1.4;
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
}
}
}
}
@@ -1,14 +1,11 @@
// 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.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
}
}
@@ -1,13 +1,33 @@
// 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.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHardRock : ModHardRock
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
{
public override double ScoreMultiplier => 1;
public override bool Ranked => false;
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
}
}
}
}
@@ -2,16 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map any harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
@@ -0,0 +1,37 @@
// 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.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModScoreV2 : ModScoreV2, IApplicableToBeatmap
{
public void ApplyToBeatmap(IBeatmap beatmap)
{
foreach (var ho in beatmap.HitObjects)
{
switch (ho)
{
case Note note:
{
var hitWindows = (ManiaHitWindows)note.HitWindows;
hitWindows.ScoreV2Active = true;
break;
}
case HoldNote hold:
{
var headWindows = (ManiaHitWindows)hold.Head.HitWindows;
var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows;
headWindows.ScoreV2Active = tailWindows.ScoreV2Active = true;
break;
}
}
}
}
}
}
@@ -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();
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -47,5 +48,8 @@ namespace osu.Game.Rulesets.Mania.Replays
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is ManiaReplayFrame maniaFrame && Time == maniaFrame.Time && Actions.SequenceEqual(maniaFrame.Actions);
}
}
@@ -1,25 +1,107 @@
// 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 readonly double multiplier;
public 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);
public ManiaHitWindows()
: this(1)
private double speedMultiplier = 1;
/// <summary>
/// Multiplier used to compensate for the playback speed of the track speeding up or slowing down.
/// The goal of this multiplier is to keep hit windows independent of track speed.
/// <list type="bullet">
/// <item>When the track speed is above 1, the hit window ranges are multiplied by <see cref="SpeedMultiplier"/>, because the time elapses faster.</item>
/// <item>When the track speed is below 1, the hit window ranges are also multiplied by <see cref="SpeedMultiplier"/>, because the time elapses slower.</item>
/// </list>
/// </summary>
public double SpeedMultiplier
{
get => speedMultiplier;
set
{
speedMultiplier = value;
updateWindows();
}
}
public ManiaHitWindows(double multiplier)
private double difficultyMultiplier = 1;
/// <summary>
/// Multiplier used to make the gameplay more or less difficult.
/// <list type="bullet">
/// <item>When the <see cref="DifficultyMultiplier"/> is above 1, the hit windows decrease to make the gameplay harder.</item>
/// <item>When the <see cref="DifficultyMultiplier"/> is below 1, the hit windows increase to make the gameplay easier.</item>
/// </list>
/// </summary>
public double DifficultyMultiplier
{
this.multiplier = multiplier;
get => difficultyMultiplier;
set
{
difficultyMultiplier = value;
updateWindows();
}
}
private double totalMultiplier => speedMultiplier / difficultyMultiplier;
private double overallDifficulty;
private bool classicModActive;
public bool ClassicModActive
{
get => classicModActive;
set
{
classicModActive = value;
updateWindows();
}
}
private bool scoreV2Active;
public bool ScoreV2Active
{
get => scoreV2Active;
set
{
scoreV2Active = value;
updateWindows();
}
}
private bool isConvert;
public bool IsConvert
{
get => isConvert;
set
{
isConvert = value;
updateWindows();
}
}
private double perfect;
private double great;
private double good;
private double ok;
private double meh;
private double miss;
public override bool IsHitResultAllowed(HitResult result)
{
switch (result)
@@ -36,11 +118,73 @@ 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)
{
overallDifficulty = difficulty;
updateWindows();
}
private void updateWindows()
{
if (ClassicModActive && !ScoreV2Active)
{
if (IsConvert)
{
perfect = Math.Floor(16 * totalMultiplier) + 0.5;
great = Math.Floor((Math.Round(overallDifficulty) > 4 ? 34 : 47) * totalMultiplier) + 0.5;
good = Math.Floor((Math.Round(overallDifficulty) > 4 ? 67 : 77) * totalMultiplier) + 0.5;
ok = Math.Floor(97 * totalMultiplier) + 0.5;
meh = Math.Floor(121 * totalMultiplier) + 0.5;
miss = Math.Floor(158 * totalMultiplier) + 0.5;
}
else
{
double invertedOd = Math.Clamp(10 - overallDifficulty, 0, 10);
perfect = Math.Floor(16 * totalMultiplier) + 0.5;
great = Math.Floor((34 + 3 * invertedOd) * totalMultiplier) + 0.5;
good = Math.Floor((67 + 3 * invertedOd) * totalMultiplier) + 0.5;
ok = Math.Floor((97 + 3 * invertedOd) * totalMultiplier) + 0.5;
meh = Math.Floor((121 + 3 * invertedOd) * totalMultiplier) + 0.5;
miss = Math.Floor((158 + 3 * invertedOd) * totalMultiplier) + 0.5;
}
}
else
{
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, PERFECT_WINDOW_RANGE) * totalMultiplier) + 0.5;
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, great_window_range) * totalMultiplier) + 0.5;
good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, good_window_range) * totalMultiplier) + 0.5;
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, ok_window_range) * totalMultiplier) + 0.5;
meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, meh_window_range) * totalMultiplier) + 0.5;
miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, miss_window_range) * totalMultiplier) + 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);
}
}
}
}
@@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
@@ -36,22 +35,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test]
public void TestPlayfieldBasedSize()
{
ModFlashlight mod = new OsuModFlashlight();
OsuModFlashlight flashlight;
CreateModTest(new ModTestData
{
Mod = mod,
Mods = [flashlight = new OsuModFlashlight(), new OsuModBarrelRoll()],
PassCondition = () =>
{
var flashlightOverlay = Player.DrawableRuleset.Overlays
.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>()
.First();
return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize());
// the combo check is here because the flashlight radius decreases for the first time at 100 combo
// and hardcoding it here eliminates the need to meddle in flashlight internals further by e.g. exposing `GetComboScaleFor()`
return flashlightOverlay.GetSize() < flashlight.DefaultFlashlightSize && Player.GameplayState.ScoreProcessor.Combo.Value < 100;
}
});
AddStep("adjust playfield scale", () =>
Player.DrawableRuleset.Playfield.Scale = new Vector2(.5f));
}
[Test]
@@ -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
});
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new OsuModHalfTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
@@ -56,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new OsuModDoubleTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01));
@@ -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")]
@@ -78,6 +78,16 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke])));
}
[Test]
public void TestPressAndReleaseOnSameFrame()
{
seekTo(0);
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
AddStep("press X", () => InputManager.PressKey(Key.X));
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton])));
}
private void seekTo(double time)
{
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
@@ -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[]
@@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests;
using osu.Game.Tests.Visual;
using osuTK;
@@ -152,6 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}
[Test]
[FlakyTest]
public void TestSpinPerMinuteOnRewind()
{
double estimatedSpm = 0;
@@ -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),
@@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
overallDifficulty = (80 - greatHitWindow) / 6;
overallDifficulty = (79.5 - greatHitWindow) / 6;
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
if (osuAttributes.SliderCount > 0)
@@ -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);
+8
View File
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
@@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!";
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
}
}
}
@@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
}
@@ -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";
@@ -12,9 +12,9 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@@ -83,7 +83,12 @@ namespace osu.Game.Rulesets.Osu.Mods
{
}
public override Judgement CreateJudgement() => new OsuJudgement();
public override Judgement CreateJudgement() => new StrictTrackingTailJudgement();
}
public class StrictTrackingTailJudgement : SliderTailCircle.TailJudgement
{
public override HitResult MinResult => HitResult.LargeTickMiss;
}
private partial class StrictTrackingDrawableSliderTail : DrawableSliderTail
@@ -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
{
+5 -4
View File
@@ -40,6 +40,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu
@@ -365,18 +366,18 @@ namespace osu.Game.Rulesets.Osu
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
/// <seealso cref="OsuHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
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;
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -47,5 +48,8 @@ namespace osu.Game.Rulesets.Osu.Replays
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is OsuReplayFrame osuFrame && Time == osuFrame.Time && Position == osuFrame.Position && Actions.SequenceEqual(osuFrame.Actions);
}
}
+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);
@@ -148,5 +148,96 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1
});
/// <summary>
/// Regression tests a case of 1/3rd conversion where there are exactly div-3 number of hitobjects.
/// </summary>
[Test]
public void TestOnlyOneThirdConversion()
{
CreateModTest(new ModTestData
{
Mod = new TaikoModSimplifiedRhythm
{
OneThirdConversion = { Value = true },
},
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1333, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 1666, Type = HitType.Centre }, // mod moves this to 1500
new Hit { StartTime = 2000, Type = HitType.Centre },
new Hit { StartTime = 2333, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 2666, Type = HitType.Centre }, // mod moves this to 2500
},
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1200),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1700),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2200),
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
new TaikoReplayFrame(2700),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 4 && Player.ScoreProcessor.Accuracy.Value == 1
});
}
/// <summary>
/// Regression tests a case of 1/6th conversion where there are exactly div-6 number of hitobjects.
/// </summary>
[Test]
public void TestOnlyOneSixthConversion() => CreateModTest(new ModTestData
{
Mod = new TaikoModSimplifiedRhythm
{
OneSixthConversion = { Value = true }
},
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1166, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 1333, Type = HitType.Centre }, // mod moves this to 1250
new Hit { StartTime = 1500, Type = HitType.Centre },
new Hit { StartTime = 1666, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 1833, Type = HitType.Centre }, // mod moves this to 1750
new Hit { StartTime = 2000, Type = HitType.Centre },
new Hit { StartTime = 2166, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 2333, Type = HitType.Centre }, // mod moves this to 2250
new Hit { StartTime = 2500, Type = HitType.Centre },
new Hit { StartTime = 2666, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 2833, Type = HitType.Centre }, // mod moves this to 2750
},
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1200),
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
new TaikoReplayFrame(1450),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1600),
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
new TaikoReplayFrame(1800),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2200),
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
new TaikoReplayFrame(2450),
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
new TaikoReplayFrame(2600),
new TaikoReplayFrame(2750, TaikoAction.LeftCentre),
new TaikoReplayFrame(2800),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1
});
}
}
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Mods;
namespace osu.Game.Rulesets.Taiko.Tests
{
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModHalfTime()]);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01));
}
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModDoubleTime()]);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01));
}
@@ -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)}";
}
}
@@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
difficulty.SliderMultiplier *= slider_multiplier;
}
}
@@ -1,6 +1,7 @@
// 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.Mods;
@@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.SliderMultiplier *= slider_multiplier;
}
}
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
var taikoBeatmap = (TaikoBeatmap)beatmap;
var controlPointInfo = taikoBeatmap.ControlPointInfo;
Hit[] hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast<Hit>().ToArray();
Hit[] hits = taikoBeatmap.HitObjects.OfType<Hit>().ToArray();
if (hits.Length == 0)
return;
@@ -61,10 +61,10 @@ namespace osu.Game.Rulesets.Taiko.Mods
if (inPattern)
{
// pattern continues
if (snapValue == baseRhythm) continue;
if (snapValue == baseRhythm)
continue;
inPattern = false;
processPattern(i);
}
else
@@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
if (indexInPattern % 3 == 1)
taikoBeatmap.HitObjects.Remove(hits[j]);
else if (indexInPattern % 3 == 2)
hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm;
hits[j].StartTime = hits[j - 2].StartTime + controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm;
break;
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -42,5 +43,8 @@ namespace osu.Game.Rulesets.Taiko.Replays
return new LegacyReplayFrame(Time, null, null, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TaikoReplayFrame taikoFrame && Time == taikoFrame.Time && Actions.SequenceEqual(taikoFrame.Actions);
}
}
@@ -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);
}
}
}
}
+5 -4
View File
@@ -38,6 +38,7 @@ using osu.Game.Rulesets.Taiko.Configuration;
using osu.Game.Rulesets.Taiko.Edit.Setup;
using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Taiko
{
@@ -270,14 +271,14 @@ namespace osu.Game.Rulesets.Taiko
}
/// <seealso cref="TaikoHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double rate = ModUtils.CalculateRateWithMods(mods);
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);
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using ManagedBass;
using Moq;
using NUnit.Framework;
using osu.Framework.Audio.Track;
@@ -10,7 +11,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using osuTK.Audio;
namespace osu.Game.Tests.Editing.Checks
{
@@ -28,9 +31,13 @@ namespace osu.Game.Tests.Editing.Checks
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }
Metadata = new BeatmapMetadata()
}
};
// 0 = No output device. This still allows decoding.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
throw new AudioException("Could not initialize Bass.");
}
[Test]
@@ -54,6 +61,14 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestAcceptableOgg()
{
var context = getContext(208, useOgg: true);
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestNullBitrate()
{
@@ -87,6 +102,17 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
}
[Test]
public void TestTooHighBitrateOgg()
{
var context = getContext(250, useOgg: true);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
}
[Test]
public void TestTooLowBitrate()
{
@@ -98,24 +124,41 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate);
}
private BeatmapVerifierContext getContext(int? audioBitrate)
private BeatmapVerifierContext getContext(int? audioBitrate, bool useOgg = false)
{
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate).Object);
// Update the audio filename and beatmapset files based on the format being tested
string audioFileName = useOgg ? "abc123.ogg" : "abc123.mp3";
string fileExtension = useOgg ? "ogg" : "mp3";
beatmap.Metadata.AudioFile = audioFileName;
beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo
{
Files = { CheckTestHelpers.CreateMockFile(fileExtension) }
};
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate, useOgg).Object);
}
/// <summary>
/// Returns the mock of the working beatmap with the given audio properties.
/// </summary>
/// <param name="audioBitrate">The bitrate of the audio file the beatmap uses.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate)
/// <param name="useOgg">Whether to use an OGG sample instead of MP3.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate, bool useOgg = false)
{
var mockTrack = new Mock<OsuTestScene.ClockBackedTestWorkingBeatmap.TrackVirtualManual>(new FramedClock(), "virtual");
mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate);
// Use real audio samples for format detection
string samplePath = useOgg ? "Samples/test-sample.ogg" : "Samples/test-sample-cut.mp3";
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack.Object);
// Return a fresh stream each time GetStream is called to avoid disposed stream issues
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(() => TestResources.OpenResource(samplePath));
return mockWorkingBeatmap;
}
}
@@ -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;
}
+40 -6
View File
@@ -259,9 +259,6 @@ namespace osu.Game.Tests.Mods
new MultiplayerTestScenario(true, true, [new OsuModPerfect()], []),
new MultiplayerTestScenario(true, true, [new OsuModDoubleTime()], []),
new MultiplayerTestScenario(true, true, [new OsuModNightcore()], []),
new MultiplayerTestScenario(true, true, [new OsuModHidden()], []),
new MultiplayerTestScenario(true, true, [new OsuModFlashlight()], []),
new MultiplayerTestScenario(true, true, [new OsuModAccuracyChallenge()], []),
new MultiplayerTestScenario(true, true, [new OsuModDifficultyAdjust()], []),
new MultiplayerTestScenario(true, true, [new ModWindUp()], []),
new MultiplayerTestScenario(true, true, [new ModWindDown()], []),
@@ -345,10 +342,47 @@ namespace osu.Game.Tests.Mods
{
foreach (var mod in ruleset.CreateAllMods())
{
if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && !commonAcronyms.Contains(mod.Acronym))
if (mod.ValidForFreestyleAsRequiredMod && !mod.UserPlayable)
Assert.Fail($"Mod {mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but is not playable!");
if (mod.ValidForFreestyleAsRequiredMod && !mod.HasImplementation)
Assert.Fail($"Mod {mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but is not implemented!");
if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && mod.HasImplementation && !commonAcronyms.Contains(mod.Acronym))
Assert.Fail($"{mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but does not exist in all four basic rulesets!");
if (!mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && commonAcronyms.Contains(mod.Acronym))
Assert.Fail($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets!");
}
}
});
}
[Test]
public void TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets()
{
Dictionary<(string firstMod, string secondMod), bool> compatibilityMap = new Dictionary<(string, string), bool>();
Assert.Multiple(() =>
{
for (int rulesetId = 0; rulesetId < 4; ++rulesetId)
{
var rulesetStore = new AssemblyRulesetStore();
var ruleset = rulesetStore.GetRuleset(rulesetId)!.CreateInstance();
var modsValidForFreestyleAsRequired = ruleset.CreateAllMods().Where(m => m.ValidForFreestyleAsRequiredMod).OrderBy(m => m.Acronym).ToList();
for (int i = 0; i < modsValidForFreestyleAsRequired.Count; i++)
{
for (int j = i; j < modsValidForFreestyleAsRequired.Count; ++j)
{
var first = modsValidForFreestyleAsRequired[i];
var second = modsValidForFreestyleAsRequired[j];
bool compatible = ModUtils.CheckCompatibleSet([first, second]);
if (!compatibilityMap.TryGetValue((first.Acronym, second.Acronym), out bool previousCompatible))
compatibilityMap[(first.Acronym, second.Acronym)] = compatible;
else if (previousCompatible != compatible)
Assert.Fail($"{first.Acronym} and {second.Acronym} declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} while not being consistently compatible in all four rulesets!");
}
}
}
});
@@ -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());
}
}
}
@@ -383,6 +383,9 @@ namespace osu.Game.Tests.NonVisual
IsImportant = isImportant;
FrameIndex = frameIndex;
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TestReplayFrame testFrame && Time == testFrame.Time && IsImportant == testFrame.IsImportant && FrameIndex == testFrame.FrameIndex;
}
private class TestInputHandler : FramedReplayInputHandler<TestReplayFrame>
@@ -0,0 +1,212 @@
// 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);
}
[Test]
public void TestFixedReleaseStreamWrittenToConfig()
{
AddStep("add manager", () =>
{
config = new OsuConfigManager(LocalStorage);
config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
Child = new DependencyProvidingContainer
{
CachedDependencies = [(typeof(OsuConfigManager), config)],
Child = manager = new TestUpdateManager(ReleaseStream.Tachyon)
};
});
AddAssert("release stream set to tachyon", () => config.Get<ReleaseStream>(OsuSetting.ReleaseStream), () => Is.EqualTo(ReleaseStream.Tachyon));
}
private partial class TestUpdateManager : UpdateManager
{
public override ReleaseStream? FixedReleaseStream { get; }
public bool IsPending { get; private set; }
public int Invocations { get; private set; }
public int Completions { get; private set; }
private TaskCompletionSource<bool>? pendingCheck;
public TestUpdateManager(ReleaseStream? fixedReleaseStream = null)
{
FixedReleaseStream = fixedReleaseStream;
}
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>();
}
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ namespace osu.Game.Tests.Resources
{
// Create random metadata, then we can check if sorting works based on these
Artist = "Some Artist " + RNG.Next(0, 9),
Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}",
Title = $"Some Song (set id {setId:000000}) {Guid.NewGuid()}",
Author = { Username = "Some Guy " + RNG.Next(0, 9) },
};
@@ -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);
@@ -615,6 +615,25 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
[Test]
public void TestUndoAfterQuickDeletingObjectWhileDragged()
{
AddStep("add hitobject", () => EditorBeatmap.Add(
new HitCircle { StartTime = 0, Position = new Vector2(200, 200) }
));
moveMouseToObject(() => EditorBeatmap.HitObjects[0]);
AddStep("hold left click", () => InputManager.PressButton(MouseButton.Left));
AddStep("drag hitobject to different position", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.BottomRight));
AddStep("click middle mouse button", () => InputManager.Click(MouseButton.Middle));
AddStep("release left click", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count, () => Is.Zero);
AddStep("undo", () => Editor.Undo());
AddAssert("one hitobject in beatmap", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
}
[Test]
public void TestShiftModifierMaintainsAspectRatio()
{
@@ -232,8 +232,8 @@ namespace osu.Game.Tests.Visual.Editing
EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 });
});
ensureEditorLoaded();
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo));
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
@@ -274,6 +274,14 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo));
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != firstDifficultyName;
});
ensureEditorLoaded();
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect points", () =>
@@ -285,8 +293,8 @@ namespace osu.Game.Tests.Visual.Editing
EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 });
});
ensureEditorLoaded();
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new TaikoRuleset().RulesetInfo));
AddUntilStep("wait for created", () =>
@@ -629,6 +637,8 @@ namespace osu.Game.Tests.Visual.Editing
StartTime = 1000
}
}));
ensureEditorLoaded();
AddStep("save beatmap", () => Editor.Save());
AddStep("try to create new catch difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo));
@@ -765,7 +775,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3"));
}
private void ensureEditorLoaded() => AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded);
private void ensureEditorLoaded() => AddUntilStep("wait for editor load", () => Editor.ReadyForUse && DialogOverlay.IsLoaded);
private void createNewDifficulty()
{
@@ -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);
@@ -45,6 +45,44 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}
/// <summary>
/// If we already have an old score with enough hit events and the new score doesn't have enough, continue displaying the old one rather than showing the user "play too short" message.
/// </summary>
[Test]
public void TestTooShortToDisplay_HasPreviousValidScore()
{
const double average_error = -4.5;
const double initial_offset = -2;
AddStep("Set offset non-neutral", () => offsetControl.Current.Value = initial_offset);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Set reference score", () =>
{
offsetControl.ReferenceScore.Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
};
});
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Set short reference score", () =>
{
offsetControl.ReferenceScore.Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
};
});
AddUntilStep("Still calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddAssert("Offset is adjusted", () => offsetControl.Current.Value == initial_offset - average_error);
}
[Test]
public void TestNotEnoughTimedHitEvents()
{
@@ -125,10 +163,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error);
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
recreateControl();
AddStep("Set same reference score", () => offsetControl.ReferenceScore.Value = referenceScore);
@@ -141,6 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestCalibrationFromNonZero()
{
ScoreInfo referenceScore = null!;
const double average_error = -4.5;
const double initial_offset = -2;
@@ -148,7 +184,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Set reference score", () =>
{
offsetControl.ReferenceScore.Value = new ScoreInfo
offsetControl.ReferenceScore.Value = referenceScore = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
@@ -158,9 +194,10 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddAssert("Offset is adjusted", () => offsetControl.Current.Value == initial_offset - average_error);
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
recreateControl();
AddStep("Set same reference score", () => offsetControl.ReferenceScore.Value = referenceScore);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}
@@ -209,10 +246,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddAssert("Offset is adjusted", () => offsetControl.Current.Value, () => Is.EqualTo(initial_offset - average_error));
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Clean up beatmap", () => Realm.Write(r => r.RemoveAll<BeatmapInfo>()));
}
@@ -236,10 +270,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error);
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}
private void recreateControl()
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("toggle expanded", () =>
{
if (leaderboard.IsNotNull())
leaderboard.ForceExpand.Value = !leaderboard.ForceExpand.Value;
leaderboard.CollapseDuringGameplay.Value = !leaderboard.CollapseDuringGameplay.Value;
});
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
@@ -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) },
@@ -136,7 +136,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
public bool IsPartial { get; } = false;
public TestGameplayLeaderboardProvider()
{
@@ -147,8 +146,8 @@ namespace osu.Game.Tests.Visual.Gameplay
User = new APIUser { Username = $"User {i}" },
TotalScore = (20 - i) * 50_000,
Accuracy = i * 0.05,
Combo = i * 50
}, i == 19));
MaxCombo = i * 50,
}, i == 19, GameplayLeaderboardScore.ComboDisplayMode.Highest));
}
}
}

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