1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 18:07:23 +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"
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.52"
"Microsoft.Build.Traversal": "2.1.1"
}
}

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<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="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
Name = @"Fruit Count",
Content = fruits.ToString(),
Icon = FontAwesome.Regular.Circle
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
},
new BeatmapStatistic
{
Name = @"Juice Stream Count",
Content = juiceStreams.ToString(),
Icon = FontAwesome.Regular.Circle
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
},
new BeatmapStatistic
{
Name = @"Banana Shower Count",
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.Scoring;
using System;
using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
[ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset
{
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 double ApproachRate;
public int MaxCombo;
}
}

View File

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

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<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="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

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

View File

@ -5,7 +5,6 @@ using osu.Game.Rulesets.Mania.Objects;
using System;
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
@ -167,8 +166,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
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);
computeDensity(time);
}

View File

@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public readonly double EndTime;
public readonly double SegmentDuration;
private readonly int spanCount;
public readonly int SpanCount;
private PatternType convertType;
@ -42,20 +41,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats;
spanCount = repeatsData?.SpanCount() ?? 1;
SpanCount = repeatsData?.SpanCount() ?? 1;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
// 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
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
EndTime = hitObject.StartTime + osuDuration;
SegmentDuration = (EndTime - HitObject.StartTime) / spanCount;
SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
}
public override IEnumerable<Pattern> Generate()
@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern;
}
if (spanCount > 1)
if (SpanCount > 1)
{
if (SegmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1);
@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (SegmentDuration <= 120)
{
convertType |= PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, spanCount + 1);
return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
}
if (SegmentDuration <= 160)
@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (duration >= 4000)
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 generateHoldAndNormalNotes(HitObject.StartTime);
@ -251,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
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);
startTime += SegmentDuration;
@ -302,7 +301,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
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);
@ -393,7 +392,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
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);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
@ -447,7 +446,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var rowPattern = new Pattern();
for (int i = 0; i <= spanCount; i++)
for (int i = 0; i <= SpanCount; i++)
{
if (!(ignoreHead && startTime == HitObject.StartTime))
{

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
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);
}

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.Skills;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@ -43,6 +44,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
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.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
};
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
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();
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
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();
}
}

View File

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

View File

@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Mania
new SettingsSlider<double, TimeSlider>
{
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
{
[TestFixture]
[Timeout(10000)]
public class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
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.Storyboards;
using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
{
@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private AudioManager audioManager { get; set; }
private TrackVirtualManual track;
protected override bool Autoplay => autoplay;
private bool autoplay;
@ -44,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = (TrackVirtualManual)working.Track;
return working;
}
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache)
@ -72,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("enable autoplay", () => autoplay = true);
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;
retrieveDrawableSlider(sliderIndex);
@ -97,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("have autoplay", () => autoplay = true);
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;
retrieveDrawableSlider(sliderIndex);
@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
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));
}

View File

@ -25,7 +25,6 @@ using osu.Game.Scoring;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
{
@ -34,18 +33,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private AudioManager audioManager { get; set; }
private TrackVirtualManual track;
protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = (TrackVirtualManual)working.Track;
return working;
}
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
@ -55,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
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());
}
@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
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;
// 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,
@ -230,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
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));
}

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<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="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

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

View File

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

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
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();
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
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();
}
}

View File

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

View File

@ -30,14 +30,12 @@ using osu.Game.Scoring;
using osu.Game.Skinning;
using System;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu
{
[ExcludeFromDynamicCompile]
public class OsuRuleset : Ruleset, ILegacyRuleset
{
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);
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);
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)
{
if (pointGrid.Content.Length == 0)
if (pointGrid.Content.Count == 0)
return;
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
{
[TestFixture]
[Timeout(10000)]
public class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
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";
[TestCase(2.9811338051242915d, "diffcalc-test")]
[TestCase(2.9811338051242915d, "diffcalc-test-strong")]
[TestCase(2.2867022617692685d, "diffcalc-test")]
[TestCase(2.2867022617692685d, "diffcalc-test-strong")]
public void Test(double expected, string name)
=> base.Test(expected, name);

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<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="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
@ -22,20 +21,20 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
new BeatmapStatistic
{
Name = @"Hit Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = hits.ToString(),
Icon = FontAwesome.Regular.Circle
},
new BeatmapStatistic
{
Name = @"Drumroll Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = drumrolls.ToString(),
Icon = FontAwesome.Regular.Circle
},
new BeatmapStatistic
{
Name = @"Swell Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
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.
// 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.Objects;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
/// <summary>
/// Represents a single hit object in taiko difficulty calculation.
/// </summary>
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)
{
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 double StaminaStrain;
public double RhythmStrain;
public double ColourStrain;
public double ApproachRate;
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@ -19,39 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
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)
: 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)
return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
HitWindows hitWindows = new TaikoHitWindows();
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() };
new Colour(),
new Rhythm(),
new Stamina(true),
new Stamina(false),
};
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
@ -60,5 +44,124 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
new TaikoModEasy(),
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
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))
strainValue *= 1.025;

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
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();
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
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();
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
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();
}
}

