1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-21 03:02:54 +08:00

Merge branch 'master' into catch-combo-counter

This commit is contained in:
Dan Balasescu 2020-09-15 17:20:31 +09:00 committed by GitHub
commit 0761aab42f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
313 changed files with 6735 additions and 1969 deletions

View File

@ -5,6 +5,6 @@
"version": "3.1.100" "version": "3.1.100"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.52" "Microsoft.Build.Traversal": "2.1.1"
} }
} }

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.812.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.819.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.911.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -14,6 +14,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue> public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{ {
Name = @"Fruit Count", Name = @"Fruit Count",
Content = fruits.ToString(), Content = fruits.ToString(),
Icon = FontAwesome.Regular.Circle CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Juice Stream Count", Name = @"Juice Stream Count",
Content = juiceStreams.ToString(), Content = juiceStreams.ToString(),
Icon = FontAwesome.Regular.Circle CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Banana Shower Count", Name = @"Banana Shower Count",
Content = bananaShowers.ToString(), Content = bananaShowers.ToString(),
Icon = FontAwesome.Regular.Circle CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
} }
}; };
} }

View File

@ -21,13 +21,11 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using System; using System;
using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch namespace osu.Game.Rulesets.Catch
{ {
[ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset public class CatchRuleset : Ruleset, ILegacyRuleset
{ {
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableCatchRuleset(this, beatmap, mods); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableCatchRuleset(this, beatmap, mods);

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Difficulty
public class CatchDifficultyAttributes : DifficultyAttributes public class CatchDifficultyAttributes : DifficultyAttributes
{ {
public double ApproachRate; public double ApproachRate;
public int MaxCombo;
} }
} }

View File

@ -14,11 +14,13 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class ManiaBeatmapConversionTest : BeatmapConversionTest<ManiaConvertMapping, ConvertValue> public class ManiaBeatmapConversionTest : BeatmapConversionTest<ManiaConvertMapping, ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase("basic")] [TestCase("basic")]
[TestCase("zero-length-slider")]
public void Test(string name) => base.Test(name); public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject) protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
@ -41,14 +40,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Note Count", Name = @"Note Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = notes.ToString(), Content = notes.ToString(),
Icon = FontAwesome.Regular.Circle
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Hold Note Count", Name = @"Hold Note Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = holdnotes.ToString(), Content = holdnotes.ToString(),
Icon = FontAwesome.Regular.Circle
}, },
}; };
} }

View File

@ -5,7 +5,6 @@ using osu.Game.Rulesets.Mania.Objects;
using System; using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -167,8 +166,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
var positionData = original as IHasPosition; var positionData = original as IHasPosition;
for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration) for (int i = 0; i <= generator.SpanCount; i++)
{ {
double time = original.StartTime + generator.SegmentDuration * i;
recordNote(time, positionData?.Position ?? Vector2.Zero); recordNote(time, positionData?.Position ?? Vector2.Zero);
computeDensity(time); computeDensity(time);
} }

View File

@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public readonly double EndTime; public readonly double EndTime;
public readonly double SegmentDuration; public readonly double SegmentDuration;
public readonly int SpanCount;
private readonly int spanCount;
private PatternType convertType; private PatternType convertType;
@ -42,20 +41,20 @@ 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; SpanCount = repeatsData?.SpanCount() ?? 1;
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 // The true distance, accounting for any repeats
double distance = (distanceData?.Distance ?? 0) * spanCount; double distance = (distanceData?.Distance ?? 0) * SpanCount;
// The velocity of the osu! hit object - calculated as the velocity of a slider // The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength; double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
// The duration of the osu! hit object // The duration of the osu! hit object
double osuDuration = distance / osuVelocity; double osuDuration = distance / osuVelocity;
EndTime = hitObject.StartTime + osuDuration; EndTime = hitObject.StartTime + osuDuration;
SegmentDuration = (EndTime - HitObject.StartTime) / spanCount; SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
} }
public override IEnumerable<Pattern> Generate() public override IEnumerable<Pattern> Generate()
@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern; return pattern;
} }
if (spanCount > 1) if (SpanCount > 1)
{ {
if (SegmentDuration <= 90) if (SegmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1); return generateRandomHoldNotes(HitObject.StartTime, 1);
@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (SegmentDuration <= 120) if (SegmentDuration <= 120)
{ {
convertType |= PatternType.ForceNotStack; convertType |= PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, spanCount + 1); return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
} }
if (SegmentDuration <= 160) if (SegmentDuration <= 160)
@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (duration >= 4000) if (duration >= 4000)
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0); return generateNRandomNotes(HitObject.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(HitObject.StartTime);
return generateHoldAndNormalNotes(HitObject.StartTime); return generateHoldAndNormalNotes(HitObject.StartTime);
@ -251,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
bool increasing = Random.NextDouble() > 0.5; bool increasing = Random.NextDouble() > 0.5;
for (int i = 0; i <= spanCount; i++) for (int i = 0; i <= SpanCount; i++)
{ {
addToPattern(pattern, column, startTime, startTime); addToPattern(pattern, column, startTime, startTime);
startTime += SegmentDuration; startTime += SegmentDuration;
@ -302,7 +301,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
for (int i = 0; i <= spanCount; i++) for (int i = 0; i <= SpanCount; i++)
{ {
addToPattern(pattern, nextColumn, startTime, startTime); addToPattern(pattern, nextColumn, startTime, startTime);
@ -393,7 +392,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern(); var pattern = new Pattern();
int columnRepeat = Math.Min(spanCount, TotalColumns); int columnRepeat = Math.Min(SpanCount, TotalColumns);
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)
@ -447,7 +446,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var rowPattern = new Pattern(); var rowPattern = new Pattern();
for (int i = 0; i <= spanCount; i++) for (int i = 0; i <= SpanCount; i++)
{ {
if (!(ignoreHead && startTime == HitObject.StartTime)) if (!(ignoreHead && startTime == HitObject.StartTime))
{ {

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{ {
base.InitialiseDefaults(); base.InitialiseDefaults();
Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 1); Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
} }

View File

@ -11,6 +11,7 @@ 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.Mods; using osu.Game.Rulesets.Mania.Mods;
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.Scoring; using osu.Game.Rulesets.Scoring;
@ -43,6 +44,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
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)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills Skills = skills
}; };
} }

View File

@ -1,6 +1,8 @@
// 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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
@ -14,6 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{ {
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint(); public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
} }
} }

View File

@ -1,6 +1,8 @@
// 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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{ {
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint(); public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
} }
} }

View File

@ -12,7 +12,6 @@ using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Testing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
@ -35,7 +34,6 @@ using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania namespace osu.Game.Rulesets.Mania
{ {
[ExcludeFromDynamicCompile]
public class ManiaRuleset : Ruleset, ILegacyRuleset public class ManiaRuleset : Ruleset, ILegacyRuleset
{ {
/// <summary> /// <summary>

View File

@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Mania
new SettingsSlider<double, TimeSlider> new SettingsSlider<double, TimeSlider>
{ {
LabelText = "Scroll speed", LabelText = "Scroll speed",
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime) Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
KeyboardStep = 5
}, },
}; };
} }

View File

@ -0,0 +1,14 @@
{
"Mappings": [{
"RandomW": 3083084786,
"RandomX": 273326509,
"RandomY": 273553282,
"RandomZ": 2659838971,
"StartTime": 4836,
"Objects": [{
"StartTime": 4836,
"EndTime": 4836,
"Column": 0
}]
}]
}

View File

@ -0,0 +1,20 @@
osu file format v14
[General]
StackLeniency: 0.7
Mode: 0
[Difficulty]
HPDrainRate:1
CircleSize:4
OverallDifficulty:1
ApproachRate:9
SliderMultiplier:2.5
SliderTickRate:0.5
[TimingPoints]
34,431.654676258993,4,1,0,50,1,0
4782,-66.6666666666667,4,1,0,20,0,0
[HitObjects]
15,199,4836,22,0,L,1,46.8750017881394

View File

@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue> public class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";

View File

@ -22,7 +22,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osuTK; using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
private TrackVirtualManual track;
protected override bool Autoplay => autoplay; protected override bool Autoplay => autoplay;
private bool autoplay; private bool autoplay;
@ -44,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double fade_in_modifier = -1200; private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = (TrackVirtualManual)working.Track;
return working;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache) private void load(RulesetConfigCache configCache)
@ -72,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
AddStep("enable autoplay", () => autoplay = true); AddStep("enable autoplay", () => autoplay = true);
base.SetUpSteps(); base.SetUpSteps();
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime; double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex); retrieveDrawableSlider(sliderIndex);
@ -97,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
AddStep("have autoplay", () => autoplay = true); AddStep("have autoplay", () => autoplay = true);
base.SetUpSteps(); base.SetUpSteps();
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime; double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex); retrieveDrawableSlider(sliderIndex);
@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => track.Seek(time)); AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
} }

View File

@ -25,7 +25,6 @@ using osu.Game.Scoring;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@ -34,18 +33,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
private TrackVirtualManual track;
protected override bool Autoplay => true; protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = (TrackVirtualManual)working.Track;
return working;
}
private DrawableSpinner drawableSpinner; private DrawableSpinner drawableSpinner;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single(); private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
@ -55,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
base.SetUpSteps(); base.SetUpSteps();
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First()); AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
} }
@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(0); addSeekStep(0);
AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
// autoplay replay frames use track time; // autoplay replay frames use track time;
// if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time. // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
// therefore we need to apply the rate adjustment to the replay itself to change from track time to real time, // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
@ -230,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => track.Seek(time)); AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
} }

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
Name = @"Circle Count", Name = @"Circle Count",
Content = circles.ToString(), Content = circles.ToString(),
Icon = FontAwesome.Regular.Circle CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Slider Count", Name = @"Slider Count",
Content = sliders.ToString(), Content = sliders.ToString(),
Icon = FontAwesome.Regular.Circle CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Spinner Count", Name = @"Spinner Count",
Content = spinners.ToString(), Content = spinners.ToString(),
Icon = FontAwesome.Regular.Circle CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
} }
}; };
} }

View File

@ -11,6 +11,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double SpeedStrain; public double SpeedStrain;
public double ApproachRate; public double ApproachRate;
public double OverallDifficulty; public double OverallDifficulty;
public int MaxCombo;
} }
} }

View File

@ -1,6 +1,8 @@
// 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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint(); public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -38,6 +39,13 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool() new SpinnerCompositionTool()
}; };
private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" };
protected override IEnumerable<BindableBool> Toggles => new[]
{
distanceSnapToggle
};
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -45,6 +53,7 @@ namespace osu.Game.Rulesets.Osu.Edit
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid(); EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid();
distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
} }
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects) protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects)
@ -87,6 +96,10 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
distanceSnapGridContainer.Clear(); distanceSnapGridContainer.Clear();
distanceSnapGridCache.Invalidate(); distanceSnapGridCache.Invalidate();
distanceSnapGrid = null;
if (!distanceSnapToggle.Value)
return;
switch (BlueprintContainer.CurrentTool) switch (BlueprintContainer.CurrentTool)
{ {

View File

@ -1,6 +1,8 @@
// 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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint(); public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
} }
} }

View File

