1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 15:22:55 +08:00

Merge branch 'master' into storyboard-skin-sprites

This commit is contained in:
Bartłomiej Dach 2020-10-20 23:01:54 +02:00
commit e2552d4c36
208 changed files with 5530 additions and 1892 deletions

View File

@ -2,7 +2,7 @@
<project version="4"> <project version="4">
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" />
</modules> </modules>
</component> </component>
</project> </project>

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1016.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1009.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1019.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,34 @@
// 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 Android.Content.PM;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game;
namespace osu.Android
{
public class GameplayScreenRotationLocker : Component
{
private Bindable<bool> localUserPlaying;
[Resolved]
private OsuGameActivity gameActivity { get; set; }
[BackgroundDependencyLoader]
private void load(OsuGame game)
{
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(updateLock, true);
}
private void updateLock(ValueChangedEvent<bool> userPlaying)
{
gameActivity.RunOnUiThread(() =>
{
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser;
});
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity public class OsuGameActivity : AndroidGameActivity
{ {
protected override Framework.Game CreateGame() => new OsuGameAndroid(); protected override Framework.Game CreateGame() => new OsuGameAndroid(this);
protected override void OnCreate(Bundle savedInstanceState) protected override void OnCreate(Bundle savedInstanceState)
{ {

View File

@ -4,6 +4,7 @@
using System; using System;
using Android.App; using Android.App;
using Android.OS; using Android.OS;
using osu.Framework.Allocation;
using osu.Game; using osu.Game;
using osu.Game.Updater; using osu.Game.Updater;
@ -11,6 +12,15 @@ namespace osu.Android
{ {
public class OsuGameAndroid : OsuGame public class OsuGameAndroid : OsuGame
{ {
[Cached]
private readonly OsuGameActivity gameActivity;
public OsuGameAndroid(OsuGameActivity activity)
: base(null)
{
gameActivity = activity;
}
public override Version AssemblyVersion public override Version AssemblyVersion
{ {
get get
@ -55,6 +65,12 @@ namespace osu.Android
} }
} }
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
} }
} }

View File

@ -21,6 +21,7 @@
<AndroidLinkTool>r8</AndroidLinkTool> <AndroidLinkTool>r8</AndroidLinkTool>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="GameplayScreenRotationLocker.cs" />
<Compile Include="OsuGameActivity.cs" /> <Compile Include="OsuGameActivity.cs" />
<Compile Include="OsuGameAndroid.cs" /> <Compile Include="OsuGameAndroid.cs" />
</ItemGroup> </ItemGroup>
@ -53,4 +54,4 @@
<AndroidResource Include="Resources\drawable\lazer.png" /> <AndroidResource Include="Resources\drawable\lazer.png" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project> </Project>

View File

@ -125,12 +125,14 @@ namespace osu.Desktop
{ {
base.SetHost(host); base.SetHost(host);
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
switch (host.Window) switch (host.Window)
{ {
// Legacy osuTK DesktopGameWindow // Legacy osuTK DesktopGameWindow
case DesktopGameWindow desktopGameWindow: case DesktopGameWindow desktopGameWindow:
desktopGameWindow.CursorState |= CursorState.Hidden; desktopGameWindow.CursorState |= CursorState.Hidden;
desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico")); desktopGameWindow.SetIconFromStream(iconStream);
desktopGameWindow.Title = Name; desktopGameWindow.Title = Name;
desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames); desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
break; break;
@ -138,6 +140,7 @@ namespace osu.Desktop
// SDL2 DesktopWindow // SDL2 DesktopWindow
case DesktopWindow desktopWindow: case DesktopWindow desktopWindow:
desktopWindow.CursorState.Value |= CursorState.Hidden; desktopWindow.CursorState.Value |= CursorState.Hidden;
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name; desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f }); desktopWindow.DragDrop += f => fileDrop(new[] { f });
break; break;

View File

@ -5,24 +5,24 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game;
using osu.Game.Configuration; using osu.Game.Configuration;
namespace osu.Desktop.Windows namespace osu.Desktop.Windows
{ {
public class GameplayWinKeyBlocker : Component public class GameplayWinKeyBlocker : Component
{ {
private Bindable<bool> allowScreenSuspension;
private Bindable<bool> disableWinKey; private Bindable<bool> disableWinKey;
private Bindable<bool> localUserPlaying;
private GameHost host; [Resolved]
private GameHost host { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, OsuConfigManager config) private void load(OsuGame game, OsuConfigManager config)
{ {
this.host = host; localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking());
allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
allowScreenSuspension.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey); disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true); disableWinKey.BindValueChanged(_ => updateBlocking(), true);
@ -30,7 +30,7 @@ namespace osu.Desktop.Windows
private void updateBlocking() private void updateBlocking()
{ {
bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value; bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
if (shouldDisable) if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable); host.InputThread.Scheduler.Add(WindowsKey.Disable);

View File

@ -123,7 +123,10 @@ namespace osu.Game.Rulesets.Catch.Tests
Origin = Anchor.Centre, Origin = Anchor.Centre,
Scale = new Vector2(4f), Scale = new Vector2(4f),
}, skin); }, skin);
});
AddStep("get trails container", () =>
{
trails = catcherArea.OfType<CatcherTrailDisplay>().Single(); trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
catcherArea.MovableCatcher.SetHyperDashState(2); catcherArea.MovableCatcher.SetHyperDashState(2);
}); });

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -56,6 +57,7 @@ namespace osu.Game.Rulesets.Catch.Objects
Volume = s.Volume Volume = s.Volume
}).ToList(); }).ToList();
int nodeIndex = 0;
SliderEventDescriptor? lastEvent = null; SliderEventDescriptor? lastEvent = null;
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Objects
case SliderEventType.Repeat: case SliderEventType.Repeat:
AddNested(new Fruit AddNested(new Fruit
{ {
Samples = Samples, Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time, StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X, X = X + Path.PositionAt(e.PathProgress).X,
}); });
@ -119,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration public double Duration
{ {
get => this.SpanCount() * Path.Distance / Velocity; get => this.SpanCount() * Path.Distance / Velocity;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. set => throw new NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
} }
public double EndTime => StartTime + Duration; public double EndTime => StartTime + Duration;

View File

@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Catch.Skinning
{ {
public class CatchLegacySkinTransformer : LegacySkinTransformer public class CatchLegacySkinTransformer : LegacySkinTransformer
{ {
/// <summary>
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
/// </summary>
private bool providesComboCounter => this.HasFont(GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score");
public CatchLegacySkinTransformer(ISkinSource source) public CatchLegacySkinTransformer(ISkinSource source)
: base(source) : base(source)
{ {
@ -20,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
{ {
if (component is HUDSkinComponent hudComponent)
{
switch (hudComponent.Component)
{
case HUDSkinComponents.ComboCounter:
// catch may provide its own combo counter; hide the default.
return providesComboCounter ? Drawable.Empty() : null;
}
}
if (!(component is CatchSkinComponent catchSkinComponent)) if (!(component is CatchSkinComponent catchSkinComponent))
return null; return null;
@ -55,11 +70,9 @@ namespace osu.Game.Rulesets.Catch.Skinning
this.GetAnimation("fruit-ryuuta", true, true, true); this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter: case CatchSkinComponents.CatchComboCounter:
var comboFont = GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. if (providesComboCounter)
if (this.HasFont(comboFont)) return new LegacyCatchComboCounter(Source);
return new LegacyComboCounter(Source);
break; break;
} }

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Skinning
/// <summary> /// <summary>
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter. /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
/// </summary> /// </summary>
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter public class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter
{ {
private readonly LegacyRollingCounter counter; private readonly LegacyRollingCounter counter;
private readonly LegacyRollingCounter explosion; private readonly LegacyRollingCounter explosion;
public LegacyComboCounter(ISkin skin) public LegacyCatchComboCounter(ISkin skin)
{ {
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score"; var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f; var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;

View File

@ -145,11 +145,19 @@ namespace osu.Game.Rulesets.Catch.UI
} }
}; };
trailsTarget.Add(trails = new CatcherTrailDisplay(this)); trails = new CatcherTrailDisplay(this);
updateCatcher(); updateCatcher();
} }
protected override void LoadComplete()
{
base.LoadComplete();
// don't add in above load as we may potentially modify a parent in an unsafe manner.
trailsTarget.Add(trails);
}
/// <summary> /// <summary>
/// Creates proxied content to be displayed beneath hitobjects. /// Creates proxied content to be displayed beneath hitobjects.
/// </summary> /// </summary>

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3683365342338796d, "diffcalc-test")] [TestCase(2.3449735700206298d, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);

View File

@ -21,13 +21,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary> /// </summary>
public int TotalColumns => Stages.Sum(g => g.Columns); public int TotalColumns => Stages.Sum(g => g.Columns);
/// <summary>
/// The total number of columns that were present in this <see cref="ManiaBeatmap"/> before any user adjustments.
/// </summary>
public readonly int OriginalTotalColumns;
/// <summary> /// <summary>
/// Creates a new <see cref="ManiaBeatmap"/>. /// Creates a new <see cref="ManiaBeatmap"/>.
/// </summary> /// </summary>
/// <param name="defaultStage">The initial stages.</param> /// <param name="defaultStage">The initial stages.</param>
public ManiaBeatmap(StageDefinition defaultStage) /// <param name="originalTotalColumns">The total number of columns present before any user adjustments. Defaults to the total columns in <paramref name="defaultStage"/>.</param>
public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
{ {
Stages.Add(defaultStage); Stages.Add(defaultStage);
OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
} }
public override IEnumerable<BeatmapStatistic> GetStatistics() public override IEnumerable<BeatmapStatistic> GetStatistics()

View File

@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public bool Dual; public bool Dual;
public readonly bool IsForCurrentRuleset; public readonly bool IsForCurrentRuleset;
private readonly int originalTargetColumns;
// Internal for testing purposes // Internal for testing purposes
internal FastRandom Random { get; private set; } internal FastRandom Random { get; private set; }
@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
else else
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7)); TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
} }
originalTargetColumns = TargetColumns;
} }
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
@ -81,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap<ManiaHitObject> CreateBeatmap() protected override Beatmap<ManiaHitObject> CreateBeatmap()
{ {
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }); beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns);
if (Dual) if (Dual)
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns }); beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
@ -116,7 +120,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
prevNoteTimes.RemoveAt(0); prevNoteTimes.RemoveAt(0);
prevNoteTimes.Add(newNoteTime); prevNoteTimes.Add(newNoteTime);
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count; if (prevNoteTimes.Count >= 2)
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
} }
private double lastTime; private double lastTime;
@ -180,7 +185,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
case IHasDuration endTimeData: case IHasDuration endTimeData:
{ {
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap); conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
recordNote(endTimeData.EndTime, new Vector2(256, 192)); recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime); computeDensity(endTimeData.EndTime);

View File