View File

@ -22,7 +22,6 @@ using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Scoring;
using System;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Edit;
using osu.Game.Rulesets.Taiko.Objects;
@ -32,7 +31,6 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko
{
[ExcludeFromDynamicCompile]
public class TaikoRuleset : Ruleset, ILegacyRuleset
{
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 osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@ -19,6 +20,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats
@ -26,18 +28,33 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestFixture]
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))]
public void TestEncodeDecodeStability(string name)
{
var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name));
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded));
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
sort(decoded);
sort(decodedAfterEncode);
sort(decoded.beatmap);
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)
@ -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))
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();
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmap).Encode(writer);
new LegacyBeatmapEncoder(beatmap, beatmapSkin).Encode(writer);
stream.Position = 0;
@ -106,7 +136,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
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.Serialization;
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.Tests.Resources;
using osuTK;
@ -90,6 +92,38 @@ namespace osu.Game.Tests.Beatmaps.Formats
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]
public void TestDecodeHitObjects()
{
@ -100,6 +134,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
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));

View File

@ -15,8 +15,10 @@ using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
@ -32,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportWhenClosed()
{
// 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
{
@ -49,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDelete()
{
// 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
{
@ -70,7 +72,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenImport()
{
// 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
{
@ -96,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportWithReZip()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithReZip)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -154,7 +156,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportWithChangedFile()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithChangedFile)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -205,7 +207,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportWithDifferentFilename()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithDifferentFilename)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -257,7 +259,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportCorruptThenImport()
{
// 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
{
@ -299,7 +301,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestRollbackOnFailure()
{
// 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
{
@ -376,7 +378,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImport()
{
// 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
{
@ -404,7 +406,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
{
// 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
{
@ -438,7 +440,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportWithDuplicateBeatmapIDs()
{
// 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
{
@ -524,7 +526,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWhenFileOpen()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenFileOpen)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -546,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWithDuplicateHashes()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateHashes)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -588,7 +590,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportNestedStructure()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -633,7 +635,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWithIgnoredDirectoryInArchive()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithIgnoredDirectoryInArchive)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -687,7 +689,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestUpdateBeatmapInfo()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapInfo)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -717,7 +719,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestUpdateBeatmapFile()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapFile)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
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)
{
var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);

View File

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

View File

@ -1,10 +1,8 @@
// 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.Framework.Testing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
@ -19,7 +17,14 @@ namespace osu.Game.Tests.Gameplay
{
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());
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));
}
[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
{
public override Judgement CreateJudgement() => new TestJudgement();
@ -37,5 +51,17 @@ namespace osu.Game.Tests.Gameplay
{
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", () =>
{
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))
{
@ -103,7 +106,7 @@ namespace osu.Game.Tests.Gameplay
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
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))
{

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]
public async Task TestBasicImport()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestBasicImport"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Scores.IO
[Test]
public async Task TestImportMods()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMods"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -92,7 +92,7 @@ namespace osu.Game.Tests.Scores.IO
[Test]
public async Task TestImportStatistics()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportStatistics"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -122,7 +122,7 @@ namespace osu.Game.Tests.Scores.IO
[Test]
public async Task TestImportWithDeletedBeatmapSet()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDeletedBeatmapSet"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@ -159,7 +159,7 @@ namespace osu.Game.Tests.Scores.IO
[Test]
public async Task TestOnlineScoreIsAvailableLocally()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestOnlineScoreIsAvailableLocally"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
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;
beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]);
beatmap.LoadTrack();
}
[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.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorChangeStates : EditorTestScene
{
private EditorBeatmap editorBeatmap;
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
public override void SetUpSteps()
[Test]
public void TestSelectedObjects()
{
base.SetUpSteps();
AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single());
HitCircle obj = null;
AddStep("add hitobject", () => EditorBeatmap.Add(obj = new HitCircle { StartTime = 1000 }));
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]
@ -30,11 +29,12 @@ namespace osu.Game.Tests.Visual.Editing
{
int hitObjectCount = 0;
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count);
addUndoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
}
[Test]
@ -42,11 +42,12 @@ namespace osu.Game.Tests.Visual.Editing
{
int hitObjectCount = 0;
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count);
addRedoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
}
[Test]
@ -58,15 +59,17 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("bind removal", () =>
{
editorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
EditorBeatmap.HitObjectAdded += h => addedObject = 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("unsaved changes", () => Editor.HasUnsavedChanges);
addUndoSteps();
AddAssert("hitobject removed", () => removedObject == expectedObject);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
}
[Test]
@ -78,11 +81,11 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("bind removal", () =>
{
editorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
EditorBeatmap.HitObjectAdded += h => addedObject = 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();
AddStep("reset variables", () =>
@ -94,6 +97,17 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
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]
@ -105,12 +119,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("bind removal", () =>
{
editorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
EditorBeatmap.HitObjectAdded += h => addedObject = h;
EditorBeatmap.HitObjectRemoved += h => removedObject = h;
});
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddStep("remove object", () => editorBeatmap.Remove(expectedObject));
AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddStep("remove object", () => EditorBeatmap.Remove(expectedObject));
AddStep("reset variables", () =>
{
addedObject = null;
@ -120,6 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
addUndoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
AddAssert("no hitobject removed", () => removedObject == null);
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); // 2 steps performed, 1 undone
}
[Test]
@ -131,12 +146,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("bind removal", () =>
{
editorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
EditorBeatmap.HitObjectAdded += h => addedObject = h;
EditorBeatmap.HitObjectRemoved += h => removedObject = h;
});
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddStep("remove object", () => editorBeatmap.Remove(expectedObject));
AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddStep("remove object", () => EditorBeatmap.Remove(expectedObject));
addUndoSteps();
AddStep("reset variables", () =>
@ -148,19 +163,11 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps();
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 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());
protected override Editor CreateEditor() => new TestEditor();
private class TestEditor : Editor
{
public new void Undo() => base.Undo();
public new void Redo() => base.Redo();
}
private void addRedoSteps() => AddStep("redo", () => Editor.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 osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Tests.Visual.Editing
@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
{
new RadioButton("Item 1", () => { }),
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 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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Timing;
@ -18,8 +17,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneCompletionCancellation : OsuPlayerTestScene
{
private Track track;
[Resolved]
private AudioManager audio { get; set; }
@ -34,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
base.SetUpSteps();
// 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]
@ -73,13 +70,13 @@ namespace osu.Game.Tests.Visual.Gameplay
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);
}
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);
}
@ -91,11 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
track = working.Track;
return working;
}
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{

View File

@ -5,7 +5,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Utils;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
@ -21,19 +20,13 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private AudioManager audioManager { get; set; }
private Track track;
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;
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[Test]
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);
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));
@ -46,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
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
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
{
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osuTK;
@ -32,7 +30,10 @@ namespace osu.Game.Tests.Visual.Gameplay
requestCount = 0;
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,
Children = new Drawable[]

View File

@ -22,19 +22,32 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture]
public class TestSceneStoryboard : OsuTestScene
{
private readonly Container<DrawableStoryboard> storyboardContainer;
private Container<DrawableStoryboard> storyboardContainer;
private DrawableStoryboard storyboard;
[Cached]
private MusicController musicController = new MusicController();
[Test]
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();
AddRange(new Drawable[]
{
musicController,
new Container
{
RelativeSizeAxes = Axes.Both,
@ -58,32 +71,11 @@ namespace osu.Game.Tests.Visual.Gameplay
State = { Value = Visibility.Visible },
}
});
Beatmap.BindValueChanged(beatmapChanged, true);
}
[Test]
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 beatmapChanged(ValueChangedEvent<WorkingBeatmap> e) => loadStoryboard(e.NewValue);
private void restart()
{

View File

@ -2,8 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
@ -15,11 +15,9 @@ namespace osu.Game.Tests.Visual.Menus
public TestSceneIntroWelcome()
{
AddUntilStep("wait for load", () => getTrack() != null);
AddAssert("check if menu music loops", () => getTrack().Looping);
AddUntilStep("wait for load", () => MusicController.TrackLoaded);
AddAssert("correct track", () => Precision.AlmostEquals(MusicController.CurrentTrack.Length, 48000, 1));
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.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
@ -11,14 +10,10 @@ namespace osu.Game.Tests.Visual.Menus
{
public class TestSceneSongTicker : OsuTestScene
{
[Cached]
private MusicController musicController = new MusicController();
public TestSceneSongTicker()
{
AddRange(new Drawable[]
{
musicController,
new SongTicker
{
Anchor = Anchor.Centre,

View File

@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Menus
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));
}
/// <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
{
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.Osu;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@ -132,6 +133,12 @@ namespace osu.Game.Tests.Visual.Navigation
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)
{
AddStep("present score", () => Game.PresentScore(getImport(), type));
@ -139,13 +146,15 @@ namespace osu.Game.Tests.Visual.Navigation
switch (type)
{
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);
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
break;
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);
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
break;

View File

@ -4,14 +4,16 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO;
using osuTK;
using osuTK.Input;
@ -46,7 +48,6 @@ namespace osu.Game.Tests.Visual.Navigation
Player player = null;
WorkingBeatmap beatmap() => Game.Beatmap.Value;
Track track() => beatmap().Track;
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 fail", () => player.HasFailed);
AddUntilStep("wait for track stop", () => !track().IsRunning);
AddAssert("Ensure time before preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime);
AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying);
AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime);
pushEscape();
AddUntilStep("wait for track playing", () => track().IsRunning);
AddAssert("Ensure time wasn't reset to preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime);
AddUntilStep("wait for track playing", () => Game.MusicController.IsPlaying);
AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime);
}
[Test]
public void TestMenuMakesMusic()
{
WorkingBeatmap beatmap() => Game.Beatmap.Value;
Track track() => beatmap().Track;
TestSongSelect songSelect = null;
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());
AddUntilStep("wait for track", () => !(track() is TrackVirtual) && track().IsRunning);
AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying);
}
[Test]
@ -140,12 +138,58 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded);
AddStep("Seek close to end", () =>
{
Game.MusicController.SeekTo(Game.Beatmap.Value.Track.Length - 1000);
Game.Beatmap.Value.Track.Completed += () => trackCompleted = true;
Game.MusicController.SeekTo(Game.MusicController.CurrentTrack.Length - 1000);
Game.MusicController.CurrentTrack.Completed += () => trackCompleted = true;
});
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() =>
@ -173,6 +217,8 @@ namespace osu.Game.Tests.Visual.Navigation
private class TestSongSelect : PlaySongSelect
{
public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public class TestSceneFullscreenOverlay : OsuTestScene
{
private FullscreenOverlay overlay;
private FullscreenOverlay<OverlayHeader> overlay;
protected override void LoadComplete()
{
@ -38,10 +38,10 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("fire count 3", () => fireCount == 3);
}
private class TestFullscreenOverlay : FullscreenOverlay
private class TestFullscreenOverlay : FullscreenOverlay<OverlayHeader>
{
public TestFullscreenOverlay()
: base(OverlayColourScheme.Pink)
: base(OverlayColourScheme.Pink, null)
{
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 beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null)
AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, 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.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
@ -53,53 +53,46 @@ namespace osu.Game.Tests.Visual.SongSelect
private void showPersonalBestWithNullPosition()
{
leaderboard.TopScore = new APILegacyUserTopScoreInfo
leaderboard.TopScore = new ScoreInfo
{
Position = null,
Score = new APILegacyScoreInfo
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
User = new User
{
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, },
User = new User
Id = 6602580,
Username = @"waaiiru",
Country = new Country
{
Id = 6602580,
Username = @"waaiiru",
Country = new Country
{
FullName = @"Spain",
FlagName = @"ES",
},
FullName = @"Spain",
FlagName = @"ES",
},
}
},
};
}
private void showPersonalBest()
{
leaderboard.TopScore = new APILegacyUserTopScoreInfo
leaderboard.TopScore = new ScoreInfo
{
Position = 999,
Score = new APILegacyScoreInfo
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
User = new User
{
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, },
User = new User
Id = 6602580,
Username = @"waaiiru",
Country = new Country
{
Id = 6602580,
Username = @"waaiiru",
Country = new Country
{
FullName = @"Spain",
FlagName = @"ES",
},
FullName = @"Spain",
FlagName = @"ES",
},
}
},
};
}