@ -1,6 +1,8 @@
// 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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint(); public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
} }
} }

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Newtonsoft.Json;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -21,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
[JsonIgnore]
public double Duration public double Duration
{ {
get => EndTime - StartTime; get => EndTime - StartTime;
@ -112,8 +114,11 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public double TickDistanceMultiplier = 1; public double TickDistanceMultiplier = 1;
public HitCircle HeadCircle; [JsonIgnore]
public SliderTailCircle TailCircle; public HitCircle HeadCircle { get; protected set; }
[JsonIgnore]
public SliderTailCircle TailCircle { get; protected set; }
public Slider() public Slider()
{ {

View File

@ -30,14 +30,12 @@ using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
{ {
[ExcludeFromDynamicCompile]
public class OsuRuleset : Ruleset, ILegacyRuleset public class OsuRuleset : Ruleset, ILegacyRuleset
{ {
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableOsuRuleset(this, beatmap, mods); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableOsuRuleset(this, beatmap, mods);

View File

@ -18,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS); private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
protected new float CalculatedBorderPortion
// Roughly matches osu!stable's slider border portions.
=> base.CalculatedBorderPortion * 0.77f;
public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f);
protected override Color4 ColourAt(float position) protected override Color4 ColourAt(float position)

View File

@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
{ {
if (pointGrid.Content.Length == 0) if (pointGrid.Content.Count == 0)
return; return;
double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.

View File

@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue> public class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";

View File

@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[TestCase(2.9811338051242915d, "diffcalc-test")] [TestCase(2.2867022617692685d, "diffcalc-test")]
[TestCase(2.9811338051242915d, "diffcalc-test-strong")] [TestCase(2.2867022617692685d, "diffcalc-test-strong")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -22,20 +21,20 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Hit Count", Name = @"Hit Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = hits.ToString(), Content = hits.ToString(),
Icon = FontAwesome.Regular.Circle
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Drumroll Count", Name = @"Drumroll Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = drumrolls.ToString(), Content = drumrolls.ToString(),
Icon = FontAwesome.Regular.Circle
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Swell Count", Name = @"Swell Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
Content = swells.ToString(), Content = swells.ToString(),
Icon = FontAwesome.Regular.Circle
} }
}; };
} }

View File

@ -0,0 +1,140 @@
// 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 osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
/// <summary>
/// Detects special hit object patterns which are easier to hit using special techniques
/// than normally assumed in the fully-alternating play style.
/// </summary>
/// <remarks>
/// This component detects two basic types of patterns, leveraged by the following techniques:
/// <list>
/// <item>Rolling allows hitting patterns with quickly and regularly alternating notes with a single hand.</item>
/// <item>TL tapping makes hitting longer sequences of consecutive same-colour notes with little to no colour changes in-between.</item>
/// </list>
/// </remarks>
public class StaminaCheeseDetector
{
/// <summary>
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a roll.
/// </summary>
private const int roll_min_repetitions = 12;
/// <summary>
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a TL tap.
/// </summary>
private const int tl_min_repetitions = 16;
/// <summary>
/// The list of all <see cref="TaikoDifficultyHitObject"/>s in the map.
/// </summary>
private readonly List<TaikoDifficultyHitObject> hitObjects;
public StaminaCheeseDetector(List<TaikoDifficultyHitObject> hitObjects)
{
this.hitObjects = hitObjects;
}
/// <summary>
/// Finds and marks all objects in <see cref="hitObjects"/> that special difficulty-reducing techiques apply to
/// with the <see cref="TaikoDifficultyHitObject.StaminaCheese"/> flag.
/// </summary>
public void FindCheese()
{
findRolls(3);
findRolls(4);
findTlTap(0, HitType.Rim);
findTlTap(1, HitType.Rim);
findTlTap(0, HitType.Centre);
findTlTap(1, HitType.Centre);
}
/// <summary>
/// Finds and marks all sequences hittable using a roll.
/// </summary>
/// <param name="patternLength">The length of a single repeating pattern to consider (triplets/quadruplets).</param>
private void findRolls(int patternLength)
{
var history = new LimitedCapacityQueue<TaikoDifficultyHitObject>(2 * patternLength);
// for convenience, we're tracking the index of the item *before* our suspected repeat's start,
// as that index can be simply subtracted from the current index to get the number of elements in between
// without off-by-one errors
int indexBeforeLastRepeat = -1;
for (int i = 0; i < hitObjects.Count; i++)
{
history.Enqueue(hitObjects[i]);
if (!history.Full)
continue;
if (!containsPatternRepeat(history, patternLength))
{
// we're setting this up for the next iteration, hence the +1.
// right here this index will point at the queue's front (oldest item),
// but that item is about to be popped next loop with an enqueue.
indexBeforeLastRepeat = i - history.Count + 1;
continue;
}
int repeatedLength = i - indexBeforeLastRepeat;
if (repeatedLength < roll_min_repetitions)
continue;
markObjectsAsCheese(i, repeatedLength);
}
}
/// <summary>
/// Determines whether the objects stored in <paramref name="history"/> contain a repetition of a pattern of length <paramref name="patternLength"/>.
/// </summary>
private static bool containsPatternRepeat(LimitedCapacityQueue<TaikoDifficultyHitObject> history, int patternLength)
{
for (int j = 0; j < patternLength; j++)
{
if (history[j].HitType != history[j + patternLength].HitType)
return false;
}
return true;
}
/// <summary>
/// Finds and marks all sequences hittable using a TL tap.
/// </summary>
/// <param name="parity">Whether sequences starting with an odd- (1) or even-indexed (0) hit object should be checked.</param>
/// <param name="type">The type of hit to check for TL taps.</param>
private void findTlTap(int parity, HitType type)
{
int tlLength = -2;
for (int i = parity; i < hitObjects.Count; i += 2)
{
if (hitObjects[i].HitType == type)
tlLength += 2;
else
tlLength = -2;
if (tlLength < tl_min_repetitions)
continue;
markObjectsAsCheese(i, tlLength);
}
}
/// <summary>
/// Marks <paramref name="count"/> elements counting backwards from <paramref name="end"/> as <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
/// </summary>
private void markObjectsAsCheese(int end, int count)
{
for (int i = 0; i < count; ++i)
hitObjects[end - i].StaminaCheese = true;
}
}
}

View File

@ -1,20 +1,94 @@
// 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.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{ {
/// <summary>
/// Represents a single hit object in taiko difficulty calculation.
/// </summary>
public class TaikoDifficultyHitObject : DifficultyHitObject public class TaikoDifficultyHitObject : DifficultyHitObject
{ {
public readonly bool HasTypeChange; /// <summary>
/// The rhythm required to hit this hit object.
/// </summary>
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate) /// <summary>
/// The hit type of this hit object.
/// </summary>
public readonly HitType? HitType;
/// <summary>
/// The index of the object in the beatmap.
/// </summary>
public readonly int ObjectIndex;
/// <summary>
/// Whether the object should carry a penalty due to being hittable using special techniques
/// making it easier to do so.
/// </summary>
public bool StaminaCheese;
/// <summary>
/// Creates a new difficulty hit object.
/// </summary>
/// <param name="hitObject">The gameplay <see cref="HitObject"/> associated with this difficulty object.</param>
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="hitObject"/>.</param>
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
/// <param name="clockRate">The rate of the gameplay clock. Modified by speed-changing mods.</param>
/// <param name="objectIndex">The index of the object in the beatmap.</param>
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex)
: base(hitObject, lastObject, clockRate) : base(hitObject, lastObject, clockRate)
{ {
HasTypeChange = (lastObject as Hit)?.Type != (hitObject as Hit)?.Type; var currentHit = hitObject as Hit;
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
HitType = currentHit?.Type;
ObjectIndex = objectIndex;
}
/// <summary>
/// List of most common rhythm changes in taiko maps.
/// </summary>
/// <remarks>
/// The general guidelines for the values are:
/// <list type="bullet">
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
/// </list>
/// </remarks>
private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
{
new TaikoDifficultyHitObjectRhythm(1, 1, 0.0),
new TaikoDifficultyHitObjectRhythm(2, 1, 0.3),
new TaikoDifficultyHitObjectRhythm(1, 2, 0.5),
new TaikoDifficultyHitObjectRhythm(3, 1, 0.3),
new TaikoDifficultyHitObjectRhythm(1, 3, 0.35),
new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style)
new TaikoDifficultyHitObjectRhythm(2, 3, 0.4),
new TaikoDifficultyHitObjectRhythm(5, 4, 0.5),
new TaikoDifficultyHitObjectRhythm(4, 5, 0.7)
};
/// <summary>
/// Returns the closest rhythm change from <see cref="common_rhythms"/> required to hit this object.
/// </summary>
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding this one.</param>
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
/// <param name="clockRate">The rate of the gameplay clock.</param>
private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate)
{
double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate;
double ratio = DeltaTime / prevLength;
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
} }
} }
} }

View File

@ -0,0 +1,35 @@
// 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.
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
/// <summary>
/// Represents a rhythm change in a taiko map.
/// </summary>
public class TaikoDifficultyHitObjectRhythm
{
/// <summary>
/// The difficulty multiplier associated with this rhythm change.
/// </summary>
public readonly double Difficulty;
/// <summary>
/// The ratio of current <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
/// to previous <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/> for the rhythm change.
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
/// </summary>
public readonly double Ratio;
/// <summary>
/// Creates an object representing a rhythm change.
/// </summary>
/// <param name="numerator">The numerator for <see cref="Ratio"/>.</param>
/// <param name="denominator">The denominator for <see cref="Ratio"/></param>
/// <param name="difficulty">The difficulty multiplier associated with this rhythm change.</param>
public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty)
{
Ratio = numerator / (double)denominator;
Difficulty = difficulty;
}
}
}

View File

@ -0,0 +1,135 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
/// <summary>
/// Calculates the colour coefficient of taiko difficulty.
/// </summary>
public class Colour : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
/// <summary>
/// Maximum number of entries to keep in <see cref="monoHistory"/>.
/// </summary>
private const int mono_history_max_length = 5;
/// <summary>
/// Queue with the lengths of the last <see cref="mono_history_max_length"/> most recent mono (single-colour) patterns,
/// with the most recent value at the end of the queue.
/// </summary>
private readonly LimitedCapacityQueue<int> monoHistory = new LimitedCapacityQueue<int>(mono_history_max_length);
/// <summary>
/// The <see cref="HitType"/> of the last object hit before the one being considered.
/// </summary>
private HitType? previousHitType;
/// <summary>
/// Length of the current mono pattern.
/// </summary>
private int currentMonoLength;
protected override double StrainValueOf(DifficultyHitObject current)
{
// changing from/to a drum roll or a swell does not constitute a colour change.
// hits spaced more than a second apart are also exempt from colour strain.
if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000))
{
monoHistory.Clear();
var currentHit = current.BaseObject as Hit;
currentMonoLength = currentHit != null ? 1 : 0;
previousHitType = currentHit?.Type;
return 0.0;
}
var taikoCurrent = (TaikoDifficultyHitObject)current;
double objectStrain = 0.0;
if (previousHitType != null && taikoCurrent.HitType != previousHitType)
{
// The colour has changed.
objectStrain = 1.0;
if (monoHistory.Count < 2)
{
// There needs to be at least two streaks to determine a strain.
objectStrain = 0.0;
}
else if ((monoHistory[^1] + currentMonoLength) % 2 == 0)
{
// The last streak in the history is guaranteed to be a different type to the current streak.
// If the total number of notes in the two streaks is even, nullify this object's strain.
objectStrain = 0.0;
}
objectStrain *= repetitionPenalties();
currentMonoLength = 1;
}
else
{
currentMonoLength += 1;
}
previousHitType = taikoCurrent.HitType;
return objectStrain;
}
/// <summary>
/// The penalty to apply due to the length of repetition in colour streaks.
/// </summary>
private double repetitionPenalties()
{
const int most_recent_patterns_to_compare = 2;
double penalty = 1.0;
monoHistory.Enqueue(currentMonoLength);
for (int start = monoHistory.Count - most_recent_patterns_to_compare - 1; start >= 0; start--)
{
if (!isSamePattern(start, most_recent_patterns_to_compare))
continue;
int notesSince = 0;
for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i];
penalty *= repetitionPenalty(notesSince);
break;
}
return penalty;
}
/// <summary>
/// Determines whether the last <paramref name="mostRecentPatternsToCompare"/> patterns have repeated in the history
/// of single-colour note sequences, starting from <paramref name="start"/>.
/// </summary>
private bool isSamePattern(int start, int mostRecentPatternsToCompare)
{
for (int i = 0; i < mostRecentPatternsToCompare; i++)
{
if (monoHistory[start + i] != monoHistory[monoHistory.Count - mostRecentPatternsToCompare + i])
return false;
}
return true;
}
/// <summary>
/// Calculates the strain penalty for a colour pattern repetition.
/// </summary>
/// <param name="notesSince">The number of notes since the last repetition of the pattern.</param>
private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
}
}

View File