@ -3,8 +3,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Mania.MathUtils;
@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
@ -25,8 +26,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary> /// </summary>
private const float osu_base_scoring_distance = 100; private const float osu_base_scoring_distance = 100;
public readonly double EndTime; public readonly int StartTime;
public readonly double SegmentDuration; public readonly int EndTime;
public readonly int SegmentDuration;
public readonly int SpanCount; public readonly int SpanCount;
private PatternType convertType; private PatternType convertType;
@ -41,20 +43,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var distanceData = hitObject as IHasDistance; var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats; var repeatsData = hitObject as IHasRepeats;
SpanCount = repeatsData?.SpanCount() ?? 1; Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime); DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
// The true distance, accounting for any repeats double beatLength;
double distance = (distanceData?.Distance ?? 0) * SpanCount; #pragma warning disable 618
// The velocity of the osu! hit object - calculated as the velocity of a slider if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength; #pragma warning restore 618
// The duration of the osu! hit object beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
double osuDuration = distance / osuVelocity; else
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
EndTime = hitObject.StartTime + osuDuration; SpanCount = repeatsData?.SpanCount() ?? 1;
SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount; StartTime = (int)Math.Round(hitObject.StartTime);
// This matches stable's calculation.
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
SegmentDuration = (EndTime - StartTime) / SpanCount;
} }
public override IEnumerable<Pattern> Generate() public override IEnumerable<Pattern> Generate()
@ -76,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in originalPattern.HitObjects) foreach (var obj in originalPattern.HitObjects)
{ {
if (!Precision.AlmostEquals(EndTime, obj.GetEndTime())) if (EndTime != (int)Math.Round(obj.GetEndTime()))
intermediatePattern.Add(obj); intermediatePattern.Add(obj);
else else
endTimePattern.Add(obj); endTimePattern.Add(obj);
@ -91,35 +99,35 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (TotalColumns == 1) if (TotalColumns == 1)
{ {
var pattern = new Pattern(); var pattern = new Pattern();
addToPattern(pattern, 0, HitObject.StartTime, EndTime); addToPattern(pattern, 0, StartTime, EndTime);
return pattern; return pattern;
} }
if (SpanCount > 1) if (SpanCount > 1)
{ {
if (SegmentDuration <= 90) if (SegmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1); return generateRandomHoldNotes(StartTime, 1);
if (SegmentDuration <= 120) if (SegmentDuration <= 120)
{ {
convertType |= PatternType.ForceNotStack; convertType |= PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, SpanCount + 1); return generateRandomNotes(StartTime, SpanCount + 1);
} }
if (SegmentDuration <= 160) if (SegmentDuration <= 160)
return generateStair(HitObject.StartTime); return generateStair(StartTime);
if (SegmentDuration <= 200 && ConversionDifficulty > 3) if (SegmentDuration <= 200 && ConversionDifficulty > 3)
return generateRandomMultipleNotes(HitObject.StartTime); return generateRandomMultipleNotes(StartTime);
double duration = EndTime - HitObject.StartTime; double duration = EndTime - StartTime;
if (duration >= 4000) if (duration >= 4000)
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0); return generateNRandomNotes(StartTime, 0.23, 0, 0);
if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart) if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
return generateTiledHoldNotes(HitObject.StartTime); return generateTiledHoldNotes(StartTime);
return generateHoldAndNormalNotes(HitObject.StartTime); return generateHoldAndNormalNotes(StartTime);
} }
if (SegmentDuration <= 110) if (SegmentDuration <= 110)
@ -128,37 +136,37 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
convertType |= PatternType.ForceNotStack; convertType |= PatternType.ForceNotStack;
else else
convertType &= ~PatternType.ForceNotStack; convertType &= ~PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2); return generateRandomNotes(StartTime, SegmentDuration < 80 ? 1 : 2);
} }
if (ConversionDifficulty > 6.5) if (ConversionDifficulty > 6.5)
{ {
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0); return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03); return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
} }
if (ConversionDifficulty > 4) if (ConversionDifficulty > 4)
{ {
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0); return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0); return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
} }
if (ConversionDifficulty > 2.5) if (ConversionDifficulty > 2.5)
{ {
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0); return generateNRandomNotes(StartTime, 0.3, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0); return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
} }
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0); return generateNRandomNotes(StartTime, 0.17, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0); return generateNRandomNotes(StartTime, 0.27, 0, 0);
} }
/// <summary> /// <summary>
@ -167,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="startTime">Start time of each hold note.</param> /// <param name="startTime">Start time of each hold note.</param>
/// <param name="noteCount">Number of hold notes.</param> /// <param name="noteCount">Number of hold notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomHoldNotes(double startTime, int noteCount) private Pattern generateRandomHoldNotes(int startTime, int noteCount)
{ {
// - - - - // - - - -
// ■ - ■ ■ // ■ - ■ ■
@ -202,7 +210,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="startTime">The start time.</param> /// <param name="startTime">The start time.</param>
/// <param name="noteCount">The number of notes.</param> /// <param name="noteCount">The number of notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomNotes(double startTime, int noteCount) private Pattern generateRandomNotes(int startTime, int noteCount)
{ {
// - - - - // - - - -
// x - - - // x - - -
@ -234,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary> /// </summary>
/// <param name="startTime">The start time.</param> /// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateStair(double startTime) private Pattern generateStair(int startTime)
{ {
// - - - - // - - - -
// x - - - // x - - -
@ -286,7 +294,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary> /// </summary>
/// <param name="startTime">The start time.</param> /// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomMultipleNotes(double startTime) private Pattern generateRandomMultipleNotes(int startTime)
{ {
// - - - - // - - - -
// x - - - // x - - -
@ -329,7 +337,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="p3">The probability required for 3 hold notes to be generated.</param> /// <param name="p3">The probability required for 3 hold notes to be generated.</param>
/// <param name="p4">The probability required for 4 hold notes to be generated.</param> /// <param name="p4">The probability required for 4 hold notes to be generated.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4) private Pattern generateNRandomNotes(int startTime, double p2, double p3, double p4)
{ {
// - - - - // - - - -
// ■ - ■ ■ // ■ - ■ ■
@ -366,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH; static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability); bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample); canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes) if (canGenerateTwoNotes)
p2 = 1; p2 = 1;
@ -379,7 +387,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary> /// </summary>
/// <param name="startTime">The first hold note start time.</param> /// <param name="startTime">The first hold note start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateTiledHoldNotes(double startTime) private Pattern generateTiledHoldNotes(int startTime)
{ {
// - - - - // - - - -
// ■ ■ ■ ■ // ■ ■ ■ ■
@ -394,6 +402,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int columnRepeat = Math.Min(SpanCount, TotalColumns); int columnRepeat = Math.Min(SpanCount, TotalColumns);
// Due to integer rounding, this is not guaranteed to be the same as EndTime (the class-level variable).
int endTime = startTime + SegmentDuration * SpanCount;
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
@ -401,7 +412,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i < columnRepeat; i++) for (int i = 0; i < columnRepeat; i++)
{ {
nextColumn = FindAvailableColumn(nextColumn, pattern); nextColumn = FindAvailableColumn(nextColumn, pattern);
addToPattern(pattern, nextColumn, startTime, EndTime); addToPattern(pattern, nextColumn, startTime, endTime);
startTime += SegmentDuration; startTime += SegmentDuration;
} }
@ -413,7 +424,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary> /// </summary>
/// <param name="startTime">The start time of notes.</param> /// <param name="startTime">The start time of notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateHoldAndNormalNotes(double startTime) private Pattern generateHoldAndNormalNotes(int startTime)
{ {
// - - - - // - - - -
// ■ x x - // ■ x x -
@ -448,7 +459,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i <= SpanCount; i++) for (int i = 0; i <= SpanCount; i++)
{ {
if (!(ignoreHead && startTime == HitObject.StartTime)) if (!(ignoreHead && startTime == StartTime))
{ {
for (int j = 0; j < noteCount; j++) for (int j = 0; j < noteCount; j++)
{ {
@ -471,19 +482,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary> /// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param> /// <param name="time">The time to retrieve the sample info list from.</param>
/// <returns></returns> /// <returns></returns>
private IList<HitSampleInfo> sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples; private IList<HitSampleInfo> sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
/// <summary> /// <summary>
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>. /// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
/// </summary> /// </summary>
/// <param name="time">The time to retrieve node samples at.</param> /// <param name="time">The time to retrieve node samples at.</param>
private List<IList<HitSampleInfo>> nodeSamplesAt(double time) private List<IList<HitSampleInfo>> nodeSamplesAt(int time)
{ {
if (!(HitObject is IHasPathWithRepeats curveData)) if (!(HitObject is IHasPathWithRepeats curveData))
return null; return null;
// mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
// avoid slicing the list & creating copies, if at all possible. // avoid slicing the list & creating copies, if at all possible.
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList(); return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
@ -496,7 +506,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="column">The column to add the note to.</param> /// <param name="column">The column to add the note to.</param>
/// <param name="startTime">The start time of the note.</param> /// <param name="startTime">The start time of the note.</param>
/// <param name="endTime">The end time of the note (set to <paramref name="startTime"/> for a non-hold note).</param> /// <param name="endTime">The end time of the note (set to <paramref name="startTime"/> for a non-hold note).</param>
private void addToPattern(Pattern pattern, int column, double startTime, double endTime) private void addToPattern(Pattern pattern, int column, int startTime, int endTime)
{ {
ManiaHitObject newObject; ManiaHitObject newObject;

View File

@ -14,12 +14,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
internal class EndTimeObjectPatternGenerator : PatternGenerator internal class EndTimeObjectPatternGenerator : PatternGenerator
{ {
private readonly double endTime; private readonly int endTime;
private readonly PatternType convertType;
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap) public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, new Pattern(), originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{ {
endTime = (HitObject as IHasDuration)?.EndTime ?? 0; endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
convertType = PreviousPattern.ColumnWithObjects == TotalColumns
? PatternType.None
: PatternType.ForceNotStack;
} }
public override IEnumerable<Pattern> Generate() public override IEnumerable<Pattern> Generate()
@ -40,18 +45,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
break; break;
case 8: case 8:
addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold); addToPattern(pattern, getRandomColumn(), generateHold);
break; break;
default: default:
if (TotalColumns > 0) addToPattern(pattern, getRandomColumn(0), generateHold);
addToPattern(pattern, GetRandomColumn(), generateHold);
break; break;
} }
return pattern; return pattern;
} }
private int getRandomColumn(int? lowerBound = null)
{
if ((convertType & PatternType.ForceNotStack) > 0)
return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound, patterns: PreviousPattern);
return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound);
}
/// <summary> /// <summary>
/// Constructs and adds a note to a pattern. /// Constructs and adds a note to a pattern.
/// </summary> /// </summary>

View File

@ -397,7 +397,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 4: case 4:
centreProbability = 0; centreProbability = 0;
p2 = Math.Min(p2 * 2, 0.2);
// Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
// But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
// so it needs to be converted to from a probability and then back after the multiplication.
p2 = 1 - Math.Max((1 - p2) * 2, 0.8);
p3 = 0; p3 = 0;
break; break;
@ -408,11 +412,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 6: case 6:
centreProbability = 0; centreProbability = 0;
p2 = Math.Min(p2 * 2, 0.5);
p3 = Math.Min(p3 * 2, 0.15); // Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
// But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
// so it needs to be converted to from a probability and then back after the multiplication.
p2 = 1 - Math.Max((1 - p2) * 2, 0.5);
p3 = 1 - Math.Max((1 - p3) * 2, 0.85);
break; break;
} }
// The stable values were allowed to exceed 1, which indicate <0% probability.
// These values needs to be clamped otherwise GetRandomNoteCount() will throw an exception.
p2 = Math.Clamp(p2, 0, 1);
p3 = Math.Clamp(p3, 0, 1);
double centreVal = Random.NextDouble(); double centreVal = Random.NextDouble();
int noteCount = GetRandomNoteCount(p2, p3); int noteCount = GetRandomNoteCount(p2, p3);

View File

@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
public class ManiaDifficultyAttributes : DifficultyAttributes public class ManiaDifficultyAttributes : DifficultyAttributes
{ {
public double GreatHitWindow; public double GreatHitWindow;
public double ScoreMultiplier;
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -10,10 +11,12 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills; using osu.Game.Rulesets.Mania.Difficulty.Skills;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Difficulty namespace osu.Game.Rulesets.Mania.Difficulty
@ -23,11 +26,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private const double star_scaling_factor = 0.018; private const double star_scaling_factor = 0.018;
private readonly bool isForCurrentRuleset; private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty;
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
originalOverallDifficulty = beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
} }
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
@ -40,64 +45,33 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return new ManiaDifficultyAttributes return new ManiaDifficultyAttributes
{ {
StarRating = difficultyValue(skills) * star_scaling_factor, StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods, Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
ScoreMultiplier = getScoreMultiplier(beatmap, mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills Skills = skills
}; };
} }
private double difficultyValue(Skill[] skills)
{
// Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section
var overall = skills.OfType<Overall>().Single();
var aggregatePeaks = new List<double>(Enumerable.Repeat(0.0, overall.StrainPeaks.Count));
foreach (var individual in skills.OfType<Individual>())
{
for (int i = 0; i < individual.StrainPeaks.Count; i++)
{
double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i];
if (aggregate > aggregatePeaks[i])
aggregatePeaks[i] = aggregate;
}
}
aggregatePeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
foreach (double strain in aggregatePeaks)
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{ {
for (int i = 1; i < beatmap.HitObjects.Count; i++) var sortedObjects = beatmap.HitObjects.ToArray();
yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
for (int i = 1; i < sortedObjects.Length; i++)
yield return new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate);
} }
protected override Skill[] CreateSkills(IBeatmap beatmap) // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
{ {
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns; new Strain(((ManiaBeatmap)beatmap).TotalColumns)
};
var skills = new List<Skill> { new Overall(columnCount) };
for (int i = 0; i < columnCount; i++)
skills.Add(new Individual(i, columnCount));
return skills.ToArray();
}
protected override Mod[] DifficultyAdjustmentMods protected override Mod[] DifficultyAdjustmentMods
{ {
@ -122,12 +96,73 @@ namespace osu.Game.Rulesets.Mania.Difficulty
new ManiaModKey3(), new ManiaModKey3(),
new ManiaModKey4(), new ManiaModKey4(),
new ManiaModKey5(), new ManiaModKey5(),
new MultiMod(new ManiaModKey5(), new ManiaModDualStages()),
new ManiaModKey6(), new ManiaModKey6(),
new MultiMod(new ManiaModKey6(), new ManiaModDualStages()),
new ManiaModKey7(), new ManiaModKey7(),
new MultiMod(new ManiaModKey7(), new ManiaModDualStages()),
new ManiaModKey8(), new ManiaModKey8(),
new MultiMod(new ManiaModKey8(), new ManiaModDualStages()),
new ManiaModKey9(), new ManiaModKey9(),
new MultiMod(new ManiaModKey9(), new ManiaModDualStages()),
}).ToArray(); }).ToArray();
} }
} }
private int getHitWindow300(Mod[] mods)
{
if (isForCurrentRuleset)
{
double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
return applyModAdjustments(34 + 3 * od, mods);
}
if (Math.Round(originalOverallDifficulty) > 4)
return applyModAdjustments(34, mods);
return applyModAdjustments(47, mods);
static int applyModAdjustments(double value, Mod[] mods)
{
if (mods.Any(m => m is ManiaModHardRock))
value /= 1.4;
else if (mods.Any(m => m is ManiaModEasy))
value *= 1.4;
if (mods.Any(m => m is ManiaModDoubleTime))
value *= 1.5;
else if (mods.Any(m => m is ManiaModHalfTime))
value *= 0.75;
return (int)value;
}
}
private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
{
double scoreMultiplier = 1;
foreach (var m in mods)
{
switch (m)
{
case ManiaModNoFail _:
case ManiaModEasy _:
case ManiaModHalfTime _:
scoreMultiplier *= 0.5;
break;
}
}
var maniaBeatmap = (ManiaBeatmap)beatmap;
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
if (diff > 0)
scoreMultiplier *= 0.9;
else if (diff < 0)
scoreMultiplier *= 0.9 + 0.04 * diff;
return scoreMultiplier;
}
} }
} }