View File

@ -3,9 +3,8 @@
using System.ComponentModel;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Screens.Select.Options;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
@ -16,10 +15,13 @@ namespace osu.Game.Tests.Visual.SongSelect
{
var overlay = new BeatmapOptionsOverlay();
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, Color4.Purple, null, Key.Number1);
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(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null, Key.Number4);
var colours = new OsuColour();
overlay.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, null);
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);

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.Shapes;
using osuTK.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.SongSelect
@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.SongSelect
public TestSceneUserTopScoreContainer()
{
UserTopScoreContainer topScoreContainer;
UserTopScoreContainer<ScoreInfo> topScoreContainer;
Add(dialogOverlay = new DialogOverlay
{
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect
RelativeSizeAxes = Axes.Both,
Colour = Color4.DarkGreen,
},
topScoreContainer = new UserTopScoreContainer
topScoreContainer = new UserTopScoreContainer<ScoreInfo>(s => new LeaderboardScore(s, s.Position, false))
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
@ -52,69 +52,60 @@ namespace osu.Game.Tests.Visual.SongSelect
var scores = new[]
{
new APILegacyUserTopScoreInfo
new ScoreInfo
{
Position = 999,
Score = new APILegacyScoreInfo
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
User = new User
{
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, },
User = new User
Id = 6602580,
Username = @"waaiiru",
Country = new Country
{
Id = 6602580,
Username = @"waaiiru",
Country = new Country
{
FullName = @"Spain",
FlagName = @"ES",
},
FullName = @"Spain",
FlagName = @"ES",
},
}
},
},
new APILegacyUserTopScoreInfo
new ScoreInfo
{
Position = 110000,
Score = new APILegacyScoreInfo
Rank = ScoreRank.X,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
User = new User
{
Rank = ScoreRank.X,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
User = new User
Id = 4608074,
Username = @"Skycries",
Country = new Country
{
Id = 4608074,
Username = @"Skycries",
Country = new Country
{
FullName = @"Brazil",
FlagName = @"BR",
},
FullName = @"Brazil",
FlagName = @"BR",
},
}
},
},
new APILegacyUserTopScoreInfo
new ScoreInfo
{
Position = 22333,
Score = new APILegacyScoreInfo
Rank = ScoreRank.S,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
User = new User
{
Rank = ScoreRank.S,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
User = new User
Id = 1541390,
Username = @"Toukai",
Country = new Country
{
Id = 1541390,
Username = @"Toukai",
Country = new Country
{
FullName = @"Canada",
FlagName = @"CA",
},
FullName = @"Canada",
FlagName = @"CA",
},
}
},
}
};