@ -0,0 +1,167 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
/// <summary>
/// Calculates the rhythm coefficient of taiko difficulty.
/// </summary>
public class Rhythm : Skill
{
protected override double SkillMultiplier => 10;
protected override double StrainDecayBase => 0;
/// <summary>
/// The note-based decay for rhythm strain.
/// </summary>
/// <remarks>
/// <see cref="StrainDecayBase"/> is not used here, as it's time- and not note-based.
/// </remarks>
private const double strain_decay = 0.96;
/// <summary>
/// Maximum number of entries in <see cref="rhythmHistory"/>.
/// </summary>
private const int rhythm_history_max_length = 8;
/// <summary>
/// Contains the last <see cref="rhythm_history_max_length"/> changes in note sequence rhythms.
/// </summary>
private readonly LimitedCapacityQueue<TaikoDifficultyHitObject> rhythmHistory = new LimitedCapacityQueue<TaikoDifficultyHitObject>(rhythm_history_max_length);
/// <summary>
/// Contains the rolling rhythm strain.
/// Used to apply per-note decay.
/// </summary>
private double currentStrain;
/// <summary>
/// Number of notes since the last rhythm change has taken place.
/// </summary>
private int notesSinceRhythmChange;
protected override double StrainValueOf(DifficultyHitObject current)
{
// drum rolls and swells are exempt.
if (!(current.BaseObject is Hit))
{
resetRhythmAndStrain();
return 0.0;
}
currentStrain *= strain_decay;
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
notesSinceRhythmChange += 1;
// rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
if (hitObject.Rhythm.Difficulty == 0.0)
{
return 0.0;
}
double objectStrain = hitObject.Rhythm.Difficulty;
objectStrain *= repetitionPenalties(hitObject);
objectStrain *= patternLengthPenalty(notesSinceRhythmChange);
objectStrain *= speedPenalty(hitObject.DeltaTime);
// careful - needs to be done here since calls above read this value
notesSinceRhythmChange = 0;
currentStrain += objectStrain;
return currentStrain;
}
/// <summary>
/// Returns a penalty to apply to the current hit object caused by repeating rhythm changes.
/// </summary>
/// <remarks>
/// Repetitions of more recent patterns are associated with a higher penalty.
/// </remarks>
/// <param name="hitObject">The current hit object being considered.</param>
private double repetitionPenalties(TaikoDifficultyHitObject hitObject)
{
double penalty = 1;
rhythmHistory.Enqueue(hitObject);
for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++)
{
for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--)
{
if (!samePattern(start, mostRecentPatternsToCompare))
continue;
int notesSince = hitObject.ObjectIndex - rhythmHistory[start].ObjectIndex;
penalty *= repetitionPenalty(notesSince);
break;
}
}
return penalty;
}
/// <summary>
/// Determines whether the rhythm change pattern starting at <paramref name="start"/> is a repeat of any of the
/// <paramref name="mostRecentPatternsToCompare"/>.
/// </summary>
private bool samePattern(int start, int mostRecentPatternsToCompare)
{
for (int i = 0; i < mostRecentPatternsToCompare; i++)
{
if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm)
return false;
}
return true;
}
/// <summary>
/// Calculates a single rhythm repetition penalty.
/// </summary>
/// <param name="notesSince">Number of notes since the last repetition of a rhythm change.</param>
private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
/// <summary>
/// Calculates a penalty based on the number of notes since the last rhythm change.
/// Both rare and frequent rhythm changes are penalised.
/// </summary>
/// <param name="patternLength">Number of notes since the last rhythm change.</param>
private static double patternLengthPenalty(int patternLength)
{
double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0);
double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0);
return Math.Min(shortPatternPenalty, longPatternPenalty);
}
/// <summary>
/// Calculates a penalty for objects that do not require alternating hands.
/// </summary>
/// <param name="deltaTime">Time (in milliseconds) since the last hit object.</param>
private double speedPenalty(double deltaTime)
{
if (deltaTime < 80) return 1;
if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime);
resetRhythmAndStrain();
return 0.0;
}
/// <summary>
/// Resets the rolling strain value and <see cref="notesSinceRhythmChange"/> counter.
/// </summary>
private void resetRhythmAndStrain()
{
currentStrain = 0.0;
notesSinceRhythmChange = 0;
}
}
}

View File

@ -0,0 +1,113 @@
// 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.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
/// <summary>
/// Calculates the stamina coefficient of taiko difficulty.
/// </summary>
/// <remarks>
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
/// </remarks>
public class Stamina : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
/// <summary>
/// Maximum number of entries to keep in <see cref="notePairDurationHistory"/>.
/// </summary>
private const int max_history_length = 2;
/// <summary>
/// The index of the hand this <see cref="Stamina"/> instance is associated with.
/// </summary>
/// <remarks>
/// The value of 0 indicates the left hand (full alternating gameplay starting with left hand is assumed).
/// This naturally translates onto index offsets of the objects in the map.
/// </remarks>
private readonly int hand;
/// <summary>
/// Stores the last <see cref="max_history_length"/> durations between notes hit with the hand indicated by <see cref="hand"/>.
/// </summary>
private readonly LimitedCapacityQueue<double> notePairDurationHistory = new LimitedCapacityQueue<double>(max_history_length);
/// <summary>
/// Stores the <see cref="DifficultyHitObject.DeltaTime"/> of the last object that was hit by the <i>other</i> hand.
/// </summary>
private double offhandObjectDuration = double.MaxValue;
/// <summary>
/// Creates a <see cref="Stamina"/> skill.
/// </summary>
/// <param name="rightHand">Whether this instance is performing calculations for the right hand.</param>
public Stamina(bool rightHand)
{
hand = rightHand ? 1 : 0;
}
protected override double StrainValueOf(DifficultyHitObject current)
{
if (!(current.BaseObject is Hit))
{
return 0.0;
}
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
if (hitObject.ObjectIndex % 2 == hand)
{
double objectStrain = 1;
if (hitObject.ObjectIndex == 1)
return 1;
notePairDurationHistory.Enqueue(hitObject.DeltaTime + offhandObjectDuration);
double shortestRecentNote = notePairDurationHistory.Min();
objectStrain += speedBonus(shortestRecentNote);
if (hitObject.StaminaCheese)
objectStrain *= cheesePenalty(hitObject.DeltaTime + offhandObjectDuration);
return objectStrain;
}
offhandObjectDuration = hitObject.DeltaTime;
return 0;
}
/// <summary>
/// Applies a penalty for hit objects marked with <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
/// </summary>
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
private double cheesePenalty(double notePairDuration)
{
if (notePairDuration > 125) return 1;
if (notePairDuration < 100) return 0.6;
return 0.6 + (notePairDuration - 100) * 0.016;
}
/// <summary>
/// Applies a speed bonus dependent on the time since the last hit performed using this hand.
/// </summary>
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
private double speedBonus(double notePairDuration)
{
if (notePairDuration >= 200) return 0;
double bonus = 200 - notePairDuration;
bonus *= bonus;
return bonus / 100000;
}
}
}

View File

@ -1,95 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
public class Strain : Skill
{
private const double rhythm_change_base_threshold = 0.2;
private const double rhythm_change_base = 2.0;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.3;
private ColourSwitch lastColourSwitch = ColourSwitch.None;
private int sameColourCount = 1;
protected override double StrainValueOf(DifficultyHitObject current)
{
double addition = 1;
// We get an extra addition if we are not a slider or spinner
if (current.LastObject is Hit && current.BaseObject is Hit && current.BaseObject.StartTime - current.LastObject.StartTime < 1000)
{
if (hasColourChange(current))
addition += 0.75;
if (hasRhythmChange(current))
addition += 1;
}
else
{
lastColourSwitch = ColourSwitch.None;
sameColourCount = 1;
}
double additionFactor = 1;
// Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50
if (current.DeltaTime < 50)
additionFactor = 0.4 + 0.6 * current.DeltaTime / 50;
return additionFactor * addition;
}
private bool hasRhythmChange(DifficultyHitObject current)
{
// We don't want a division by zero if some random mapper decides to put two HitObjects at the same time.
if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0)
return false;
double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime);
if (timeElapsedRatio >= 8)
return false;
double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0;
return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold;
}
private bool hasColourChange(DifficultyHitObject current)
{
var taikoCurrent = (TaikoDifficultyHitObject)current;
if (!taikoCurrent.HasTypeChange)
{
sameColourCount++;
return false;
}
var oldColourSwitch = lastColourSwitch;
var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd;
lastColourSwitch = newColourSwitch;
sameColourCount = 1;
// We only want a bonus if the parity of the color switch changes
return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch;
}
private enum ColourSwitch
{
None,
Even,
Odd
}
}
}

View File

@ -7,7 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
public class TaikoDifficultyAttributes : DifficultyAttributes public class TaikoDifficultyAttributes : DifficultyAttributes
{ {
public double StaminaStrain;
public double RhythmStrain;
public double ColourStrain;
public double ApproachRate;
public double GreatHitWindow; public double GreatHitWindow;
public int MaxCombo;
} }
} }

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;
@ -19,39 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
public class TaikoDifficultyCalculator : DifficultyCalculator public class TaikoDifficultyCalculator : DifficultyCalculator
{ {
private const double star_scaling_factor = 0.04125; private const double rhythm_skill_multiplier = 0.014;
private const double colour_skill_multiplier = 0.01;
private const double stamina_skill_multiplier = 0.02;
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
} }
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
{ {
if (beatmap.HitObjects.Count == 0) new Colour(),
return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; new Rhythm(),
new Stamina(true),
HitWindows hitWindows = new TaikoHitWindows(); new Stamina(false),
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
return new TaikoDifficultyAttributes
{
StarRating = skills.Single().DifficultyValue() * star_scaling_factor,
Mods = mods,
// 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,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
Skills = skills
}; };
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
for (int i = 1; i < beatmap.HitObjects.Count; i++)
yield return new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
}
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() };
protected override Mod[] DifficultyAdjustmentMods => new Mod[] protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{ {
@ -60,5 +44,124 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
new TaikoModEasy(), new TaikoModEasy(),
new TaikoModHardRock(), new TaikoModHardRock(),
}; };
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
List<TaikoDifficultyHitObject> taikoDifficultyHitObjects = new List<TaikoDifficultyHitObject>();
for (int i = 2; i < beatmap.HitObjects.Count; i++)
{
taikoDifficultyHitObjects.Add(
new TaikoDifficultyHitObject(
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i
)
);
}
new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese();
return taikoDifficultyHitObjects;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{
if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
var colour = (Colour)skills[0];
var rhythm = (Rhythm)skills[1];
var staminaRight = (Stamina)skills[2];
var staminaLeft = (Stamina)skills[3];
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier;
double staminaPenalty = simpleColourPenalty(staminaRating, colourRating);
staminaRating *= staminaPenalty;
double combinedRating = locallyCombinedDifficulty(colour, rhythm, staminaRight, staminaLeft, staminaPenalty);
double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating);
double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
starRating = rescale(starRating);
HitWindows hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
return new TaikoDifficultyAttributes
{
StarRating = starRating,
Mods = mods,
StaminaStrain = staminaRating,
RhythmStrain = rhythmRating,
ColourStrain = colourRating,
// 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,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
Skills = skills
};
}
/// <summary>
/// Calculates the penalty for the stamina skill for maps with low colour difficulty.
/// </summary>
/// <remarks>
/// Some maps (especially converts) can be easy to read despite a high note density.
/// This penalty aims to reduce the star rating of such maps by factoring in colour difficulty to the stamina skill.
/// </remarks>
private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty)
{
if (colorDifficulty <= 0) return 0.79 - 0.25;
return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2;
}
/// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
/// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
/// <param name="values">The coefficients of the vector.</param>
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
/// <summary>
/// Returns the partial star rating of the beatmap, calculated using peak strains from all sections of the map.
/// </summary>
/// <remarks>
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
/// </remarks>
private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty)
{
List<double> peaks = new List<double>();
for (int i = 0; i < colour.StrainPeaks.Count; i++)
{
double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier;
double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak));
}
double difficulty = 0;
double weight = 1;
foreach (double strain in peaks.OrderByDescending(d => d))
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
/// <summary>
/// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars.
/// </summary>
/// <param name="sr">The raw star rating value before re-scaling.</param>
private double rescale(double sr)
{
if (sr < 0) return sr;
return 10.43 * Math.Log(sr / 8 + 1);
}
} }
} }

View File

