mirror of
https://github.com/ppy/osu.git
synced 2026-05-24 13:30:46 +08:00
Merge branch 'master' into NotStirred/master
This commit is contained in:
@@ -131,7 +131,7 @@ jobs:
|
||||
|
||||
build-only-ios:
|
||||
name: Build only (iOS)
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Install .NET Workloads
|
||||
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
|
||||
run: dotnet workload install ios
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c Debug osu.iOS.slnf
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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.715.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
new CheckBananaShowerGap(),
|
||||
new CheckConcurrentObjects(),
|
||||
|
||||
// Spread
|
||||
new CheckCatchLowestDiffDrainTime(),
|
||||
|
||||
// Settings
|
||||
new CheckCatchAbnormalDifficultySettings(),
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
{
|
||||
public class CheckCatchLowestDiffDrainTime : CheckLowestDiffDrainTime
|
||||
{
|
||||
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
|
||||
{
|
||||
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21catch#general
|
||||
yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Platter");
|
||||
yield return (DifficultyRating.Insane, new TimeSpan(0, 3, 15).TotalMilliseconds, "Rain");
|
||||
yield return (DifficultyRating.Expert, new TimeSpan(0, 4, 0).TotalMilliseconds, "Overdose");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHoldNotesAlmostConcurrentOnSameColumn()
|
||||
{
|
||||
assertAlmostConcurrentSame(new List<HitObject>
|
||||
{
|
||||
createHoldNote(startTime: 100, endTime: 400.75d, column: 1),
|
||||
createHoldNote(startTime: 408, endTime: 700.75d, column: 1)
|
||||
});
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitobjects)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitobjects)), Is.Empty);
|
||||
@@ -65,7 +75,17 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains("s are concurrent here")));
|
||||
}
|
||||
|
||||
private void assertAlmostConcurrentSame(List<HitObject> hitobjects)
|
||||
{
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateAlmostConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains("s are less than 10ms apart")));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitobjects)
|
||||
|
||||
@@ -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,14 +28,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks
|
||||
continue;
|
||||
|
||||
// Two hitobjects cannot be concurrent without also being concurrent with all objects in between.
|
||||
// So if the next object is not concurrent, then we know no future objects will be either.
|
||||
if (!AreConcurrent(hitobject, nextHitobject))
|
||||
// So if the next object is not concurrent or almost concurrent, then we know no future objects will be either.
|
||||
if (!AreConcurrent(hitobject, nextHitobject) && !AreAlmostConcurrent(hitobject, nextHitobject))
|
||||
break;
|
||||
|
||||
if (hitobject.GetType() == nextHitobject.GetType())
|
||||
yield return new IssueTemplateConcurrentSame(this).Create(hitobject, nextHitobject);
|
||||
else
|
||||
yield return new IssueTemplateConcurrentDifferent(this).Create(hitobject, nextHitobject);
|
||||
if (AreConcurrent(hitobject, nextHitobject))
|
||||
{
|
||||
yield return new IssueTemplateConcurrent(this).Create(hitobject, nextHitobject);
|
||||
}
|
||||
else if (AreAlmostConcurrent(hitobject, nextHitobject))
|
||||
{
|
||||
yield return new IssueTemplateAlmostConcurrent(this).Create(hitobject, nextHitobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Checks
|
||||
{
|
||||
public class CheckManiaLowestDiffDrainTime : CheckLowestDiffDrainTime
|
||||
{
|
||||
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
|
||||
{
|
||||
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21mania#rules
|
||||
yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Hard");
|
||||
yield return (DifficultyRating.Insane, new TimeSpan(0, 2, 45).TotalMilliseconds, "Insane");
|
||||
yield return (DifficultyRating.Expert, new TimeSpan(0, 3, 30).TotalMilliseconds, "Expert");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
// Compose
|
||||
new CheckManiaConcurrentObjects(),
|
||||
|
||||
// Spread
|
||||
new CheckManiaLowestDiffDrainTime(),
|
||||
|
||||
// Settings
|
||||
new CheckKeyCount(),
|
||||
new CheckManiaAbnormalDifficultySettings(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
{
|
||||
public class CheckOsuLowestDiffDrainTime : CheckLowestDiffDrainTime
|
||||
{
|
||||
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
|
||||
{
|
||||
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21#general
|
||||
yield return (DifficultyRating.Hard, new TimeSpan(0, 3, 30).TotalMilliseconds, "Hard");
|
||||
yield return (DifficultyRating.Insane, new TimeSpan(0, 4, 15).TotalMilliseconds, "Insane");
|
||||
yield return (DifficultyRating.Expert, new TimeSpan(0, 5, 0).TotalMilliseconds, "Expert");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
new CheckTimeDistanceEquality(),
|
||||
new CheckLowDiffOverlaps(),
|
||||
new CheckTooShortSliders(),
|
||||
new CheckOsuLowestDiffDrainTime(),
|
||||
|
||||
// Settings
|
||||
new CheckOsuAbnormalDifficultySettings(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit.Checks
|
||||
{
|
||||
public class CheckTaikoLowestDiffDrainTime : CheckLowestDiffDrainTime
|
||||
{
|
||||
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
|
||||
{
|
||||
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21taiko#general
|
||||
yield return (DifficultyRating.Hard, new TimeSpan(0, 3, 30).TotalMilliseconds, "Muzukashii");
|
||||
yield return (DifficultyRating.Insane, new TimeSpan(0, 4, 15).TotalMilliseconds, "Oni");
|
||||
yield return (DifficultyRating.Expert, new TimeSpan(0, 5, 0).TotalMilliseconds, "Inner Oni");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
// Compose
|
||||
new CheckConcurrentObjects(),
|
||||
|
||||
// Spread
|
||||
new CheckTaikoLowestDiffDrainTime(),
|
||||
|
||||
// Settings
|
||||
new CheckTaikoAbnormalDifficultySettings(),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Framework.Extensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO.Legacy;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Replays;
|
||||
@@ -321,6 +322,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
CountryCode = CountryCode.PL
|
||||
};
|
||||
scoreInfo.ClientVersion = "2023.1221.0";
|
||||
scoreInfo.Pauses.AddRange([111111, 222222, 333333]);
|
||||
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
var score = new Score
|
||||
@@ -345,6 +347,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0"));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.RealmUser.OnlineID, Is.EqualTo(3035836));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Pauses, Is.EquivalentTo(new[] { 111111, 222222, 333333 }));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,16 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesAlmostConcurrentWarning()
|
||||
{
|
||||
assertAlmostConcurrentSame(new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 100 },
|
||||
new HitCircle { StartTime = 108 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSlidersSeparate()
|
||||
{
|
||||
@@ -97,6 +107,16 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderAndCircleAlmostConcurrent()
|
||||
{
|
||||
assertAlmostConcurrentDifferent(new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 100, endTime: 400.75d).Object,
|
||||
new HitCircle { StartTime = 408 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManyObjectsConcurrent()
|
||||
{
|
||||
@@ -110,8 +130,14 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(3));
|
||||
Assert.That(issues.Where(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent).ToList(), Has.Count.EqualTo(2));
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrent));
|
||||
|
||||
// Should have 1 same-type concurrent (Slider & Slider) and 2 different-type concurrent (Slider & Circle)
|
||||
var sameTypeIssues = issues.Where(issue => issue.ToString().Contains("s are concurrent here")).ToList();
|
||||
var differentTypeIssues = issues.Where(issue => issue.ToString().Contains(" and ") && issue.ToString().Contains("are concurrent here")).ToList();
|
||||
|
||||
Assert.That(sameTypeIssues, Has.Count.EqualTo(1));
|
||||
Assert.That(differentTypeIssues, Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
private Mock<Slider> getSliderMock(double startTime, double endTime, int repeats = 0)
|
||||
@@ -144,7 +170,8 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains("s are concurrent here")));
|
||||
}
|
||||
|
||||
private void assertConcurrentDifferent(List<HitObject> hitobjects, int count = 1)
|
||||
@@ -152,7 +179,26 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains(" and ") && issue.ToString().Contains("are concurrent here")));
|
||||
}
|
||||
|
||||
private void assertAlmostConcurrentSame(List<HitObject> hitobjects)
|
||||
{
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateAlmostConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains("s are less than 10ms apart")));
|
||||
}
|
||||
|
||||
private void assertAlmostConcurrentDifferent(List<HitObject> hitobjects)
|
||||
{
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateAlmostConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains(" and ") && issue.ToString().Contains("are less than 10ms apart")));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitobjects)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckInconsistentMetadataTest
|
||||
{
|
||||
private CheckInconsistentMetadata check = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckInconsistentMetadata();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConsistentMetadata()
|
||||
{
|
||||
var metadata = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata, metadata);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInconsistentArtist()
|
||||
{
|
||||
var metadata1 = createMetadata("Artist One", "Test Title", "Test Source", "Test Creator", "tag1 tag2");
|
||||
var metadata2 = createMetadata("Artist Two", "Test Title", "Test Source", "Test Creator", "tag1 tag2");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields);
|
||||
Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent artist fields"));
|
||||
Assert.That(issues[0].ToString(), Contains.Substring("Artist One"));
|
||||
Assert.That(issues[0].ToString(), Contains.Substring("Artist Two"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInconsistentTitle()
|
||||
{
|
||||
var metadata1 = createMetadata("Test Artist", "Title One", "Test Source", "Test Creator", "tag1 tag2");
|
||||
var metadata2 = createMetadata("Test Artist", "Title Two", "Test Source", "Test Creator", "tag1 tag2");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields);
|
||||
Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent title fields"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInconsistentUnicodeArtist()
|
||||
{
|
||||
var metadata1 = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2", unicodeArtist: "Test Unicode Artist 1");
|
||||
var metadata2 = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2", unicodeArtist: "Test Unicode Artist 2");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields);
|
||||
Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent unicode artist fields"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInconsistentSource()
|
||||
{
|
||||
var metadata1 = createMetadata("Test Artist", "Test Title", "Source One", "Test Creator", "tag1 tag2");
|
||||
var metadata2 = createMetadata("Test Artist", "Test Title", "Source Two", "Test Creator", "tag1 tag2");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields);
|
||||
Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent source fields"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInconsistentCreator()
|
||||
{
|
||||
var metadata1 = createMetadata("Test Artist", "Test Title", "Test Source", "Creator One", "tag1 tag2");
|
||||
var metadata2 = createMetadata("Test Artist", "Test Title", "Test Source", "Creator Two", "tag1 tag2");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields);
|
||||
Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent creator fields"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInconsistentTags()
|
||||
{
|
||||
var metadata1 = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2 tag3");
|
||||
var metadata2 = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag4 tag5");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentTags);
|
||||
Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent tags"));
|
||||
Assert.That(issues[0].ToString(), Contains.Substring("tag2 tag3 tag4 tag5"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleInconsistencies()
|
||||
{
|
||||
var metadata1 = createMetadata("Artist One", "Title One", "Test Source", "Test Creator", "tag1 tag2");
|
||||
var metadata2 = createMetadata("Artist Two", "Title Two", "Test Source", "Test Creator", "tag3 tag4");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(3)); // artist, title, tags
|
||||
Assert.That(issues.Count(i => i.Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields), Is.EqualTo(2));
|
||||
Assert.That(issues.Count(i => i.Template is CheckInconsistentMetadata.IssueTemplateInconsistentTags), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleDifficulty()
|
||||
{
|
||||
var metadata = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyStringFieldsAreConsistent()
|
||||
{
|
||||
var metadata1 = createMetadata("Test Artist", "Test Title", "", "Test Creator", "");
|
||||
var metadata2 = createMetadata("Test Artist", "Test Title", "", "Test Creator", "");
|
||||
var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2);
|
||||
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
|
||||
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
private BeatmapMetadata createMetadata(string artist, string title, string source, string creator, string tags, string unicodeArtist = "", string unicodeTitle = "")
|
||||
{
|
||||
return new BeatmapMetadata(new RealmUser { Username = creator })
|
||||
{
|
||||
Artist = artist,
|
||||
Title = title,
|
||||
Source = source,
|
||||
Tags = tags,
|
||||
ArtistUnicode = unicodeArtist,
|
||||
TitleUnicode = unicodeTitle
|
||||
};
|
||||
}
|
||||
|
||||
private IBeatmap[] createBeatmapSetWithMetadata(params BeatmapMetadata[] metadata)
|
||||
{
|
||||
var beatmapSet = new BeatmapSetInfo();
|
||||
var beatmaps = new IBeatmap[metadata.Length];
|
||||
|
||||
for (int i = 0; i < metadata.Length; i++)
|
||||
{
|
||||
beatmaps[i] = createBeatmapWithMetadata(metadata[i], $"Difficulty {i + 1}");
|
||||
beatmaps[i].BeatmapInfo.BeatmapSet = beatmapSet;
|
||||
}
|
||||
|
||||
// Configure the beatmapset to contain all the beatmap infos
|
||||
foreach (var beatmap in beatmaps)
|
||||
beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo);
|
||||
|
||||
return beatmaps;
|
||||
}
|
||||
|
||||
private Beatmap createBeatmapWithMetadata(BeatmapMetadata metadata, string difficultyName)
|
||||
{
|
||||
return new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
DifficultyName = difficultyName,
|
||||
Metadata = metadata
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext createContextWithMultipleDifficulties(IBeatmap currentBeatmap, IBeatmap[] allDifficulties)
|
||||
{
|
||||
return new BeatmapVerifierContext(
|
||||
currentBeatmap,
|
||||
new TestWorkingBeatmap(currentBeatmap),
|
||||
DifficultyRating.ExpertPlus,
|
||||
beatmapInfo => allDifficulties.FirstOrDefault(b => b.BeatmapInfo.Equals(beatmapInfo))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckLowestDiffDrainTimeTest
|
||||
{
|
||||
private TestCheckLowestDiffDrainTime check = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new TestCheckLowestDiffDrainTime();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleDifficultyMeetsRequirement()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime(4 * 60 * 1000, 3.5, "Hard"); // 4 minutes
|
||||
assertOk(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleDifficultyTooShort()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime(2 * 60 * 1000, 3.5, "Hard"); // 2 minutes - too short for Hard
|
||||
assertTooShort(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHardDifficultyAtThreshold()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime((3 * 60 + 30) * 1000, 3.5, "Hard"); // Exactly 3:30
|
||||
assertOk(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHardDifficultyJustUnderThreshold()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime((3 * 60 + 29) * 1000, 3.5, "Hard"); // 3:29 - just under threshold
|
||||
assertTooShort(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInsaneDifficultyAtThreshold()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime((4 * 60 + 15) * 1000, 4.5, "Insane"); // Exactly 4:15
|
||||
assertOk(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInsaneDifficultyTooShort()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime(4 * 60 * 1000, 4.5, "Insane"); // 4:00 - too short for Insane
|
||||
assertTooShort(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExpertDifficultyAtThreshold()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime(5 * 60 * 1000, 5.5, "Expert"); // Exactly 5:00
|
||||
assertOk(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExpertDifficultyTooShort()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime((4 * 60 + 30) * 1000, 5.5, "Expert"); // 4:30 - too short for Expert
|
||||
assertTooShort(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEasyDifficultyMeetsRequirement()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime(2 * 60 * 1000, 1.5, "Easy"); // 2 minutes - should be ok for Easy
|
||||
assertOk(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalDifficultyMeetsRequirement()
|
||||
{
|
||||
var beatmap = createBeatmapWithDrainTime(2 * 60 * 1000, 2.5, "Normal"); // 2 minutes - should be ok for Normal
|
||||
assertOk(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleDifficultiesMeetsRequirement()
|
||||
{
|
||||
var difficulties = new List<IBeatmap>
|
||||
{
|
||||
createBeatmapWithDrainTime((3 * 60 + 30) * 1000, 3.5, "Hard"), // Hard - lowest difficulty, 3:30
|
||||
createBeatmapWithDrainTime((3 * 60 + 30) * 1000, 4.5, "Insane"),
|
||||
createBeatmapWithDrainTime((3 * 60 + 30) * 1000, 5.5, "Expert")
|
||||
};
|
||||
|
||||
// All should be ok because lowest difficulty is Hard and drain time meets Hard requirement
|
||||
assertOkWithMultipleDifficulties(difficulties[0], difficulties);
|
||||
assertOkWithMultipleDifficulties(difficulties[1], difficulties);
|
||||
assertOkWithMultipleDifficulties(difficulties[2], difficulties);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleDifficultiesTooShort()
|
||||
{
|
||||
var difficulties = new List<IBeatmap>
|
||||
{
|
||||
createBeatmapWithDrainTime(4 * 60 * 1000, 4.5, "Insane"), // Insane - lowest difficulty, 4:00
|
||||
createBeatmapWithDrainTime(4 * 60 * 1000, 5.5, "Expert") // Same drain time
|
||||
};
|
||||
|
||||
// Should be too short because lowest difficulty is Insane and requires 4:15
|
||||
assertTooShortWithMultipleDifficulties(difficulties[0], difficulties);
|
||||
assertTooShortWithMultipleDifficulties(difficulties[1], difficulties);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayTimeVsDrainTimeNotHighestDifficulty()
|
||||
{
|
||||
var expertBeatmap = createBeatmapWithPlayTime(5 * 60 * 1000, 5.5, "Expert"); // 5:00 play time
|
||||
expertBeatmap.Breaks.Add(new BreakPeriod(60000, 100000)); // 40-second break
|
||||
|
||||
var difficulties = new List<IBeatmap>
|
||||
{
|
||||
expertBeatmap, // Expert - 5:00 play, 4:20 drain
|
||||
createBeatmapWithPlayTime(5 * 60 * 1000, 6.5, "ExpertPlus") // ExpertPlus - highest difficulty
|
||||
};
|
||||
|
||||
// The Expert difficulty (not highest) should use play time (5:00) and pass the Expert requirement
|
||||
assertOkWithMultipleDifficulties(difficulties[0], difficulties);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayTimeVsDrainTimeHighestDifficulty()
|
||||
{
|
||||
var expertBeatmap = createBeatmapWithPlayTime(5 * 60 * 1000, 5.5, "Expert"); // 5:00 play time
|
||||
expertBeatmap.Breaks.Add(new BreakPeriod(60000, 100000)); // 40-second break
|
||||
|
||||
// As the highest difficulty with breaks > 30s, it should use drain time and fail
|
||||
assertTooShort(expertBeatmap);
|
||||
}
|
||||
|
||||
private IBeatmap createBeatmapWithDrainTime(double drainTimeMs, double starRating = 3.5, string difficultyName = "Default")
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
StarRating = starRating,
|
||||
DifficultyName = difficultyName,
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitObject { StartTime = 0 },
|
||||
new HitObject { StartTime = drainTimeMs } // Last object at drain time
|
||||
}
|
||||
};
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
private IBeatmap createBeatmapWithPlayTime(double playTimeMs, double starRating = 3.5, string difficultyName = "Default")
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
StarRating = starRating,
|
||||
DifficultyName = difficultyName,
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitObject { StartTime = 0 },
|
||||
new HitObject { StartTime = playTimeMs } // Last object at play time
|
||||
}
|
||||
};
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
private void assertOk(IBeatmap beatmap)
|
||||
{
|
||||
var difficultyRating = StarDifficulty.GetDifficultyRating(beatmap.BeatmapInfo.StarRating);
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertTooShort(IBeatmap beatmap)
|
||||
{
|
||||
var difficultyRating = StarDifficulty.GetDifficultyRating(beatmap.BeatmapInfo.StarRating);
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.First().Template is CheckLowestDiffDrainTime.IssueTemplateTooShort);
|
||||
}
|
||||
|
||||
private void assertOkWithMultipleDifficulties(IBeatmap currentBeatmap, IEnumerable<IBeatmap> allDifficulties)
|
||||
{
|
||||
var context = createContextWithMultipleDifficulties(currentBeatmap, allDifficulties);
|
||||
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertTooShortWithMultipleDifficulties(IBeatmap currentBeatmap, IEnumerable<IBeatmap> allDifficulties)
|
||||
{
|
||||
var context = createContextWithMultipleDifficulties(currentBeatmap, allDifficulties);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.First().Template is CheckLowestDiffDrainTime.IssueTemplateTooShort);
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext createContextWithMultipleDifficulties(IBeatmap currentBeatmap, IEnumerable<IBeatmap> allDifficulties)
|
||||
{
|
||||
var beatmapSet = new BeatmapSetInfo();
|
||||
var beatmapInfos = allDifficulties.Select(d => d.BeatmapInfo).ToList();
|
||||
|
||||
// Set up the beatmapset with all difficulties
|
||||
beatmapSet.Beatmaps.AddRange(beatmapInfos);
|
||||
currentBeatmap.BeatmapInfo.BeatmapSet = beatmapSet;
|
||||
|
||||
// Create a resolver that returns the appropriate working beatmap for each difficulty
|
||||
var difficultyDict = allDifficulties.ToDictionary(d => d.BeatmapInfo, d => new TestWorkingBeatmap(d));
|
||||
|
||||
// Use the current beatmap's star rating to determine its difficulty rating
|
||||
var currentDifficultyRating = StarDifficulty.GetDifficultyRating(currentBeatmap.BeatmapInfo.StarRating);
|
||||
|
||||
return new BeatmapVerifierContext(
|
||||
currentBeatmap,
|
||||
new TestWorkingBeatmap(currentBeatmap),
|
||||
currentDifficultyRating,
|
||||
beatmapInfo => difficultyDict.TryGetValue(beatmapInfo, out var workingBeatmap) ? workingBeatmap.Beatmap : null
|
||||
);
|
||||
}
|
||||
|
||||
private class TestCheckLowestDiffDrainTime : CheckLowestDiffDrainTime
|
||||
{
|
||||
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
|
||||
{
|
||||
// Same thresholds as `CheckOsuLowestDiffDrainTime` for testing
|
||||
yield return (DifficultyRating.Hard, new TimeSpan(0, 3, 30).TotalMilliseconds, "Hard");
|
||||
yield return (DifficultyRating.Insane, new TimeSpan(0, 4, 15).TotalMilliseconds, "Insane");
|
||||
yield return (DifficultyRating.Expert, new TimeSpan(0, 5, 0).TotalMilliseconds, "Expert");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -342,13 +342,14 @@ namespace osu.Game.Tests.Mods
|
||||
{
|
||||
foreach (var mod in ruleset.CreateAllMods())
|
||||
{
|
||||
if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && !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)
|
||||
Assert.Fail($"Mod {mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but is not playable!");
|
||||
|
||||
// downgraded to warning, because there are valid reasons why they may still not be specified to be valid for freestyle as required
|
||||
// (see `TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets()` test case below).
|
||||
if (!mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && commonAcronyms.Contains(mod.Acronym))
|
||||
Assert.Warn($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets.");
|
||||
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!");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,6 +40,11 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Author = { Username = "The Author" },
|
||||
Source = "unit tests",
|
||||
Tags = "look for tags too",
|
||||
UserTags =
|
||||
{
|
||||
"song representation/simple",
|
||||
"style/clean",
|
||||
}
|
||||
},
|
||||
DifficultyName = "version as well",
|
||||
Length = 2500,
|
||||
@@ -292,6 +297,33 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[TestCase("simple", false)]
|
||||
[TestCase("\"style/clean\"", false)]
|
||||
[TestCase("\"style/clean\"!", false)]
|
||||
[TestCase("iNiS-style", true)]
|
||||
[TestCase("\"reading/visually dense\"!", true)]
|
||||
public void TestCriteriaMatchingUserTags(string query, bool filtered)
|
||||
{
|
||||
var beatmap = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria { UserTag = { SearchTerm = query } };
|
||||
var carouselItem = new CarouselBeatmap(beatmap);
|
||||
carouselItem.Filter(criteria);
|
||||
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapMustHaveAtLeastOneTagIfUserTagFilterActive()
|
||||
{
|
||||
var beatmap = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria { UserTag = { SearchTerm = "simple" } };
|
||||
var carouselItem = new CarouselBeatmap(beatmap);
|
||||
carouselItem.BeatmapInfo.Metadata.UserTags.Clear();
|
||||
carouselItem.Filter(criteria);
|
||||
|
||||
Assert.True(carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomRulesetCriteria([Values(null, true, false)] bool? matchCustomCriteria)
|
||||
{
|
||||
|
||||
@@ -85,16 +85,6 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||
}
|
||||
|
||||
/*
|
||||
* The following tests have been written a bit strangely (they don't check exact
|
||||
* bound equality with what the filter says).
|
||||
* This is to account for floating-point arithmetic issues.
|
||||
* For example, specifying a bpm<140 filter would previously match beatmaps with BPM
|
||||
* of 139.99999, which would be displayed in the UI as 140.
|
||||
* Due to this the tests check the last tick inside the range and the first tick
|
||||
* outside of the range.
|
||||
*/
|
||||
|
||||
[TestCase("star")]
|
||||
[TestCase("stars")]
|
||||
public void TestApplyStarQueries(string variant)
|
||||
@@ -105,11 +95,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.IsNotNull(filterCriteria.StarDifficulty.Max);
|
||||
Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d);
|
||||
Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d);
|
||||
Assert.AreEqual(filterCriteria.StarDifficulty.Max, 4.00d);
|
||||
Assert.IsNull(filterCriteria.StarDifficulty.Min);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStarQueriesInclusive()
|
||||
{
|
||||
const string query = "stars>=6";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(filterCriteria.StarDifficulty.Min, 6.00d);
|
||||
Assert.True(filterCriteria.StarDifficulty.IsLowerInclusive);
|
||||
Assert.IsNull(filterCriteria.StarDifficulty.Max);
|
||||
}
|
||||
|
||||
/*
|
||||
* The following tests have been written a bit strangely (they don't check exact
|
||||
* bound equality with what the filter says).
|
||||
* This is to account for floating-point arithmetic issues.
|
||||
* For example, specifying a bpm<140 filter would previously match beatmaps with BPM
|
||||
* of 139.99999, which would be displayed in the UI as 140.
|
||||
* Due to this the tests check the last tick inside the range and the first tick
|
||||
* outside of the range.
|
||||
*/
|
||||
|
||||
[Test]
|
||||
public void TestApplyApproachRateQueries()
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
public void TestResultIfOnlyParentHitWindowIsEmpty()
|
||||
{
|
||||
var testObject = new TestHitObject(HitWindows.Empty);
|
||||
HitObject nested = new TestHitObject(new HitWindows());
|
||||
HitObject nested = new TestHitObject(new DefaultHitWindows());
|
||||
testObject.AddNested(nested);
|
||||
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
|
||||
|
||||
@@ -43,8 +43,8 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestResultIfParentHitWindowsIsNotEmpty()
|
||||
{
|
||||
var testObject = new TestHitObject(new HitWindows());
|
||||
HitObject nested = new TestHitObject(new HitWindows());
|
||||
var testObject = new TestHitObject(new DefaultHitWindows());
|
||||
HitObject nested = new TestHitObject(new DefaultHitWindows());
|
||||
testObject.AddNested(nested);
|
||||
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
HitObject nested = new TestHitObject(HitWindows.Empty);
|
||||
firstObject.AddNested(nested);
|
||||
|
||||
var secondObject = new TestHitObject(new HitWindows());
|
||||
var secondObject = new TestHitObject(new DefaultHitWindows());
|
||||
testDrawableRuleset.HitObjects = new List<HitObject> { firstObject, secondObject };
|
||||
|
||||
Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, secondObject.HitWindows);
|
||||
|
||||
@@ -21,5 +21,18 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
Assert.AreEqual(expectedOutput, input.FormatAccuracy().ToString());
|
||||
}
|
||||
|
||||
[TestCase(3, "3.00")]
|
||||
[TestCase(3.3, "3.30")]
|
||||
[TestCase(3.55, "3.55")]
|
||||
[TestCase(3.553, "3.55")]
|
||||
[TestCase(3.557, "3.55")]
|
||||
[TestCase(3.9999, "3.99")]
|
||||
[TestCase(3.999999, "3.99")]
|
||||
[TestCase(4, "4.00")]
|
||||
public void TestStarRatingFormatting(double input, string expectedOutput)
|
||||
{
|
||||
Assert.AreEqual(expectedOutput, input.FormatStarRating().ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,79 +12,79 @@ namespace osu.Game.Tests.Online.Chat
|
||||
[Test]
|
||||
public void TestContainsUsernameMidlinePositive()
|
||||
{
|
||||
Assert.IsTrue(MessageNotifier.CheckContainsUsername("This is a test message", "Test"));
|
||||
Assert.IsTrue(MessageNotifier.MatchUsername("This is a test message", "Test").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameStartOfLinePositive()
|
||||
{
|
||||
Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test message", "Test"));
|
||||
Assert.IsTrue(MessageNotifier.MatchUsername("Test message", "Test").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameEndOfLinePositive()
|
||||
{
|
||||
Assert.IsTrue(MessageNotifier.CheckContainsUsername("This is a test", "Test"));
|
||||
Assert.IsTrue(MessageNotifier.MatchUsername("This is a test", "Test").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameMidlineNegative()
|
||||
{
|
||||
Assert.IsFalse(MessageNotifier.CheckContainsUsername("This is a testmessage for notifications", "Test"));
|
||||
Assert.IsFalse(MessageNotifier.MatchUsername("This is a testmessage for notifications", "Test").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameStartOfLineNegative()
|
||||
{
|
||||
Assert.IsFalse(MessageNotifier.CheckContainsUsername("Testmessage", "Test"));
|
||||
Assert.IsFalse(MessageNotifier.MatchUsername("Testmessage", "Test").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameEndOfLineNegative()
|
||||
{
|
||||
Assert.IsFalse(MessageNotifier.CheckContainsUsername("This is a notificationtest", "Test"));
|
||||
Assert.IsFalse(MessageNotifier.MatchUsername("This is a notificationtest", "Test").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameBetweenPunctuation()
|
||||
{
|
||||
Assert.IsTrue(MessageNotifier.CheckContainsUsername("Hello 'test'-message", "Test"));
|
||||
Assert.IsTrue(MessageNotifier.MatchUsername("Hello 'test'-message", "Test").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameUnicode()
|
||||
{
|
||||
Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test \u0460\u0460 message", "\u0460\u0460"));
|
||||
Assert.IsTrue(MessageNotifier.MatchUsername("Test \u0460\u0460 message", "\u0460\u0460").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameUnicodeNegative()
|
||||
{
|
||||
Assert.IsFalse(MessageNotifier.CheckContainsUsername("Test ha\u0460\u0460o message", "\u0460\u0460"));
|
||||
Assert.IsFalse(MessageNotifier.MatchUsername("Test ha\u0460\u0460o message", "\u0460\u0460").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameSpecialCharactersPositive()
|
||||
{
|
||||
Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test [#^-^#] message", "[#^-^#]"));
|
||||
Assert.IsTrue(MessageNotifier.MatchUsername("Test [#^-^#] message", "[#^-^#]").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameSpecialCharactersNegative()
|
||||
{
|
||||
Assert.IsFalse(MessageNotifier.CheckContainsUsername("Test pad[#^-^#]oru message", "[#^-^#]"));
|
||||
Assert.IsFalse(MessageNotifier.MatchUsername("Test pad[#^-^#]oru message", "[#^-^#]").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameAtSign()
|
||||
{
|
||||
Assert.IsTrue(MessageNotifier.CheckContainsUsername("@username hi", "username"));
|
||||
Assert.IsTrue(MessageNotifier.MatchUsername("@username hi", "username").Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContainsUsernameColon()
|
||||
{
|
||||
Assert.IsTrue(MessageNotifier.CheckContainsUsername("username: hi", "username"));
|
||||
Assert.IsTrue(MessageNotifier.MatchUsername("username: hi", "username").Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user