View File

@ -1,47 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Individual : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.125;
private readonly double[] holdEndTimes;
private readonly int column;
public Individual(int column, int columnCount)
{
this.column = column;
holdEndTimes = new double[columnCount];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
try
{
if (maniaCurrent.BaseObject.Column != column)
return 0;
// We give a slight bonus if something is held meanwhile
return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
}
finally
{
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
}
}
}
}

View File

@ -1,56 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Overall : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.3;
private readonly double[] holdEndTimes;
private readonly int columnCount;
public Overall(int columnCount)
{
this.columnCount = columnCount;
holdEndTimes = new double[columnCount];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
double holdFactor = 1.0; // Factor in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
for (int i = 0; i < columnCount; i++)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i])
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending.
// Releasing multiple notes at the same time is just as easy as releasing one
if (endTime == holdEndTimes[i])
holdAddition = 0;
// We give a slight bonus if something is held meanwhile
if (holdEndTimes[i] > endTime)
holdFactor = 1.25;
}
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
return (1 + holdAddition) * holdFactor;
}
}
}

View File

@ -0,0 +1,80 @@
// 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.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Strain : Skill
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
private readonly double[] holdEndTimes;
private readonly double[] individualStrains;
private double individualStrain;
private double overallStrain;
public Strain(int totalColumns)
{
holdEndTimes = new double[totalColumns];
individualStrains = new double[totalColumns];
overallStrain = 1;
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
var column = maniaCurrent.BaseObject.Column;
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
// Fill up the holdEndTimes array
for (int i = 0; i < holdEndTimes.Length; ++i)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
holdAddition = 0;
// We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
holdFactor = 1.25;
// Decay individual strains
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
}
holdEndTimes[column] = endTime;
// Increase individual strain in own column
individualStrains[column] += 2.0 * holdFactor;
individualStrain = individualStrains[column];
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;
return individualStrain + overallStrain - CurrentStrain;
}
protected override double GetPeakStrain(double offset)
=> applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base)
+ applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base);
private double applyDecay(double value, double deltaTime, double decayBase)
=> value * Math.Pow(decayBase, deltaTime / 1000);
}
}

View File

@ -0,0 +1,165 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
namespace osu.Game.Rulesets.Mania.MathUtils
{
/// <summary>
/// Provides access to .NET4.0 unstable sorting methods.
/// </summary>
/// <remarks>
/// Source: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs
/// Copyright (c) Microsoft Corporation. All rights reserved.
/// </remarks>
internal static class LegacySortHelper<T>
{
private const int quick_sort_depth_threshold = 32;
public static void Sort(T[] keys, IComparer<T> comparer)
{
if (keys == null)
throw new ArgumentNullException(nameof(keys));
if (keys.Length == 0)
return;
comparer ??= Comparer<T>.Default;
depthLimitedQuickSort(keys, 0, keys.Length - 1, comparer, quick_sort_depth_threshold);
}
private static void depthLimitedQuickSort(T[] keys, int left, int right, IComparer<T> comparer, int depthLimit)
{
do
{
if (depthLimit == 0)
{
heapsort(keys, left, right, comparer);
return;
}
int i = left;
int j = right;
// pre-sort the low, middle (pivot), and high values in place.
// this improves performance in the face of already sorted data, or
// data that is made up of multiple sorted runs appended together.
int middle = i + ((j - i) >> 1);
swapIfGreater(keys, comparer, i, middle); // swap the low with the mid point
swapIfGreater(keys, comparer, i, j); // swap the low with the high
swapIfGreater(keys, comparer, middle, j); // swap the middle with the high
T x = keys[middle];
do
{
while (comparer.Compare(keys[i], x) < 0) i++;
while (comparer.Compare(x, keys[j]) < 0) j--;
Contract.Assert(i >= left && j <= right, "(i>=left && j<=right) Sort failed - Is your IComparer bogus?");
if (i > j) break;
if (i < j)
{
T key = keys[i];
keys[i] = keys[j];
keys[j] = key;
}
i++;
j--;
} while (i <= j);
// The next iteration of the while loop is to "recursively" sort the larger half of the array and the
// following calls recrusively sort the smaller half. So we subtrack one from depthLimit here so
// both sorts see the new value.
depthLimit--;
if (j - left <= right - i)
{
if (left < j) depthLimitedQuickSort(keys, left, j, comparer, depthLimit);
left = i;
}
else
{
if (i < right) depthLimitedQuickSort(keys, i, right, comparer, depthLimit);
right = j;
}
} while (left < right);
}
private static void heapsort(T[] keys, int lo, int hi, IComparer<T> comparer)
{
Contract.Requires(keys != null);
Contract.Requires(comparer != null);
Contract.Requires(lo >= 0);
Contract.Requires(hi > lo);
Contract.Requires(hi < keys.Length);
int n = hi - lo + 1;
for (int i = n / 2; i >= 1; i = i - 1)
{
downHeap(keys, i, n, lo, comparer);
}
for (int i = n; i > 1; i = i - 1)
{
swap(keys, lo, lo + i - 1);
downHeap(keys, 1, i - 1, lo, comparer);
}
}
private static void downHeap(T[] keys, int i, int n, int lo, IComparer<T> comparer)
{
Contract.Requires(keys != null);
Contract.Requires(comparer != null);
Contract.Requires(lo >= 0);
Contract.Requires(lo < keys.Length);
T d = keys[lo + i - 1];
while (i <= n / 2)
{
var child = 2 * i;
if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
{
child++;
}
if (!(comparer.Compare(d, keys[lo + child - 1]) < 0))
break;
keys[lo + i - 1] = keys[lo + child - 1];
i = child;
}
keys[lo + i - 1] = d;
}
private static void swap(T[] a, int i, int j)
{
if (i != j)
{
T t = a[i];
a[i] = a[j];
a[j] = t;
}
}
private static void swapIfGreater(T[] keys, IComparer<T> comparer, int a, int b)
{
if (a != b)
{
if (comparer.Compare(keys[a], keys[b]) > 0)
{
T key = keys[a];
keys[a] = keys[b];
keys[b] = key;
}
}
}
}
}

View File

@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
typeof(ManiaModKey7), typeof(ManiaModKey7),
typeof(ManiaModKey8), typeof(ManiaModKey8),
typeof(ManiaModKey9), typeof(ManiaModKey9),
typeof(ManiaModKey10),
}.Except(new[] { GetType() }).ToArray(); }.Except(new[] { GetType() }).ToArray();
} }
} }

View File

@ -10,7 +10,7 @@
["soft-hitnormal"], ["soft-hitnormal"],
["drum-hitnormal"] ["drum-hitnormal"]
], ],
"Samples": ["drum-hitnormal"] "Samples": ["-hitnormal"]
}, { }, {
"StartTime": 1875.0, "StartTime": 1875.0,
"EndTime": 2750.0, "EndTime": 2750.0,
@ -19,7 +19,7 @@
["soft-hitnormal"], ["soft-hitnormal"],
["drum-hitnormal"] ["drum-hitnormal"]
], ],
"Samples": ["drum-hitnormal"] "Samples": ["-hitnormal"]
}] }]
}, { }, {
"StartTime": 3750.0, "StartTime": 3750.0,

View File

@ -0,0 +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.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestPlayfieldBorder : OsuTestScene
{
public TestPlayfieldBorder()
{
Bindable<PlayfieldBorderStyle> playfieldBorderStyle = new Bindable<PlayfieldBorderStyle>();
AddStep("add drawables", () =>
{
Child = new Container
{
Size = new Vector2(400, 300),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new PlayfieldBorder
{
PlayfieldBorderStyle = { BindTarget = playfieldBorderStyle }
}
}
};
});
AddStep("Set none", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.None);
AddStep("Set corners", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Corners);
AddStep("Set full", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Full);
}
}
}

View File

@ -3,6 +3,7 @@
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Configuration namespace osu.Game.Rulesets.Osu.Configuration
{ {
@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
Set(OsuRulesetSetting.SnakingInSliders, true); Set(OsuRulesetSetting.SnakingInSliders, true);
Set(OsuRulesetSetting.SnakingOutSliders, true); Set(OsuRulesetSetting.SnakingOutSliders, true);
Set(OsuRulesetSetting.ShowCursorTrail, true); Set(OsuRulesetSetting.ShowCursorTrail, true);
Set(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
} }
} }
@ -26,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
{ {
SnakingInSliders, SnakingInSliders,
SnakingOutSliders, SnakingOutSliders,
ShowCursorTrail ShowCursorTrail,
PlayfieldBorderStyle,
} }
} }