@ -78,10 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
strainValue *= Math.Pow(0.985, countMiss); strainValue *= Math.Pow(0.985, countMiss);
// Combo scaling
if (Attributes.MaxCombo > 0)
strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(Attributes.MaxCombo, 0.5), 1.0);
if (mods.Any(m => m is ModHidden)) if (mods.Any(m => m is ModHidden))
strainValue *= 1.025; strainValue *= 1.025;

View File

@ -1,6 +1,8 @@
// 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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{ {
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
} }
} }

View File

@ -1,6 +1,8 @@
// 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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{ {
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
} }
} }

View File

@ -1,6 +1,8 @@
// 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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{ {
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
} }
} }

View File

@ -22,7 +22,6 @@ using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Edit; using osu.Game.Rulesets.Taiko.Edit;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -32,7 +31,6 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko namespace osu.Game.Rulesets.Taiko
{ {
[ExcludeFromDynamicCompile]
public class TaikoRuleset : Ruleset, ILegacyRuleset public class TaikoRuleset : Ruleset, ILegacyRuleset
{ {
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableTaikoRuleset(this, beatmap, mods); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);

View File

@ -10,6 +10,7 @@ using System.Text;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
@ -19,6 +20,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats namespace osu.Game.Tests.Beatmaps.Formats
@ -26,18 +28,33 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestFixture] [TestFixture]
public class LegacyBeatmapEncoderTest public class LegacyBeatmapEncoderTest
{ {
private static IEnumerable<string> allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu")); private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu"));
[TestCaseSource(nameof(allBeatmaps))] [TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStability(string name) public void TestEncodeDecodeStability(string name)
{ {
var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name)); var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded)); var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
sort(decoded); sort(decoded.beatmap);
sort(decodedAfterEncode); sort(decodedAfterEncode.beatmap);
Assert.That(decodedAfterEncode.Serialize(), Is.EqualTo(decoded.Serialize())); Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
}
private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
{
// equal to null, no need to SequenceEqual
if (a.ComboColours == null && b.ComboColours == null)
return true;
if (a.ComboColours == null || b.ComboColours == null)
return false;
return a.ComboColours.SequenceEqual(b.ComboColours);
} }
private void sort(IBeatmap beatmap) private void sort(IBeatmap beatmap)
@ -50,18 +67,31 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
private IBeatmap decodeFromLegacy(Stream stream) private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name)
{ {
using (var reader = new LineBufferedReader(stream)) using (var reader = new LineBufferedReader(stream))
return convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader)); {
var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader);
var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name);
return (convert(beatmap), beatmapSkin);
}
} }
private Stream encodeToLegacy(IBeatmap beatmap) private class TestLegacySkin : LegacySkin
{ {
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName)
{
}
}
private Stream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream(); var stream = new MemoryStream();
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmap).Encode(writer); new LegacyBeatmapEncoder(beatmap, beatmapSkin).Encode(writer);
stream.Position = 0; stream.Position = 0;
@ -106,7 +136,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override Texture GetBackground() => throw new NotImplementedException(); protected override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
} }
} }
} }

View File

@ -11,6 +11,8 @@ using osu.Game.Beatmaps.Formats;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IO.Serialization; using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
@ -90,6 +92,38 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(2, difficulty.SliderTickRate); Assert.AreEqual(2, difficulty.SliderTickRate);
} }
[Test]
public void TestDecodePostConverted()
{
var converted = new OsuBeatmapConverter(decodeAsJson(normal), new OsuRuleset()).Convert();
var processor = new OsuBeatmapProcessor(converted);
processor.PreProcess();
foreach (var o in converted.HitObjects)
o.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty);
processor.PostProcess();
var beatmap = converted.Serialize().Deserialize<Beatmap>();
var curveData = beatmap.HitObjects[0] as IHasPathWithRepeats;
var positionData = beatmap.HitObjects[0] as IHasPosition;
Assert.IsNotNull(positionData);
Assert.IsNotNull(curveData);
Assert.AreEqual(90, curveData.Path.Distance);
Assert.AreEqual(new Vector2(192, 168), positionData.Position);
Assert.AreEqual(956, beatmap.HitObjects[0].StartTime);
Assert.IsTrue(beatmap.HitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL));
positionData = beatmap.HitObjects[1] as IHasPosition;
Assert.IsNotNull(positionData);
Assert.AreEqual(new Vector2(304, 56), positionData.Position);
Assert.AreEqual(1285, beatmap.HitObjects[1].StartTime);
Assert.IsTrue(beatmap.HitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP));
}
[Test] [Test]
public void TestDecodeHitObjects() public void TestDecodeHitObjects()
{ {
@ -100,6 +134,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsNotNull(positionData); Assert.IsNotNull(positionData);
Assert.IsNotNull(curveData); Assert.IsNotNull(curveData);
Assert.AreEqual(90, curveData.Path.Distance);
Assert.AreEqual(new Vector2(192, 168), positionData.Position); Assert.AreEqual(new Vector2(192, 168), positionData.Position);
Assert.AreEqual(956, beatmap.HitObjects[0].StartTime); Assert.AreEqual(956, beatmap.HitObjects[0].StartTime);
Assert.IsTrue(beatmap.HitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); Assert.IsTrue(beatmap.HitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL));

View File

@ -15,8 +15,10 @@ using osu.Framework.Extensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Users;
using SharpCompress.Archives; using SharpCompress.Archives;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
using SharpCompress.Common; using SharpCompress.Common;
@ -32,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportWhenClosed() public async Task TestImportWhenClosed()
{ {
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenClosed))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -49,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDelete() public async Task TestImportThenDelete()
{ {
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDelete))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -70,7 +72,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenImport() public async Task TestImportThenImport()
{ {
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImport))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -96,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
public async Task TestImportThenImportWithReZip() public async Task TestImportThenImportWithReZip()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithReZip))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -154,7 +156,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
public async Task TestImportThenImportWithChangedFile() public async Task TestImportThenImportWithChangedFile()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithChangedFile))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -205,7 +207,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
public async Task TestImportThenImportWithDifferentFilename() public async Task TestImportThenImportWithDifferentFilename()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithDifferentFilename))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -257,7 +259,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportCorruptThenImport() public async Task TestImportCorruptThenImport()
{ {
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportCorruptThenImport))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -299,7 +301,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestRollbackOnFailure() public async Task TestRollbackOnFailure()
{ {
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestRollbackOnFailure))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -376,7 +378,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImport() public async Task TestImportThenDeleteThenImport()
{ {
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDeleteThenImport))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -404,7 +406,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
{ {
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(TestImportThenDeleteThenImportWithOnlineIDMismatch)}-{set}")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost(set.ToString()))
{ {
try try
{ {
@ -438,7 +440,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportWithDuplicateBeatmapIDs() public async Task TestImportWithDuplicateBeatmapIDs()
{ {
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateBeatmapIDs))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -524,7 +526,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
public async Task TestImportWhenFileOpen() public async Task TestImportWhenFileOpen()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenFileOpen))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -546,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
public async Task TestImportWithDuplicateHashes() public async Task TestImportWithDuplicateHashes()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateHashes))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -588,7 +590,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
public async Task TestImportNestedStructure() public async Task TestImportNestedStructure()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -633,7 +635,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
public async Task TestImportWithIgnoredDirectoryInArchive() public async Task TestImportWithIgnoredDirectoryInArchive()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithIgnoredDirectoryInArchive))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -687,7 +689,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
public async Task TestUpdateBeatmapInfo() public async Task TestUpdateBeatmapInfo()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapInfo))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -717,7 +719,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
public async Task TestUpdateBeatmapFile() public async Task TestUpdateBeatmapFile()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapFile))) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -756,6 +758,63 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
} }
[Test]
public void TestCreateNewEmptyBeatmap()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER);
manager.Save(working.BeatmapInfo, working.Beatmap);
var retrievedSet = manager.GetAllUsableBeatmapSets()[0];
// Check that the new file is referenced correctly by attempting a retrieval
Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap;
Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(0));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestCreateNewBeatmapWithObject()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER);
((Beatmap)working.Beatmap).HitObjects.Add(new HitCircle { StartTime = 5000 });
manager.Save(working.BeatmapInfo, working.Beatmap);
var retrievedSet = manager.GetAllUsableBeatmapSets()[0];
// Check that the new file is referenced correctly by attempting a retrieval
Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap;
Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1));
Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000));
}
finally
{
host.Exit();
}
}
}
public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false)
{ {
var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);

View File

@ -23,15 +23,19 @@ namespace osu.Game.Tests.Beatmaps
[Test] [Test]
public void TestHitObjectAddEvent() public void TestHitObjectAddEvent()
{ {
var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
HitObject addedObject = null;
editorBeatmap.HitObjectAdded += h => addedObject = h;
var hitCircle = new HitCircle(); var hitCircle = new HitCircle();
editorBeatmap.Add(hitCircle); HitObject addedObject = null;
Assert.That(addedObject, Is.EqualTo(hitCircle)); EditorBeatmap editorBeatmap = null;
AddStep("add beatmap", () =>
{
Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
editorBeatmap.HitObjectAdded += h => addedObject = h;
});
AddStep("add hitobject", () => editorBeatmap.Add(hitCircle));
AddAssert("received add event", () => addedObject == hitCircle);
} }
/// <summary> /// <summary>
@ -41,13 +45,15 @@ namespace osu.Game.Tests.Beatmaps
public void HitObjectRemoveEvent() public void HitObjectRemoveEvent()
{ {
var hitCircle = new HitCircle(); var hitCircle = new HitCircle();
var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject removedObject = null; HitObject removedObject = null;
EditorBeatmap editorBeatmap = null;
AddStep("add beatmap", () =>
{
Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
editorBeatmap.HitObjectRemoved += h => removedObject = h; editorBeatmap.HitObjectRemoved += h => removedObject = h;
});
editorBeatmap.Remove(hitCircle); AddStep("remove hitobject", () => editorBeatmap.Remove(editorBeatmap.HitObjects.First()));
Assert.That(removedObject, Is.EqualTo(hitCircle)); AddAssert("received remove event", () => removedObject == hitCircle);
} }
/// <summary> /// <summary>
@ -147,6 +153,7 @@ namespace osu.Game.Tests.Beatmaps
public void TestResortWhenStartTimeChanged() public void TestResortWhenStartTimeChanged()
{ {
var hitCircle = new HitCircle { StartTime = 1000 }; var hitCircle = new HitCircle { StartTime = 1000 };
var editorBeatmap = new EditorBeatmap(new OsuBeatmap var editorBeatmap = new EditorBeatmap(new OsuBeatmap
{ {
HitObjects = HitObjects =

View File

@ -0,0 +1,221 @@
// 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.Text;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.Collections;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Collections.IO
{
[TestFixture]
public class ImportCollectionsTest
{
[Test]
public async Task TestImportEmptyDatabase()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
await osu.CollectionManager.Import(new MemoryStream());
Assert.That(osu.CollectionManager.Collections.Count, Is.Zero);
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportWithNoBeatmaps()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.Zero);
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.Zero);
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportWithBeatmaps()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(1));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(12));
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportMalformedDatabase()
{
bool exceptionThrown = false;
UnhandledExceptionEventHandler setException = (_, __) => exceptionThrown = true;
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
AppDomain.CurrentDomain.UnhandledException += setException;
var osu = loadOsu(host, true);
using (var ms = new MemoryStream())
{
using (var bw = new BinaryWriter(ms, Encoding.UTF8, true))
{
for (int i = 0; i < 10000; i++)
bw.Write((byte)i);
}
ms.Seek(0, SeekOrigin.Begin);
await osu.CollectionManager.Import(ms);
}
Assert.That(host.UpdateThread.Running, Is.True);
Assert.That(exceptionThrown, Is.False);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(0));
}
finally
{
host.Exit();
AppDomain.CurrentDomain.UnhandledException -= setException;
}
}
}
[Test]
public async Task TestSaveAndReload()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
// Move first beatmap from second collection into the first.
osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]);
osu.CollectionManager.Collections[1].Beatmaps.RemoveAt(0);
// Rename the second collecction.
osu.CollectionManager.Collections[1].Name.Value = "Another";
}
finally
{
host.Exit();
}
}
using (HeadlessGameHost host = new HeadlessGameHost("TestSaveAndReload"))
{
try
{
var osu = loadOsu(host, true);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Another"));
Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(11));
}
finally
{
host.Exit();
}
}
}
private TestOsuGameBase loadOsu(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
#pragma warning disable 4014
Task.Run(() => host.Run(osu));
#pragma warning restore 4014
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
private class TestOsuGameBase : OsuGameBase
{
public CollectionManager CollectionManager { get; private set; }
private readonly bool withBeatmap;
public TestOsuGameBase(bool withBeatmap)
{
this.withBeatmap = withBeatmap;
}
[BackgroundDependencyLoader]
private void load()
{
// Beatmap must be imported before the collection manager is loaded.
if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
AddInternal(CollectionManager = new CollectionManager(Storage));
}
}
}
}