View File

@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface
{
private readonly NowPlayingOverlay np;
[Cached]
private MusicController musicController = new MusicController();
public TestSceneBeatSyncedContainer()
{
Clock = new FramedClock();
@ -36,7 +33,6 @@ namespace osu.Game.Tests.Visual.UserInterface
AddRange(new Drawable[]
{
musicController,
new BeatContainer
{
Anchor = Anchor.BottomCentre,
@ -71,6 +67,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private readonly Box flashLayer;
[Resolved]
private MusicController musicController { get; set; }
public BeatContainer()
{
RelativeSizeAxes = Axes.X;
@ -165,7 +164,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (timingPoints.Count == 0) return 0;
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);
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform;
using osu.Framework.Testing;
@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.UserInterface
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
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));
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.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.UserInterface
@ -20,18 +15,11 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private MusicController musicController = new MusicController();
private WorkingBeatmap currentBeatmap;
private NowPlayingOverlay nowPlayingOverlay;
private RulesetStore rulesets;
[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);
nowPlayingOverlay = new NowPlayingOverlay
@ -51,44 +39,5 @@ namespace osu.Game.Tests.Visual.UserInterface
AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state);
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.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Music;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestScenePlaylistOverlay : OsuTestScene
public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene
{
private readonly BindableList<BeatmapSetInfo> beatmapSets = new BindableList<BeatmapSetInfo>();
private PlaylistOverlay playlistOverlay;
[SetUp]
public void Setup() => Schedule(() =>
{
PlaylistOverlay overlay;
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(300, 500),
Child = overlay = new PlaylistOverlay
Child = playlistOverlay = new PlaylistOverlay
{
Anchor = 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 Track GetTrack() => trackStore.Get(firstAudioFile);
protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile);
private string firstAudioFile
{

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" 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="NUnit3TestAdapter" Version="3.17.0" />
<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>
<ItemGroup Label="Package References">
<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="NUnit3TestAdapter" Version="3.17.0" />
</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.
private Bindable<DateTimeOffset> bindable;
private Bindable<DateTimeOffset> bindable = new Bindable<DateTimeOffset>();
public DateTextBox()
{
base.Bindable = new Bindable<string>();
((OsuTextBox)Control).OnCommit = (sender, newText) =>
{
try

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