View File

@ -21,6 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
InternalChild = circlePiece = new HitCirclePiece(); InternalChild = circlePiece = new HitCirclePiece();
} }
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();

View File

@ -56,7 +56,18 @@ namespace osu.Game.Rulesets.Osu.Edit
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }); LayerBelowRuleset.AddRange(new Drawable[]
{
new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
},
distanceSnapGridContainer = new Container
{
RelativeSizeAxes = Axes.Both
}
});
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();

View File

@ -137,6 +137,10 @@ namespace osu.Game.Rulesets.Osu.Objects
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
// For now, the samples are attached to and played by the slider itself at the correct end time.
Samples = this.GetNodeSamples(repeatCount + 1);
} }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@ -230,15 +234,12 @@ namespace osu.Game.Rulesets.Osu.Objects
tick.Samples = sampleList; tick.Samples = sampleList;
foreach (var repeat in NestedHitObjects.OfType<SliderRepeat>()) foreach (var repeat in NestedHitObjects.OfType<SliderRepeat>())
repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1); repeat.Samples = this.GetNodeSamples(repeat.RepeatIndex + 1);
if (HeadCircle != null) if (HeadCircle != null)
HeadCircle.Samples = getNodeSamples(0); HeadCircle.Samples = this.GetNodeSamples(0);
} }
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
public override Judgement CreateJudgement() => new OsuIgnoreJudgement(); public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;

View File

@ -17,12 +17,16 @@ using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Osu.Configuration;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.UI namespace osu.Game.Rulesets.Osu.UI
{ {
public class OsuPlayfield : Playfield public class OsuPlayfield : Playfield
{ {
private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles; private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies; private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer; private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
@ -33,12 +37,19 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
private readonly Bindable<bool> playfieldBorderStyle = new BindableBool();
private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>(); private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>();
public OsuPlayfield() public OsuPlayfield()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
playfieldBorder = new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
Depth = 3
},
spinnerProxies = new ProxyContainer spinnerProxies = new ProxyContainer
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
@ -76,6 +87,12 @@ namespace osu.Game.Rulesets.Osu.UI
AddRangeInternal(poolDictionary.Values); AddRangeInternal(poolDictionary.Values);
} }
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
{
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
}
public override void Add(DrawableHitObject h) public override void Add(DrawableHitObject h)
{ {
h.OnNewResult += onNewResult; h.OnNewResult += onNewResult;

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI namespace osu.Game.Rulesets.Osu.UI
{ {
@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Osu.UI
LabelText = "Cursor trail", LabelText = "Cursor trail",
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail) Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
}, },
new SettingsEnumDropdown<PlayfieldBorderStyle>
{
LabelText = "Playfield border style",
Current = config.GetBindable<PlayfieldBorderStyle>(OsuRulesetSetting.PlayfieldBorderStyle),
},
}; };
} }
} }

View File

@ -28,5 +28,28 @@ namespace osu.Game.Tests.Beatmaps
Assert.That(key1, Is.EqualTo(key2)); Assert.That(key1, Is.EqualTo(key2));
} }
[TestCase(1.3, DifficultyRating.Easy)]
[TestCase(1.993, DifficultyRating.Easy)]
[TestCase(1.998, DifficultyRating.Normal)]
[TestCase(2.4, DifficultyRating.Normal)]
[TestCase(2.693, DifficultyRating.Normal)]
[TestCase(2.698, DifficultyRating.Hard)]
[TestCase(3.5, DifficultyRating.Hard)]
[TestCase(3.993, DifficultyRating.Hard)]
[TestCase(3.997, DifficultyRating.Insane)]
[TestCase(5.0, DifficultyRating.Insane)]
[TestCase(5.292, DifficultyRating.Insane)]
[TestCase(5.297, DifficultyRating.Expert)]
[TestCase(6.2, DifficultyRating.Expert)]
[TestCase(6.493, DifficultyRating.Expert)]
[TestCase(6.498, DifficultyRating.ExpertPlus)]
[TestCase(8.3, DifficultyRating.ExpertPlus)]
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
{
var actualBracket = BeatmapDifficultyManager.GetDifficultyRating(starRating);
Assert.AreEqual(expectedBracket, actualBracket);
}
} }
} }

View File

@ -651,5 +651,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsInstanceOf<LegacyDifficultyCalculatorBeatmapDecoder>(decoder); Assert.IsInstanceOf<LegacyDifficultyCalculatorBeatmapDecoder>(decoder);
} }
} }
[Test]
public void TestMultiSegmentSliders()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("multi-segment-slider.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream);
// Multi-segment
var first = ((IHasPath)decoded.HitObjects[0]).Path;
Assert.That(first.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(first.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
Assert.That(first.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
Assert.That(first.ControlPoints[1].Type.Value, Is.EqualTo(null));
Assert.That(first.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
Assert.That(first.ControlPoints[2].Type.Value, Is.EqualTo(PathType.Bezier));
Assert.That(first.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(68, 15)));
Assert.That(first.ControlPoints[3].Type.Value, Is.EqualTo(null));
Assert.That(first.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(259, -132)));
Assert.That(first.ControlPoints[4].Type.Value, Is.EqualTo(null));
Assert.That(first.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(92, -107)));
Assert.That(first.ControlPoints[5].Type.Value, Is.EqualTo(null));
// Single-segment
var second = ((IHasPath)decoded.HitObjects[1]).Path;
Assert.That(second.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(second.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
Assert.That(second.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null));
Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null));
// Implicit multi-segment
var third = ((IHasPath)decoded.HitObjects[2]).Path;
Assert.That(third.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(third.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(0, 192)));
Assert.That(third.ControlPoints[1].Type.Value, Is.EqualTo(null));
Assert.That(third.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(224, 192)));
Assert.That(third.ControlPoints[2].Type.Value, Is.EqualTo(null));
Assert.That(third.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(224, 0)));
Assert.That(third.ControlPoints[3].Type.Value, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(224, -192)));
Assert.That(third.ControlPoints[4].Type.Value, Is.EqualTo(null));
Assert.That(third.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(480, -192)));
Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null));
Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0)));
Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null));
}
}
} }
} }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore(); private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu")); private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
[TestCaseSource(nameof(allBeatmaps))] [TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStability(string name) public void TestEncodeDecodeStability(string name)

View File

@ -111,6 +111,7 @@ namespace osu.Game.Tests.NonVisual
var osu = LoadOsuIntoHost(host); var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>(); var storage = osu.Dependencies.Get<Storage>();
var osuStorage = storage as MigratableStorage;
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes. // Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
string originalDirectory = storage.GetFullPath("."); string originalDirectory = storage.GetFullPath(".");
@ -137,13 +138,15 @@ namespace osu.Game.Tests.NonVisual
Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache"))); Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
foreach (var file in OsuStorage.IGNORE_FILES) Assert.That(osuStorage, Is.Not.Null);
foreach (var file in osuStorage.IgnoreFiles)
{ {
Assert.That(File.Exists(Path.Combine(originalDirectory, file))); Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False); Assert.That(storage.Exists(file), Is.False);
} }
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES) foreach (var dir in osuStorage.IgnoreDirectories)
{ {
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir))); Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.ExistsDirectory(dir), Is.False); Assert.That(storage.ExistsDirectory(dir), Is.False);

View File

@ -94,6 +94,52 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA); Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
} }
[Test]
public void TestMultiModFlattening()
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(4, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(combinations[3] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
}
[Test]
public void TestIncompatibleThroughMultiMod()
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
}
[Test]
public void TestIncompatibleWithSameInstanceViaMultiMod()
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
}
private class ModA : Mod private class ModA : Mod
{ {
public override string Name => nameof(ModA); public override string Name => nameof(ModA);
@ -112,6 +158,13 @@ namespace osu.Game.Tests.NonVisual
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) }; public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
} }
private class ModC : Mod
{
public override string Name => nameof(ModC);
public override string Acronym => nameof(ModC);
public override double ScoreMultiplier => 1;
}
private class ModIncompatibleWithA : Mod private class ModIncompatibleWithA : Mod
{ {
public override string Name => $"Incompatible With {nameof(ModA)}"; public override string Name => $"Incompatible With {nameof(ModA)}";

View File

@ -197,5 +197,22 @@ namespace osu.Game.Tests.NonVisual.Filtering
carouselItem.Filter(criteria); carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value); Assert.AreEqual(filtered, carouselItem.Filtered.Value);
} }
[TestCase("202010", true)]
[TestCase("20201010", false)]
[TestCase("153", true)]
[TestCase("1535", false)]
public void TestCriteriaMatchingBeatmapIDs(string query, bool filtered)
{
var beatmap = getExampleBeatmap();
beatmap.OnlineBeatmapID = 20201010;
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = 1535 };
var criteria = new FilterCriteria { SearchText = query };
var carouselItem = new CarouselBeatmap(beatmap);
carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
} }
} }

View File

@ -0,0 +1,11 @@
osu file format v128
[HitObjects]
// Multi-segment
63,301,1000,6,0,P|224:57|B|439:298|131:316|322:169|155:194,1,1040,0|0,0:0|0:0,0:0:0:0:
// Single-segment
63,301,2000,6,0,P|224:57|439:298,1,1040,0|0,0:0|0:0,0:0:0:0:
// Implicit multi-segment
32,192,3000,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,2 @@
[General]
Version: 1.0

View File