View File

@ -351,7 +351,7 @@ namespace osu.Game.Tests.Editing
using (var encoded = new MemoryStream()) using (var encoded = new MemoryStream())
{ {
using (var sw = new StreamWriter(encoded)) using (var sw = new StreamWriter(encoded))
new LegacyBeatmapEncoder(beatmap).Encode(sw); new LegacyBeatmapEncoder(beatmap, null).Encode(sw);
return encoded.ToArray(); return encoded.ToArray();
} }

View File

@ -1,10 +1,8 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -19,7 +17,14 @@ namespace osu.Game.Tests.Gameplay
{ {
GameplayClockContainer gcc = null; GameplayClockContainer gcc = null;
AddStep("create container", () => Add(gcc = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty<Mod>(), 0))); AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gcc = new GameplayClockContainer(working, 0));
});
AddStep("start track", () => gcc.Start()); AddStep("start track", () => gcc.Start());
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0); AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
} }

View File

@ -28,6 +28,20 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0)); Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0));
} }
[Test]
public void TestOnlyBonusScore()
{
var beatmap = new Beatmap<TestBonusHitObject> { HitObjects = { new TestBonusHitObject() } };
var scoreProcessor = new ScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a judgement
scoreProcessor.ApplyResult(new JudgementResult(new TestBonusHitObject(), new TestBonusJudgement()) { Type = HitResult.Perfect });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(100));
}
private class TestHitObject : HitObject private class TestHitObject : HitObject
{ {
public override Judgement CreateJudgement() => new TestJudgement(); public override Judgement CreateJudgement() => new TestJudgement();
@ -37,5 +51,17 @@ namespace osu.Game.Tests.Gameplay
{ {
protected override int NumericResultFor(HitResult result) => 100; protected override int NumericResultFor(HitResult result) => 100;
} }
private class TestBonusHitObject : HitObject
{
public override Judgement CreateJudgement() => new TestBonusJudgement();
}
private class TestBonusJudgement : Judgement
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result) => 100;
}
} }
} }

View File

@ -59,7 +59,10 @@ namespace osu.Game.Tests.Gameplay
AddStep("create container", () => AddStep("create container", () =>
{ {
Add(gameplayContainer = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty<Mod>(), 0)); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gameplayContainer = new GameplayClockContainer(working, 0));
gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
{ {
@ -103,7 +106,7 @@ namespace osu.Game.Tests.Gameplay
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
SelectedMods.Value = new[] { testedMod }; SelectedMods.Value = new[] { testedMod };
Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0)); Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0));
gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{ {

View File

@ -0,0 +1,119 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Difficulty.Utils;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class LimitedCapacityQueueTest
{
private const int capacity = 3;
private LimitedCapacityQueue<int> queue;
[SetUp]
public void SetUp()
{
queue = new LimitedCapacityQueue<int>(capacity);
}
[Test]
public void TestEmptyQueue()
{
Assert.AreEqual(0, queue.Count);
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[0]);
Assert.Throws<InvalidOperationException>(() => _ = queue.Dequeue());
int count = 0;
foreach (var _ in queue)
count++;
Assert.AreEqual(0, count);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public void TestBelowCapacity(int count)
{
for (int i = 0; i < count; ++i)
queue.Enqueue(i);
Assert.AreEqual(count, queue.Count);
for (int i = 0; i < count; ++i)
Assert.AreEqual(i, queue[i]);
int j = 0;
foreach (var item in queue)
Assert.AreEqual(j++, item);
for (int i = queue.Count; i < queue.Count + capacity; i++)
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[i]);
}
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
public void TestEnqueueAtFullCapacity(int count)
{
for (int i = 0; i < count; ++i)
queue.Enqueue(i);
Assert.AreEqual(capacity, queue.Count);
for (int i = 0; i < queue.Count; ++i)
Assert.AreEqual(count - capacity + i, queue[i]);
int j = count - capacity;
foreach (var item in queue)
Assert.AreEqual(j++, item);
for (int i = queue.Count; i < queue.Count + capacity; i++)
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[i]);
}
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
public void TestDequeueAtFullCapacity(int count)
{
for (int i = 0; i < count; ++i)
queue.Enqueue(i);
for (int i = 0; i < capacity; ++i)
{
Assert.AreEqual(count - capacity + i, queue.Dequeue());
Assert.AreEqual(2 - i, queue.Count);
}
Assert.Throws<InvalidOperationException>(() => queue.Dequeue());
}
[Test]
public void TestClearQueue()
{
queue.Enqueue(3);
queue.Enqueue(5);
Assert.AreEqual(2, queue.Count);
queue.Clear();
Assert.AreEqual(0, queue.Count);
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[0]);
queue.Enqueue(7);
Assert.AreEqual(1, queue.Count);
Assert.AreEqual(7, queue[0]);
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[1]);
queue.Enqueue(9);
Assert.AreEqual(2, queue.Count);
Assert.AreEqual(9, queue[1]);
}
}
}

Binary file not shown.

View File

@ -27,7 +27,7 @@ namespace osu.Game.Tests.Scores.IO
[Test] [Test]
public async Task TestBasicImport() public async Task TestBasicImport()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestBasicImport")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Scores.IO
[Test] [Test]
public async Task TestImportMods() public async Task TestImportMods()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMods")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -92,7 +92,7 @@ namespace osu.Game.Tests.Scores.IO
[Test] [Test]
public async Task TestImportStatistics() public async Task TestImportStatistics()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportStatistics")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -122,7 +122,7 @@ namespace osu.Game.Tests.Scores.IO
[Test] [Test]
public async Task TestImportWithDeletedBeatmapSet() public async Task TestImportWithDeletedBeatmapSet()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDeletedBeatmapSet")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {
@ -159,7 +159,7 @@ namespace osu.Game.Tests.Scores.IO
[Test] [Test]
public async Task TestOnlineScoreIsAvailableLocally() public async Task TestOnlineScoreIsAvailableLocally()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestOnlineScoreIsAvailableLocally")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
try try
{ {

View File

@ -26,6 +26,7 @@ namespace osu.Game.Tests.Skins
{ {
var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result; var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result;
beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]);
beatmap.LoadTrack();
} }
[Test] [Test]

View File

@ -0,0 +1,244 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Collections
{
public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene
{
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly DialogOverlay dialogOverlay;
private readonly CollectionManager manager;
private RulesetStore rulesets;
private BeatmapManager beatmapManager;
private ManageCollectionsDialog dialog;
public TestSceneManageCollectionsDialog()
{
base.Content.AddRange(new Drawable[]
{
manager = new CollectionManager(LocalStorage),
content = new Container { RelativeSizeAxes = Axes.Both },
dialogOverlay = new DialogOverlay()
});
}
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(manager);
dependencies.Cache(dialogOverlay);
return dependencies;
}
[SetUp]
public void SetUp() => Schedule(() =>
{
manager.Collections.Clear();
Child = dialog = new ManageCollectionsDialog();
});
[SetUpSteps]
public void SetUpSteps()
{
AddStep("show dialog", () => dialog.Show());
}
[Test]
public void TestHideDialog()
{
AddWaitStep("wait for animation", 3);
AddStep("hide dialog", () => dialog.Hide());
}
[Test]
public void TestLastItemIsPlaceholder()
{
AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model));
}
[Test]
public void TestAddCollectionExternal()
{
AddStep("add collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "First collection" } }));
assertCollectionCount(1);
assertCollectionName(0, "First collection");
AddStep("add another collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "Second collection" } }));
assertCollectionCount(2);
assertCollectionName(1, "Second collection");
}
[Test]
public void TestFocusPlaceholderDoesNotCreateCollection()
{
AddStep("focus placeholder", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem>().Last());
InputManager.Click(MouseButton.Left);
});
assertCollectionCount(0);
}
[Test]
public void TestAddCollectionViaPlaceholder()
{
DrawableCollectionListItem placeholderItem = null;
AddStep("focus placeholder", () =>
{
InputManager.MoveMouseTo(placeholderItem = dialog.ChildrenOfType<DrawableCollectionListItem>().Last());
InputManager.Click(MouseButton.Left);
});
// Done directly via the collection since InputManager methods cannot add text to textbox...
AddStep("change collection name", () => placeholderItem.Model.Name.Value = "a");
assertCollectionCount(1);
AddAssert("collection now exists", () => manager.Collections.Contains(placeholderItem.Model));
AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model));
}
[Test]
public void TestRemoveCollectionExternal()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" } },
}));
AddStep("remove first collection", () => manager.Collections.RemoveAt(0));
assertCollectionCount(1);
assertCollectionName(0, "2");
}
[Test]
public void TestRemoveCollectionViaButton()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
}));
assertCollectionCount(2);
AddStep("click first delete button", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog not displayed", () => dialogOverlay.CurrentDialog == null);
assertCollectionCount(1);
assertCollectionName(0, "2");
AddStep("click first delete button", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog);
AddStep("click confirmation", () =>
{
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
InputManager.Click(MouseButton.Left);
});
assertCollectionCount(0);
}
[Test]
public void TestCollectionNotRemovedWhenDialogCancelled()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
}));
assertCollectionCount(1);
AddStep("click first delete button", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog);
AddStep("click cancellation", () =>
{
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().Last());
InputManager.Click(MouseButton.Left);
});
assertCollectionCount(1);
}
[Test]
public void TestCollectionRenamedExternal()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" } },
}));
AddStep("change first collection name", () => manager.Collections[0].Name.Value = "First");
assertCollectionName(0, "First");
}
[Test]
public void TestCollectionRenamedOnTextChange()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" } },
}));
assertCollectionCount(2);
AddStep("change first collection name", () => dialog.ChildrenOfType<TextBox>().First().Text = "First");
AddAssert("collection has new name", () => manager.Collections[0].Name.Value == "First");
}
private void assertCollectionCount(int count)
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count(i => i.IsCreated.Value) == count);
private void assertCollectionName(int index, string name)
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionListItem>().ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
}
}

View File

