1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-01 18:09:58 +08:00

Merge branch 'master' into noto-bopomofo

This commit is contained in:
Dean Herbert
2025-07-16 18:41:01 +09:00
committed by GitHub
Unverified
266 changed files with 5384 additions and 4016 deletions
+1 -1
View File
@@ -50,7 +50,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright>
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2024 ppy Pty Ltd <contact@ppy.sh>.
Copyright (c) 2025 ppy Pty Ltd <contact@ppy.sh>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1 -1
View File
@@ -8,7 +8,7 @@
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
<PackageTags>dotnet-new;templates;osu</PackageTags>
<TargetFramework>netstandard2.1</TargetFramework>
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.604.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.715.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+11 -37
View File
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Android.App;
using Android.Content.PM;
using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Game;
@@ -21,58 +23,30 @@ namespace osu.Android
[Cached]
private readonly OsuGameActivity gameActivity;
private readonly PackageInfo packageInfo;
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
public OsuGameAndroid(OsuGameActivity activity)
: base(null)
{
gameActivity = activity;
packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
}
public override Version AssemblyVersion
public override string Version
{
get
{
var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
if (!IsDeployedBuild)
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
try
{
// We store the osu! build number in the "VersionCode" field to better support google play releases.
// If we were to use the main build number, it would require a new submission each time (similar to TestFlight).
// In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time.
//
// We also need to be aware that older SDK versions store this as a 32bit int.
//
// Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060
// https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
string versionName;
if (OperatingSystem.IsAndroidVersionAtLeast(28))
{
versionName = packageInfo.LongVersionCode.ToString();
// ensure we only read the trailing portion of long (the part we are interested in).
versionName = versionName.Substring(versionName.Length - 9);
}
else
{
#pragma warning disable CS0618 // Type or member is obsolete
// this is required else older SDKs will report missing method exception.
versionName = packageInfo.VersionCode.ToString();
#pragma warning restore CS0618 // Type or member is obsolete
}
// undo play store version garbling (as mentioned above).
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
catch
{
}
return new Version(packageInfo.VersionName.AsNonNull());
return packageInfo.VersionName.AsNonNull();
}
}
public override Version AssemblyVersion => new Version(packageInfo.VersionName.AsNonNull().Split('-').First());
protected override void LoadComplete()
{
base.LoadComplete();
+1 -1
View File
@@ -123,7 +123,7 @@ namespace osu.Desktop
public override bool RestartAppWhenExited()
{
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
return true;
}
+1 -1
View File
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<language>en-AU</language>
</metadata>
<files>
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModHalfTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
}
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModDoubleTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
}
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
@@ -23,21 +24,21 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
new BeatmapStatistic
{
Name = @"Fruits",
Name = BeatmapStatisticStrings.Fruits,
Content = fruits.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
BarDisplayLength = fruits / (float)sum,
},
new BeatmapStatistic
{
Name = @"Juice Streams",
Name = BeatmapStatisticStrings.JuiceStreams,
Content = juiceStreams.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
BarDisplayLength = juiceStreams / (float)sum,
},
new BeatmapStatistic
{
Name = @"Banana Showers",
Name = BeatmapStatisticStrings.BananaShowers,
Content = bananaShowers.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
BarDisplayLength = Math.Min(bananaShowers / 10f, 1),
+3 -1
View File
@@ -33,6 +33,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Catch
@@ -265,9 +266,10 @@ namespace osu.Game.Rulesets.Catch
}
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
preempt /= rate;
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -219,5 +220,40 @@ namespace osu.Game.Rulesets.Catch.Edit
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
}
#region Clipboard handling
public override string ConvertSelectionToString()
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
// 1,2,3,4 ...
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
public override void SelectFromTimestamp(double timestamp, string objectDescription)
{
if (!selection_regex.IsMatch(objectDescription))
return;
List<CatchHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<CatchHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] splitDescription = objectDescription.Split(',');
for (int i = 0; i < splitDescription.Length; i++)
{
if (!int.TryParse(splitDescription[i], out int combo) || combo < 1)
continue;
CatchHitObject? current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo);
if (current == null)
continue;
EditorBeatmap.SelectedHitObjects.Add(current);
if (i < splitDescription.Length - 1)
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
#endregion
}
}
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
get
{
if (UserAdjustedSettingsCount != 1)
if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate))
return string.Empty;
if (!CircleSize.IsDefault) return format("CS", CircleSize);
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
@@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!";
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
}
}
}
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
}
@@ -5,7 +5,6 @@ using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
@@ -38,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ManiaModScoreV2) } },
};
[TestCaseSource(nameof(mania_mod_mapping))]
@@ -9,7 +9,6 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -19,7 +18,6 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
{
protected override Ruleset CreateRuleset() => new ManiaRuleset();
@@ -72,13 +70,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -137d, HitResult.Miss },
new object[] { 5f, -138d, HitResult.Miss },
new object[] { 5f, 111d, HitResult.Ok },
new object[] { 5f, 112d, HitResult.Miss },
new object[] { 5f, 113d, HitResult.Miss },
new object[] { 5f, 114d, HitResult.Miss },
new object[] { 5f, 135d, HitResult.Miss },
new object[] { 5f, 136d, HitResult.Miss },
new object[] { 5f, 137d, HitResult.Miss },
new object[] { 5f, 138d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 112d, HitResult.Miss },
// new object[] { 5f, 113d, HitResult.Miss },
// new object[] { 5f, 114d, HitResult.Miss },
// new object[] { 5f, 135d, HitResult.Miss },
// new object[] { 5f, 136d, HitResult.Miss },
// new object[] { 5f, 137d, HitResult.Miss },
// new object[] { 5f, 138d, HitResult.Miss },
// OD = 9.3 test cases.
// PERFECT hit window is [ -14ms, 14ms]
@@ -99,13 +98,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 9.3f, 70d, HitResult.Ok },
new object[] { 9.3f, 71d, HitResult.Ok },
new object[] { 9.3f, 98d, HitResult.Ok },
new object[] { 9.3f, 99d, HitResult.Miss },
new object[] { 9.3f, 100d, HitResult.Miss },
new object[] { 9.3f, 101d, HitResult.Miss },
new object[] { 9.3f, 122d, HitResult.Miss },
new object[] { 9.3f, 123d, HitResult.Miss },
new object[] { 9.3f, 124d, HitResult.Miss },
new object[] { 9.3f, 125d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 9.3f, 99d, HitResult.Miss },
// new object[] { 9.3f, 100d, HitResult.Miss },
// new object[] { 9.3f, 101d, HitResult.Miss },
// new object[] { 9.3f, 122d, HitResult.Miss },
// new object[] { 9.3f, 123d, HitResult.Miss },
// new object[] { 9.3f, 124d, HitResult.Miss },
// new object[] { 9.3f, 125d, HitResult.Miss },
new object[] { 9.3f, -98d, HitResult.Ok },
new object[] { 9.3f, -99d, HitResult.Ok },
new object[] { 9.3f, -100d, HitResult.Meh },
@@ -145,13 +145,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -137d, HitResult.Miss },
new object[] { 5f, -138d, HitResult.Miss },
new object[] { 5f, 111d, HitResult.Ok },
new object[] { 5f, 112d, HitResult.Miss },
new object[] { 5f, 113d, HitResult.Miss },
new object[] { 5f, 114d, HitResult.Miss },
new object[] { 5f, 135d, HitResult.Miss },
new object[] { 5f, 136d, HitResult.Miss },
new object[] { 5f, 137d, HitResult.Miss },
new object[] { 5f, 138d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 112d, HitResult.Miss },
// new object[] { 5f, 113d, HitResult.Miss },
// new object[] { 5f, 114d, HitResult.Miss },
// new object[] { 5f, 135d, HitResult.Miss },
// new object[] { 5f, 136d, HitResult.Miss },
// new object[] { 5f, 137d, HitResult.Miss },
// new object[] { 5f, 138d, HitResult.Miss },
// OD = 9.3 test cases.
// PERFECT hit window is [ -16ms, 16ms]
@@ -172,13 +173,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 9.3f, 70d, HitResult.Ok },
new object[] { 9.3f, 71d, HitResult.Ok },
new object[] { 9.3f, 98d, HitResult.Ok },
new object[] { 9.3f, 99d, HitResult.Miss },
new object[] { 9.3f, 100d, HitResult.Miss },
new object[] { 9.3f, 101d, HitResult.Miss },
new object[] { 9.3f, 122d, HitResult.Miss },
new object[] { 9.3f, 123d, HitResult.Miss },
new object[] { 9.3f, 124d, HitResult.Miss },
new object[] { 9.3f, 125d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 9.3f, 99d, HitResult.Miss },
// new object[] { 9.3f, 100d, HitResult.Miss },
// new object[] { 9.3f, 101d, HitResult.Miss },
// new object[] { 9.3f, 122d, HitResult.Miss },
// new object[] { 9.3f, 123d, HitResult.Miss },
// new object[] { 9.3f, 124d, HitResult.Miss },
// new object[] { 9.3f, 125d, HitResult.Miss },
new object[] { 9.3f, -98d, HitResult.Ok },
new object[] { 9.3f, -99d, HitResult.Ok },
new object[] { 9.3f, -100d, HitResult.Meh },
@@ -207,13 +209,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 3.1f, 88d, HitResult.Ok },
new object[] { 3.1f, 89d, HitResult.Ok },
new object[] { 3.1f, 116d, HitResult.Ok },
new object[] { 3.1f, 117d, HitResult.Miss },
new object[] { 3.1f, 118d, HitResult.Miss },
new object[] { 3.1f, 119d, HitResult.Miss },
new object[] { 3.1f, 140d, HitResult.Miss },
new object[] { 3.1f, 141d, HitResult.Miss },
new object[] { 3.1f, 142d, HitResult.Miss },
new object[] { 3.1f, 143d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 3.1f, 117d, HitResult.Miss },
// new object[] { 3.1f, 118d, HitResult.Miss },
// new object[] { 3.1f, 119d, HitResult.Miss },
// new object[] { 3.1f, 140d, HitResult.Miss },
// new object[] { 3.1f, 141d, HitResult.Miss },
// new object[] { 3.1f, 142d, HitResult.Miss },
// new object[] { 3.1f, 143d, HitResult.Miss },
new object[] { 3.1f, -116d, HitResult.Ok },
new object[] { 3.1f, -117d, HitResult.Ok },
new object[] { 3.1f, -118d, HitResult.Meh },
@@ -253,13 +256,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -122d, HitResult.Miss },
new object[] { 5f, -123d, HitResult.Miss },
new object[] { 5f, 96d, HitResult.Ok },
new object[] { 5f, 97d, HitResult.Miss },
new object[] { 5f, 98d, HitResult.Miss },
new object[] { 5f, 99d, HitResult.Miss },
new object[] { 5f, 120d, HitResult.Miss },
new object[] { 5f, 121d, HitResult.Miss },
new object[] { 5f, 122d, HitResult.Miss },
new object[] { 5f, 123d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 97d, HitResult.Miss },
// new object[] { 5f, 98d, HitResult.Miss },
// new object[] { 5f, 99d, HitResult.Miss },
// new object[] { 5f, 120d, HitResult.Miss },
// new object[] { 5f, 121d, HitResult.Miss },
// new object[] { 5f, 122d, HitResult.Miss },
// new object[] { 5f, 123d, HitResult.Miss },
// OD = 3.1 test cases.
// PERFECT hit window is [ -16ms, 16ms]
@@ -280,13 +284,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 3.1f, 78d, HitResult.Ok },
new object[] { 3.1f, 79d, HitResult.Ok },
new object[] { 3.1f, 96d, HitResult.Ok },
new object[] { 3.1f, 97d, HitResult.Miss },
new object[] { 3.1f, 98d, HitResult.Miss },
new object[] { 3.1f, 99d, HitResult.Miss },
new object[] { 3.1f, 120d, HitResult.Miss },
new object[] { 3.1f, 121d, HitResult.Miss },
new object[] { 3.1f, 122d, HitResult.Miss },
new object[] { 3.1f, 123d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 3.1f, 97d, HitResult.Miss },
// new object[] { 3.1f, 98d, HitResult.Miss },
// new object[] { 3.1f, 99d, HitResult.Miss },
// new object[] { 3.1f, 120d, HitResult.Miss },
// new object[] { 3.1f, 121d, HitResult.Miss },
// new object[] { 3.1f, 122d, HitResult.Miss },
// new object[] { 3.1f, 123d, HitResult.Miss },
new object[] { 3.1f, -96d, HitResult.Ok },
new object[] { 3.1f, -97d, HitResult.Ok },
new object[] { 3.1f, -98d, HitResult.Meh },
@@ -327,13 +332,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -98d, HitResult.Miss },
new object[] { 5f, -99d, HitResult.Miss },
new object[] { 5f, 79d, HitResult.Ok },
new object[] { 5f, 80d, HitResult.Miss },
new object[] { 5f, 81d, HitResult.Miss },
new object[] { 5f, 82d, HitResult.Miss },
new object[] { 5f, 96d, HitResult.Miss },
new object[] { 5f, 97d, HitResult.Miss },
new object[] { 5f, 98d, HitResult.Miss },
new object[] { 5f, 99d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 80d, HitResult.Miss },
// new object[] { 5f, 81d, HitResult.Miss },
// new object[] { 5f, 82d, HitResult.Miss },
// new object[] { 5f, 96d, HitResult.Miss },
// new object[] { 5f, 97d, HitResult.Miss },
// new object[] { 5f, 98d, HitResult.Miss },
// new object[] { 5f, 99d, HitResult.Miss },
// OD = 9.3 test cases.
// This leads to "effective" OD of 13.02.
@@ -356,13 +362,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 9.3f, 50d, HitResult.Ok },
new object[] { 9.3f, 51d, HitResult.Ok },
new object[] { 9.3f, 69d, HitResult.Ok },
new object[] { 9.3f, 70d, HitResult.Miss },
new object[] { 9.3f, 71d, HitResult.Miss },
new object[] { 9.3f, 72d, HitResult.Miss },
new object[] { 9.3f, 86d, HitResult.Miss },
new object[] { 9.3f, 87d, HitResult.Miss },
new object[] { 9.3f, 88d, HitResult.Miss },
new object[] { 9.3f, 89d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 9.3f, 70d, HitResult.Miss },
// new object[] { 9.3f, 71d, HitResult.Miss },
// new object[] { 9.3f, 72d, HitResult.Miss },
// new object[] { 9.3f, 86d, HitResult.Miss },
// new object[] { 9.3f, 87d, HitResult.Miss },
// new object[] { 9.3f, 88d, HitResult.Miss },
// new object[] { 9.3f, 89d, HitResult.Miss },
new object[] { 9.3f, -69d, HitResult.Ok },
new object[] { 9.3f, -70d, HitResult.Ok },
new object[] { 9.3f, -71d, HitResult.Meh },
@@ -402,13 +409,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -191d, HitResult.Miss },
new object[] { 5f, -192d, HitResult.Miss },
new object[] { 5f, 155d, HitResult.Ok },
new object[] { 5f, 156d, HitResult.Miss },
new object[] { 5f, 157d, HitResult.Miss },
new object[] { 5f, 158d, HitResult.Miss },
new object[] { 5f, 189d, HitResult.Miss },
new object[] { 5f, 190d, HitResult.Miss },
new object[] { 5f, 191d, HitResult.Miss },
new object[] { 5f, 192d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 156d, HitResult.Miss },
// new object[] { 5f, 157d, HitResult.Miss },
// new object[] { 5f, 158d, HitResult.Miss },
// new object[] { 5f, 189d, HitResult.Miss },
// new object[] { 5f, 190d, HitResult.Miss },
// new object[] { 5f, 191d, HitResult.Miss },
// new object[] { 5f, 192d, HitResult.Miss },
};
private static readonly object[][] score_v1_non_convert_double_time_test_cases =
@@ -440,13 +448,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -205d, HitResult.Miss },
new object[] { 5f, -206d, HitResult.Miss },
new object[] { 5f, 167d, HitResult.Ok },
new object[] { 5f, 168d, HitResult.Miss },
new object[] { 5f, 169d, HitResult.Miss },
new object[] { 5f, 170d, HitResult.Miss },
new object[] { 5f, 203d, HitResult.Miss },
new object[] { 5f, 204d, HitResult.Miss },
new object[] { 5f, 205d, HitResult.Miss },
new object[] { 5f, 206d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 168d, HitResult.Miss },
// new object[] { 5f, 169d, HitResult.Miss },
// new object[] { 5f, 170d, HitResult.Miss },
// new object[] { 5f, 203d, HitResult.Miss },
// new object[] { 5f, 204d, HitResult.Miss },
// new object[] { 5f, 205d, HitResult.Miss },
// new object[] { 5f, 206d, HitResult.Miss },
};
private static readonly object[][] score_v1_non_convert_half_time_test_cases =
@@ -478,13 +487,14 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -103d, HitResult.Miss },
new object[] { 5f, -104d, HitResult.Miss },
new object[] { 5f, 83d, HitResult.Ok },
new object[] { 5f, 84d, HitResult.Miss },
new object[] { 5f, 85d, HitResult.Miss },
new object[] { 5f, 86d, HitResult.Miss },
new object[] { 5f, 101d, HitResult.Miss },
new object[] { 5f, 102d, HitResult.Miss },
new object[] { 5f, 103d, HitResult.Miss },
new object[] { 5f, 104d, HitResult.Miss },
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
// new object[] { 5f, 84d, HitResult.Miss },
// new object[] { 5f, 85d, HitResult.Miss },
// new object[] { 5f, 86d, HitResult.Miss },
// new object[] { 5f, 101d, HitResult.Miss },
// new object[] { 5f, 102d, HitResult.Miss },
// new object[] { 5f, 103d, HitResult.Miss },
// new object[] { 5f, 104d, HitResult.Miss },
};
private const double note_time = 300;
@@ -510,7 +520,7 @@ namespace osu.Game.Rulesets.Mania.Tests
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ModScoreV2()]
Mods = [new ManiaModScoreV2()]
}
};
@@ -12,7 +12,6 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneReplayStability : ReplayStabilityTestScene
{
private static readonly object[][] test_cases =
@@ -22,87 +21,79 @@ namespace osu.Game.Rulesets.Mania.Tests
// while round brackets `()` represent *open* or *exclusive* bounds.
// OD = 5 test cases.
// PERFECT hit window is [ -19.4ms, 19.4ms]
// GREAT hit window is [ -49.0ms, 49.0ms]
// GOOD hit window is [ -82.0ms, 82.0ms]
// OK hit window is [-112.0ms, 112.0ms]
// MEH hit window is [-136.0ms, 136.0ms]
// MISS hit window is [-173.0ms, 173.0ms]
// PERFECT hit window is [ -19.5ms, 19.5ms]
// GREAT hit window is [ -49.5ms, 49.5ms]
// GOOD hit window is [ -82.5ms, 82.5ms]
// OK hit window is [-112.5ms, 112.5ms]
// MEH hit window is [-136.5ms, 136.5ms]
// MISS hit window is [-173.5ms, 173.5ms]
new object[] { 5f, -19d, HitResult.Perfect },
new object[] { 5f, -19.2d, HitResult.Perfect },
new object[] { 5f, -19.38d, HitResult.Perfect },
// new object[] { 5f, -19.4d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
new object[] { 5f, -19.44d, HitResult.Great },
new object[] { 5f, -19.7d, HitResult.Great },
new object[] { 5f, -20d, HitResult.Great },
new object[] { 5f, -48d, HitResult.Great },
new object[] { 5f, -48.4d, HitResult.Great },
new object[] { 5f, -48.7d, HitResult.Great },
new object[] { 5f, -49d, HitResult.Great },
new object[] { 5f, -49.2d, HitResult.Good },
new object[] { 5f, -49.2d, HitResult.Great },
new object[] { 5f, -49.7d, HitResult.Good },
new object[] { 5f, -50d, HitResult.Good },
new object[] { 5f, -81d, HitResult.Good },
new object[] { 5f, -81.2d, HitResult.Good },
new object[] { 5f, -81.7d, HitResult.Good },
new object[] { 5f, -82d, HitResult.Good },
new object[] { 5f, -82.2d, HitResult.Ok },
new object[] { 5f, -82.2d, HitResult.Good },
new object[] { 5f, -82.7d, HitResult.Ok },
new object[] { 5f, -83d, HitResult.Ok },
new object[] { 5f, -111d, HitResult.Ok },
new object[] { 5f, -111.2d, HitResult.Ok },
new object[] { 5f, -111.7d, HitResult.Ok },
new object[] { 5f, -112d, HitResult.Ok },
new object[] { 5f, -112.2d, HitResult.Meh },
new object[] { 5f, -112.2d, HitResult.Ok },
new object[] { 5f, -112.7d, HitResult.Meh },
new object[] { 5f, -113d, HitResult.Meh },
new object[] { 5f, -135d, HitResult.Meh },
new object[] { 5f, -135.2d, HitResult.Meh },
new object[] { 5f, -135.8d, HitResult.Meh },
new object[] { 5f, -136d, HitResult.Meh },
new object[] { 5f, -136.2d, HitResult.Miss },
new object[] { 5f, -136.2d, HitResult.Meh },
new object[] { 5f, -136.7d, HitResult.Miss },
new object[] { 5f, -137d, HitResult.Miss },
// OD = 9.3 test cases.
// PERFECT hit window is [ -14.67ms, 14.67ms]
// GREAT hit window is [ -36.10ms, 36.10ms]
// GOOD hit window is [ -69.10ms, 69.10ms]
// OK hit window is [ -99.10ms, 99.10ms]
// MEH hit window is [-123.10ms, 123.10ms]
// MISS hit window is [-160.10ms, 160.10ms]
// PERFECT hit window is [ -14.5ms, 14.5ms]
// GREAT hit window is [ -36.5ms, 36.5ms]
// GOOD hit window is [ -69.5ms, 69.5ms]
// OK hit window is [ -99.5ms, 99.5ms]
// MEH hit window is [-123.5ms, 123.5ms]
// MISS hit window is [-160.5ms, 160.5ms]
new object[] { 9.3f, 14d, HitResult.Perfect },
new object[] { 9.3f, 14.2d, HitResult.Perfect },
new object[] { 9.3f, 14.6d, HitResult.Perfect },
// new object[] { 9.3f, 14.67d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
new object[] { 9.3f, 14.7d, HitResult.Great },
new object[] { 9.3f, 15d, HitResult.Great },
new object[] { 9.3f, 35d, HitResult.Great },
new object[] { 9.3f, 35.3d, HitResult.Great },
new object[] { 9.3f, 35.8d, HitResult.Great },
new object[] { 9.3f, 36.05d, HitResult.Great },
new object[] { 9.3f, 36.3d, HitResult.Good },
new object[] { 9.3f, 36.3d, HitResult.Great },
new object[] { 9.3f, 36.7d, HitResult.Good },
new object[] { 9.3f, 37d, HitResult.Good },
new object[] { 9.3f, 68d, HitResult.Good },
new object[] { 9.3f, 68.4d, HitResult.Good },
new object[] { 9.3f, 68.9d, HitResult.Good },
new object[] { 9.3f, 69.07d, HitResult.Good },
new object[] { 9.3f, 69.25d, HitResult.Ok },
new object[] { 9.3f, 69.25d, HitResult.Good },
new object[] { 9.3f, 69.85d, HitResult.Ok },
new object[] { 9.3f, 70d, HitResult.Ok },
new object[] { 9.3f, 98d, HitResult.Ok },
new object[] { 9.3f, 98.3d, HitResult.Ok },
new object[] { 9.3f, 98.6d, HitResult.Ok },
new object[] { 9.3f, 99d, HitResult.Ok },
new object[] { 9.3f, 99.3d, HitResult.Meh },
new object[] { 9.3f, 99.3d, HitResult.Ok },
new object[] { 9.3f, 99.7d, HitResult.Meh },
new object[] { 9.3f, 100d, HitResult.Meh },
new object[] { 9.3f, 122d, HitResult.Meh },
new object[] { 9.3f, 122.34d, HitResult.Meh },
new object[] { 9.3f, 122.57d, HitResult.Meh },
new object[] { 9.3f, 123.04d, HitResult.Meh },
new object[] { 9.3f, 123.45d, HitResult.Miss },
new object[] { 9.3f, 123.45d, HitResult.Meh },
new object[] { 9.3f, 123.95d, HitResult.Miss },
new object[] { 9.3f, 124d, HitResult.Miss },
};
@@ -110,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCaseSource(nameof(test_cases))]
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double note_time = 100;
const double note_time = 300;
var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
@@ -42,14 +43,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
new BeatmapStatistic
{
Name = @"Notes",
Name = BeatmapStatisticStrings.Notes,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = notes.ToString(),
BarDisplayLength = notes / (float)sum,
},
new BeatmapStatistic
{
Name = @"Hold Notes",
Name = BeatmapStatisticStrings.HoldNotes,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = holdNotes.ToString(),
BarDisplayLength = holdNotes / (float)sum,
+28 -2
View File
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania
yield return new ManiaModMirror();
if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2();
yield return new ManiaModScoreV2();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@@ -296,7 +296,7 @@ namespace osu.Game.Rulesets.Mania
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
new ManiaModScoreV2(),
};
default:
@@ -414,6 +414,32 @@ namespace osu.Game.Rulesets.Mania
}), true)
};
/// <seealso cref="ManiaHitWindows"/>
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
// notably, in mania, hit windows are designed to be independent of track playback rate (see `ManiaHitWindows.SpeedMultiplier`).
// *however*, to not make matters *too* simple, mania Hard Rock and Easy differ from all other rulesets
// in that they apply multipliers *to hit window durations directly* rather than to the Overall Difficulty attribute itself.
// because the duration of hit window durations as a function of OD is not a linear function,
// this means that multiplying the OD is *not* the same thing as multiplying the hit window duration.
// in fact, the second operation is *much* harsher and will produce values much farther outside of normal operating range
// (even negative in the case of Easy).
// stable handles this wrong on song select and just assumes that it can handle mania EZ / HR the same way as all other rulesets.
double perfectHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, ManiaHitWindows.PERFECT_WINDOW_RANGE);
if (mods.Any(m => m is ManiaModHardRock))
perfectHitWindow /= ManiaModHardRock.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
else if (mods.Any(m => m is ManiaModEasy))
perfectHitWindow /= ManiaModEasy.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(perfectHitWindow, ManiaHitWindows.PERFECT_WINDOW_RANGE);
return adjustedDifficulty;
}
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
{
return new ManiaFilterCriteria();
@@ -2,12 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -17,29 +15,21 @@ namespace osu.Game.Rulesets.Mania.Mods
/// <remarks>
/// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same.
/// </remarks>
public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject
public interface IManiaRateAdjustmentMod : IApplicableToHitObject
{
BindableNumber<double> SpeedChange { get; }
HitWindows HitWindows { get; set; }
void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty)
{
HitWindows = new ManiaHitWindows(SpeedChange.Value);
HitWindows.SetDifficulty(difficulty.OverallDifficulty);
}
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Note:
hitObject.HitWindows = HitWindows;
((ManiaHitWindows)hitObject.HitWindows).SpeedMultiplier = SpeedChange.Value;
break;
case HoldNote hold:
hold.Head.HitWindows = HitWindows;
hold.Tail.HitWindows = HitWindows;
((ManiaHitWindows)hold.Head.HitWindows).SpeedMultiplier = SpeedChange.Value;
((ManiaHitWindows)hold.Tail.HitWindows).SpeedMultiplier = SpeedChange.Value;
break;
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => Name;
public abstract int KeyCount { get; }
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
public override double ScoreMultiplier => 0.9;
public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
@@ -1,11 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModClassic : ModClassic
public class ManiaModClassic : ModClassic, IApplicableToBeatmap
{
public void ApplyToBeatmap(IBeatmap beatmap)
{
bool isConvert = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
foreach (var ho in beatmap.HitObjects)
{
switch (ho)
{
case Note note:
{
var hitWindows = (ManiaHitWindows)note.HitWindows;
hitWindows.IsConvert = isConvert;
hitWindows.ClassicModActive = true;
break;
}
case HoldNote hold:
{
var headWindows = (ManiaHitWindows)hold.Head.HitWindows;
var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows;
headWindows.IsConvert = tailWindows.IsConvert = isConvert;
headWindows.ClassicModActive = tailWindows.ClassicModActive = true;
break;
}
}
}
}
}
}
@@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
}
}
@@ -1,16 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
+21 -1
View File
@@ -2,12 +2,32 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModEasy : ModEasyWithExtraLives
public class ManiaModEasy : ModEasyWithExtraLives, IApplicableToHitObject
{
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1 / 1.4;
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
}
}
}
}
@@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
}
}
@@ -1,13 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHardRock : ModHardRock
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
{
public override double ScoreMultiplier => 1;
public override bool Ranked => false;
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
}
}
}
}
@@ -2,16 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map any harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
@@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModScoreV2 : ModScoreV2, IApplicableToBeatmap
{
public void ApplyToBeatmap(IBeatmap beatmap)
{
foreach (var ho in beatmap.HitObjects)
{
switch (ho)
{
case Note note:
{
var hitWindows = (ManiaHitWindows)note.HitWindows;
hitWindows.ScoreV2Active = true;
break;
}
case HoldNote hold:
{
var headWindows = (ManiaHitWindows)hold.Head.HitWindows;
var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows;
headWindows.ScoreV2Active = tailWindows.ScoreV2Active = true;
break;
}
}
}
}
}
}
@@ -197,6 +197,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public override void OnKilled()
{
base.OnKilled();
// flush the final state of holding on kill.
// this matters because some skin implementations like legacy skin
// insert drawables in the hierarchy that are not a child of this DHO
// (see `LegacyBodyPiece` and related machinations with `lightContainer` being added at column level)
isHolding.Value = Result.IsHolding(Time.Current);
(bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
}
@@ -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
+4 -3
View File
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
@@ -22,21 +23,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
new BeatmapStatistic
{
Name = "Circles",
Name = BeatmapStatisticStrings.Circles,
Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
BarDisplayLength = circles / (float)sum,
},
new BeatmapStatistic
{
Name = "Sliders",
Name = BeatmapStatisticStrings.Sliders,
Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
BarDisplayLength = sliders / (float)sum,
},
new BeatmapStatistic
{
Name = @"Spinners",
Name = BeatmapStatisticStrings.Spinners,
Content = spinners.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
BarDisplayLength = Math.Min(spinners / 10f, 1),
@@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
overallDifficulty = (80 - greatHitWindow) / 6;
overallDifficulty = (79.5 - greatHitWindow) / 6;
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
if (osuAttributes.SliderCount > 0)
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
get
{
if (UserAdjustedSettingsCount != 1)
if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate))
return string.Empty;
if (!CircleSize.IsDefault) return format("CS", CircleSize);
+8
View File
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
@@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!";
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
}
}
}
@@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
}
@@ -19,7 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Magnetised";
public override string Acronym => "MG";
@@ -12,9 +12,9 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@@ -83,7 +83,12 @@ namespace osu.Game.Rulesets.Osu.Mods
{
}
public override Judgement CreateJudgement() => new OsuJudgement();
public override Judgement CreateJudgement() => new StrictTrackingTailJudgement();
}
public class StrictTrackingTailJudgement : SliderTailCircle.TailJudgement
{
public override HitResult MinResult => HitResult.LargeTickMiss;
}
private partial class StrictTrackingDrawableSliderTail : DrawableSliderTail
@@ -115,10 +115,6 @@ namespace osu.Game.Rulesets.Osu.Mods
#region Reduce AR (IApplicableToDifficulty)
public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
{
}
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
// Decrease AR to increase preempt time
+2 -2
View File
@@ -21,12 +21,12 @@ namespace osu.Game.Rulesets.Osu.Objects
/// <summary>
/// The RPM required to clear the spinner at ODs [ 0, 5, 10 ].
/// </summary>
private static readonly (int min, int mid, int max) clear_rpm_range = (90, 150, 225);
private static readonly DifficultyRange clear_rpm_range = new DifficultyRange(90, 150, 225);
/// <summary>
/// The RPM required to complete the spinner and receive full score at ODs [ 0, 5, 10 ].
/// </summary>
private static readonly (int min, int mid, int max) complete_rpm_range = (250, 380, 430);
private static readonly DifficultyRange complete_rpm_range = new DifficultyRange(250, 380, 430);
public double EndTime
{
+5 -4
View File
@@ -40,6 +40,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu
@@ -365,18 +366,18 @@ namespace osu.Game.Rulesets.Osu
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
/// <seealso cref="OsuHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
preempt /= rate;
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, OsuHitWindows.GREAT_WINDOW_RANGE);
greatHitWindow /= rate;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, OsuHitWindows.GREAT_WINDOW_RANGE);
return adjustedDifficulty;
}
+36 -8
View File
@@ -1,24 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuHitWindows : HitWindows
{
public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(80, 50, 20);
public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(140, 100, 60);
public static readonly DifficultyRange MEH_WINDOW_RANGE = new DifficultyRange(200, 150, 100);
/// <summary>
/// osu! ruleset has a fixed miss window regardless of difficulty settings.
/// </summary>
public const double MISS_WINDOW = 400;
internal static readonly DifficultyRange[] OSU_RANGES =
{
new DifficultyRange(HitResult.Great, 80, 50, 20),
new DifficultyRange(HitResult.Ok, 140, 100, 60),
new DifficultyRange(HitResult.Meh, 200, 150, 100),
new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW),
};
private double great;
private double ok;
private double meh;
public override bool IsHitResultAllowed(HitResult result)
{
@@ -34,6 +36,32 @@ namespace osu.Game.Rulesets.Osu.Scoring
return false;
}
protected override DifficultyRange[] GetRanges() => OSU_RANGES;
public override void SetDifficulty(double difficulty)
{
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE)) - 0.5;
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE)) - 0.5;
meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, MEH_WINDOW_RANGE)) - 0.5;
}
public override double WindowFor(HitResult result)
{
switch (result)
{
case HitResult.Great:
return great;
case HitResult.Ok:
return ok;
case HitResult.Meh:
return meh;
case HitResult.Miss:
return MISS_WINDOW;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
}
}
@@ -171,13 +171,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 6 });
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 10 });
var hitWindows = new HitWindows();
var hitWindows = new DefaultHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
PerformTest(new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time - hitWindows.WindowFor(HitResult.Great), TaikoAction.LeftCentre),
new TaikoReplayFrame(hit_time - (hitWindows.WindowFor(HitResult.Great) + 0.1), TaikoAction.LeftCentre),
}, beatmap);
AssertJudgementCount(1);
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Mods;
namespace osu.Game.Rulesets.Taiko.Tests
{
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModHalfTime()]);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01));
}
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModDoubleTime()]);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01));
}
@@ -15,7 +15,6 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
{
protected override string? ExportLocation => null;
@@ -177,7 +176,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new TaikoModHardRock()]
Mods = [new TaikoModEasy()]
}
};
@@ -12,7 +12,6 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneReplayStability : ReplayStabilityTestScene
{
private static readonly object[][] test_cases =
@@ -22,40 +21,38 @@ namespace osu.Game.Rulesets.Taiko.Tests
// while round brackets `()` represent *open* or *exclusive* bounds.
// OD = 5 test cases.
// GREAT hit window is [-35ms, 35ms]
// OK hit window is [-80ms, 80ms]
// MISS hit window is [-95ms, 95ms]
// GREAT hit window is [-34.5ms, 34.5ms]
// OK hit window is [-79.5ms, 79.5ms]
// MISS hit window is [-94.5ms, 94.5ms]
new object[] { 5f, -34d, HitResult.Great },
new object[] { 5f, -34.2d, HitResult.Great },
new object[] { 5f, -34.7d, HitResult.Great },
new object[] { 5f, -35d, HitResult.Great },
new object[] { 5f, -34.7d, HitResult.Ok },
new object[] { 5f, -35d, HitResult.Ok },
new object[] { 5f, -35.2d, HitResult.Ok },
new object[] { 5f, -35.8d, HitResult.Ok },
new object[] { 5f, -36d, HitResult.Ok },
new object[] { 5f, -79d, HitResult.Ok },
new object[] { 5f, -79.3d, HitResult.Ok },
new object[] { 5f, -79.7d, HitResult.Ok },
new object[] { 5f, -80d, HitResult.Ok },
new object[] { 5f, -79.7d, HitResult.Miss },
new object[] { 5f, -80d, HitResult.Miss },
new object[] { 5f, -80.2d, HitResult.Miss },
new object[] { 5f, -80.8d, HitResult.Miss },
new object[] { 5f, -81d, HitResult.Miss },
// OD = 7.8 test cases.
// GREAT hit window is [-26.6ms, 26.6ms]
// OK hit window is [-63.2ms, 63.2ms]
// MISS hit window is [-81.0ms, 81.0ms]
new object[] { 7.8f, -26d, HitResult.Great },
new object[] { 7.8f, -26.4d, HitResult.Great },
new object[] { 7.8f, -26.59d, HitResult.Great },
new object[] { 7.8f, -26.8d, HitResult.Ok },
new object[] { 7.8f, -27d, HitResult.Ok },
new object[] { 7.8f, -27.1d, HitResult.Ok },
new object[] { 7.8f, -63d, HitResult.Ok },
new object[] { 7.8f, -63.18d, HitResult.Ok },
new object[] { 7.8f, -63.4d, HitResult.Ok },
new object[] { 7.8f, -63.7d, HitResult.Miss },
new object[] { 7.8f, -64d, HitResult.Miss },
new object[] { 7.8f, -64.2d, HitResult.Miss },
// GREAT hit window is [-25.5ms, 25.5ms]
// OK hit window is [-62.5ms, 62.5ms]
// MISS hit window is [-80.5ms, 80.5ms]
new object[] { 7.8f, -25d, HitResult.Great },
new object[] { 7.8f, -25.4d, HitResult.Great },
new object[] { 7.8f, -25.8d, HitResult.Ok },
new object[] { 7.8f, -26d, HitResult.Ok },
new object[] { 7.8f, -26.1d, HitResult.Ok },
new object[] { 7.8f, -62d, HitResult.Ok },
new object[] { 7.8f, -62.4d, HitResult.Ok },
new object[] { 7.8f, -62.7d, HitResult.Miss },
new object[] { 7.8f, -63d, HitResult.Miss },
new object[] { 7.8f, -63.2d, HitResult.Miss },
};
[TestCaseSource(nameof(test_cases))]
@@ -14,26 +14,27 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected override IResourceStore<byte[]> RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples)));
[TestCase("taiko-normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
[TestCase("taiko-normal-hitnormal2", "taiko-normal-hitnormal")]
[TestCase("hitnormal", "hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string beatmapSkinSampleName, string userSkinSampleName)
{
SetupSkins(expectedSample, expectedSample);
SetupSkins(beatmapSkinSampleName, userSkinSampleName);
CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expectedSample);
AssertBeatmapLookup(beatmapSkinSampleName);
}
[TestCase("taiko-normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
[TestCase("", "taiko-normal-hitnormal")]
[TestCase("taiko-normal-hitnormal", "taiko-normal-hitnormal")]
[TestCase("", "hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string beatmapSkinSampleName, string userSkinSampleName)
{
SetupSkins(string.Empty, expectedSample);
SetupSkins(beatmapSkinSampleName, userSkinSampleName);
CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
AssertUserLookup(expectedSample);
AssertUserLookup(userSkinSampleName);
}
[TestCase("taiko-normal-hitnormal2")]
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Beatmaps
@@ -22,21 +23,21 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
new BeatmapStatistic
{
Name = @"Hits",
Name = BeatmapStatisticStrings.Hits,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = hits.ToString(),
BarDisplayLength = hits / (float)sum,
},
new BeatmapStatistic
{
Name = @"Drumrolls",
Name = BeatmapStatisticStrings.Drumrolls,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = drumRolls.ToString(),
BarDisplayLength = drumRolls / (float)sum,
},
new BeatmapStatistic
{
Name = @"Swells",
Name = BeatmapStatisticStrings.Swells,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
Content = swells.ToString(),
BarDisplayLength = Math.Min(swells / 10f, 1),
@@ -25,16 +25,16 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
get
{
if (UserAdjustedSettingsCount != 1)
if (!IsExactlyOneSettingChanged(ScrollSpeed, OverallDifficulty, DrainRate))
return string.Empty;
if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed);
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
if (!DrainRate.IsDefault) return format("HP", DrainRate);
if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed, 2);
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty, 1);
if (!DrainRate.IsDefault) return format("HP", DrainRate, 1);
return string.Empty;
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
string format(string acronym, DifficultyBindable bindable, int digits) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(digits)}";
}
}
@@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
difficulty.SliderMultiplier *= slider_multiplier;
}
}
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.SliderMultiplier *= slider_multiplier;
}
}
@@ -1,18 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Scoring
{
public class TaikoHitWindows : HitWindows
{
internal static readonly DifficultyRange[] TAIKO_RANGES =
{
new DifficultyRange(HitResult.Great, 50, 35, 20),
new DifficultyRange(HitResult.Ok, 120, 80, 50),
new DifficultyRange(HitResult.Miss, 135, 95, 70),
};
public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(50, 35, 20);
public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(120, 80, 50);
public static readonly DifficultyRange MISS_WINDOW_RANGE = new DifficultyRange(135, 95, 70);
private double great;
private double ok;
private double miss;
public override bool IsHitResultAllowed(HitResult result)
{
@@ -27,6 +30,29 @@ namespace osu.Game.Rulesets.Taiko.Scoring
return false;
}
protected override DifficultyRange[] GetRanges() => TAIKO_RANGES;
public override void SetDifficulty(double difficulty)
{
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE)) - 0.5;
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE)) - 0.5;
miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, MISS_WINDOW_RANGE)) - 0.5;
}
public override double WindowFor(HitResult result)
{
switch (result)
{
case HitResult.Great:
return great;
case HitResult.Ok:
return ok;
case HitResult.Miss:
return miss;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
}
}
+5 -4
View File
@@ -38,6 +38,7 @@ using osu.Game.Rulesets.Taiko.Configuration;
using osu.Game.Rulesets.Taiko.Edit.Setup;
using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Taiko
{
@@ -270,14 +271,14 @@ namespace osu.Game.Rulesets.Taiko
}
/// <seealso cref="TaikoHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double rate = ModUtils.CalculateRateWithMods(mods);
var greatHitWindowRange = TaikoHitWindows.TAIKO_RANGES.Single(range => range.Result == HitResult.Great);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE);
greatHitWindow /= rate;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, TaikoHitWindows.GREAT_WINDOW_RANGE);
return adjustedDifficulty;
}
@@ -211,6 +211,31 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8));
}
[Test]
public void TestEncodeStabilityOfSliderWithFractionalCoordinates()
{
Slider originalSlider = new Slider
{
Position = new Vector2(0.6f),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(25.6f, 78.4f)),
new PathControlPoint(new Vector2(55.8f, 34.2f)),
})
};
var beatmap = new Beatmap
{
HitObjects = { originalSlider }
};
var encoded = encodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
var decodedAfterEncode = decodeFromLegacy(encoded, string.Empty, version: LegacyBeatmapEncoder.FIRST_LAZER_VERSION);
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
Assert.That(decodedSlider.Path.ControlPoints.Select(p => p.Position),
Is.EquivalentTo(originalSlider.Path.ControlPoints.Select(p => p.Position)));
}
private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
{
// equal to null, no need to SequenceEqual
@@ -233,11 +258,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name)
private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name, int version = LegacyDecoder<Beatmap>.LATEST_VERSION)
{
using (var reader = new LineBufferedReader(stream))
{
var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader);
var beatmap = new LegacyBeatmapDecoder(version) { ApplyOffsets = false }.Decode(reader);
var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name);
stream.Seek(0, SeekOrigin.Begin);
beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader);
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using ManagedBass;
using Moq;
using NUnit.Framework;
using osu.Framework.Audio.Track;
@@ -10,7 +11,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using osuTK.Audio;
namespace osu.Game.Tests.Editing.Checks
{
@@ -28,9 +31,13 @@ namespace osu.Game.Tests.Editing.Checks
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }
Metadata = new BeatmapMetadata()
}
};
// 0 = No output device. This still allows decoding.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
throw new AudioException("Could not initialize Bass.");
}
[Test]
@@ -54,6 +61,14 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestAcceptableOgg()
{
var context = getContext(208, useOgg: true);
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestNullBitrate()
{
@@ -87,6 +102,17 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
}
[Test]
public void TestTooHighBitrateOgg()
{
var context = getContext(250, useOgg: true);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
}
[Test]
public void TestTooLowBitrate()
{
@@ -98,24 +124,41 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate);
}
private BeatmapVerifierContext getContext(int? audioBitrate)
private BeatmapVerifierContext getContext(int? audioBitrate, bool useOgg = false)
{
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate).Object);
// Update the audio filename and beatmapset files based on the format being tested
string audioFileName = useOgg ? "abc123.ogg" : "abc123.mp3";
string fileExtension = useOgg ? "ogg" : "mp3";
beatmap.Metadata.AudioFile = audioFileName;
beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo
{
Files = { CheckTestHelpers.CreateMockFile(fileExtension) }
};
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate, useOgg).Object);
}
/// <summary>
/// Returns the mock of the working beatmap with the given audio properties.
/// </summary>
/// <param name="audioBitrate">The bitrate of the audio file the beatmap uses.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate)
/// <param name="useOgg">Whether to use an OGG sample instead of MP3.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate, bool useOgg = false)
{
var mockTrack = new Mock<OsuTestScene.ClockBackedTestWorkingBeatmap.TrackVirtualManual>(new FramedClock(), "virtual");
mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate);
// Use real audio samples for format detection
string samplePath = useOgg ? "Samples/test-sample.ogg" : "Samples/test-sample-cut.mp3";
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack.Object);
// Return a fresh stream each time GetStream is called to avoid disposed stream issues
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(() => TestResources.OpenResource(samplePath));
return mockWorkingBeatmap;
}
}
@@ -359,7 +359,7 @@ namespace osu.Game.Tests.Gameplay
}
public override Judgement CreateJudgement() => new TestJudgement(maxResult);
protected override HitWindows CreateHitWindows() => new HitWindows();
protected override HitWindows CreateHitWindows() => new DefaultHitWindows();
private class TestJudgement : Judgement
{
@@ -64,11 +64,9 @@ namespace osu.Game.Tests.Gameplay
/// <summary>
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
/// normal-hitnormal2
/// normal-hitnormal
/// hitnormal
/// </summary>
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
{
@@ -162,7 +160,6 @@ namespace osu.Game.Tests.Gameplay
/// Tests that a control point that provides a custom sample of 2 causes <see cref="TestDefaultCustomSampleFromBeatmap"/>.
/// </summary>
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestControlPointCustomSampleFromBeatmap(string sampleName)
{
@@ -166,11 +166,7 @@ namespace osu.Game.Tests.Mods
/// </summary>
private BeatmapDifficulty applyDifficulty(BeatmapDifficulty difficulty)
{
// ensure that ReadFromDifficulty doesn't pollute the values.
var newDifficulty = difficulty.Clone();
testMod.ReadFromDifficulty(difficulty);
testMod.ApplyToDifficulty(newDifficulty);
return newDifficulty;
}
+7 -6
View File
@@ -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!");
}
}
});
@@ -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());
}
}
}
@@ -136,14 +136,39 @@ namespace osu.Game.Tests.NonVisual
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++;
@@ -32,7 +32,7 @@ using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osu.Game.Storyboards.Drawables;
using osu.Game.Tests.Resources;
using osuTK;
@@ -325,7 +325,7 @@ namespace osu.Game.Tests.Visual.Background
private void setupUserSettings()
{
AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen());
AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmapInfo != null);
AddUntilStep("Song select has selection", () => songSelect.Carousel?.CurrentSelection != null);
AddStep("Set default user settings", () =>
{
SelectedMods.Value = new[] { new OsuModNoFail() };
@@ -340,7 +340,7 @@ namespace osu.Game.Tests.Visual.Background
rulesets?.Dispose();
}
private partial class DummySongSelect : PlaySongSelect
private partial class DummySongSelect : SoloSongSelect
{
private FadeAccessibleBackground background;
@@ -355,7 +355,7 @@ namespace osu.Game.Tests.Visual.Background
public readonly Bindable<double> DimLevel = new BindableDouble();
public readonly Bindable<double> BlurLevel = new BindableDouble();
public new BeatmapCarousel Carousel => base.Carousel;
public BeatmapCarousel Carousel => this.ChildrenOfType<BeatmapCarousel>().SingleOrDefault();
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
@@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim);
public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White;
public bool IsBackgroundUndimmed() => background.CurrentColour == new Color4(0.9f, 0.9f, 0.9f, 1f);
public bool IsUserBlurApplied() => Precision.AlmostEquals(background.CurrentBlur, new Vector2((float)BlurLevel.Value * BackgroundScreenBeatmap.USER_BLUR_FACTOR), 0.1f);
@@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
public bool IsBackgroundBlur() => Precision.AlmostEquals(background.CurrentBlur, new Vector2(BACKGROUND_BLUR), 0.1f);
public bool IsBackgroundBlur() => Precision.AlmostBigger(background.CurrentBlur.X, 0, 0.1f);
public bool CheckBackgroundBlur(Vector2 expected) => Precision.AlmostEquals(background.CurrentBlur, expected, 0.1f);
@@ -15,7 +15,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
@@ -190,7 +190,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Set tags again", () => EditorBeatmap.BeatmapInfo.Metadata.Tags = tags_to_discard);
AddStep("Exit editor", () => Editor.Exit());
AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save);
}
@@ -14,7 +14,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Editing
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Editing
() => Is.EqualTo(1));
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
addStepClickLink("00:00:000 (1)", waitForSeek: false);
AddUntilStep("received 'must be in edit'",
@@ -151,12 +151,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("Wait for song select", () =>
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.BeatmapSetsLoaded
&& Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect
&& songSelect.CarouselItemsPresented
);
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
AddStep("Open editor for ruleset", () =>
((PlaySongSelect)Game.ScreenStack.CurrentScreen)
((SoloSongSelect)Game.ScreenStack.CurrentScreen)
.Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
);
AddUntilStep("Wait for editor open", () => editor?.ReadyForUse == true);
@@ -9,7 +9,6 @@ using osu.Game.Audio;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
@@ -74,8 +73,8 @@ namespace osu.Game.Tests.Visual.Gameplay
//
// We want to keep seeking while asserting various test conditions, so
// continue to seek until we unset the flag.
var gameplayClockContainer = Player.ChildrenOfType<GameplayClockContainer>().First();
gameplayClockContainer.Seek(gameplayClockContainer.CurrentTime > 30000 ? 0 : 60000);
var gameplayClockContainer = Player?.GameplayClockContainer;
gameplayClockContainer?.Seek(gameplayClockContainer.CurrentTime > 30000 ? 0 : 60000);
}
}
@@ -65,30 +65,30 @@ namespace osu.Game.Tests.Visual.Gameplay
{
new HitCircle
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
},
new HitCircle
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }
},
new HitCircle
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
},
new HitCircle
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
},
new Slider
{
HitWindows = new HitWindows(),
HitWindows = new DefaultHitWindows(),
StartTime = t += spacing,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
@@ -6,11 +6,16 @@ using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -157,6 +162,51 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500);
}
[Test]
public void TestReplayDoesNotFailUntilRunningOutOfFrames()
{
var score = new Score
{
ScoreInfo = TestResources.CreateTestScoreInfo(Beatmap.Value.BeatmapInfo),
Replay = new Replay
{
Frames =
{
new OsuReplayFrame(0, Vector2.Zero),
new OsuReplayFrame(10000, Vector2.Zero),
}
}
};
score.ScoreInfo.Mods = [];
score.ScoreInfo.Rank = ScoreRank.F;
AddStep("set global state", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
Ruleset.Value = Beatmap.Value.BeatmapInfo.Ruleset;
SelectedMods.Value = score.ScoreInfo.Mods;
});
AddStep("create player", () => Player = new TestReplayPlayer(score, showResults: false));
AddStep("load player", () => LoadScreen(Player));
AddUntilStep("wait for loaded", () => Player.IsCurrentScreen());
AddStep("seek to 8000", () => Player.Seek(8000));
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddAssert("player failed after 10000", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(10000));
}
[Test]
public void TestPlayerLoaderSettingsHover()
{
loadPlayerWithBeatmap();
AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False);
AddStep("move mouse to right of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopRight));
AddUntilStep("wait for settings overlay visible", () => settingsOverlay().Expanded.Value, () => Is.True);
AddStep("move mouse to centre of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False);
PlayerSettingsOverlay settingsOverlay() => Player.ChildrenOfType<PlayerSettingsOverlay>().Single();
}
private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
{
AddStep("create player", () =>
@@ -0,0 +1,75 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Visual.Menus
{
public partial class TestSceneToolbarRulesetSelector : OsuTestScene
{
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets, OsuGameBase game)
{
TestRuleset.Resources = new TestResourceStore(game.Resources);
Dependencies.CacheAs<RulesetStore>(new TestRulesetStore(rulesets));
Child = new Container
{
RelativeSizeAxes = Axes.X,
Height = Toolbar.HEIGHT,
Child = new ToolbarRulesetSelector(),
};
}
private class TestRulesetStore : RulesetStore
{
public TestRulesetStore(RulesetStore store)
{
AvailableRulesets = store.AvailableRulesets.Append(new TestRuleset().RulesetInfo);
}
public override IEnumerable<RulesetInfo> AvailableRulesets { get; }
}
private class TestRuleset : Ruleset
{
public static IResourceStore<byte[]> Resources { get; set; } = null!;
public override IEnumerable<Mod> GetModsFor(ModType type) => Enumerable.Empty<Mod>();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => null!;
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
public override IResourceStore<byte[]> CreateResourceStore() => Resources;
public override string Description => "Test Ruleset";
public override string ShortName => "test";
}
private class TestResourceStore : ResourceStore<byte[]>
{
public TestResourceStore(IResourceStore<byte[]> store)
: base(store)
{
}
protected override IEnumerable<string> GetFilenames(string name) => base.GetFilenames(name)
.Select(s => s.Replace("UI/ruleset-select-test", "Gameplay/failsound"));
}
}
}
@@ -150,6 +150,24 @@ namespace osu.Game.Tests.Visual.Menus
});
});
// cross-reference: `TestSceneOverallRanking.TestRoundingTreatment()`.
AddStep("Test rounding treatment", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
GlobalRank = 111_111,
PP = 5071.495M
},
new UserStatistics
{
GlobalRank = 111_111,
PP = 5072.99M
});
});
AddStep("No change 1", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
@@ -53,6 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private BeatmapSetInfo importedSet2 = null!;
private TestMultiplayerComponents multiplayerComponents = null!;
@@ -81,12 +82,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
importedSet2 = beatmaps.Import(CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet!)!.Value.Detach();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
});
AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
@@ -1095,6 +1099,112 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("global beatmap matches second playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID));
}
/// <summary>
/// Tests that the local user is not able to change their play style if they haven't downloaded the beatmap (beatmap carousel will be empty).
/// </summary>
[Test]
public void TestCanNotEditDifficultyIfNotDownloaded()
{
IBeatmap roomBeatmap = null!;
createRoom(() =>
{
roomBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
return new Room
{
Name = "Test Room",
QueueMode = QueueMode.AllPlayers,
Playlist =
[
new PlaylistItem(CreateAPIBeatmap(roomBeatmap.BeatmapInfo))
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Freestyle = true
}
]
};
});
AddAssert("editing disallowed", () => !this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().UserStyleEditingEnabled);
AddStep("import beatmap", () => beatmaps.Import(roomBeatmap.BeatmapInfo.BeatmapSet!));
AddAssert("editing allowed", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().UserStyleEditingEnabled);
}
/// <summary>
/// Test that the user selection screen is not exited when the beatmap is changed to the same set.
/// </summary>
[Test]
public void TestUserStyleSelectionDoesNotExitWhenBeatmapSetNotChanged()
{
createRoom(() => new Room
{
Name = "Test Room",
QueueMode = QueueMode.AllPlayers,
Playlist =
[
new PlaylistItem(importedSet.Beatmaps.First())
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Freestyle = true
}
]
});
AddStep("open user style selection", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().ShowUserStyleSelect());
AddUntilStep("style selection screen opened", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() == true);
AddStep("change beatmap", () =>
{
var newItem = multiplayerClient.ServerRoom!.Playlist[0].Clone();
var newBeatmap = importedSet.Beatmaps.Last();
newItem.BeatmapID = newBeatmap.OnlineID;
newItem.BeatmapChecksum = newBeatmap.MD5Hash;
multiplayerClient.EditPlaylistItem(newItem);
});
AddWaitStep("wait for potential beatmap change", 2);
AddAssert("style selection screen still open", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() == true);
}
/// <summary>
/// Tests that the user selection screen is exited when the beatmap is changed to another set.
/// </summary>
[Test]
public void TestUserStyleSelectionExitedWhenBeatmapSetChanged()
{
createRoom(() => new Room
{
Name = "Test Room",
QueueMode = QueueMode.AllPlayers,
Playlist =
[
new PlaylistItem(importedSet.Beatmaps.First())
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Freestyle = true
}
]
});
AddStep("open user style selection", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().ShowUserStyleSelect());
AddUntilStep("style selection screen opened", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() == true);
AddStep("change beatmap set", () =>
{
var newItem = multiplayerClient.ServerRoom!.Playlist[0].Clone();
var newBeatmap = importedSet2.Beatmaps.Last();
newItem.BeatmapID = newBeatmap.OnlineID;
newItem.BeatmapChecksum = newBeatmap.MD5Hash;
multiplayerClient.EditPlaylistItem(newItem);
});
AddUntilStep("selected beatmap changed", () => Beatmap.Value.BeatmapInfo.Equals(importedSet2.Beatmaps.First()));
AddUntilStep("style selection screen closed", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() != true);
}
private void enterGameplay()
{
pressReadyButton();
@@ -13,6 +13,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
@@ -53,10 +54,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
Dependencies.CacheAs<BeatmapStore>(new RealmDetachedBeatmapStore());
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
}
public override void SetUpSteps()
@@ -308,7 +316,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => RoomJoined);
ClickButtonWhenEnabled<UserModSelectButton>();
AddAssert("mod select shows unranked", () => this.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
AddUntilStep("mod select shows unranked", () => this.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
AddStep("select flashlight", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick());
@@ -165,23 +165,75 @@ namespace osu.Game.Tests.Visual.Multiplayer
Name = "A host-only room",
QueueMode = QueueMode.HostOnly,
Type = MatchType.HeadToHead,
RoomID = 1337,
}),
new MultiplayerRoomPanel(new Room
{
Name = "An all-players, team-versus room",
QueueMode = QueueMode.AllPlayers,
Type = MatchType.TeamVersus
Type = MatchType.TeamVersus,
RoomID = 1338,
}),
new MultiplayerRoomPanel(new Room
{
Name = "A round-robin room",
QueueMode = QueueMode.AllPlayersRoundRobin,
Type = MatchType.HeadToHead
Type = MatchType.HeadToHead,
RoomID = 1339,
}),
}
});
}
[Test]
public void TestRoomWithLongTitle()
{
AddStep("create rooms", () => Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new[]
{
new MultiplayerRoomPanel(new Room
{
Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.",
QueueMode = QueueMode.HostOnly,
Type = MatchType.HeadToHead,
RoomID = 1337,
}),
}
});
}
[Test]
public void TestRoomWithUpdatedRoomID()
{
Room room = null!;
AddStep("create rooms", () => Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new[]
{
new MultiplayerRoomPanel(room = new Room
{
Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.",
QueueMode = QueueMode.HostOnly,
Type = MatchType.HeadToHead,
}),
}
});
AddWaitStep("wait", 3);
AddStep("set room ID", () => room.RoomID = 1337);
AddWaitStep("wait", 3);
AddStep("clear room ID", () => room.RoomID = null);
}
private RoomPanel createLoungeRoom(Room room)
{
room.Host ??= new APIUser { Username = "peppy", Id = 2 };
@@ -26,8 +26,8 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osuTK.Input;
@@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("exit", () => getEditor().Exit());
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect
&& songSelect.Beatmap.Value is DummyWorkingBeatmap);
}
@@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("switch ruleset at song select", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddStep("open editor", () => ((SoloSongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddAssert("editor ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
@@ -187,8 +187,8 @@ namespace osu.Game.Tests.Visual.Navigation
});
AddAssert("gameplay ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(SoloSongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
AddAssert("previous ruleset restored", () => Game.Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo));
}
@@ -289,8 +289,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true));
AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying);
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(SoloSongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying);
AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true));
@@ -352,13 +352,13 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("wait for song select",
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.BeatmapSetsLoaded);
&& Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect
&& songSelect.CarouselItemsPresented);
}
private void openEditor()
{
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddStep("open editor", () => ((SoloSongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
}
@@ -5,7 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Navigation
InputManager.Key(Key.P);
});
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
}
[Test]
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("state is play", () => buttons.State == ButtonSystemState.Play);
AddStep("press P", () => InputManager.Key(Key.P));
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
}
}
}
@@ -15,7 +15,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
@@ -54,10 +54,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
PushAndConfirm(() => new PlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault);
AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for carousel load", () => songSelect.CarouselItemsPresented);
AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
@@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Navigation
.AsEnumerable()
.First(k => k.RulesetName == "osu" && k.ActionInt == 0);
private Screens.Select.SongSelect songSelect => Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect;
private SoloSongSelect songSelect => Game.ScreenStack.CurrentScreen as SoloSongSelect;
private Player player => Game.ScreenStack.CurrentScreen as Player;
@@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Game.Configuration;
using osu.Game.Screens.Play;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
@@ -83,9 +84,9 @@ namespace osu.Game.Tests.Visual.Navigation
private void loadToPlayerNonBreakTime()
{
Player? player = null;
Screens.Select.SongSelect songSelect = null!;
PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
SoloSongSelect songSelect = null!;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
@@ -17,9 +17,9 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPerformAtSongSelect()
{
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(SoloSongSelect) }));
AddAssert("did perform", () => actionPerformed);
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
}
[Test]
public void TestPerformAtMenuFromSongSelect()
{
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("returned to menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("Press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(SoloSongSelect) }));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
AddAssert("did perform", () => actionPerformed);
}
@@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Navigation
private void importAndWaitForSongSelect()
{
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddUntilStep("beatmap updated", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID == 241526);
}
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -81,11 +81,9 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(osuImport);
var maniaImport = importBeatmap(2, new ManiaRuleset().RulesetInfo);
confirmBeatmapInSongSelect(maniaImport);
presentAndConfirm(maniaImport);
var catchImport = importBeatmap(3, new CatchRuleset().RulesetInfo);
confirmBeatmapInSongSelect(catchImport);
presentAndConfirm(catchImport);
// Ruleset is always changed.
@@ -103,11 +101,9 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(osuImport);
var maniaImport = importBeatmap(2, new ManiaRuleset().RulesetInfo);
confirmBeatmapInSongSelect(maniaImport);
presentAndConfirm(maniaImport);
var catchImport = importBeatmap(3, new CatchRuleset().RulesetInfo);
confirmBeatmapInSongSelect(catchImport);
presentAndConfirm(catchImport);
// force ruleset to osu!mania
@@ -178,14 +174,14 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddUntilStep("wait for carousel loaded", () =>
{
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().SingleOrDefault()?.IsLoaded == true;
});
AddUntilStep("beatmap in song select", () =>
{
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().Single().BeatmapSets.Any(b => b.MatchesOnlineID(getImport()));
var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetInfo bsi && bsi.MatchesOnlineID(getImport()));
});
}
@@ -193,7 +189,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset));
}
@@ -203,7 +199,7 @@ namespace osu.Game.Tests.Visual.Navigation
Predicate<BeatmapInfo> pred = b => b.OnlineID == importedID * 1024 + 2;
AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 1024 + 2));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.OnlineID, () => Is.EqualTo(expectedRulesetOnlineID ?? getImport().Beatmaps.First().Ruleset.OnlineID));
}
@@ -18,7 +18,8 @@ using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using FilterControl = osu.Game.Screens.SelectV2.FilterControl;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -96,9 +97,9 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestFromSongSelectWithFilter([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq");
AddStep("filter to nothing", () => ((SoloSongSelect)Game.ScreenStack.CurrentScreen).ChildrenOfType<FilterControl>().Single().Search("fdsajkl;fgewq"));
AddUntilStep("wait for no results", () => Beatmap.IsDefault);
var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
@@ -109,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
@@ -121,7 +122,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestFromSongSelect([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
var firstImport = importScore(1);
presentAndConfirm(firstImport, type);
@@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
var firstImport = importScore(1);
presentAndConfirm(firstImport, type);
@@ -147,7 +148,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestPresentTwoImportsWithSameOnlineIDButDifferentHashes([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
var firstImport = importScore(1);
presentAndConfirm(firstImport, type);
@@ -160,7 +161,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestScoreRefetchIgnoresEmptyHash()
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented);
importScore(-1, hash: string.Empty);
importScore(3, hash: @"deadbeef");
@@ -26,7 +26,6 @@ using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Overlays;
@@ -49,20 +48,13 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Resources;
using osu.Game.Utils;
using osuTK;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.Select.BeatmapCarousel;
using CollectionDropdown = osu.Game.Collections.CollectionDropdown;
using FilterControl = osu.Game.Screens.Select.FilterControl;
using FooterButtonRandom = osu.Game.Screens.Select.FooterButtonRandom;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -83,6 +75,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
PushAndConfirm(() => playlistScreen = new Screens.OnlinePlay.Playlists.Playlists());
AddUntilStep("wait for lounge", () => (playlistScreen.CurrentSubScreen as LoungeSubScreen)?.IsLoaded == true);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -146,62 +139,70 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitSongSelectWithEscape()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
ModSelectOverlay modSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddStep("Show mods overlay", () =>
{
modSelect = songSelect!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect.Show();
});
AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible);
pushEscape();
AddAssert("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
AddAssert("Overlay was hidden", () => modSelect.State.Value == Visibility.Hidden);
exitViaEscapeAndConfirm();
}
[Test]
public void TestEnterGameplayWhileFilteringToNoSelection()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("force selection", () =>
AddStep("force selection and change filter immediately", () =>
{
songSelect.FinaliseSelection();
songSelect.FilterControl.CurrentTextSearch.Value = "test";
InputManager.Key(Key.Enter);
songSelect.ChildrenOfType<FilterControl>().Single().Search("test");
});
AddUntilStep("wait for player", () => !songSelect.IsCurrentScreen());
AddStep("return to song select", () => songSelect.MakeCurrent());
AddUntilStep("wait for selection lost", () => songSelect.Beatmap.IsDefault);
AddUntilStep("selection not lost", () => !songSelect.Beatmap.IsDefault);
AddUntilStep("placeholder visible", () => songSelect.ChildrenOfType<NoResultsPlaceholder>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestSongSelectBackActionHandling()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for filter control", () => filterControlTextBox().IsLoaded);
AddStep("set filter", () => filterControlTextBox().Current.Value = "test");
AddStep("press back", () => InputManager.Click(MouseButton.Button1));
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen, () => Is.EqualTo(songSelect));
AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value));
AddStep("set filter again", () => filterControlTextBox().Current.Value = "test");
AddStep("open collections dropdown", () =>
{
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionDropdown>().Single());
InputManager.MoveMouseTo(songSelect.ChildrenOfType<Screens.SelectV2.CollectionDropdown>().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("press back once", () => InputManager.Click(MouseButton.Button1));
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
AddAssert("collections dropdown closed", () => songSelect
.ChildrenOfType<CollectionDropdown>().Single()
.ChildrenOfType<Screens.SelectV2.CollectionDropdown>().Single()
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1));
@@ -210,17 +211,17 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back a third time", () => InputManager.Click(MouseButton.Button1));
ConfirmAtMainMenu();
TextBox filterControlTextBox() => songSelect.ChildrenOfType<FilterControl.FilterControlTextBox>().Single();
FilterControl.SongSelectSearchTextBox filterControlTextBox() => songSelect.ChildrenOfType<FilterControl.SongSelectSearchTextBox>().Single();
}
[Test]
public void TestSongSelectRandomRewindButton()
{
Guid? originalSelection = null;
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("Add two beatmaps", () =>
{
@@ -247,41 +248,6 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestSongSelectScrollHandling()
{
TestPlaySongSelect songSelect = null;
double scrollPosition = 0;
AddStep("set game volume to max", () => Game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.VolumeUniversal, 1d));
AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType<VolumeOverlay>().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden));
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition());
AddStep("move to left side", () => InputManager.MoveMouseTo(
songSelect.ChildrenOfType<Screens.Select.SongSelect.LeftSideInteractionContainer>().Single().ScreenSpaceDrawQuad.TopLeft + new Vector2(1)));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition));
AddRepeatStep("alt-scroll down", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.ScrollVerticalBy(-1);
InputManager.ReleaseKey(Key.AltLeft);
}, 5);
AddAssert("game volume decreased", () => Game.Dependencies.Get<FrameworkConfigManager>().Get<double>(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1));
AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType<BeatmapCarousel>().Single()));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition));
double getCarouselScrollPosition() => Game.ChildrenOfType<UserTrackingScrollContainer<DrawableCarouselItem>>().Single().Current;
}
[Test]
public void TestNewSongSelectScrollHandling()
{
SoloSongSelect songSelect = null;
double scrollPosition = 0;
@@ -293,6 +259,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for beatmap", () => Game.ChildrenOfType<PanelBeatmapSet>().Any());
// TODO: this logic can likely be removed when we fix https://github.com/ppy/osu/issues/33379
// It should be probably be immediate in this case.
AddWaitStep("wait for scroll", 10);
AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition());
@@ -325,7 +293,7 @@ namespace osu.Game.Tests.Visual.Navigation
}, 5);
AddAssert("game volume decreased", () => Game.Dependencies.Get<FrameworkConfigManager>().Get<double>(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1));
AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType<Screens.SelectV2.BeatmapCarousel>().Single()));
AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType<BeatmapCarousel>().Single()));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition));
@@ -339,21 +307,21 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestOpenModSelectOverlayUsingAction()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddStep("Show mods overlay", () => InputManager.Key(Key.F1));
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddAssert("Overlay was shown", () => songSelect!.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
}
[Test]
public void TestAttemptPlayBeatmapWrongHashFails()
{
Screens.Select.SongSelect songSelect = null;
Screens.SelectV2.SongSelect songSelect = null;
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
@@ -384,11 +352,11 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestAttemptPlayBeatmapMissingFails()
{
Screens.Select.SongSelect songSelect = null;
Screens.SelectV2.SongSelect songSelect = null;
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
@@ -418,9 +386,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -461,9 +429,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely());
@@ -515,9 +483,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely());
@@ -558,9 +526,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -663,7 +631,7 @@ namespace osu.Game.Tests.Visual.Navigation
playToResults();
ScoreInfo score = null;
LeaderboardScore scorePanel = null;
BeatmapLeaderboardScore scorePanel = null;
AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score);
@@ -672,18 +640,11 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
() => Game.ChildrenOfType<Dropdown<BeatmapLeaderboardScope>>().First().Current.Value = BeatmapLeaderboardScope.Local);
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<BeatmapLeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
AddStep("open options", () => InputManager.Key(Key.F3));
AddStep("choose clear all scores", () => InputManager.Key(Key.Number4));
AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog == null);
AddStep("Clear all scores", () => Game.Dependencies.Get<ScoreManager>().Delete());
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
@@ -696,7 +657,7 @@ namespace osu.Game.Tests.Visual.Navigation
playToResults();
ScoreInfo score = null;
LeaderboardScore scorePanel = null;
BeatmapLeaderboardScore scorePanel = null;
AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score);
@@ -705,9 +666,9 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
() => Game.ChildrenOfType<Dropdown<BeatmapLeaderboardScope>>().First().Current.Value = BeatmapLeaderboardScope.Local);
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<BeatmapLeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
AddStep("right click panel", () =>
{
@@ -718,7 +679,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("click delete", () =>
{
var dropdownItem = Game
.ChildrenOfType<PlayBeatmapDetailArea>().First()
.ChildrenOfType<BeatmapLeaderboardWedge>().First()
.ChildrenOfType<OsuContextMenu>().First()
.ChildrenOfType<DrawableOsuMenuItem>().First(i => i.Item.Text.ToString() == "Delete");
@@ -744,9 +705,9 @@ namespace osu.Game.Tests.Visual.Navigation
IWorkingBeatmap beatmap() => Game.Beatmap.Value;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
@@ -777,9 +738,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestMenuMakesMusic()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice);
@@ -791,7 +752,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPushSongSelectAndPressBackButtonImmediately()
{
AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect()));
AddStep("push song select", () => Game.ScreenStack.Push(new SoloSongSelect()));
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
ConfirmAtMainMenu();
@@ -800,18 +761,23 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitSongSelectWithClick()
{
TestPlaySongSelect songSelect = null;
SoloSongSelect songSelect = null;
ModSelectOverlay modSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddStep("Show mods overlay", () =>
{
modSelect = songSelect!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect.Show();
});
AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible);
AddStep("Move mouse to dimmed area", () => InputManager.MoveMouseTo(new Vector2(
songSelect.ScreenSpaceDrawQuad.TopLeft.X + 1,
songSelect.ScreenSpaceDrawQuad.TopLeft.Y + songSelect.ScreenSpaceDrawQuad.Height / 2)));
AddStep("Click left mouse button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
AddUntilStep("Overlay was hidden", () => modSelect.State.Value == Visibility.Hidden);
exitViaBackButtonAndConfirm();
}
@@ -876,10 +842,18 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded);
TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
SoloSongSelect songSelect = null;
ModSelectOverlay modSelect = null;
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddStep("Show mods overlay", () =>
{
modSelect = songSelect!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect.Show();
});
AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible);
AddStep("Show mods overlay", () => modSelect.Show());
AddStep("Change ruleset to osu!taiko", () =>
{
@@ -890,7 +864,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType<ToolbarRulesetSelector>().Single().Current.Value.OnlineID == 1);
AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddAssert("Mods overlay still visible", () => modSelect.State.Value == Visibility.Visible);
}
[Test]
@@ -900,10 +874,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show());
AddStep("Show options overlay", () => InputManager.Key(Key.F3));
AddUntilStep("Options overlay visible", () => this.ChildrenOfType<FooterButtonOptions.Popover>().SingleOrDefault()?.State.Value == Visibility.Visible);
AddStep("Change ruleset to osu!taiko", () =>
{
@@ -914,7 +890,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType<ToolbarRulesetSelector>().Single().Current.Value.OnlineID == 1);
AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible);
AddAssert("Options overlay still visible", () => this.ChildrenOfType<FooterButtonOptions.Popover>().Single().State.Value == Visibility.Visible);
}
[Test]
@@ -1186,7 +1162,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitGameFromSongSelect()
{
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
exitViaEscapeAndConfirm();
pushEscape(); // returns to osu! logo
@@ -1201,6 +1177,8 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitWithHoldDisabled()
{
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0));
AddStep("press escape twice rapidly", () =>
@@ -1256,10 +1234,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("close settings sidebar", () => InputManager.Key(Key.Escape));
Screens.Select.SongSelect songSelect = null;
Screens.SelectV2.SongSelect songSelect = null;
AddRepeatStep("go to solo", () => InputManager.Key(Key.P), 3);
AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect) != null);
AddUntilStep("wait for beatmap sets loaded", () => songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.SelectV2.SongSelect) != null);
AddUntilStep("wait for beatmap sets loaded", () => songSelect.CarouselItemsPresented);
AddStep("switch to osu! ruleset", () =>
{
@@ -1269,7 +1247,7 @@ namespace osu.Game.Tests.Visual.Navigation
});
AddStep("touch beatmap wedge", () =>
{
var wedge = Game.ChildrenOfType<BeatmapInfoWedge>().Single();
var wedge = Game.ChildrenOfType<BeatmapTitleWedge>().Single();
var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
@@ -1285,7 +1263,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
AddStep("touch beatmap wedge", () =>
{
var wedge = Game.ChildrenOfType<BeatmapInfoWedge>().Single();
var wedge = Game.ChildrenOfType<BeatmapTitleWedge>().Single();
var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
@@ -1302,7 +1280,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("click beatmap wedge", () =>
{
InputManager.MoveMouseTo(Game.ChildrenOfType<BeatmapInfoWedge>().Single());
InputManager.MoveMouseTo(Game.ChildrenOfType<BeatmapTitleWedge>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
@@ -1313,7 +1291,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
BeatmapSetInfo beatmapSet = null;
PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
AddUntilStep("wait for selected", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet));
AddStep("select", () => InputManager.Key(Key.Enter));
@@ -1343,9 +1321,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitSongSelectAndImmediatelyClickLogo()
{
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -1374,9 +1352,9 @@ namespace osu.Game.Tests.Visual.Navigation
{
BeatmapSetInfo beatmap = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
@@ -1405,9 +1383,9 @@ namespace osu.Game.Tests.Visual.Navigation
IWorkingBeatmap beatmap() => Game.Beatmap.Value;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
Screens.SelectV2.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@@ -1445,12 +1423,5 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("Click back button", () => InputManager.Click(MouseButton.Left));
ConfirmAtMainMenu();
}
public partial class TestPlaySongSelect : PlaySongSelect
{
public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
}
}
}
@@ -17,6 +17,7 @@ using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Online.API;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets.Mods;
@@ -26,17 +27,19 @@ using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Screens.SelectV2;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps.IO;
using osuTK;
using osuTK.Input;
using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
namespace osu.Game.Tests.Visual.Navigation
{
public partial class TestSceneSkinEditorNavigation : OsuGameTestScene
{
private TestPlaySongSelect songSelect;
private SoloSongSelect songSelect;
private ModSelectOverlay modSelect => songSelect.ChildrenOfType<ModSelectOverlay>().First();
private SkinEditor skinEditor => Game.ChildrenOfType<SkinEditor>().FirstOrDefault();
[Test]
@@ -331,10 +334,10 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestModOverlayClosesOnOpeningSkinEditor()
{
advanceToSongSelect();
AddStep("open mod overlay", () => songSelect.ModSelectOverlay.Show());
AddStep("open mod overlay", () => modSelect.Show());
openSkinEditor();
AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
AddUntilStep("mod overlay closed", () => modSelect.State.Value == Visibility.Hidden);
}
[Test]
@@ -448,8 +451,8 @@ namespace osu.Game.Tests.Visual.Navigation
private void advanceToSongSelect()
{
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented);
}
private void openSkinEditor()
@@ -9,7 +9,9 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
@@ -126,6 +128,71 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime);
}
[Test]
public void TestAutoplayShortcutReturnsInitialModsOnExit()
{
PushAndConfirm(() => new SoloSongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault);
AddStep("open mod select", () => InputManager.Key(Key.F1));
AddStep("search magnetised", () => this.ChildrenOfType<ModSelectOverlay>().Single().SearchTerm = "MG");
AddStep("select", () => InputManager.Key(Key.Enter));
AddAssert("magnetised selected", () => Game.SelectedMods.Value.Single(), Is.TypeOf<OsuModMagnetised>);
AddStep("configure mod", () => ((OsuModMagnetised)Game.SelectedMods.Value.Single()).AttractionStrength.Value = 1.0f);
pushEscape();
pushEscape();
AddStep("press ctrl+enter", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Enter);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
return Game.ScreenStack.CurrentScreen is Player;
});
AddAssert("only autoplay selected", () => Game.SelectedMods.Value.Single(), Is.TypeOf<OsuModAutoplay>);
pushEscape();
waitForScreen<SoloSongSelect>();
AddAssert("magnetised selected", () => Game.SelectedMods.Value.Single(), Is.TypeOf<OsuModMagnetised>);
AddAssert("mod configured", () => ((OsuModMagnetised)Game.SelectedMods.Value.Single()).AttractionStrength.Value, () => Is.EqualTo(1.0f));
}
[Test]
public void TestLeaderboardCorrectInPlayer()
{
IWorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new SoloSongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("switch to next difficulty and immediately press enter", () =>
{
InputManager.Key(Key.Down);
Schedule(() => InputManager.Key(Key.Enter));
});
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
return Game.ScreenStack.CurrentScreen is Player;
});
AddAssert("leaderboard matches gameplay beatmap", () => Game.ChildrenOfType<LeaderboardManager>().Single().CurrentCriteria?.Beatmap, () => Is.EqualTo(beatmap().BeatmapInfo));
}
private Func<Player> playToResults()
{
var player = playToCompletion();
@@ -10,6 +10,8 @@ using osu.Game.Rulesets;
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Sprites;
@@ -193,7 +195,8 @@ namespace osu.Game.Tests.Visual.Online
overlay.ShowBeatmapSet(set);
});
AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.OnlineID == overlay.Header.RulesetSelector.Current.Value.OnlineID));
AddAssert("shown beatmaps of current ruleset",
() => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.OnlineID == overlay.Header.RulesetSelector.Current.Value.OnlineID));
AddAssert("left-most beatmap selected", () => overlay.Header.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
}
@@ -243,6 +246,30 @@ namespace osu.Game.Tests.Visual.Online
});
}
[Test]
public void TestBeatmapSetHasVideoOrStoryboard()
{
AddStep("show beatmapset with video", () =>
{
var beatmapSet = getBeatmapSet();
beatmapSet.HasVideo = true;
overlay.ShowBeatmapSet(beatmapSet);
});
AddStep("show beatmapset with storyboard", () =>
{
var beatmapSet = getBeatmapSet();
beatmapSet.HasStoryboard = true;
overlay.ShowBeatmapSet(beatmapSet);
});
AddStep("show beatmapset with video and storyboard", () =>
{
var beatmapSet = getBeatmapSet();
beatmapSet.HasVideo = true;
beatmapSet.HasStoryboard = true;
overlay.ShowBeatmapSet(beatmapSet);
});
}
[Test]
public void TestSelectedModsDontAffectStatistics()
{
@@ -349,6 +376,39 @@ namespace osu.Game.Tests.Visual.Online
});
}
[Test]
public void TestBeatmapsetWithDeletedUser()
{
AddStep("show map with deleted user", () =>
{
JObject jsonBlob = JObject.FromObject(getBeatmapSet(), new JsonSerializer
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
jsonBlob["user"] = JToken.Parse(
"""
{
"avatar_url": null,
"country_code": null,
"default_group": "default",
"id": null,
"is_active": false,
"is_bot": false,
"is_deleted": true,
"is_online": false,
"is_supporter": false,
"last_visit": null,
"pm_friends_only": false,
"profile_colour": null,
"username": "[deleted user]"
}
""");
overlay.ShowBeatmapSet(JsonConvert.DeserializeObject<APIBeatmapSet>(JsonConvert.SerializeObject(jsonBlob)));
});
}
private APIBeatmapSet createManyDifficultiesBeatmapSet()
{
var set = getBeatmapSet();
@@ -274,6 +274,29 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("all panels have non-negative position", () => this.ChildrenOfType<ScorePanel>().All(p => p.ScorePosition.Value > 0));
}
[Test]
public void TestPresentInvalidOnlineScore()
{
AddStep("set up invalid user score", () =>
{
userScore.OnlineID = -1;
userScore.TotalScore = 0;
});
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResultsWithScore(() => userScore);
AddUntilStep("wait for user score to be displayed", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Any());
AddWaitStep("wait for any more potential scores", 5);
AddAssert("only 1 score visible", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Count(), () => Is.EqualTo(1));
AddUntilStep("left loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden);
AddUntilStep("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
}
private void createResultsWithScore(Func<ScoreInfo> getScore)
{
AddStep("load results", () =>
@@ -359,7 +382,7 @@ namespace osu.Game.Tests.Visual.Playlists
switch (request)
{
case ShowPlaylistScoreRequest s:
if (userScore == null)
if (userScore == null || userScore.OnlineID == -1)
triggerFail(s);
else
triggerSuccess(s, () => createUserResponse(userScore));
@@ -367,7 +390,7 @@ namespace osu.Game.Tests.Visual.Playlists
break;
case ShowPlaylistUserScoreRequest u:
if (userScore == null)
if (userScore == null || userScore.OnlineID == -1)
triggerFail(u);
else
triggerSuccess(u, () => createUserResponse(userScore));
@@ -46,6 +46,32 @@ namespace osu.Game.Tests.Visual.Ranking
});
}
// cross-reference: `TestSceneToolbarUserButton.TestTransientUserStatisticsDisplay()`, "Test rounding treatment" step.
[Test]
public void TestRoundingTreatment()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_071.495M
},
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072.99M
});
}
[Test]
public void TestAllDecreased()
{
@@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -14,10 +16,12 @@ using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Ranking;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneUserTagControl : OsuTestScene
public partial class TestSceneUserTagControl : OsuManualInputManagerTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
@@ -63,6 +67,8 @@ namespace osu.Game.Tests.Visual.Ranking
beatmapSet.Beatmaps.Single().TopTags =
[
new APIBeatmapTag { TagId = 3, VoteCount = 9 },
new APIBeatmapTag { TagId = 2, VoteCount = 8 },
new APIBeatmapTag { TagId = 0, VoteCount = 7 },
];
Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500);
return true;
@@ -79,6 +85,11 @@ namespace osu.Game.Tests.Visual.Ranking
return false;
};
});
}
[Test]
public void TestRulesetSupport()
{
AddStep("show for osu! beatmap", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
@@ -86,6 +97,7 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap.Value = working;
recreateControl();
});
AddStep("show for taiko beatmap", () =>
{
var working = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo);
@@ -95,6 +107,47 @@ namespace osu.Game.Tests.Visual.Ranking
});
}
[Test]
public void TestTagsDoNotMoveUntilMouseMovesAway()
{
AddStep("show", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.BeatmapInfo.OnlineID = 42;
Beatmap.Value = working;
recreateControl();
});
AddUntilStep("wait for ready", () => getTagFlow().Count, () => Is.EqualTo(4));
AddAssert("tag 2 is second", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(1));
AddStep("vote for tag 2", () =>
{
InputManager.MoveMouseTo(getDrawableTagById(2));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(9));
AddStep("remove vote for tag 2", () =>
{
InputManager.MoveMouseTo(getDrawableTagById(2));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("tag 2 not voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(8));
AddAssert("tag 2 is still second", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(1));
AddStep("vote for tag 2", () =>
{
InputManager.MoveMouseTo(getDrawableTagById(2));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(9));
AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero));
AddAssert("tag 2 reordered to first", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(0));
FillFlowContainer<UserTagControl.DrawableUserTag> getTagFlow() => this.ChildrenOfType<FillFlowContainer<UserTagControl.DrawableUserTag>>().Single();
UserTagControl.DrawableUserTag getDrawableTagById(long id) => getTagFlow().Single(t => t.UserTag.Id == id);
}
private void recreateControl()
{
Child = new PopoverContainer
@@ -119,7 +119,6 @@ namespace osu.Game.Tests.Visual.SongSelect
{
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
});
@@ -140,7 +139,6 @@ namespace osu.Game.Tests.Visual.SongSelect
var difficultyAdjustMod = ruleset.CreateMod<OsuModDifficultyAdjust>().AsNonNull();
var originalDifficulty = advancedStats.BeatmapInfo.Difficulty;
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
advancedStats.Mods.Value = new[] { difficultyAdjustMod };

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