@ -53,5 +53,263 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
} }
/// <summary>
/// Test to see that all <see cref="HitResult"/>s contribute to score portions in correct amounts.
/// </summary>
/// <param name="scoringMode">Scoring mode to test.</param>
/// <param name="hitResult">The <see cref="HitResult"/> that will be applied to selected hit objects.</param>
/// <param name="maxResult">The maximum <see cref="HitResult"/> achievable.</param>
/// <param name="expectedScore">Expected score after all objects have been judged, rounded to the nearest integer.</param>
/// <remarks>
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
/// <para>
/// For standardised scoring, <paramref name="expectedScore"/> is calculated using the following formula:
/// 1_000_000 * (((3 * <paramref name="hitResult"/>) / (4 * <paramref name="maxResult"/>)) * 30% + (bestCombo / maxCombo) * 70%)
/// </para>
/// <para>
/// For classic scoring, <paramref name="expectedScore"/> is calculated using the following formula:
/// <paramref name="hitResult"/> / <paramref name="maxResult"/> * 936
/// where 936 is simplified from:
/// 75% * 4 * 300 * (1 + 1/25)
/// </para>
/// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 478_571)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0)
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0)
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 700_030)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 700_150)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 535)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] // (((3 * 300) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] // (((3 * 350) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] // (0 * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] // (((3 * 10) / (4 * 10)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] // (((3 * 50) / (4 * 50)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] // (0 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] // (0 * 1 * 300) * (1 + 0 / 25) * 3 * 50 (bonus points)
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
{
var minResult = new TestJudgement(hitResult).MinResult;
IBeatmap fourObjectBeatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = new List<HitObject>(Enumerable.Repeat(new TestHitObject(maxResult), 4))
};
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fourObjectBeatmap);
for (int i = 0; i < 4; i++)
{
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement())
{
Type = i == 2 ? minResult : hitResult
};
scoreProcessor.ApplyResult(judgementResult);
}
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
}
/// <remarks>
/// This test uses a beatmap with four small ticks and one object with the <see cref="Judgement.MaxResult"/> of <see cref="HitResult.Ok"/>.
/// Its goal is to ensure that with the <see cref="ScoringMode"/> of <see cref="ScoringMode.Standardised"/>,
/// small ticks contribute to the accuracy portion, but not the combo portion.
/// In contrast, <see cref="ScoringMode.Classic"/> does not have separate combo and accuracy portion (they are multiplied by each other).
/// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
IEnumerable<HitObject> hitObjects = Enumerable
.Repeat(new TestHitObject(HitResult.SmallTickHit), 4)
.Append(new TestHitObject(HitResult.Ok));
IBeatmap fiveObjectBeatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = hitObjects.ToList()
};
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fiveObjectBeatmap);
for (int i = 0; i < 4; i++)
{
var judgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects[i], new Judgement())
{
Type = i == 2 ? HitResult.SmallTickMiss : hitResult
};
scoreProcessor.ApplyResult(judgementResult);
}
var lastJudgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects.Last(), new Judgement())
{
Type = HitResult.Ok
};
scoreProcessor.ApplyResult(lastJudgementResult);
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
}
[Test]
public void TestEmptyBeatmap(
[Values(ScoringMode.Standardised, ScoringMode.Classic)]
ScoringMode scoringMode)
{
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo()));
Assert.IsTrue(Precision.AlmostEquals(0, scoreProcessor.TotalScore.Value));
}
[TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)]
[TestCase(HitResult.Meh, HitResult.Miss)]
[TestCase(HitResult.Ok, HitResult.Miss)]
[TestCase(HitResult.Good, HitResult.Miss)]
[TestCase(HitResult.Great, HitResult.Miss)]
[TestCase(HitResult.Perfect, HitResult.Miss)]
[TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)]
[TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)]
[TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)]
[TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)]
public void TestMinResults(HitResult hitResult, HitResult expectedMinResult)
{
Assert.AreEqual(expectedMinResult, new TestJudgement(hitResult).MinResult);
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, true)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, false)]
[TestCase(HitResult.SmallTickHit, false)]
[TestCase(HitResult.LargeTickMiss, true)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, false)]
[TestCase(HitResult.LargeBonus, false)]
public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.AffectsCombo());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, true)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, true)]
[TestCase(HitResult.SmallTickHit, true)]
[TestCase(HitResult.LargeTickMiss, true)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, false)]
[TestCase(HitResult.LargeBonus, false)]
public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.AffectsAccuracy());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, false)]
[TestCase(HitResult.Meh, false)]
[TestCase(HitResult.Ok, false)]
[TestCase(HitResult.Good, false)]
[TestCase(HitResult.Great, false)]
[TestCase(HitResult.Perfect, false)]
[TestCase(HitResult.SmallTickMiss, false)]
[TestCase(HitResult.SmallTickHit, false)]
[TestCase(HitResult.LargeTickMiss, false)]
[TestCase(HitResult.LargeTickHit, false)]
[TestCase(HitResult.SmallBonus, true)]
[TestCase(HitResult.LargeBonus, true)]
public void TestIsBonus(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.IsBonus());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, true)]
[TestCase(HitResult.Miss, false)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, false)]
[TestCase(HitResult.SmallTickHit, true)]
[TestCase(HitResult.LargeTickMiss, false)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, true)]
[TestCase(HitResult.LargeBonus, true)]
public void TestIsHit(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.IsHit());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, true)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, true)]
[TestCase(HitResult.SmallTickHit, true)]
[TestCase(HitResult.LargeTickMiss, true)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, true)]
[TestCase(HitResult.LargeBonus, true)]
public void TestIsScorable(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.IsScorable());
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }
public TestJudgement(HitResult maxResult)
{
MaxResult = maxResult;
}
}
private class TestHitObject : HitObject
{
private readonly HitResult maxResult;
public override Judgement CreateJudgement()
{
return new TestJudgement(maxResult);
}
public TestHitObject(HitResult maxResult)
{
this.maxResult = maxResult;
}
}
} }
} }

View File

@ -0,0 +1,47 @@
// 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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneComboCounter : SkinnableTestScene
{
private IEnumerable<SkinnableComboCounter> comboCounters => CreatedDrawables.OfType<SkinnableComboCounter>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create combo counters", () => SetContents(() =>
{
var comboCounter = new SkinnableComboCounter();
comboCounter.Current.Value = 1;
return comboCounter;
}));
}
[Test]
public void TestComboCounterIncrementing()
{
AddRepeatStep("increase combo", () =>
{
foreach (var counter in comboCounters)
counter.Current.Value++;
}, 10);
AddStep("reset combo", () =>
{
foreach (var counter in comboCounters)
counter.Current.Value = 0;
});
}
}
}

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("get variables", () => AddStep("get variables", () =>
{ {
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First().GameplayClock; gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First();
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First(); slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
samples = slider.ChildrenOfType<DrawableSample>().ToArray(); samples = slider.ChildrenOfType<DrawableSample>().ToArray();
}); });

View File

@ -2,23 +2,29 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene public class TestSceneHUDOverlay : SkinnableTestScene
{ {
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
@ -26,6 +32,24 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
[Test]
public void TestComboCounterIncrementing()
{
createNew();
AddRepeatStep("increase combo", () =>
{
foreach (var hud in hudOverlays)
hud.ComboCounter.Current.Value++;
}, 10);
AddStep("reset combo", () =>
{
foreach (var hud in hudOverlays)
hud.ComboCounter.Current.Value = 0;
});
}
[Test] [Test]
public void TestShownByDefault() public void TestShownByDefault()
{ {
@ -45,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha); createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
AddUntilStep("wait for load", () => hudOverlay.IsAlive); AddUntilStep("wait for load", () => hudOverlay.IsAlive);
AddAssert("initial alpha was less than 1", () => initialAlpha != null && initialAlpha < 1); AddAssert("initial alpha was less than 1", () => initialAlpha < 1);
} }
[Test] [Test]
@ -53,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
createNew(); createNew();
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
@ -65,17 +89,17 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestExternalHideDoesntAffectConfig() public void TestExternalHideDoesntAffectConfig()
{ {
bool originalConfigValue = false; HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringBreaks;
createNew(); createNew();
AddStep("get original config value", () => originalConfigValue = config.Get<bool>(OsuSetting.ShowInterface)); AddStep("get original config value", () => originalConfigValue = config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddAssert("config unchanged", () => originalConfigValue == config.Get<bool>(OsuSetting.ShowInterface)); AddAssert("config unchanged", () => originalConfigValue == config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
AddAssert("config unchanged", () => originalConfigValue == config.Get<bool>(OsuSetting.ShowInterface)); AddAssert("config unchanged", () => originalConfigValue == config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
} }
[Test] [Test]
@ -89,14 +113,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set keycounter visible false", () => AddStep("set keycounter visible false", () =>
{ {
config.Set<bool>(OsuSetting.KeyOverlay, false); config.Set<bool>(OsuSetting.KeyOverlay, false);
hudOverlay.KeyCounter.AlwaysVisible.Value = false; hudOverlays.ForEach(h => h.KeyCounter.AlwaysVisible.Value = false);
}); });
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent); AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent);
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddStep("set showhud true", () => hudOverlays.ForEach(h => h.ShowHud.Value = true));
AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent); AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
@ -107,13 +131,22 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("create overlay", () => AddStep("create overlay", () =>
{ {
Child = hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>()); SetContents(() =>
{
hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>());
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
action?.Invoke(hudOverlay); hudOverlay.ComboCounter.Current.Value = 1;
action?.Invoke(hudOverlay);
return hudOverlay;
});
}); });
} }
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
} }
} }

View File

@ -22,8 +22,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private BarHitErrorMeter barMeter; private BarHitErrorMeter barMeter;
private BarHitErrorMeter barMeter2; private BarHitErrorMeter barMeter2;
private BarHitErrorMeter barMeter3;
private ColourHitErrorMeter colourMeter; private ColourHitErrorMeter colourMeter;
private ColourHitErrorMeter colourMeter2; private ColourHitErrorMeter colourMeter2;
private ColourHitErrorMeter colourMeter3;
private HitWindows hitWindows; private HitWindows hitWindows;
public TestSceneHitErrorMeter() public TestSceneHitErrorMeter()
@ -115,6 +117,13 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
}); });
Add(barMeter3 = new BarHitErrorMeter(hitWindows, true)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Rotation = 270,
});
Add(colourMeter = new ColourHitErrorMeter(hitWindows) Add(colourMeter = new ColourHitErrorMeter(hitWindows)
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
@ -128,6 +137,14 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 } Margin = new MarginPadding { Left = 50 }
}); });
Add(colourMeter3 = new ColourHitErrorMeter(hitWindows)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Rotation = 270,
Margin = new MarginPadding { Left = 50 }
});
} }
private void newJudgement(double offset = 0) private void newJudgement(double offset = 0)
@ -140,8 +157,10 @@ namespace osu.Game.Tests.Visual.Gameplay
barMeter.OnNewJudgement(judgement); barMeter.OnNewJudgement(judgement);
barMeter2.OnNewJudgement(judgement); barMeter2.OnNewJudgement(judgement);
barMeter3.OnNewJudgement(judgement);
colourMeter.OnNewJudgement(judgement); colourMeter.OnNewJudgement(judgement);
colourMeter2.OnNewJudgement(judgement); colourMeter2.OnNewJudgement(judgement);
colourMeter3.OnNewJudgement(judgement);
} }
} }
} }

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -35,6 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestPlayerLoaderContainer container; private TestPlayerLoaderContainer container;
private TestPlayer player; private TestPlayer player;
private bool epilepsyWarning;
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
@ -59,6 +62,7 @@ namespace osu.Game.Tests.Visual.Gameplay
beforeLoadAction?.Invoke(); beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
Beatmap.Value.BeatmapInfo.EpilepsyWarning = epilepsyWarning;
foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track); mod.ApplyToTrack(Beatmap.Value.Track);
@ -251,6 +255,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for player load", () => player.IsLoaded); AddUntilStep("wait for player load", () => player.IsLoaded);
} }
[TestCase(true)]
[TestCase(false)]
public void TestEpilepsyWarning(bool warning)
{
AddStep("change epilepsy warning", () => epilepsyWarning = warning);
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType<EpilepsyWarning>().Any() == warning);
}
private class TestPlayerLoaderContainer : Container private class TestPlayerLoaderContainer : Container
{ {
[Cached] [Cached]

View File

@ -1,68 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public class TestSceneScoreCounter : OsuTestScene
{
public TestSceneScoreCounter()
{
int numerator = 0, denominator = 0;
ScoreCounter score = new ScoreCounter(7)
{
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Margin = new MarginPadding(20),
};
Add(score);
ComboCounter comboCounter = new StandardComboCounter
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Margin = new MarginPadding(10),
};
Add(comboCounter);
PercentageCounter accuracyCounter = new PercentageCounter
{
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Position = new Vector2(-20, 60),
};
Add(accuracyCounter);
AddStep(@"Reset all", delegate
{
score.Current.Value = 0;
comboCounter.Current.Value = 0;
numerator = denominator = 0;
accuracyCounter.SetFraction(0, 0);
});
AddStep(@"Hit! :D", delegate
{
score.Current.Value += 300 + (ulong)(300.0 * (comboCounter.Current.Value > 0 ? comboCounter.Current.Value - 1 : 0) / 25.0);
comboCounter.Increment();
numerator++;
denominator++;
accuracyCounter.SetFraction(numerator, denominator);
});
AddStep(@"miss...", delegate
{
comboCounter.Current.Value = 0;
denominator++;
accuracyCounter.SetFraction(numerator, denominator);
});
}
}
}

View File