@ -1,28 +1,27 @@
// 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.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
public class TestSceneEditorChangeStates : EditorTestScene public class TestSceneEditorChangeStates : EditorTestScene
{ {
private EditorBeatmap editorBeatmap;
protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
public override void SetUpSteps() [Test]
public void TestSelectedObjects()
{ {
base.SetUpSteps(); HitCircle obj = null;
AddStep("add hitobject", () => EditorBeatmap.Add(obj = new HitCircle { StartTime = 1000 }));
AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single()); AddStep("select hitobject", () => EditorBeatmap.SelectedHitObjects.Add(obj));
AddAssert("confirm 1 selected", () => EditorBeatmap.SelectedHitObjects.Count == 1);
AddStep("deselect hitobject", () => EditorBeatmap.SelectedHitObjects.Remove(obj));
AddAssert("confirm 0 selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
} }
[Test] [Test]
@ -30,11 +29,12 @@ namespace osu.Game.Tests.Visual.Editing
{ {
int hitObjectCount = 0; int hitObjectCount = 0;
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count);
addUndoSteps(); addUndoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
} }
[Test] [Test]
@ -42,11 +42,12 @@ namespace osu.Game.Tests.Visual.Editing
{ {
int hitObjectCount = 0; int hitObjectCount = 0;
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count);
addRedoSteps(); addRedoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
} }
[Test] [Test]
@ -58,15 +59,17 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("bind removal", () => AddStep("bind removal", () =>
{ {
editorBeatmap.HitObjectAdded += h => addedObject = h; EditorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h; EditorBeatmap.HitObjectRemoved += h => removedObject = h;
}); });
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddAssert("hitobject added", () => addedObject == expectedObject); AddAssert("hitobject added", () => addedObject == expectedObject);
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
addUndoSteps(); addUndoSteps();
AddAssert("hitobject removed", () => removedObject == expectedObject); AddAssert("hitobject removed", () => removedObject == expectedObject);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
} }
[Test] [Test]
@ -78,11 +81,11 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("bind removal", () => AddStep("bind removal", () =>
{ {
editorBeatmap.HitObjectAdded += h => addedObject = h; EditorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h; EditorBeatmap.HitObjectRemoved += h => removedObject = h;
}); });
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
addUndoSteps(); addUndoSteps();
AddStep("reset variables", () => AddStep("reset variables", () =>
@ -94,6 +97,17 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps(); addRedoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
AddAssert("no hitobject removed", () => removedObject == null); AddAssert("no hitobject removed", () => removedObject == null);
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
}
[Test]
public void TestAddObjectThenSaveHasNoUnsavedChanges()
{
AddStep("add hitobject", () => EditorBeatmap.Add(new HitCircle { StartTime = 1000 }));
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
AddStep("save changes", () => Editor.Save());
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
} }
[Test] [Test]
@ -105,12 +119,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("bind removal", () => AddStep("bind removal", () =>
{ {
editorBeatmap.HitObjectAdded += h => addedObject = h; EditorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h; EditorBeatmap.HitObjectRemoved += h => removedObject = h;
}); });
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddStep("remove object", () => editorBeatmap.Remove(expectedObject)); AddStep("remove object", () => EditorBeatmap.Remove(expectedObject));
AddStep("reset variables", () => AddStep("reset variables", () =>
{ {
addedObject = null; addedObject = null;
@ -120,6 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
addUndoSteps(); addUndoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
AddAssert("no hitobject removed", () => removedObject == null); AddAssert("no hitobject removed", () => removedObject == null);
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); // 2 steps performed, 1 undone
} }
[Test] [Test]
@ -131,12 +146,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("bind removal", () => AddStep("bind removal", () =>
{ {
editorBeatmap.HitObjectAdded += h => addedObject = h; EditorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h; EditorBeatmap.HitObjectRemoved += h => removedObject = h;
}); });
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddStep("remove object", () => editorBeatmap.Remove(expectedObject)); AddStep("remove object", () => EditorBeatmap.Remove(expectedObject));
addUndoSteps(); addUndoSteps();
AddStep("reset variables", () => AddStep("reset variables", () =>
@ -148,19 +163,11 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps(); addRedoSteps();
AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo) AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo)
AddAssert("no hitobject added", () => addedObject == null); AddAssert("no hitobject added", () => addedObject == null);
AddAssert("no changes", () => !Editor.HasUnsavedChanges); // end result is empty beatmap, matching original state
} }
private void addUndoSteps() => AddStep("undo", () => ((TestEditor)Editor).Undo()); private void addUndoSteps() => AddStep("undo", () => Editor.Undo());
private void addRedoSteps() => AddStep("redo", () => ((TestEditor)Editor).Redo()); private void addRedoSteps() => AddStep("redo", () => Editor.Redo());
protected override Editor CreateEditor() => new TestEditor();
private class TestEditor : Editor
{
public new void Undo() => base.Undo();
public new void Redo() => base.Redo();
}
} }
} }

View File

@ -0,0 +1,154 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorClipboard : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestCutRemovesObjects()
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("cut hitobject", () => Editor.Cut());
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
}
[TestCase(1000)]
[TestCase(2000)]
public void TestCutPaste(double newTime)
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("cut hitobject", () => Editor.Cut());
AddStep("move forward in time", () => EditorClock.Seek(newTime));
AddStep("paste hitobject", () => Editor.Paste());
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime);
}
[Test]
public void TestCutPasteSlider()
{
var addedObject = new Slider
{
StartTime = 1000,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100, 0), PathType.Bezier)
}
}
};
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("cut hitobject", () => Editor.Cut());
AddStep("paste hitobject", () => Editor.Paste());
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
AddAssert("path matches", () =>
{
var path = ((Slider)EditorBeatmap.HitObjects.Single()).Path;
return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints);
});
}
[Test]
public void TestCutPasteSpinner()
{
var addedObject = new Spinner
{
StartTime = 1000,
Duration = 5000
};
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("cut hitobject", () => Editor.Cut());
AddStep("paste hitobject", () => Editor.Paste());
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
AddAssert("duration matches", () => ((Spinner)EditorBeatmap.HitObjects.Single()).Duration == 5000);
}
[Test]
public void TestCopyPaste()
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("copy hitobject", () => Editor.Copy());
AddStep("move forward in time", () => EditorClock.Seek(2000));
AddStep("paste hitobject", () => Editor.Paste());
AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2);
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000);
}
[Test]
public void TestCutNothing()
{
AddStep("cut hitobject", () => Editor.Cut());
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
}
[Test]
public void TestCopyNothing()
{
AddStep("copy hitobject", () => Editor.Copy());
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
}
[Test]
public void TestPasteNothing()
{
AddStep("paste hitobject", () => Editor.Paste());
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
}
}
}

View File

@ -3,6 +3,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
new RadioButton("Item 1", () => { }), new RadioButton("Item 1", () => { }),
new RadioButton("Item 2", () => { }), new RadioButton("Item 2", () => { }),
new RadioButton("Item 3", () => { }), new RadioButton("Item 3", () => { }, () => new SpriteIcon { Icon = FontAwesome.Regular.Angry }),
new RadioButton("Item 4", () => { }), new RadioButton("Item 4", () => { }),
new RadioButton("Item 5", () => { }) new RadioButton("Item 5", () => { })
} }

View File

@ -0,0 +1,32 @@
// 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.Allocation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneSetupScreen : EditorClockTestScene
{
[Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
public TestSceneSetupScreen()
{
editorBeatmap = new EditorBeatmap(new OsuBeatmap());
}
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Child = new SetupScreen();
}
}
}

View File

@ -4,7 +4,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -18,8 +17,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneCompletionCancellation : OsuPlayerTestScene public class TestSceneCompletionCancellation : OsuPlayerTestScene
{ {
private Track track;
[Resolved] [Resolved]
private AudioManager audio { get; set; } private AudioManager audio { get; set; }
@ -34,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
base.SetUpSteps(); base.SetUpSteps();
// Ensure track has actually running before attempting to seek // Ensure track has actually running before attempting to seek
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
} }
[Test] [Test]
@ -73,13 +70,13 @@ namespace osu.Game.Tests.Visual.Gameplay
private void complete() private void complete()
{ {
AddStep("seek to completion", () => track.Seek(5000)); AddStep("seek to completion", () => Beatmap.Value.Track.Seek(5000));
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
} }
private void cancel() private void cancel()
{ {
AddStep("rewind to cancel", () => track.Seek(4000)); AddStep("rewind to cancel", () => Beatmap.Value.Track.Seek(4000));
AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value);
} }
@ -91,11 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
track = working.Track;
return working;
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{ {

View File

@ -5,7 +5,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -21,19 +20,13 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
private Track track; protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = working.Track;
return working;
}
[Test] [Test]
public void TestNoJudgementsOnRewind() public void TestNoJudgementsOnRewind()
{ {
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000); addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7));
@ -46,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => track.Seek(time)); AddStep($"seek to {time}", () => Beatmap.Value.Track.Seek(time));
// Allow a few frames of lenience // Allow a few frames of lenience
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));

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.Linq;
using NUnit.Framework;
using osu.Game.Overlays;
using osu.Game.Rulesets;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneOverlayActivation : OsuPlayerTestScene
{
protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer;
[Test]
public void TestGameplayOverlayActivation()
{
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
}
[Test]
public void TestGameplayOverlayActivationPaused()
{
AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("pause gameplay", () => Player.Pause());
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
}
[Test]
public void TestGameplayOverlayActivationReplayLoaded()
{
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("load a replay", () => Player.DrawableRuleset.HasReplayLoaded.Value = true);
AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
}
[Test]
public void TestGameplayOverlayActivationBreaks()
{
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("seek to break", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime));
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
AddStep("seek to break end", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime));
AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
}
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OverlayTestPlayer();
protected class OverlayTestPlayer : TestPlayer
{
public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value;
}
}
}

View File

@ -48,7 +48,10 @@ namespace osu.Game.Tests.Visual.Gameplay
private class ExampleContainer : PlayerSettingsGroup private class ExampleContainer : PlayerSettingsGroup
{ {
protected override string Title => @"example"; public ExampleContainer()
: base("example")
{
}
} }
} }
} }

View File

@ -1,11 +1,9 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK; using osuTK;
@ -32,7 +30,10 @@ namespace osu.Game.Tests.Visual.Gameplay
requestCount = 0; requestCount = 0;
increment = skip_time; increment = skip_time;
Child = gameplayClockContainer = new GameplayClockContainer(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), Array.Empty<Mod>(), 0) var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
working.LoadTrack();
Child = gameplayClockContainer = new GameplayClockContainer(working, 0)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]

View File

@ -22,19 +22,32 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture] [TestFixture]
public class TestSceneStoryboard : OsuTestScene public class TestSceneStoryboard : OsuTestScene
{ {
private readonly Container<DrawableStoryboard> storyboardContainer; private Container<DrawableStoryboard> storyboardContainer;
private DrawableStoryboard storyboard; private DrawableStoryboard storyboard;
[Cached] [Test]
private MusicController musicController = new MusicController(); public void TestStoryboard()
{
AddStep("Restart", restart);
AddToggleStep("Passing", passing =>
{
if (storyboard != null) storyboard.Passing = passing;
});
}
public TestSceneStoryboard() [Test]
public void TestStoryboardMissingVideo()
{
AddStep("Load storyboard with missing video", loadStoryboardNoVideo);
}
[BackgroundDependencyLoader]
private void load()
{ {
Clock = new FramedClock(); Clock = new FramedClock();
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
musicController,
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -58,32 +71,11 @@ namespace osu.Game.Tests.Visual.Gameplay
State = { Value = Visibility.Visible }, State = { Value = Visibility.Visible },
} }
}); });
Beatmap.BindValueChanged(beatmapChanged, true);
} }
[Test] private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> e) => loadStoryboard(e.NewValue);
public void TestStoryboard()
{
AddStep("Restart", restart);
AddToggleStep("Passing", passing =>
{
if (storyboard != null) storyboard.Passing = passing;
});
}
[Test]
public void TestStoryboardMissingVideo()
{
AddStep("Load storyboard with missing video", loadStoryboardNoVideo);
}
[BackgroundDependencyLoader]
private void load()
{
Beatmap.ValueChanged += beatmapChanged;
}
private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> e)
=> loadStoryboard(e.NewValue);
private void restart() private void restart()
{ {

View File

@ -2,8 +2,8 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus namespace osu.Game.Tests.Visual.Menus
@ -15,11 +15,9 @@ namespace osu.Game.Tests.Visual.Menus
public TestSceneIntroWelcome() public TestSceneIntroWelcome()
{ {
AddUntilStep("wait for load", () => getTrack() != null); AddUntilStep("wait for load", () => MusicController.TrackLoaded);
AddAssert("correct track", () => Precision.AlmostEquals(MusicController.CurrentTrack.Length, 48000, 1));
AddAssert("check if menu music loops", () => getTrack().Looping); AddAssert("check if menu music loops", () => MusicController.CurrentTrack.Looping);
} }
private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track;
} }
} }

View File

@ -0,0 +1,83 @@
// 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.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Navigation;
namespace osu.Game.Tests.Visual.Menus
{
public class TestSceneMusicActionHandling : OsuGameTestScene
{
private GlobalActionContainer globalActionContainer => Game.ChildrenOfType<GlobalActionContainer>().First();
[Test]
public void TestMusicPlayAction()
{
AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething());
AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.IsUserPaused);
AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.IsUserPaused);
}
[Test]
public void TestMusicNavigationActions()
{
int importId = 0;
Queue<(WorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null;
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
}
},
Metadata = new BeatmapMetadata
{
Artist = $"a test map {importId++}",
Title = "title",
}
}).Wait(), 5);
AddStep("import beatmap with track", () =>
{
var setWithTrack = Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result;
Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Beatmaps.First());
});
AddStep("bind to track change", () =>
{
trackChangeQueue = new Queue<(WorkingBeatmap, TrackChangeDirection)>();
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
});
AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000));
AddUntilStep("wait for current time to update", () => Game.MusicController.CurrentTrack.CurrentTime > 5000);
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
AddAssert("no track change", () => trackChangeQueue.Count == 0);
AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000);
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
AddAssert("track changed to previous", () =>
trackChangeQueue.Count == 1 &&
trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev);
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
AddAssert("track changed to next", () =>
trackChangeQueue.Count == 1 &&
trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Next);
}
}
}

View File

@ -1,7 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -11,14 +10,10 @@ namespace osu.Game.Tests.Visual.Menus
{ {
public class TestSceneSongTicker : OsuTestScene public class TestSceneSongTicker : OsuTestScene
{ {
[Cached]
private MusicController musicController = new MusicController();
public TestSceneSongTicker() public TestSceneSongTicker()
{ {
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
musicController,
new SongTicker new SongTicker
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Menus
public class TestToolbar : Toolbar public class TestToolbar : Toolbar
{ {
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode; public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode as Bindable<OverlayActivation>;
} }
} }
} }

View File

@ -162,6 +162,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value));
} }
/// <summary>
/// Tests that the global mod instances are not retained by the rooms, as global mod instances are retained and re-used by the mod select overlay.
/// </summary>
[Test]
public void TestGlobalModInstancesNotRetained()
{
OsuModDoubleTime mod = null;
AddStep("set dt mod and store", () =>
{
SelectedMods.Value = new[] { new OsuModDoubleTime() };
// Mod select overlay replaces our mod.
mod = (OsuModDoubleTime)SelectedMods.Value[0];
});
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2);
AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
}
private class TestMatchSongSelect : MatchSongSelect private class TestMatchSongSelect : MatchSongSelect
{ {
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails; public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;

View File

@ -12,6 +12,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -132,6 +133,12 @@ namespace osu.Game.Tests.Visual.Navigation
return () => imported; return () => imported;
} }
/// <summary>
/// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time.
/// There's a case where they may succeed incorrectly if we don't compare against the previous instance.
/// </summary>
private IScreen lastWaitedScreen;
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type) private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
{ {
AddStep("present score", () => Game.PresentScore(getImport(), type)); AddStep("present score", () => Game.PresentScore(getImport(), type));
@ -139,13 +146,15 @@ namespace osu.Game.Tests.Visual.Navigation
switch (type) switch (type)
{ {
case ScorePresentType.Results: case ScorePresentType.Results:
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen); AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID); AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID); AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
break; break;
case ScorePresentType.Gameplay: case ScorePresentType.Gameplay:
AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is ReplayPlayerLoader); AddUntilStep("wait for player loader", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ReplayPlayerLoader);
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
AddUntilStep("correct score displayed", () => ((ReplayPlayerLoader)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID); AddUntilStep("correct score displayed", () => ((ReplayPlayerLoader)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID); AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
break; break;

View File

@ -4,14 +4,16 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -46,7 +48,6 @@ namespace osu.Game.Tests.Visual.Navigation
Player player = null; Player player = null;
WorkingBeatmap beatmap() => Game.Beatmap.Value; WorkingBeatmap beatmap() => Game.Beatmap.Value;
Track track() => beatmap().Track;
PushAndConfirm(() => new TestSongSelect()); PushAndConfirm(() => new TestSongSelect());
@ -62,30 +63,27 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
AddUntilStep("wait for fail", () => player.HasFailed); AddUntilStep("wait for fail", () => player.HasFailed);
AddUntilStep("wait for track stop", () => !track().IsRunning); AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying);
AddAssert("Ensure time before preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime); AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime);
pushEscape(); pushEscape();
AddUntilStep("wait for track playing", () => track().IsRunning); AddUntilStep("wait for track playing", () => Game.MusicController.IsPlaying);
AddAssert("Ensure time wasn't reset to preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime); AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime);
} }
[Test] [Test]
public void TestMenuMakesMusic() public void TestMenuMakesMusic()
{ {
WorkingBeatmap beatmap() => Game.Beatmap.Value;
Track track() => beatmap().Track;
TestSongSelect songSelect = null; TestSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestSongSelect());
AddUntilStep("wait for no track", () => track() is TrackVirtual); AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice);
AddStep("return to menu", () => songSelect.Exit()); AddStep("return to menu", () => songSelect.Exit());
AddUntilStep("wait for track", () => !(track() is TrackVirtual) && track().IsRunning); AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying);
} }
[Test] [Test]
@ -140,12 +138,58 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded); AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded);
AddStep("Seek close to end", () => AddStep("Seek close to end", () =>
{ {
Game.MusicController.SeekTo(Game.Beatmap.Value.Track.Length - 1000); Game.MusicController.SeekTo(Game.MusicController.CurrentTrack.Length - 1000);
Game.Beatmap.Value.Track.Completed += () => trackCompleted = true; Game.MusicController.CurrentTrack.Completed += () => trackCompleted = true;
}); });
AddUntilStep("Track was completed", () => trackCompleted); AddUntilStep("Track was completed", () => trackCompleted);
AddUntilStep("Track was restarted", () => Game.Beatmap.Value.Track.IsRunning); AddUntilStep("Track was restarted", () => Game.MusicController.IsPlaying);
}
[Test]
public void TestModSelectInput()
{
TestSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddStep("Change ruleset to osu!taiko", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.Number2);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.Number2);
});
AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType<ToolbarRulesetSelector>().Single().Current.Value.ID == 1);
AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
}
[Test]
public void TestBeatmapOptionsInput()
{
TestSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect());
AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show());
AddStep("Change ruleset to osu!taiko", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.Number2);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.Number2);
});
AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType<ToolbarRulesetSelector>().Single().Current.Value.ID == 1);
AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible);
} }
private void pushEscape() => private void pushEscape() =>
@ -173,6 +217,8 @@ namespace osu.Game.Tests.Visual.Navigation
private class TestSongSelect : PlaySongSelect private class TestSongSelect : PlaySongSelect
{ {
public ModSelectOverlay ModSelectOverlay => ModSelect; public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
} }
} }
} }

View File

@ -12,7 +12,7 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture] [TestFixture]
public class TestSceneFullscreenOverlay : OsuTestScene public class TestSceneFullscreenOverlay : OsuTestScene
{ {
private FullscreenOverlay overlay; private FullscreenOverlay<OverlayHeader> overlay;
protected override void LoadComplete() protected override void LoadComplete()
{ {
@ -38,10 +38,10 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("fire count 3", () => fireCount == 3); AddAssert("fire count 3", () => fireCount == 3);
} }
private class TestFullscreenOverlay : FullscreenOverlay private class TestFullscreenOverlay : FullscreenOverlay<OverlayHeader>
{ {
public TestFullscreenOverlay() public TestFullscreenOverlay()
: base(OverlayColourScheme.Pink) : base(OverlayColourScheme.Pink, null)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby());
AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null) AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)
{ {
BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null } BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null }
}); });

View File

@ -1,40 +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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile.Sections;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneProfileCounterPill : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red);
private readonly CounterPill pill;
private readonly BindableInt value = new BindableInt();
public TestSceneProfileCounterPill()
{
Child = pill = new CounterPill
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = { BindTarget = value }
};
}
[Test]
public void TestVisibility()
{
AddStep("Set value to 0", () => value.Value = 0);
AddAssert("Check hidden", () => !pill.IsPresent);
AddStep("Set value to 10", () => value.Value = 10);
AddAssert("Check visible", () => pill.IsPresent);
}
}
}

View File

@ -5,9 +5,9 @@ using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
@ -53,16 +53,13 @@ namespace osu.Game.Tests.Visual.SongSelect
private void showPersonalBestWithNullPosition() private void showPersonalBestWithNullPosition()
{ {
leaderboard.TopScore = new APILegacyUserTopScoreInfo leaderboard.TopScore = new ScoreInfo
{
Position = null,
Score = new APILegacyScoreInfo
{ {
Rank = ScoreRank.XH, Rank = ScoreRank.XH,
Accuracy = 1, Accuracy = 1,
MaxCombo = 244, MaxCombo = 244,
TotalScore = 1707827, TotalScore = 1707827,
Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, }, Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
User = new User User = new User
{ {
Id = 6602580, Id = 6602580,
@ -73,22 +70,19 @@ namespace osu.Game.Tests.Visual.SongSelect
FlagName = @"ES", FlagName = @"ES",
}, },
}, },
}
}; };
} }
private void showPersonalBest() private void showPersonalBest()
{ {
leaderboard.TopScore = new APILegacyUserTopScoreInfo leaderboard.TopScore = new ScoreInfo
{ {
Position = 999, Position = 999,
Score = new APILegacyScoreInfo
{
Rank = ScoreRank.XH, Rank = ScoreRank.XH,
Accuracy = 1, Accuracy = 1,
MaxCombo = 244, MaxCombo = 244,
TotalScore = 1707827, TotalScore = 1707827,
Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, }, Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
User = new User User = new User
{ {
Id = 6602580, Id = 6602580,
@ -99,7 +93,6 @@ namespace osu.Game.Tests.Visual.SongSelect
FlagName = @"ES", FlagName = @"ES",
}, },
}, },
}
}; };
} }

View File

@ -3,9 +3,8 @@
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect namespace osu.Game.Tests.Visual.SongSelect
{ {
@ -16,10 +15,13 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
var overlay = new BeatmapOptionsOverlay(); var overlay = new BeatmapOptionsOverlay();
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, Color4.Purple, null, Key.Number1); var colours = new OsuColour();
overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, Color4.Purple, null, Key.Number2);
overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, Color4.Pink, null, Key.Number3); overlay.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, null);
overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null, Key.Number4); overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, null);
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null);
overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, null);
overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, null);
Add(overlay); Add(overlay);

View File

@ -0,0 +1,237 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneFilterControl : OsuManualInputManagerTestScene
{
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly CollectionManager collectionManager;
private RulesetStore rulesets;
private BeatmapManager beatmapManager;
private FilterControl control;
public TestSceneFilterControl()
{
base.Content.AddRange(new Drawable[]
{
collectionManager = new CollectionManager(LocalStorage),
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(collectionManager);
return dependencies;
}
[SetUp]
public void SetUp() => Schedule(() =>
{
collectionManager.Collections.Clear();
Child = control = new FilterControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = FilterControl.HEIGHT,
};
});
[Test]
public void TestEmptyCollectionFilterContainsAllBeatmaps()
{
assertCollectionDropdownContains("All beatmaps");
assertCollectionHeaderDisplays("All beatmaps");
}
[Test]
public void TestCollectionAddedToDropdown()
{
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
assertCollectionDropdownContains("1");
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionRemovedFromDropdown()
{
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
AddStep("remove collection", () => collectionManager.Collections.RemoveAt(0));
assertCollectionDropdownContains("1", false);
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionRenamed()
{
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("select collection", () =>
{
var dropdown = control.ChildrenOfType<CollectionFilterDropdown>().Single();
dropdown.Current.Value = dropdown.ItemSource.ElementAt(1);
});
addExpandHeaderStep();
AddStep("change name", () => collectionManager.Collections[0].Name.Value = "First");
assertCollectionDropdownContains("First");
assertCollectionHeaderDisplays("First");
}
[Test]
public void TestAllBeatmapFilterDoesNotHaveAddButton()
{
addExpandHeaderStep();
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
}
[Test]
public void TestCollectionFilterHasAddButton()
{
addExpandHeaderStep();
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
}
[Test]
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
{
addExpandHeaderStep();
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
}
[Test]
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
AddStep("add beatmap to collection", () => collectionManager.Collections[0].Beatmaps.Add(Beatmap.Value.BeatmapInfo));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
AddStep("remove beatmap from collection", () => collectionManager.Collections[0].Beatmaps.Clear());
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
}
[Test]
public void TestButtonAddsAndRemovesBeatmap()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
addClickAddOrRemoveButtonStep(1);
AddAssert("collection contains beatmap", () => collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
addClickAddOrRemoveButtonStep(1);
AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
}
[Test]
public void TestManageCollectionsFilterIsNotSelected()
{
addExpandHeaderStep();
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("select collection", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
addExpandHeaderStep();
AddStep("click manage collections filter", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItems().Last());
InputManager.Click(MouseButton.Left);
});
AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name.Value == "1");
}
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
=> AddAssert($"collection dropdown header displays '{collectionName}'",
() => shouldDisplay == (control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
() => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType<FillFlowContainer>().OfType<IHasText>().First().Text == collectionName)));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItems().ElementAt(index).ChildrenOfType<IconButton>().Single();
private void addExpandHeaderStep() => AddStep("expand header", () =>
{
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single());
InputManager.Click(MouseButton.Left);
});
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
{
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
InputManager.Click(MouseButton.Left);
});
private IEnumerable<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems()
=> control.ChildrenOfType<CollectionFilterDropdown>().Single().ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
}
}