@ -0,0 +1,49 @@
// 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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableAccuracyCounter : SkinnableTestScene
{
private IEnumerable<SkinnableAccuracyCounter> accuracyCounters => CreatedDrawables.OfType<SkinnableAccuracyCounter>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create combo counters", () => SetContents(() =>
{
var accuracyCounter = new SkinnableAccuracyCounter();
accuracyCounter.Current.Value = 1;
return accuracyCounter;
}));
}
[Test]
public void TestChangingAccuracy()
{
AddStep(@"Reset all", delegate
{
foreach (var s in accuracyCounters)
s.Current.Value = 1;
});
AddStep(@"Hit! :D", delegate
{
foreach (var s in accuracyCounters)
s.Current.Value -= 0.023f;
});
}
}
}

View File

@ -0,0 +1,61 @@
// 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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableHealthDisplay : SkinnableTestScene
{
private IEnumerable<SkinnableHealthDisplay> healthDisplays => CreatedDrawables.OfType<SkinnableHealthDisplay>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create health displays", () =>
{
SetContents(() => new SkinnableHealthDisplay());
});
AddStep(@"Reset all", delegate
{
foreach (var s in healthDisplays)
s.Current.Value = 1;
});
}
[Test]
public void TestHealthDisplayIncrementing()
{
AddRepeatStep(@"decrease hp", delegate
{
foreach (var healthDisplay in healthDisplays)
healthDisplay.Current.Value -= 0.08f;
}, 10);
AddRepeatStep(@"increase hp without flash", delegate
{
foreach (var healthDisplay in healthDisplays)
healthDisplay.Current.Value += 0.1f;
}, 3);
AddRepeatStep(@"increase hp with flash", delegate
{
foreach (var healthDisplay in healthDisplays)
{
healthDisplay.Current.Value += 0.1f;
healthDisplay.Flash(new JudgementResult(null, new OsuJudgement()));
}
}, 3);
}
}
}

View File

@ -0,0 +1,54 @@
// 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 NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableScoreCounter : SkinnableTestScene
{
private IEnumerable<SkinnableScoreCounter> scoreCounters => CreatedDrawables.OfType<SkinnableScoreCounter>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create combo counters", () => SetContents(() =>
{
var comboCounter = new SkinnableScoreCounter();
comboCounter.Current.Value = 1;
return comboCounter;
}));
}
[Test]
public void TestScoreCounterIncrementing()
{
AddStep(@"Reset all", delegate
{
foreach (var s in scoreCounters)
s.Current.Value = 0;
});
AddStep(@"Hit! :D", delegate
{
foreach (var s in scoreCounters)
s.Current.Value += 300;
});
}
[Test]
public void TestVeryLargeScore()
{
AddStep("set large score", () => scoreCounters.ForEach(counter => counter.Current.Value = 1_000_000_000));
}
}
}

View File

@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -22,27 +21,24 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneSkinnableSound : OsuTestScene public class TestSceneSkinnableSound : OsuTestScene
{ {
[Cached(typeof(ISamplePlaybackDisabler))]
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
private TestSkinSourceContainer skinSource; private TestSkinSourceContainer skinSource;
private PausableSkinnableSound skinnableSound; private PausableSkinnableSound skinnableSound;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUpSteps()
{ {
gameplayClock.IsPaused.Value = false; AddStep("setup hierarchy", () =>
Children = new Drawable[]
{ {
skinSource = new TestSkinSourceContainer Children = new Drawable[]
{ {
Clock = gameplayClock, skinSource = new TestSkinSourceContainer
RelativeSizeAxes = Axes.Both, {
Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide")) RelativeSizeAxes = Axes.Both,
}, Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
}; },
}); };
});
}
[Test] [Test]
public void TestStoppedSoundDoesntResumeAfterPause() public void TestStoppedSoundDoesntResumeAfterPause()
@ -62,8 +58,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for sample to stop playing", () => !sample.Playing); AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddWaitStep("wait a bit", 5); AddWaitStep("wait a bit", 5);
AddAssert("sample not playing", () => !sample.Playing); AddAssert("sample not playing", () => !sample.Playing);
@ -82,8 +79,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for sample to start playing", () => sample.Playing); AddUntilStep("wait for sample to start playing", () => sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing); AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddUntilStep("wait for sample to start playing", () => sample.Playing);
} }
[Test] [Test]
@ -98,10 +98,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample playing", () => sample.Playing); AddAssert("sample playing", () => sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); AddUntilStep("sample not playing", () => !sample.Playing);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddAssert("sample not playing", () => !sample.Playing); AddAssert("sample not playing", () => !sample.Playing);
AddAssert("sample not playing", () => !sample.Playing); AddAssert("sample not playing", () => !sample.Playing);
@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample playing", () => sample.Playing); AddAssert("sample playing", () => sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing); AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("trigger skin change", () => skinSource.TriggerSourceChanged()); AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
@ -133,20 +134,25 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
AddAssert("new sample stopped", () => !sample.Playing); AddAssert("new sample stopped", () => !sample.Playing);
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddWaitStep("wait a bit", 5); AddWaitStep("wait a bit", 5);
AddAssert("new sample not played", () => !sample.Playing); AddAssert("new sample not played", () => !sample.Playing);
} }
[Cached(typeof(ISkinSource))] [Cached(typeof(ISkinSource))]
private class TestSkinSourceContainer : Container, ISkinSource [Cached(typeof(ISamplePlaybackDisabler))]
private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
{ {
[Resolved] [Resolved]
private ISkinSource source { get; set; } private ISkinSource source { get; set; }
public event Action SourceChanged; public event Action SourceChanged;
public Bindable<bool> SamplePlaybackDisabled { get; } = new Bindable<bool>();
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled;
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component); public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);

View File

@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -40,6 +41,12 @@ namespace osu.Game.Tests.Visual.SongSelect
this.rulesets = rulesets; this.rulesets = rulesets;
} }
[Test]
public void TestManyPanels()
{
loadBeatmaps(count: 5000, randomDifficulties: true);
}
[Test] [Test]
public void TestKeyRepeat() public void TestKeyRepeat()
{ {
@ -394,7 +401,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false)); AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz"); AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!")); AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!", StringComparison.Ordinal));
} }
[Test] [Test]
@ -707,21 +714,22 @@ namespace osu.Game.Tests.Visual.SongSelect
checkVisibleItemCount(true, 15); checkVisibleItemCount(true, 15);
} }
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null) private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null, bool randomDifficulties = false)
{ {
createCarousel(carouselAdjust);
if (beatmapSets == null)
{
beatmapSets = new List<BeatmapSetInfo>();
for (int i = 1; i <= set_count; i++)
beatmapSets.Add(createTestBeatmapSet(i));
}
bool changed = false; bool changed = false;
AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () =>
createCarousel(c =>
{ {
carouselAdjust?.Invoke(c);
if (beatmapSets == null)
{
beatmapSets = new List<BeatmapSetInfo>();
for (int i = 1; i <= (count ?? set_count); i++)
beatmapSets.Add(createTestBeatmapSet(i, randomDifficulties));
}
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria()); carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
carousel.BeatmapSetsChanged = () => changed = true; carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets; carousel.BeatmapSets = beatmapSets;
@ -807,7 +815,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private bool selectedBeatmapVisible() private bool selectedBeatmapVisible()
{ {
var currentlySelected = carousel.Items.Find(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected); var currentlySelected = carousel.Items.FirstOrDefault(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected);
if (currentlySelected == null) if (currentlySelected == null)
return true; return true;
@ -820,7 +828,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Selection is visible", selectedBeatmapVisible); AddAssert("Selection is visible", selectedBeatmapVisible);
} }
private BeatmapSetInfo createTestBeatmapSet(int id) private BeatmapSetInfo createTestBeatmapSet(int id, bool randomDifficultyCount = false)
{ {
return new BeatmapSetInfo return new BeatmapSetInfo
{ {
@ -834,42 +842,37 @@ namespace osu.Game.Tests.Visual.SongSelect
Title = $"test set #{id}!", Title = $"test set #{id}!",
AuthorString = string.Concat(Enumerable.Repeat((char)('z' - Math.Min(25, id - 1)), 5)) AuthorString = string.Concat(Enumerable.Repeat((char)('z' - Math.Min(25, id - 1)), 5))
}, },
Beatmaps = new List<BeatmapInfo>(new[] Beatmaps = getBeatmaps(randomDifficultyCount ? RNG.Next(1, 20) : 3).ToList()
{
new BeatmapInfo
{
OnlineBeatmapID = id * 10,
Version = "Normal",
StarDifficulty = 2,
BaseDifficulty = new BeatmapDifficulty
{
OverallDifficulty = 3.5f,
}
},
new BeatmapInfo
{
OnlineBeatmapID = id * 10 + 1,
Version = "Hard",
StarDifficulty = 5,
BaseDifficulty = new BeatmapDifficulty
{
OverallDifficulty = 5,
}
},
new BeatmapInfo
{
OnlineBeatmapID = id * 10 + 2,
Version = "Insane",
StarDifficulty = 6,
BaseDifficulty = new BeatmapDifficulty
{
OverallDifficulty = 7,
}
},
}),
}; };
} }
private IEnumerable<BeatmapInfo> getBeatmaps(int count)
{
int id = 0;
for (int i = 0; i < count; i++)
{
float diff = (float)i / count * 10;
string version = "Normal";
if (diff > 6.6)
version = "Insane";
else if (diff > 3.3)
version = "Hard";
yield return new BeatmapInfo
{
OnlineBeatmapID = id++ * 10,
Version = version,
StarDifficulty = diff,
BaseDifficulty = new BeatmapDifficulty
{
OverallDifficulty = diff,
}
};
}
}
private BeatmapSetInfo createTestBeatmapSetWithManyDifficulties(int id) private BeatmapSetInfo createTestBeatmapSetWithManyDifficulties(int id)
{ {
var toReturn = new BeatmapSetInfo var toReturn = new BeatmapSetInfo
@ -908,10 +911,25 @@ namespace osu.Game.Tests.Visual.SongSelect
private class TestBeatmapCarousel : BeatmapCarousel private class TestBeatmapCarousel : BeatmapCarousel
{ {
public new List<DrawableCarouselItem> Items => base.Items;
public bool PendingFilterTask => PendingFilter != null; public bool PendingFilterTask => PendingFilter != null;
public IEnumerable<DrawableCarouselItem> Items
{
get
{
foreach (var item in ScrollableContent)
{
yield return item;
if (item is DrawableCarouselBeatmapSet set)
{
foreach (var difficulty in set.DrawableBeatmaps)
yield return difficulty;
}
}
}
}
protected override IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => Enumerable.Empty<BeatmapSetInfo>(); protected override IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => Enumerable.Empty<BeatmapSetInfo>();
} }
} }

View File

@ -507,7 +507,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var selectedPanel = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(s => s.Item.State.Value == CarouselItemState.Selected); var selectedPanel = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(s => s.Item.State.Value == CarouselItemState.Selected);
// special case for converts checked here. // special case for converts checked here.
return selectedPanel.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>().All(i => return selectedPanel.ChildrenOfType<FilterableDifficultyIcon>().All(i =>
i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0); i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0);
}); });
@ -606,10 +606,10 @@ namespace osu.Game.Tests.Visual.SongSelect
set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(); set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First();
}); });
DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null; FilterableDifficultyIcon difficultyIcon = null;
AddStep("Find an icon", () => AddStep("Find an icon", () =>
{ {
difficultyIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>() difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex()); .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
}); });
@ -634,13 +634,13 @@ namespace osu.Game.Tests.Visual.SongSelect
})); }));
BeatmapInfo filteredBeatmap = null; BeatmapInfo filteredBeatmap = null;
DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null; FilterableDifficultyIcon filteredIcon = null;
AddStep("Get filtered icon", () => AddStep("Get filtered icon", () =>
{ {
filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM); filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM);
int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap);
filteredIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex); filteredIcon = set.ChildrenOfType<FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex);
}); });
AddStep("Click on a filtered difficulty", () => AddStep("Click on a filtered difficulty", () =>
@ -674,10 +674,10 @@ namespace osu.Game.Tests.Visual.SongSelect
return set != null; return set != null;
}); });
DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null; FilterableDifficultyIcon difficultyIcon = null;
AddStep("Find an icon for different ruleset", () => AddStep("Find an icon for different ruleset", () =>
{ {
difficultyIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>() difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.First(icon => icon.Item.Beatmap.Ruleset.ID == 3); .First(icon => icon.Item.Beatmap.Ruleset.ID == 3);
}); });
@ -725,10 +725,10 @@ namespace osu.Game.Tests.Visual.SongSelect
return set != null; return set != null;
}); });
DrawableCarouselBeatmapSet.FilterableGroupedDifficultyIcon groupIcon = null; FilterableGroupedDifficultyIcon groupIcon = null;
AddStep("Find group icon for different ruleset", () => AddStep("Find group icon for different ruleset", () =>
{ {
groupIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableGroupedDifficultyIcon>() groupIcon = set.ChildrenOfType<FilterableGroupedDifficultyIcon>()
.First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3); .First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3);
}); });
@ -821,9 +821,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap);
private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, DrawableCarouselBeatmapSet.FilterableDifficultyIcon icon) private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon)
{ {
return set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>().ToList().FindIndex(i => i == icon); return set.ChildrenOfType<FilterableDifficultyIcon>().ToList().FindIndex(i => i == icon);
} }
private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id)); private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id));

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -18,10 +19,11 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneModSettings : OsuTestScene public class TestSceneModSettings : OsuManualInputManagerTestScene
{ {
private TestModSelectOverlay modSelect; private TestModSelectOverlay modSelect;
@ -95,6 +97,41 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value)); AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
} }
[Test]
public void TestMultiModSettingsUnboundWhenCopied()
{
MultiMod original = null;
MultiMod copy = null;
AddStep("create mods", () =>
{
original = new MultiMod(new OsuModDoubleTime());
copy = (MultiMod)original.CreateCopy();
});
AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2);
AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value));
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value));
}
[Test]
public void TestCustomisationMenuNoClickthrough()
{
createModSelect();
openModSelect();
AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f));
AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1);
AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMod)));
AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMod).IsHovered);
AddStep("left click mod", () => InputManager.Click(MouseButton.Left));
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
AddStep("right click mod", () => InputManager.Click(MouseButton.Right));
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
}
private void createModSelect() private void createModSelect()
{ {
AddStep("create mod select", () => AddStep("create mod select", () =>
@ -121,9 +158,16 @@ namespace osu.Game.Tests.Visual.UserInterface
public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded); public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
public ModButton GetModButton(Mod mod)
{
return ModSectionsContainer.ChildrenOfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
}
public void SelectMod(Mod mod) => public void SelectMod(Mod mod) =>
ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) GetModButton(mod).SelectNext(1);
.ButtonsContainer.OfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1);
public void SetModSettingsWidth(float newWidth) =>
ModSettingsContainer.Width = newWidth;
} }
public class TestRulesetInfo : RulesetInfo public class TestRulesetInfo : RulesetInfo

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -59,7 +60,7 @@ namespace osu.Game.Tests
get get
{ {
using (var reader = getZipReader()) using (var reader = getZipReader())
return reader.Filenames.First(f => f.EndsWith(".mp3")); return reader.Filenames.First(f => f.EndsWith(".mp3", StringComparison.Ordinal));
} }
} }
@ -73,7 +74,7 @@ namespace osu.Game.Tests
protected override Beatmap CreateBeatmap() protected override Beatmap CreateBeatmap()
{ {
using (var reader = getZipReader()) using (var reader = getZipReader())
using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu")))) using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu", StringComparison.Ordinal))))
using (var beatmapReader = new LineBufferedReader(beatmapStream)) using (var beatmapReader = new LineBufferedReader(beatmapStream))
return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader); return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader);
} }

View File

@ -0,0 +1,169 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.Tournament.Configuration;
using osu.Game.Tests;
namespace osu.Game.Tournament.Tests.NonVisual
{
[TestFixture]
public class CustomTourneyDirectoryTest
{
[Test]
public void TestDefaultDirectory()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default")));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestCustomDirectory()
{
using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file.
{
string osuDesktopStorage = basePath(nameof(TestCustomDirectory));
const string custom_tournament = "custom";
// need access before the game has constructed its own storage yet.
Storage storage = new DesktopStorage(osuDesktopStorage, host);
// manual cleaning so we can prepare a config file.
storage.DeleteDirectory(string.Empty);
using (var storageConfig = new TournamentStorageManager(storage))
storageConfig.Set(StorageConfig.CurrentTournament, custom_tournament);
try
{
var osu = loadOsu(host);
storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", custom_tournament)));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestMigration()
{
using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) // don't use clean run as we are writing test files for migration.
{
string osuRoot = basePath(nameof(TestMigration));
string configFile = Path.Combine(osuRoot, "tournament.ini");
if (File.Exists(configFile))
File.Delete(configFile);
// Recreate the old setup that uses "tournament" as the base path.
string oldPath = Path.Combine(osuRoot, "tournament");
string videosPath = Path.Combine(oldPath, "videos");
string modsPath = Path.Combine(oldPath, "mods");
string flagsPath = Path.Combine(oldPath, "flags");
Directory.CreateDirectory(videosPath);
Directory.CreateDirectory(modsPath);
Directory.CreateDirectory(flagsPath);
// Define testing files corresponding to the specific file migrations that are needed
string bracketFile = Path.Combine(osuRoot, "bracket.json");
string drawingsConfig = Path.Combine(osuRoot, "drawings.ini");
string drawingsFile = Path.Combine(osuRoot, "drawings.txt");
string drawingsResult = Path.Combine(osuRoot, "drawings_results.txt");
// Define sample files to test recursive copying
string videoFile = Path.Combine(videosPath, "video.mp4");
string modFile = Path.Combine(modsPath, "mod.png");
string flagFile = Path.Combine(flagsPath, "flag.png");
File.WriteAllText(bracketFile, "{}");
File.WriteAllText(drawingsConfig, "test");
File.WriteAllText(drawingsFile, "test");
File.WriteAllText(drawingsResult, "test");
File.WriteAllText(videoFile, "test");
File.WriteAllText(modFile, "test");
File.WriteAllText(flagFile, "test");
try
{
var osu = loadOsu(host);
var storage = osu.Dependencies.Get<Storage>();
string migratedPath = Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default");
videosPath = Path.Combine(migratedPath, "videos");
modsPath = Path.Combine(migratedPath, "mods");
flagsPath = Path.Combine(migratedPath, "flags");
videoFile = Path.Combine(videosPath, "video.mp4");
modFile = Path.Combine(modsPath, "mod.png");
flagFile = Path.Combine(flagsPath, "flag.png");
Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath));
Assert.True(storage.Exists("bracket.json"));
Assert.True(storage.Exists("drawings.txt"));
Assert.True(storage.Exists("drawings_results.txt"));
Assert.True(storage.Exists("drawings.ini"));
Assert.True(storage.Exists(videoFile));
Assert.True(storage.Exists(modFile));
Assert.True(storage.Exists(flagFile));
}
finally
{
host.Storage.Delete("tournament.ini");
host.Storage.DeleteDirectory("tournaments");
host.Exit();
}
}
}
private TournamentGameBase loadOsu(GameHost host)
{
var osu = new TournamentGameBase();
Task.Run(() => host.Run(osu));
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 90000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
private string basePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance);
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Video; using osu.Framework.Graphics.Video;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Tournament.IO;
namespace osu.Game.Tournament.Components namespace osu.Game.Tournament.Components
{ {
@ -17,7 +18,6 @@ namespace osu.Game.Tournament.Components
private readonly string filename; private readonly string filename;
private readonly bool drawFallbackGradient; private readonly bool drawFallbackGradient;
private Video video; private Video video;
private ManualClock manualClock; private ManualClock manualClock;
public TourneyVideo(string filename, bool drawFallbackGradient = false) public TourneyVideo(string filename, bool drawFallbackGradient = false)
@ -27,9 +27,9 @@ namespace osu.Game.Tournament.Components
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(TournamentStorage storage) private void load(TournamentVideoResourceStore storage)
{ {
var stream = storage.GetStream($@"videos/{filename}"); var stream = storage.GetStream(filename);
if (stream != null) if (stream != null)
{ {

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Configuration;
using osu.Framework.Platform;
namespace osu.Game.Tournament.Configuration
{
public class TournamentStorageManager : IniConfigManager<StorageConfig>
{
protected override string Filename => "tournament.ini";
public TournamentStorageManager(Storage storage)
: base(storage)
{
}
}
public enum StorageConfig
{
CurrentTournament,
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.IO;
using System.IO;
using osu.Game.Tournament.Configuration;
namespace osu.Game.Tournament.IO
{
public class TournamentStorage : MigratableStorage
{
private const string default_tournament = "default";
private readonly Storage storage;
private readonly TournamentStorageManager storageConfig;
public TournamentStorage(Storage storage)
: base(storage.GetStorageForDirectory("tournaments"), string.Empty)
{
this.storage = storage;
storageConfig = new TournamentStorageManager(storage);
if (storage.Exists("tournament.ini"))
{
ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament)));
}
else
Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament));
Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
}
public override void Migrate(Storage newStorage)
{
// this migration only happens once on moving to the per-tournament storage system.
// listed files are those known at that point in time.
// this can be removed at some point in the future (6 months obsoletion would mean 2021-04-19)
var source = new DirectoryInfo(storage.GetFullPath("tournament"));
var destination = new DirectoryInfo(newStorage.GetFullPath("."));
if (source.Exists)
{
Logger.Log("Migrating tournament assets to default tournament storage.");
CopyRecursive(source, destination);
DeleteRecursive(source);
}
moveFileIfExists("bracket.json", destination);
moveFileIfExists("drawings.txt", destination);
moveFileIfExists("drawings_results.txt", destination);
moveFileIfExists("drawings.ini", destination);
ChangeTargetStorage(newStorage);
storageConfig.Set(StorageConfig.CurrentTournament, default_tournament);
storageConfig.Save();
}
private void moveFileIfExists(string file, DirectoryInfo destination)
{
if (!storage.Exists(file))
return;
Logger.Log($"Migrating {file} to default tournament storage.");
var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file));
AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true));
fileInfo.Delete();
}
}
}

View File

@ -4,12 +4,12 @@
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Platform; using osu.Framework.Platform;
namespace osu.Game.Tournament namespace osu.Game.Tournament.IO
{ {
internal class TournamentStorage : NamespacedResourceStore<byte[]> public class TournamentVideoResourceStore : NamespacedResourceStore<byte[]>
{ {
public TournamentStorage(Storage storage) public TournamentVideoResourceStore(Storage storage)
: base(new StorageBackedResourceStore(storage), "tournament") : base(new StorageBackedResourceStore(storage), "videos")
{ {
AddExtension("m4v"); AddExtension("m4v");
AddExtension("avi"); AddExtension("avi");

View File

@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Screens.Drawings
if (string.IsNullOrEmpty(line)) if (string.IsNullOrEmpty(line))
continue; continue;
if (line.ToUpperInvariant().StartsWith("GROUP")) if (line.ToUpperInvariant().StartsWith("GROUP", StringComparison.Ordinal))
continue; continue;
// ReSharper disable once AccessToModifiedClosure // ReSharper disable once AccessToModifiedClosure

View File

@ -8,11 +8,12 @@ using Newtonsoft.Json;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.IO.Stores;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
using osu.Game.Tournament.IO;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Input; using osuTK.Input;
@ -23,13 +24,8 @@ namespace osu.Game.Tournament
public class TournamentGameBase : OsuGameBase public class TournamentGameBase : OsuGameBase
{ {
private const string bracket_filename = "bracket.json"; private const string bracket_filename = "bracket.json";
private LadderInfo ladder; private LadderInfo ladder;
private TournamentStorage storage;
private Storage storage;
private TournamentStorage tournamentStorage;
private DependencyContainer dependencies; private DependencyContainer dependencies;
private FileBasedIPC ipc; private FileBasedIPC ipc;
@ -39,15 +35,14 @@ namespace osu.Game.Tournament
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(Storage storage) private void load(Storage baseStorage)
{ {
Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly));
dependencies.CacheAs(tournamentStorage = new TournamentStorage(storage)); dependencies.CacheAs<Storage>(storage = new TournamentStorage(baseStorage));
dependencies.Cache(new TournamentVideoResourceStore(storage));
Textures.AddStore(new TextureLoaderStore(tournamentStorage)); Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
this.storage = storage;
readBracket(); readBracket();

View File

@ -13,9 +13,12 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -124,13 +127,22 @@ namespace osu.Game.Beatmaps
/// <returns>The <see cref="DifficultyRating"/> that best describes <paramref name="starRating"/>.</returns> /// <returns>The <see cref="DifficultyRating"/> that best describes <paramref name="starRating"/>.</returns>
public static DifficultyRating GetDifficultyRating(double starRating) public static DifficultyRating GetDifficultyRating(double starRating)
{ {
if (starRating < 2.0) return DifficultyRating.Easy; if (Precision.AlmostBigger(starRating, 6.5, 0.005))
if (starRating < 2.7) return DifficultyRating.Normal; return DifficultyRating.ExpertPlus;
if (starRating < 4.0) return DifficultyRating.Hard;
if (starRating < 5.3) return DifficultyRating.Insane;
if (starRating < 6.5) return DifficultyRating.Expert;
return DifficultyRating.ExpertPlus; if (Precision.AlmostBigger(starRating, 5.3, 0.005))
return DifficultyRating.Expert;
if (Precision.AlmostBigger(starRating, 4.0, 0.005))
return DifficultyRating.Insane;
if (Precision.AlmostBigger(starRating, 2.7, 0.005))
return DifficultyRating.Hard;
if (Precision.AlmostBigger(starRating, 2.0, 0.005))
return DifficultyRating.Normal;
return DifficultyRating.Easy;
} }
private CancellationTokenSource trackedUpdateCancellationSource; private CancellationTokenSource trackedUpdateCancellationSource;
@ -228,6 +240,24 @@ namespace osu.Game.Beatmaps
return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo); return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo);
} }
catch (BeatmapInvalidForRulesetException e)
{
// Conversion has failed for the given ruleset, so return the difficulty in the beatmap's default ruleset.
// Ensure the beatmap's default ruleset isn't the one already being converted to.
// This shouldn't happen as it means something went seriously wrong, but if it does an endless loop should be avoided.
if (rulesetInfo.Equals(beatmapInfo.Ruleset))
{
Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset}).");
return difficultyCache[key] = new StarDifficulty();
}
// Check the cache first because this is now a different ruleset than the one previously guarded against.
if (tryGetExisting(beatmapInfo, beatmapInfo.Ruleset, Array.Empty<Mod>(), out var existingDefault, out var existingDefaultKey))
return existingDefault;
return computeDifficulty(existingDefaultKey, beatmapInfo, beatmapInfo.Ruleset);
}
catch catch
{ {
return difficultyCache[key] = new StarDifficulty(); return difficultyCache[key] = new StarDifficulty();

View File

@ -92,13 +92,14 @@ namespace osu.Game.Beatmaps
public bool LetterboxInBreaks { get; set; } public bool LetterboxInBreaks { get; set; }
public bool WidescreenStoryboard { get; set; } public bool WidescreenStoryboard { get; set; }
public bool EpilepsyWarning { get; set; }
// Editor // Editor
// This bookmarks stuff is necessary because DB doesn't know how to store int[] // This bookmarks stuff is necessary because DB doesn't know how to store int[]
[JsonIgnore] [JsonIgnore]
public string StoredBookmarks public string StoredBookmarks
{ {
get => string.Join(",", Bookmarks); get => string.Join(',', Bookmarks);
set set
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))

View File

@ -19,6 +19,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
@ -36,6 +37,7 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary> /// </summary>
[ExcludeFromDynamicCompile]
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable
{ {
/// <summary> /// <summary>
@ -389,7 +391,7 @@ namespace osu.Game.Beatmaps
protected override BeatmapSetInfo CreateModel(ArchiveReader reader) protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{ {
// let's make sure there are actually .osu files to import. // let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrEmpty(mapName)) if (string.IsNullOrEmpty(mapName))
{ {
@ -417,7 +419,7 @@ namespace osu.Game.Beatmaps
{ {
var beatmapInfos = new List<BeatmapInfo>(); var beatmapInfos = new List<BeatmapInfo>();
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu"))) foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
{ {
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
using (var ms = new MemoryStream()) // we need a memory stream so we can seek using (var ms = new MemoryStream()) // we need a memory stream so we can seek

View File

@ -13,6 +13,7 @@ using osu.Framework.Development;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
@ -23,6 +24,7 @@ namespace osu.Game.Beatmaps
{ {
public partial class BeatmapManager public partial class BeatmapManager
{ {
[ExcludeFromDynamicCompile]
private class BeatmapOnlineLookupQueue : IDisposable private class BeatmapOnlineLookupQueue : IDisposable
{ {
private readonly IAPIProvider api; private readonly IAPIProvider api;

View File

@ -8,6 +8,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -17,6 +18,7 @@ namespace osu.Game.Beatmaps
{ {
public partial class BeatmapManager public partial class BeatmapManager
{ {
[ExcludeFromDynamicCompile]
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{ {
private readonly IResourceStore<byte[]> store; private readonly IResourceStore<byte[]> store;

View File

@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
public string Hash { get; set; } public string Hash { get; set; }
public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb"))?.Filename; public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
public List<BeatmapSetFileInfo> Files { get; set; } public List<BeatmapSetFileInfo> Files { get; set; }

View File

@ -47,7 +47,10 @@ namespace osu.Game.Beatmaps.Drawables
private readonly IReadOnlyList<Mod> mods; private readonly IReadOnlyList<Mod> mods;
private readonly bool shouldShowTooltip; private readonly bool shouldShowTooltip;
private readonly IBindable<StarDifficulty> difficultyBindable = new Bindable<StarDifficulty>();
private readonly bool performBackgroundDifficultyLookup;
private readonly Bindable<StarDifficulty> difficultyBindable = new Bindable<StarDifficulty>();
private Drawable background; private Drawable background;
@ -70,10 +73,12 @@ namespace osu.Game.Beatmaps.Drawables
/// </summary> /// </summary>
/// <param name="beatmap">The beatmap to show the difficulty of.</param> /// <param name="beatmap">The beatmap to show the difficulty of.</param>
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param> /// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true) /// <param name="performBackgroundDifficultyLookup">Whether to perform difficulty lookup (including calculation if necessary).</param>
public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true)
{ {
this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap));
this.shouldShowTooltip = shouldShowTooltip; this.shouldShowTooltip = shouldShowTooltip;
this.performBackgroundDifficultyLookup = performBackgroundDifficultyLookup;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -112,9 +117,13 @@ namespace osu.Game.Beatmaps.Drawables
// the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment)
Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }
}, },
new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0),
}; };
if (performBackgroundDifficultyLookup)
iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0));
else
difficultyBindable.Value = new StarDifficulty(beatmap.StarDifficulty, 0);
difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating)); difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating));
} }

View File

@ -175,6 +175,10 @@ namespace osu.Game.Beatmaps.Formats
case @"WidescreenStoryboard": case @"WidescreenStoryboard":
beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1; beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"EpilepsyWarning":
beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1;
break;
} }
} }
@ -307,12 +311,7 @@ namespace osu.Game.Beatmaps.Formats
double start = getOffsetTime(Parsing.ParseDouble(split[1])); double start = getOffsetTime(Parsing.ParseDouble(split[1]));
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))); double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
var breakEvent = new BreakPeriod(start, end); beatmap.Breaks.Add(new BreakPeriod(start, end));
if (!breakEvent.HasEffect)
return;
beatmap.Breaks.Add(breakEvent);
break; break;
} }
} }

View File

@ -92,7 +92,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
var pair = SplitKeyVal(line); var pair = SplitKeyVal(line);
bool isCombo = pair.Key.StartsWith(@"Combo"); bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal);
string[] split = pair.Value.Split(','); string[] split = pair.Value.Split(',');