View File

@ -6,11 +6,11 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Tests.Visual.SongSelect namespace osu.Game.Tests.Visual.SongSelect
@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.SongSelect
public TestSceneUserTopScoreContainer() public TestSceneUserTopScoreContainer()
{ {
UserTopScoreContainer topScoreContainer; UserTopScoreContainer<ScoreInfo> topScoreContainer;
Add(dialogOverlay = new DialogOverlay Add(dialogOverlay = new DialogOverlay
{ {
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.DarkGreen, Colour = Color4.DarkGreen,
}, },
topScoreContainer = new UserTopScoreContainer topScoreContainer = new UserTopScoreContainer<ScoreInfo>(s => new LeaderboardScore(s, s.Position, false))
{ {
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
@ -52,16 +52,14 @@ namespace osu.Game.Tests.Visual.SongSelect
var scores = new[] var scores = new[]
{ {
new APILegacyUserTopScoreInfo new ScoreInfo
{ {
Position = 999, Position = 999,
Score = new APILegacyScoreInfo
{
Rank = ScoreRank.XH, Rank = ScoreRank.XH,
Accuracy = 1, Accuracy = 1,
MaxCombo = 244, MaxCombo = 244,
TotalScore = 1707827, TotalScore = 1707827,
Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, }, Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
User = new User User = new User
{ {
Id = 6602580, Id = 6602580,
@ -72,13 +70,10 @@ namespace osu.Game.Tests.Visual.SongSelect
FlagName = @"ES", FlagName = @"ES",
}, },
}, },
}
}, },
new APILegacyUserTopScoreInfo new ScoreInfo
{ {
Position = 110000, Position = 110000,
Score = new APILegacyScoreInfo
{
Rank = ScoreRank.X, Rank = ScoreRank.X,
Accuracy = 1, Accuracy = 1,
MaxCombo = 244, MaxCombo = 244,
@ -93,13 +88,10 @@ namespace osu.Game.Tests.Visual.SongSelect
FlagName = @"BR", FlagName = @"BR",
}, },
}, },
}
}, },
new APILegacyUserTopScoreInfo new ScoreInfo
{ {
Position = 22333, Position = 22333,
Score = new APILegacyScoreInfo
{
Rank = ScoreRank.S, Rank = ScoreRank.S,
Accuracy = 1, Accuracy = 1,
MaxCombo = 244, MaxCombo = 244,
@ -115,7 +107,6 @@ namespace osu.Game.Tests.Visual.SongSelect
}, },
}, },
} }
}
}; };
AddStep(@"Trigger visibility", topScoreContainer.ToggleVisibility); AddStep(@"Trigger visibility", topScoreContainer.ToggleVisibility);

View File

@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
private readonly NowPlayingOverlay np; private readonly NowPlayingOverlay np;
[Cached]
private MusicController musicController = new MusicController();
public TestSceneBeatSyncedContainer() public TestSceneBeatSyncedContainer()
{ {
Clock = new FramedClock(); Clock = new FramedClock();
@ -36,7 +33,6 @@ namespace osu.Game.Tests.Visual.UserInterface
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
musicController,
new BeatContainer new BeatContainer
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
@ -71,6 +67,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private readonly Box flashLayer; private readonly Box flashLayer;
[Resolved]
private MusicController musicController { get; set; }
public BeatContainer() public BeatContainer()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -165,7 +164,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (timingPoints.Count == 0) return 0; if (timingPoints.Count == 0) return 0;
if (timingPoints[^1] == current) if (timingPoints[^1] == current)
return (int)Math.Ceiling((Beatmap.Value.Track.Length - current.Time) / current.BeatLength); return (int)Math.Ceiling((musicController.CurrentTrack.Length - current.Time) / current.BeatLength);
return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength);
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.UserInterface
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, Audio, dependencies.Get<GameHost>(), Beatmap.Default)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
beatmap = beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result.Beatmaps[0]; beatmap = beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result.Beatmaps[0];

View File

@ -1,15 +1,10 @@
// 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.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -20,18 +15,11 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached] [Cached]
private MusicController musicController = new MusicController(); private MusicController musicController = new MusicController();
private WorkingBeatmap currentBeatmap;
private NowPlayingOverlay nowPlayingOverlay; private NowPlayingOverlay nowPlayingOverlay;
private RulesetStore rulesets;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, GameHost host) private void load()
{ {
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
nowPlayingOverlay = new NowPlayingOverlay nowPlayingOverlay = new NowPlayingOverlay
@ -51,44 +39,5 @@ namespace osu.Game.Tests.Visual.UserInterface
AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state); AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state);
AddStep(@"hide", () => nowPlayingOverlay.Hide()); AddStep(@"hide", () => nowPlayingOverlay.Hide());
} }
private BeatmapManager manager { get; set; }
private int importId;
[Test]
public void TestPrevTrackBehavior()
{
// ensure we have at least two beatmaps available.
AddRepeatStep("import beatmap", () => manager.Import(new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
}
},
Metadata = new BeatmapMetadata
{
Artist = $"a test map {importId++}",
Title = "title",
}
}).Wait(), 5);
AddStep(@"Next track", () => musicController.NextTrack());
AddStep("Store track", () => currentBeatmap = Beatmap.Value);
AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000));
AddUntilStep(@"Wait for current time to update", () => currentBeatmap.Track.CurrentTime > 5000);
AddStep(@"Set previous", () => musicController.PreviousTrack());
AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value);
AddUntilStep("Wait for current time to update", () => currentBeatmap.Track.CurrentTime < 5000);
AddStep(@"Set previous", () => musicController.PreviousTrack());
AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value);
}
} }
} }

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 NUnit.Framework;
using osu.Game.Overlays.Profile.Sections;
using osu.Framework.Testing;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Framework.Allocation;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestScenePaginatedContainerHeader : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
private PaginatedContainerHeader header;
[Test]
public void TestHiddenCounter()
{
AddStep("Create header", () => createHeader("Header with hidden counter", CounterVisibilityState.AlwaysHidden));
AddAssert("Value is 0", () => header.Current.Value == 0);
AddAssert("Counter is hidden", () => header.ChildrenOfType<CounterPill>().First().Alpha == 0);
AddStep("Set count 10", () => header.Current.Value = 10);
AddAssert("Value is 10", () => header.Current.Value == 10);
AddAssert("Counter is hidden", () => header.ChildrenOfType<CounterPill>().First().Alpha == 0);
}
[Test]
public void TestVisibleCounter()
{
AddStep("Create header", () => createHeader("Header with visible counter", CounterVisibilityState.AlwaysVisible));
AddAssert("Value is 0", () => header.Current.Value == 0);
AddAssert("Counter is visible", () => header.ChildrenOfType<CounterPill>().First().Alpha == 1);
AddStep("Set count 10", () => header.Current.Value = 10);
AddAssert("Value is 10", () => header.Current.Value == 10);
AddAssert("Counter is visible", () => header.ChildrenOfType<CounterPill>().First().Alpha == 1);
}
[Test]
public void TestVisibleWhenZeroCounter()
{
AddStep("Create header", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenZero));
AddAssert("Value is 0", () => header.Current.Value == 0);
AddAssert("Counter is visible", () => header.ChildrenOfType<CounterPill>().First().Alpha == 1);
AddStep("Set count 10", () => header.Current.Value = 10);
AddAssert("Value is 10", () => header.Current.Value == 10);
AddAssert("Counter is hidden", () => header.ChildrenOfType<CounterPill>().First().Alpha == 0);
AddStep("Set count 0", () => header.Current.Value = 0);
AddAssert("Value is 0", () => header.Current.Value == 0);
AddAssert("Counter is visible", () => header.ChildrenOfType<CounterPill>().First().Alpha == 1);
}
[Test]
public void TestInitialVisibility()
{
AddStep("Create header with 0 value", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenZero, 0));
AddAssert("Value is 0", () => header.Current.Value == 0);
AddAssert("Counter is visible", () => header.ChildrenOfType<CounterPill>().First().Alpha == 1);
AddStep("Create header with 1 value", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenZero, 1));
AddAssert("Value is 1", () => header.Current.Value == 1);
AddAssert("Counter is hidden", () => header.ChildrenOfType<CounterPill>().First().Alpha == 0);
}
private void createHeader(string text, CounterVisibilityState state, int initialValue = 0)
{
Clear();
Add(header = new PaginatedContainerHeader(text, state)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = { Value = initialValue }
});
}
}
}

View File

@ -2,32 +2,35 @@
// 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.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; 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.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Overlays.Music; using osu.Game.Overlays.Music;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestScenePlaylistOverlay : OsuTestScene public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene
{ {
private readonly BindableList<BeatmapSetInfo> beatmapSets = new BindableList<BeatmapSetInfo>(); private readonly BindableList<BeatmapSetInfo> beatmapSets = new BindableList<BeatmapSetInfo>();
private PlaylistOverlay playlistOverlay;
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
{ {
PlaylistOverlay overlay;
Child = new Container Child = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(300, 500), Size = new Vector2(300, 500),
Child = overlay = new PlaylistOverlay Child = playlistOverlay = new PlaylistOverlay
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -53,7 +56,45 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
} }
overlay.BeatmapSets.BindTo(beatmapSets); playlistOverlay.BeatmapSets.BindTo(beatmapSets);
}); });
[Test]
public void TestRearrangeItems()
{
AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any());
AddStep("hold 1st item handle", () =>
{
var handle = this.ChildrenOfType<PlaylistItem.PlaylistItemHandle>().First();
InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left);
});
AddStep("drag to 5th", () =>
{
var item = this.ChildrenOfType<PlaylistItem>().ElementAt(4);
InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.Centre);
});
AddAssert("song 1 is 5th", () => beatmapSets[4].Metadata.Title == "Some Song 1");
AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left));
}
[Test]
public void TestFiltering()
{
AddStep("set filter to \"10\"", () =>
{
var filterControl = playlistOverlay.ChildrenOfType<FilterControl>().Single();
filterControl.Search.Current.Value = "10";
});
AddAssert("results filtered correctly",
() => playlistOverlay.ChildrenOfType<PlaylistItem>()
.Where(item => item.MatchingFilter)
.All(item => item.FilterTerms.Any(term => term.Contains("10"))));
}
} }
} }

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
protected override Track GetTrack() => trackStore.Get(firstAudioFile); protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile);
private string firstAudioFile private string firstAudioFile
{ {

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

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 NUnit.Framework;
using osu.Game.Tests.Visual;
using osu.Game.Tournament.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tournament.Tests.Components
{
public class TestSceneDateTextBox : OsuManualInputManagerTestScene
{
private DateTextBox textBox;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = textBox = new DateTextBox
{
Width = 0.3f
};
});
[Test]
public void TestCommitWithoutSettingBindable()
{
AddStep("click textbox", () =>
{
InputManager.MoveMouseTo(textBox);
InputManager.Click(MouseButton.Left);
});
AddStep("unfocus", () =>
{
InputManager.MoveMouseTo(Vector2.Zero);
InputManager.Click(MouseButton.Left);
});
}
}
}

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
</ItemGroup> </ItemGroup>

View File

@ -22,11 +22,12 @@ namespace osu.Game.Tournament.Components
} }
// hold a reference to the provided bindable so we don't have to in every settings section. // hold a reference to the provided bindable so we don't have to in every settings section.
private Bindable<DateTimeOffset> bindable; private Bindable<DateTimeOffset> bindable = new Bindable<DateTimeOffset>();
public DateTextBox() public DateTextBox()
{ {
base.Bindable = new Bindable<string>(); base.Bindable = new Bindable<string>();
((OsuTextBox)Control).OnCommit = (sender, newText) => ((OsuTextBox)Control).OnCommit = (sender, newText) =>
{ {
try try

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