View File

@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Timing
public double Duration => EndTime - StartTime; public double Duration => EndTime - StartTime;
/// <summary> /// <summary>
/// Whether the break has any effect. Breaks that are too short are culled before they are added to the beatmap. /// Whether the break has any effect.
/// </summary> /// </summary>
public bool HasEffect => Duration >= MIN_BREAK_DURATION; public bool HasEffect => Duration >= MIN_BREAK_DURATION;

View File

@ -0,0 +1,20 @@
// 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.ComponentModel;
namespace osu.Game.Configuration
{
public enum HUDVisibilityMode
{
Never,
[Description("Hide during gameplay")]
HideDuringGameplay,
[Description("Hide during breaks")]
HideDuringBreaks,
Always
}
}

View File

@ -90,7 +90,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.HitLighting, true); Set(OsuSetting.HitLighting, true);
Set(OsuSetting.ShowInterface, true); Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
Set(OsuSetting.ShowProgressGraph, true); Set(OsuSetting.ShowProgressGraph, true);
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
Set(OsuSetting.FadePlayfieldWhenHealthLow, true); Set(OsuSetting.FadePlayfieldWhenHealthLow, true);
@ -190,7 +190,7 @@ namespace osu.Game.Configuration
AlwaysPlayFirstComboBreak, AlwaysPlayFirstComboBreak,
ScoreMeter, ScoreMeter,
FloatingComments, FloatingComments,
ShowInterface, HUDVisibilityMode,
ShowProgressGraph, ShowProgressGraph,
ShowHealthDisplayWhenCantFail, ShowHealthDisplayWhenCantFail,
FadePlayfieldWhenHealthLow, FadePlayfieldWhenHealthLow,

View File

@ -16,7 +16,10 @@ namespace osu.Game.Configuration
[Description("Hit Error (right)")] [Description("Hit Error (right)")]
HitErrorRight, HitErrorRight,
[Description("Hit Error (both)")] [Description("Hit Error (bottom)")]
HitErrorBottom,
[Description("Hit Error (left+right)")]
HitErrorBoth, HitErrorBoth,
[Description("Colour (left)")] [Description("Colour (left)")]
@ -25,7 +28,10 @@ namespace osu.Game.Configuration
[Description("Colour (right)")] [Description("Colour (right)")]
ColourRight, ColourRight,
[Description("Colour (both)")] [Description("Colour (left+right)")]
ColourBoth, ColourBoth,
[Description("Colour (bottom)")]
ColourBottom,
} }
} }

View File

@ -279,7 +279,7 @@ namespace osu.Game.Database
// for now, concatenate all .osu files in the set to create a unique hash. // for now, concatenate all .osu files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream(); MemoryStream hashable = new MemoryStream();
foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(f.Filename.EndsWith)).OrderBy(f => f.Filename)) foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename))
{ {
using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath)) using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath))
s.CopyTo(hashable); s.CopyTo(hashable);
@ -593,7 +593,7 @@ namespace osu.Game.Database
var fileInfos = new List<TFileModel>(); var fileInfos = new List<TFileModel>();
string prefix = reader.Filenames.GetCommonPrefix(); string prefix = reader.Filenames.GetCommonPrefix();
if (!(prefix.EndsWith("/") || prefix.EndsWith("\\"))) if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
prefix = string.Empty; prefix = string.Empty;
// import files to manager // import files to manager

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