Compare commits
319 Commits
2020.910.0
...
2020.923.0
@@ -38,7 +38,13 @@ If you are looking to install or test osu! without setting up a development envi
|
||||
|
||||
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
|
||||
|
||||
## Developing or debugging
|
||||
## Developing a custom ruleset
|
||||
|
||||
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu-templates).
|
||||
|
||||
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
|
||||
|
||||
## Developing osu!
|
||||
|
||||
Please make sure you have the following prerequisites:
|
||||
|
||||
|
||||
@@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.910.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.923.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 923 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
Schedule(() =>
|
||||
{
|
||||
area.AttemptCatch(fruit);
|
||||
area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
|
||||
area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
|
||||
|
||||
drawable.Expire();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public class TestSceneComboCounter : CatchSkinnableTestScene
|
||||
{
|
||||
private ScoreProcessor scoreProcessor;
|
||||
|
||||
private Color4 judgedObjectColour = Color4.White;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
scoreProcessor = new ScoreProcessor();
|
||||
|
||||
SetContents(() => new CatchComboDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(2.5f),
|
||||
});
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestCatchComboCounter()
|
||||
{
|
||||
AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20);
|
||||
AddStep("perform miss", () => performJudgement(HitResult.Miss));
|
||||
|
||||
AddStep("randomize judged object colour", () =>
|
||||
{
|
||||
judgedObjectColour = new Color4(
|
||||
RNG.NextSingle(1f),
|
||||
RNG.NextSingle(1f),
|
||||
RNG.NextSingle(1f),
|
||||
1f
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private void performJudgement(HitResult type, Judgement judgement = null)
|
||||
{
|
||||
var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };
|
||||
|
||||
var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type };
|
||||
scoreProcessor.ApplyResult(result);
|
||||
|
||||
foreach (var counter in CreatedDrawables.Cast<CatchComboDisplay>())
|
||||
counter.OnNewResult(judgedObject, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||
|
||||
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
|
||||
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
var positionData = obj as IHasXPosition;
|
||||
var comboData = obj as IHasCombo;
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
Droplet,
|
||||
CatcherIdle,
|
||||
CatcherFail,
|
||||
CatcherKiai
|
||||
CatcherKiai,
|
||||
CatchComboCounter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using static osu.Game.Skinning.LegacySkinConfiguration;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
@@ -52,6 +53,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
case CatchSkinComponents.CatcherKiai:
|
||||
return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
|
||||
this.GetAnimation("fruit-ryuuta", true, true, true);
|
||||
|
||||
case CatchSkinComponents.CatchComboCounter:
|
||||
var comboFont = GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
||||
|
||||
// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
|
||||
if (this.HasFont(comboFont))
|
||||
return new LegacyComboCounter(Source);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using static osu.Game.Skinning.LegacySkinConfiguration;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
|
||||
/// </summary>
|
||||
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
|
||||
{
|
||||
private readonly LegacyRollingCounter counter;
|
||||
|
||||
private readonly LegacyRollingCounter explosion;
|
||||
|
||||
public LegacyComboCounter(ISkin skin)
|
||||
{
|
||||
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
||||
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Alpha = 0f;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
Scale = new Vector2(0.8f);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
explosion = new LegacyRollingCounter(skin, fontName, fontOverlap)
|
||||
{
|
||||
Alpha = 0.65f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1.5f),
|
||||
},
|
||||
counter = new LegacyRollingCounter(skin, fontName, fontOverlap)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private int lastDisplayedCombo;
|
||||
|
||||
public void UpdateCombo(int combo, Color4? hitObjectColour = null)
|
||||
{
|
||||
if (combo == lastDisplayedCombo)
|
||||
return;
|
||||
|
||||
// There may still be existing transforms to the counter (including value change after 250ms),
|
||||
// finish them immediately before new transforms.
|
||||
counter.SetCountWithoutRolling(lastDisplayedCombo);
|
||||
|
||||
lastDisplayedCombo = combo;
|
||||
|
||||
if (Time.Elapsed < 0)
|
||||
{
|
||||
// needs more work to make rewind somehow look good.
|
||||
// basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle).
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Combo fell to zero, roll down and fade out the counter.
|
||||
if (combo == 0)
|
||||
{
|
||||
counter.Current.Value = 0;
|
||||
explosion.Current.Value = 0;
|
||||
|
||||
this.FadeOut(400, Easing.Out);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.FadeInFromZero().Then().Delay(1000).FadeOut(300);
|
||||
|
||||
counter.ScaleTo(1.5f)
|
||||
.ScaleTo(0.8f, 250, Easing.Out)
|
||||
.OnComplete(c => c.SetCountWithoutRolling(combo));
|
||||
|
||||
counter.Delay(250)
|
||||
.ScaleTo(1f)
|
||||
.ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30);
|
||||
|
||||
explosion.Colour = hitObjectColour ?? Color4.White;
|
||||
|
||||
explosion.SetCountWithoutRolling(combo);
|
||||
explosion.ScaleTo(1.5f)
|
||||
.ScaleTo(1.9f, 400, Easing.Out)
|
||||
.FadeOutFromOne(400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a component that displays a skinned <see cref="ICatchComboCounter"/> and handles combo judgement results for updating it accordingly.
|
||||
/// </summary>
|
||||
public class CatchComboDisplay : SkinnableDrawable
|
||||
{
|
||||
private int currentCombo;
|
||||
|
||||
[CanBeNull]
|
||||
public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
|
||||
|
||||
public CatchComboDisplay()
|
||||
: base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
|
||||
{
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
|
||||
{
|
||||
base.SkinChanged(skin, allowFallback);
|
||||
ComboCounter?.UpdateCombo(currentCombo);
|
||||
}
|
||||
|
||||
public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result)
|
||||
{
|
||||
if (!result.Judgement.AffectsCombo || !result.HasResult)
|
||||
return;
|
||||
|
||||
if (result.Type == HitResult.Miss)
|
||||
{
|
||||
updateCombo(0, null);
|
||||
return;
|
||||
}
|
||||
|
||||
updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
|
||||
{
|
||||
if (!result.Judgement.AffectsCombo || !result.HasResult)
|
||||
return;
|
||||
|
||||
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
|
||||
}
|
||||
|
||||
private void updateCombo(int newCombo, Color4? hitObjectColour)
|
||||
{
|
||||
currentCombo = newCombo;
|
||||
ComboCounter?.UpdateCombo(newCombo, hitObjectColour);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
explodingFruitContainer,
|
||||
CatcherArea.MovableCatcher.CreateProxiedContent(),
|
||||
HitObjectContainer,
|
||||
CatcherArea
|
||||
CatcherArea,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public override void Add(DrawableHitObject h)
|
||||
{
|
||||
h.OnNewResult += onNewResult;
|
||||
h.OnRevertResult += onRevertResult;
|
||||
|
||||
base.Add(h);
|
||||
|
||||
@@ -70,6 +71,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
}
|
||||
|
||||
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
=> CatcherArea.OnResult((DrawableCatchHitObject)judgedObject, result);
|
||||
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
|
||||
|
||||
private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
=> CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation;
|
||||
|
||||
public readonly Catcher MovableCatcher;
|
||||
private readonly CatchComboDisplay comboDisplay;
|
||||
|
||||
public Container ExplodingFruitTarget
|
||||
{
|
||||
@@ -34,10 +35,22 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public CatcherArea(BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
|
||||
Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
|
||||
Children = new Drawable[]
|
||||
{
|
||||
comboDisplay = new CatchComboDisplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.None,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.Centre,
|
||||
Margin = new MarginPadding { Bottom = 350f },
|
||||
X = CatchPlayfield.CENTER_X
|
||||
},
|
||||
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
|
||||
};
|
||||
}
|
||||
|
||||
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
|
||||
public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result)
|
||||
{
|
||||
if (result.Judgement is IgnoreJudgement)
|
||||
return;
|
||||
@@ -86,8 +99,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
else
|
||||
MovableCatcher.Drop();
|
||||
}
|
||||
|
||||
comboDisplay.OnNewResult(fruit, result);
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
|
||||
=> comboDisplay.OnRevertResult(fruit, result);
|
||||
|
||||
public void OnReleased(CatchAction action)
|
||||
{
|
||||
}
|
||||
@@ -105,6 +123,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (state?.CatcherX != null)
|
||||
MovableCatcher.X = state.CatcherX.Value;
|
||||
|
||||
comboDisplay.X = MovableCatcher.X;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface providing a set of methods to update the combo counter.
|
||||
/// </summary>
|
||||
public interface ICatchComboCounter : IDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the counter to animate a transition from the old combo value it had to the current provided one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is called regardless of whether the clock is rewinding.
|
||||
/// </remarks>
|
||||
/// <param name="combo">The new combo value.</param>
|
||||
/// <param name="hitObjectColour">The colour of the object if hit, null on miss.</param>
|
||||
void UpdateCombo(int combo, Color4? hitObjectColour = null);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using osu.Game.Rulesets.Mania.Objects;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -68,14 +69,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original)
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
|
||||
|
||||
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
|
||||
Random = new FastRandom(seed);
|
||||
|
||||
return base.ConvertBeatmap(original);
|
||||
return base.ConvertBeatmap(original, cancellationToken);
|
||||
}
|
||||
|
||||
protected override Beatmap<ManiaHitObject> CreateBeatmap()
|
||||
@@ -88,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
|
||||
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
if (original is ManiaHitObject maniaOriginal)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
|
||||
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition);
|
||||
|
||||
protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
|
||||
protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
var positionData = original as IHasPosition;
|
||||
var comboData = original as IHasCombo;
|
||||
|
||||
@@ -34,11 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
|
||||
Alpha = 0.5f,
|
||||
Child = new Box { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
ring = new RingPiece
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
ring = new RingPiece()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -46,14 +46,24 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
distanceSnapToggle
|
||||
};
|
||||
|
||||
private BindableList<HitObject> selectedHitObjects;
|
||||
|
||||
private Bindable<HitObject> placementObject;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
|
||||
EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
||||
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
|
||||
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
|
||||
|
||||
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
|
||||
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
||||
distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
|
||||
|
||||
// we may be entering the screen with a selection already active
|
||||
updateDistanceSnapGrid();
|
||||
}
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects)
|
||||
|
||||
@@ -46,7 +46,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
private void addConnection(FollowPointConnection connection)
|
||||
{
|
||||
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
|
||||
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
|
||||
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) =>
|
||||
{
|
||||
int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value);
|
||||
|
||||
if (comp != 0)
|
||||
return comp;
|
||||
|
||||
// we always want to insert the new item after equal ones.
|
||||
// this is important for beatmaps with multiple hitobjects at the same point in time.
|
||||
// if we use standard comparison insert order, there will be a churn of connections getting re-updated to
|
||||
// the next object at the point-in-time, adding a construction/disposal overhead (see FollowPointConnection.End implementation's ClearInternal).
|
||||
// this is easily visible on https://osu.ppy.sh/beatmapsets/150945#osu/372245
|
||||
return -1;
|
||||
}));
|
||||
|
||||
if (index < connections.Count - 1)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
public class RingPiece : Container
|
||||
public class RingPiece : CircularContainer
|
||||
{
|
||||
public RingPiece()
|
||||
{
|
||||
@@ -18,21 +18,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
Masking = true;
|
||||
BorderThickness = 10;
|
||||
BorderColour = Color4.White;
|
||||
|
||||
Child = new Box
|
||||
{
|
||||
Masking = true,
|
||||
BorderThickness = 10,
|
||||
BorderColour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
}
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@@ -48,14 +49,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects()
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects();
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
int totalSpins = MaximumBonusSpins + SpinsRequired;
|
||||
|
||||
for (int i = 0; i < totalSpins; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
|
||||
|
||||
AddNested(i < SpinsRequired
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
|
||||
var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
|
||||
|
||||
return !hasFont(font)
|
||||
return !this.HasFont(font)
|
||||
? null
|
||||
: new LegacySpriteText(Source, font)
|
||||
{
|
||||
@@ -145,7 +145,5 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
return Source.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
|
||||
private bool hasFont(string fontName) => Source.GetTexture($"{fontName}-0") != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
public class DrawableTestStrongHit : DrawableHit
|
||||
{
|
||||
private readonly HitResult type;
|
||||
private readonly bool hitBoth;
|
||||
|
||||
public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true)
|
||||
: base(new Hit
|
||||
{
|
||||
IsStrong = true,
|
||||
StartTime = startTime,
|
||||
})
|
||||
{
|
||||
// in order to create nested strong hit
|
||||
HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
this.type = type;
|
||||
this.hitBoth = hitBoth;
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
|
||||
Result.Type = type;
|
||||
|
||||
var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single();
|
||||
nestedStrongHit.Result.Type = hitBoth ? type : HitResult.Miss;
|
||||
}
|
||||
|
||||
public override bool OnPressed(TaikoAction action) => false;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -15,24 +14,29 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
[TestFixture]
|
||||
public class TestSceneHitExplosion : TaikoSkinnableTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[Test]
|
||||
public void TestNormalHit()
|
||||
{
|
||||
AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great)));
|
||||
AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good)));
|
||||
AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss)));
|
||||
AddStep("Great", () => SetContents(() => getContentFor(createHit(HitResult.Great))));
|
||||
AddStep("Good", () => SetContents(() => getContentFor(createHit(HitResult.Good))));
|
||||
AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss))));
|
||||
}
|
||||
|
||||
private Drawable getContentFor(HitResult type)
|
||||
[Test]
|
||||
public void TestStrongHit([Values(false, true)] bool hitBoth)
|
||||
{
|
||||
DrawableTaikoHitObject hit;
|
||||
AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth))));
|
||||
AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth))));
|
||||
}
|
||||
|
||||
private Drawable getContentFor(DrawableTaikoHitObject hit)
|
||||
{
|
||||
return new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
hit = createHit(type),
|
||||
hit,
|
||||
new HitExplosion(hit)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@@ -43,5 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
}
|
||||
|
||||
private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
|
||||
|
||||
private DrawableTaikoHitObject createStrongHit(HitResult type, bool hitBoth)
|
||||
=> new DrawableTestStrongHit(Time.Current, type, hitBoth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -174,7 +174,9 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
|
||||
private void addMissJudgement()
|
||||
{
|
||||
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new DrawableTestHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
DrawableTestHit h;
|
||||
Add(h = new DrawableTestHit(new Hit(), HitResult.Miss));
|
||||
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
}
|
||||
|
||||
private void addBarLine(bool major, double delay = scroll_time)
|
||||
|
||||
@@ -8,6 +8,8 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Utils;
|
||||
using System.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
@@ -48,14 +50,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
|
||||
public override bool CanConvert() => true;
|
||||
|
||||
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original)
|
||||
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
// Rewrite the beatmap info to add the slider velocity multiplier
|
||||
original.BeatmapInfo = original.BeatmapInfo.Clone();
|
||||
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
|
||||
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
|
||||
|
||||
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original);
|
||||
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
|
||||
|
||||
if (original.BeatmapInfo.RulesetID == 3)
|
||||
{
|
||||
@@ -72,7 +74,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
return converted;
|
||||
}
|
||||
|
||||
protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
|
||||
protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
// Old osu! used hit sounding to determine various hit type information
|
||||
IList<HitSampleInfo> samples = obj.Samples;
|
||||
@@ -104,6 +106,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
};
|
||||
|
||||
i = (i + 1) % allSamples.Count;
|
||||
|
||||
if (Precision.AlmostEquals(0, tickSpacing))
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.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;
|
||||
int lastMarkEnd = 0;
|
||||
|
||||
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(Math.Max(lastMarkEnd, i - repeatedLength + 1), i);
|
||||
lastMarkEnd = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
int lastMarkEnd = 0;
|
||||
|
||||
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(Math.Max(lastMarkEnd, i - tlLength + 1), i);
|
||||
lastMarkEnd = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all objects from <paramref name="start"/> to <paramref name="end"/> (inclusive) as <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
|
||||
/// </summary>
|
||||
private void markObjectsAsCheese(int start, int end)
|
||||
{
|
||||
for (int i = start; i <= end; i++)
|
||||
hitObjects[i].StaminaCheese = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
yield return new TernaryStateMenuItem("Rim", action: state =>
|
||||
{
|
||||
ChangeHandler.BeginChange();
|
||||
|
||||
foreach (var h in hits)
|
||||
{
|
||||
switch (state)
|
||||
@@ -35,6 +37,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ChangeHandler.EndChange();
|
||||
})
|
||||
{
|
||||
State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) }
|
||||
@@ -47,6 +51,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
yield return new TernaryStateMenuItem("Strong", action: state =>
|
||||
{
|
||||
ChangeHandler.BeginChange();
|
||||
|
||||
foreach (var h in hits)
|
||||
{
|
||||
switch (state)
|
||||
@@ -62,6 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
EditorBeatmap?.UpdateHitObject(h);
|
||||
}
|
||||
|
||||
ChangeHandler.EndChange();
|
||||
})
|
||||
{
|
||||
State = { Value = getTernaryState(hits, h => h.IsStrong) }
|
||||
|
||||
@@ -75,7 +75,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionGood:
|
||||
case TaikoSkinComponents.TaikoExplosionGoodStrong:
|
||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||
case TaikoSkinComponents.TaikoExplosionGreatStrong:
|
||||
case TaikoSkinComponents.TaikoExplosionMiss:
|
||||
|
||||
var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
|
||||
@@ -107,8 +109,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
case TaikoSkinComponents.TaikoExplosionGood:
|
||||
return "taiko-hit100";
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionGoodStrong:
|
||||
return "taiko-hit100k";
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||
return "taiko-hit300";
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionGreatStrong:
|
||||
return "taiko-hit300k";
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type");
|
||||
|
||||
@@ -17,7 +17,9 @@ namespace osu.Game.Rulesets.Taiko
|
||||
BarLine,
|
||||
TaikoExplosionMiss,
|
||||
TaikoExplosionGood,
|
||||
TaikoExplosionGoodStrong,
|
||||
TaikoExplosionGreat,
|
||||
TaikoExplosionGreatStrong,
|
||||
Scroller,
|
||||
Mascot,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osuTK;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -9,6 +10,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
@@ -45,24 +47,41 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion());
|
||||
Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion());
|
||||
}
|
||||
|
||||
private TaikoSkinComponents getComponentName(HitResult resultType)
|
||||
private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject)
|
||||
{
|
||||
var resultType = judgedObject.Result?.Type ?? HitResult.Great;
|
||||
|
||||
switch (resultType)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
return TaikoSkinComponents.TaikoExplosionMiss;
|
||||
|
||||
case HitResult.Good:
|
||||
return TaikoSkinComponents.TaikoExplosionGood;
|
||||
return useStrongExplosion(judgedObject)
|
||||
? TaikoSkinComponents.TaikoExplosionGoodStrong
|
||||
: TaikoSkinComponents.TaikoExplosionGood;
|
||||
|
||||
case HitResult.Great:
|
||||
return TaikoSkinComponents.TaikoExplosionGreat;
|
||||
return useStrongExplosion(judgedObject)
|
||||
? TaikoSkinComponents.TaikoExplosionGreatStrong
|
||||
: TaikoSkinComponents.TaikoExplosionGreat;
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(resultType), "Invalid result type");
|
||||
throw new ArgumentOutOfRangeException(nameof(judgedObject), "Invalid result type");
|
||||
}
|
||||
|
||||
private bool useStrongExplosion(DrawableHitObject judgedObject)
|
||||
{
|
||||
if (!(judgedObject.HitObject is Hit))
|
||||
return false;
|
||||
|
||||
if (!(judgedObject.NestedHitObjects.SingleOrDefault() is DrawableStrongNestedHit nestedHit))
|
||||
return false;
|
||||
|
||||
return judgedObject.Result.Type == nestedHit.Result.Type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -205,9 +205,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
X = result.IsHit ? judgedObject.Position.X : 0,
|
||||
});
|
||||
|
||||
if (!result.IsHit)
|
||||
break;
|
||||
|
||||
var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
|
||||
|
||||
addExplosion(judgedObject, type);
|
||||
@@ -218,12 +215,16 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
private void addDrumRollHit(DrawableDrumRollTick drawableTick) =>
|
||||
drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick));
|
||||
|
||||
private void addExplosion(DrawableHitObject drawableObject, HitType type)
|
||||
/// <remarks>
|
||||
/// As legacy skins have different explosions for singular and double strong hits,
|
||||
/// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame.
|
||||
/// </remarks>
|
||||
private void addExplosion(DrawableHitObject drawableObject, HitType type) => Schedule(() =>
|
||||
{
|
||||
hitExplosionContainer.Add(new HitExplosion(drawableObject));
|
||||
if (drawableObject.HitObject.Kiai)
|
||||
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
|
||||
}
|
||||
});
|
||||
|
||||
private class ProxyContainer : LifetimeManagementContainer
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -28,17 +28,17 @@ using FileInfo = System.IO.FileInfo;
|
||||
namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
[TestFixture]
|
||||
public class ImportBeatmapTest
|
||||
public class ImportBeatmapTest : ImportTest
|
||||
{
|
||||
[Test]
|
||||
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())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
await LoadOszIntoOsu(loadOsu(host));
|
||||
await LoadOszIntoOsu(LoadOsuIntoHost(host));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -51,11 +51,11 @@ 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())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var imported = await LoadOszIntoOsu(osu);
|
||||
|
||||
@@ -72,11 +72,11 @@ 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())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var imported = await LoadOszIntoOsu(osu);
|
||||
var importedSecondTime = await LoadOszIntoOsu(osu);
|
||||
@@ -98,11 +98,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public async Task TestImportThenImportWithReZip()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
@@ -156,11 +156,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public async Task TestImportThenImportWithChangedFile()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
@@ -207,11 +207,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public async Task TestImportThenImportWithDifferentFilename()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
@@ -259,11 +259,11 @@ 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())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var imported = await LoadOszIntoOsu(osu);
|
||||
|
||||
@@ -301,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())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -314,7 +314,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
Interlocked.Increment(ref loggedExceptionCount);
|
||||
};
|
||||
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
@@ -378,11 +378,11 @@ 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())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var imported = await LoadOszIntoOsu(osu);
|
||||
|
||||
@@ -406,11 +406,11 @@ 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(set.ToString()))
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var imported = await LoadOszIntoOsu(osu);
|
||||
|
||||
@@ -440,11 +440,11 @@ 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())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var metadata = new BeatmapMetadata
|
||||
{
|
||||
@@ -496,15 +496,15 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")]
|
||||
public void TestImportOverIPC()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("host", true))
|
||||
using (HeadlessGameHost client = new CleanRunHeadlessGameHost("client", true))
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-host", true))
|
||||
using (HeadlessGameHost client = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-client", true))
|
||||
{
|
||||
try
|
||||
{
|
||||
Assert.IsTrue(host.IsPrimaryInstance);
|
||||
Assert.IsFalse(client.IsPrimaryInstance);
|
||||
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
@@ -526,11 +526,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public async Task TestImportWhenFileOpen()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
using (File.OpenRead(temp))
|
||||
await osu.Dependencies.Get<BeatmapManager>().Import(temp);
|
||||
@@ -548,11 +548,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public async Task TestImportWithDuplicateHashes()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
@@ -590,11 +590,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public async Task TestImportNestedStructure()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
@@ -635,11 +635,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public async Task TestImportWithIgnoredDirectoryInArchive()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
@@ -689,11 +689,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public async Task TestUpdateBeatmapInfo()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
@@ -719,11 +719,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public async Task TestUpdateBeatmapFile()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
@@ -761,11 +761,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public void TestCreateNewEmptyBeatmap()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER);
|
||||
@@ -788,11 +788,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
[Test]
|
||||
public void TestCreateNewBeatmapWithObject()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER);
|
||||
@@ -863,14 +863,6 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
Assert.AreEqual(expected, osu.Dependencies.Get<FileStore>().QueryFiles(f => f.ReferenceCount == 1).Count());
|
||||
}
|
||||
|
||||
private OsuGameBase loadOsu(GameHost host)
|
||||
{
|
||||
var osu = new OsuGameBase();
|
||||
Task.Run(() => host.Run(osu));
|
||||
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
||||
return osu;
|
||||
}
|
||||
|
||||
private static void ensureLoaded(OsuGameBase osu, int timeout = 60000)
|
||||
{
|
||||
IEnumerable<BeatmapSetInfo> resultSets = null;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -4,18 +4,15 @@
|
||||
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
|
||||
public class ImportCollectionsTest : ImportTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestImportEmptyDatabase()
|
||||
@@ -24,7 +21,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
await osu.CollectionManager.Import(new MemoryStream());
|
||||
|
||||
@@ -44,7 +41,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
|
||||
|
||||
@@ -70,7 +67,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host, true);
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
|
||||
|
||||
@@ -101,7 +98,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += setException;
|
||||
|
||||
var osu = loadOsu(host, true);
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
@@ -135,7 +132,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host, true);
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
|
||||
|
||||
@@ -156,7 +153,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host, true);
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
|
||||
|
||||
@@ -172,50 +169,5 @@ namespace osu.Game.Tests.Collections.IO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
|
||||
private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation
|
||||
{
|
||||
public bool NewCombo { get; } = false;
|
||||
public bool NewCombo { get; set; } = false;
|
||||
public int ComboOffset { get; } = 0;
|
||||
|
||||
public Bindable<int> IndexInCurrentComboBindable { get; } = new Bindable<int>();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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.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
|
||||
{
|
||||
public abstract class ImportTest
|
||||
{
|
||||
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
|
||||
{
|
||||
var osu = new TestOsuGameBase(withBeatmap);
|
||||
Task.Run(() => host.Run(osu));
|
||||
|
||||
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
||||
|
||||
bool ready = false;
|
||||
// wait for two update frames to be executed. this ensures that all components have had a change to run LoadComplete and hopefully avoid
|
||||
// database access (GlobalActionContainer is one to do this).
|
||||
host.UpdateThread.Scheduler.Add(() => host.UpdateThread.Scheduler.Add(() => ready = true));
|
||||
|
||||
waitForOrAssert(() => ready, @"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);
|
||||
}
|
||||
|
||||
public 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.CompilerServices;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -17,18 +16,18 @@ using osu.Game.IO;
|
||||
namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
[TestFixture]
|
||||
public class CustomDataDirectoryTest
|
||||
public class CustomDataDirectoryTest : ImportTest
|
||||
{
|
||||
[Test]
|
||||
public void TestDefaultDirectory()
|
||||
{
|
||||
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestDefaultDirectory)))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
string defaultStorageLocation = getDefaultLocationFor(nameof(TestDefaultDirectory));
|
||||
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
var storage = osu.Dependencies.Get<Storage>();
|
||||
|
||||
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
|
||||
@@ -45,14 +44,14 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (var host = new CustomTestHeadlessGameHost(nameof(TestCustomDirectory)))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
|
||||
storageConfig.Set(StorageConfig.FullPath, customPath);
|
||||
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
// switch to DI'd storage
|
||||
var storage = osu.Dependencies.Get<Storage>();
|
||||
@@ -71,14 +70,14 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (var host = new CustomTestHeadlessGameHost(nameof(TestSubDirectoryLookup)))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
|
||||
storageConfig.Set(StorageConfig.FullPath, customPath);
|
||||
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
// switch to DI'd storage
|
||||
var storage = osu.Dependencies.Get<Storage>();
|
||||
@@ -104,13 +103,13 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigration)))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
string defaultStorageLocation = getDefaultLocationFor(nameof(TestMigration));
|
||||
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
var storage = osu.Dependencies.Get<Storage>();
|
||||
|
||||
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
|
||||
@@ -165,11 +164,11 @@ namespace osu.Game.Tests.NonVisual
|
||||
string customPath = prepareCustomPath();
|
||||
string customPath2 = prepareCustomPath("-2");
|
||||
|
||||
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets)))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
const string database_filename = "client.db";
|
||||
|
||||
@@ -194,11 +193,11 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSameTargetFails)))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath));
|
||||
@@ -215,11 +214,11 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToNestedTargetFails)))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||
|
||||
@@ -244,11 +243,11 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget)))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||
|
||||
@@ -268,25 +267,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
}
|
||||
}
|
||||
|
||||
private OsuGameBase loadOsu(GameHost host)
|
||||
{
|
||||
var osu = new OsuGameBase();
|
||||
Task.Run(() => host.Run(osu));
|
||||
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
||||
|
||||
return osu;
|
||||
}
|
||||
|
||||
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
|
||||
{
|
||||
Task task = Task.Run(() =>
|
||||
{
|
||||
while (!result()) Thread.Sleep(200);
|
||||
});
|
||||
|
||||
Assert.IsTrue(task.Wait(timeout), failureMessage);
|
||||
}
|
||||
|
||||
private static string getDefaultLocationFor(string testTypeName)
|
||||
{
|
||||
string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName);
|
||||
@@ -307,14 +287,14 @@ namespace osu.Game.Tests.NonVisual
|
||||
return path;
|
||||
}
|
||||
|
||||
public class CustomTestHeadlessGameHost : HeadlessGameHost
|
||||
public class CustomTestHeadlessGameHost : CleanRunHeadlessGameHost
|
||||
{
|
||||
public Storage InitialStorage { get; }
|
||||
|
||||
public CustomTestHeadlessGameHost(string name)
|
||||
: base(name)
|
||||
public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"")
|
||||
: base(callingMethodName: callingMethodName)
|
||||
{
|
||||
string defaultStorageLocation = getDefaultLocationFor(name);
|
||||
string defaultStorageLocation = getDefaultLocationFor(callingMethodName);
|
||||
|
||||
InitialStorage = new DesktopStorage(defaultStorageLocation, this);
|
||||
InitialStorage.DeleteDirectory(string.Empty);
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -17,12 +16,11 @@ using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Scores.IO
|
||||
{
|
||||
public class ImportScoreTest
|
||||
public class ImportScoreTest : ImportTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestBasicImport()
|
||||
@@ -31,7 +29,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = await loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
var toImport = new ScoreInfo
|
||||
{
|
||||
@@ -45,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
OnlineScoreID = 12345,
|
||||
};
|
||||
|
||||
var imported = await loadIntoOsu(osu, toImport);
|
||||
var imported = await loadScoreIntoOsu(osu, toImport);
|
||||
|
||||
Assert.AreEqual(toImport.Rank, imported.Rank);
|
||||
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
|
||||
@@ -70,14 +68,14 @@ namespace osu.Game.Tests.Scores.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = await loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
var toImport = new ScoreInfo
|
||||
{
|
||||
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
|
||||
};
|
||||
|
||||
var imported = await loadIntoOsu(osu, toImport);
|
||||
var imported = await loadScoreIntoOsu(osu, toImport);
|
||||
|
||||
Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock));
|
||||
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
|
||||
@@ -96,7 +94,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = await loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
var toImport = new ScoreInfo
|
||||
{
|
||||
@@ -107,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
}
|
||||
};
|
||||
|
||||
var imported = await loadIntoOsu(osu, toImport);
|
||||
var imported = await loadScoreIntoOsu(osu, toImport);
|
||||
|
||||
Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]);
|
||||
Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]);
|
||||
@@ -126,7 +124,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = await loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
var toImport = new ScoreInfo
|
||||
{
|
||||
@@ -138,7 +136,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
}
|
||||
};
|
||||
|
||||
var imported = await loadIntoOsu(osu, toImport);
|
||||
var imported = await loadScoreIntoOsu(osu, toImport);
|
||||
|
||||
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
|
||||
var scoreManager = osu.Dependencies.Get<ScoreManager>();
|
||||
@@ -146,7 +144,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID)));
|
||||
Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true));
|
||||
|
||||
var secondImport = await loadIntoOsu(osu, imported);
|
||||
var secondImport = await loadScoreIntoOsu(osu, imported);
|
||||
Assert.That(secondImport, Is.Null);
|
||||
}
|
||||
finally
|
||||
@@ -163,9 +161,9 @@ namespace osu.Game.Tests.Scores.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = await loadOsu(host);
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
await loadIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
|
||||
await loadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
|
||||
|
||||
var scoreManager = osu.Dependencies.Get<ScoreManager>();
|
||||
|
||||
@@ -179,7 +177,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ScoreInfo> loadIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
|
||||
private async Task<ScoreInfo> loadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
|
||||
{
|
||||
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
@@ -192,33 +190,6 @@ namespace osu.Game.Tests.Scores.IO
|
||||
return scoreManager.GetAllUsableScores().FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task<OsuGameBase> loadOsu(GameHost host)
|
||||
{
|
||||
var osu = new OsuGameBase();
|
||||
|
||||
#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");
|
||||
|
||||
var beatmapFile = TestResources.GetTestBeatmapForImport();
|
||||
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
|
||||
await beatmapManager.Import(beatmapFile);
|
||||
|
||||
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 TestArchiveReader : ArchiveReader
|
||||
{
|
||||
public TestArchiveReader()
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Skinning;
|
||||
using SharpCompress.Archives.Zip;
|
||||
|
||||
namespace osu.Game.Tests.Skins.IO
|
||||
{
|
||||
public class ImportSkinTest : ImportTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestBasicImport()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk"));
|
||||
|
||||
Assert.That(imported.Name, Is.EqualTo("test skin"));
|
||||
Assert.That(imported.Creator, Is.EqualTo("skinner"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestImportTwiceWithSameMetadata()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk"));
|
||||
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk"));
|
||||
|
||||
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
|
||||
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Count, Is.EqualTo(1));
|
||||
|
||||
// the first should be overwritten by the second import.
|
||||
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestImportTwiceWithNoMetadata()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
// if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety.
|
||||
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk"));
|
||||
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk"));
|
||||
|
||||
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
|
||||
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Count, Is.EqualTo(2));
|
||||
|
||||
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
|
||||
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestImportTwiceWithDifferentMetadata()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2", "skinner"), "skin.osk"));
|
||||
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk"));
|
||||
|
||||
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
|
||||
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Count, Is.EqualTo(2));
|
||||
|
||||
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
|
||||
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MemoryStream createOsk(string name, string author)
|
||||
{
|
||||
var zipStream = new MemoryStream();
|
||||
using var zip = ZipArchive.Create();
|
||||
zip.AddEntry("skin.ini", generateSkinIni(name, author));
|
||||
zip.SaveTo(zipStream);
|
||||
return zipStream;
|
||||
}
|
||||
|
||||
private MemoryStream generateSkinIni(string name, string author)
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter(stream);
|
||||
|
||||
writer.WriteLine("[General]");
|
||||
writer.WriteLine($"Name: {name}");
|
||||
writer.WriteLine($"Author: {author}");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine($"# unique {Guid.NewGuid()}");
|
||||
|
||||
writer.Flush();
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private async Task<SkinInfo> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
return await skinManager.Import(archive);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,27 +22,16 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
{
|
||||
public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene
|
||||
{
|
||||
protected override Container<Drawable> Content => content;
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private readonly Container content;
|
||||
private readonly DialogOverlay dialogOverlay;
|
||||
private readonly CollectionManager manager;
|
||||
private DialogOverlay dialogOverlay;
|
||||
private 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)
|
||||
{
|
||||
@@ -50,14 +39,16 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
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;
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
manager = new CollectionManager(LocalStorage),
|
||||
Content,
|
||||
dialogOverlay = new DialogOverlay()
|
||||
});
|
||||
|
||||
Dependencies.Cache(manager);
|
||||
Dependencies.Cache(dialogOverlay);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
|
||||
@@ -1,30 +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();
|
||||
|
||||
protected new TestEditor Editor => (TestEditor)base.Editor;
|
||||
|
||||
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]
|
||||
@@ -32,11 +29,11 @@ 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);
|
||||
}
|
||||
|
||||
@@ -45,11 +42,11 @@ 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);
|
||||
}
|
||||
|
||||
@@ -62,11 +59,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 }));
|
||||
AddAssert("hitobject added", () => addedObject == expectedObject);
|
||||
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
|
||||
|
||||
@@ -84,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", () =>
|
||||
@@ -106,7 +103,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestAddObjectThenSaveHasNoUnsavedChanges()
|
||||
{
|
||||
AddStep("add hitobject", () => editorBeatmap.Add(new HitCircle { StartTime = 1000 }));
|
||||
AddStep("add hitobject", () => EditorBeatmap.Add(new HitCircle { StartTime = 1000 }));
|
||||
|
||||
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
|
||||
AddStep("save changes", () => Editor.Save());
|
||||
@@ -122,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;
|
||||
@@ -149,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", () =>
|
||||
@@ -172,18 +169,5 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private void addUndoSteps() => AddStep("undo", () => Editor.Undo());
|
||||
|
||||
private void addRedoSteps() => AddStep("redo", () => Editor.Redo());
|
||||
|
||||
protected override Editor CreateEditor() => new TestEditor();
|
||||
|
||||
protected class TestEditor : Editor
|
||||
{
|
||||
public new void Undo() => base.Undo();
|
||||
|
||||
public new void Redo() => base.Redo();
|
||||
|
||||
public new void Save() => base.Save();
|
||||
|
||||
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -276,7 +277,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
public override bool CanConvert() => true;
|
||||
|
||||
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
|
||||
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
yield return new TestHitObject
|
||||
{
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Audio;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Audio;
|
||||
@@ -20,6 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached]
|
||||
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||
|
||||
private TestSkinSourceContainer skinSource;
|
||||
private SkinnableSound skinnableSound;
|
||||
|
||||
[SetUp]
|
||||
@@ -29,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
skinSource = new TestSkinSourceContainer
|
||||
{
|
||||
Clock = gameplayClock,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@@ -101,5 +107,55 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("sample not playing", () => !sample.Playing);
|
||||
AddAssert("sample not playing", () => !sample.Playing);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSkinChangeDoesntPlayOnPause()
|
||||
{
|
||||
DrawableSample sample = null;
|
||||
AddStep("start sample", () =>
|
||||
{
|
||||
skinnableSound.Play();
|
||||
sample = skinnableSound.ChildrenOfType<DrawableSample>().Single();
|
||||
});
|
||||
|
||||
AddAssert("sample playing", () => sample.Playing);
|
||||
|
||||
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
|
||||
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
||||
|
||||
AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
|
||||
|
||||
AddAssert("retrieve and ensure current sample is different", () =>
|
||||
{
|
||||
DrawableSample oldSample = sample;
|
||||
sample = skinnableSound.ChildrenOfType<DrawableSample>().Single();
|
||||
return sample != oldSample;
|
||||
});
|
||||
|
||||
AddAssert("new sample stopped", () => !sample.Playing);
|
||||
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
|
||||
|
||||
AddWaitStep("wait a bit", 5);
|
||||
AddAssert("new sample not played", () => !sample.Playing);
|
||||
}
|
||||
|
||||
[Cached(typeof(ISkinSource))]
|
||||
private class TestSkinSourceContainer : Container, ISkinSource
|
||||
{
|
||||
[Resolved]
|
||||
private ISkinSource source { get; set; }
|
||||
|
||||
public event Action SourceChanged;
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source?.GetConfig<TLookup, TValue>(lookup);
|
||||
|
||||
public void TriggerSourceChanged()
|
||||
{
|
||||
SourceChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
OnlineBeatmapID = beatmapId,
|
||||
Path = "normal.osu",
|
||||
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
||||
Length = length,
|
||||
BPM = bpm,
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
protected TestOsuGame Game;
|
||||
|
||||
protected override bool UseFreshStoragePerRun => true;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
{
|
||||
|
||||
@@ -74,6 +74,13 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
private void returnToMenu()
|
||||
{
|
||||
// if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track).
|
||||
AddStep("pause audio", () =>
|
||||
{
|
||||
if (Game.MusicController.IsPlaying)
|
||||
Game.MusicController.TogglePause();
|
||||
});
|
||||
|
||||
AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
|
||||
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,13 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
private void returnToMenu()
|
||||
{
|
||||
// if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track).
|
||||
AddStep("pause audio", () =>
|
||||
{
|
||||
if (Game.MusicController.IsPlaying)
|
||||
Game.MusicController.TogglePause();
|
||||
});
|
||||
|
||||
AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
|
||||
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||
}
|
||||
@@ -133,6 +140,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));
|
||||
@@ -140,13 +153,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;
|
||||
|
||||
@@ -6,11 +6,14 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
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;
|
||||
@@ -143,6 +146,52 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
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() =>
|
||||
AddStep("Press escape", () => pressAndRelease(Key.Escape));
|
||||
|
||||
@@ -168,6 +217,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
private class TestSongSelect : PlaySongSelect
|
||||
{
|
||||
public ModSelectOverlay ModSelectOverlay => ModSelect;
|
||||
|
||||
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
public void TestMultipleLoads()
|
||||
{
|
||||
var comments = exampleComments;
|
||||
int topLevelCommentCount = exampleComments.Comments.Count(comment => comment.IsTopLevel);
|
||||
int topLevelCommentCount = exampleComments.Comments.Count;
|
||||
|
||||
AddStep("hide container", () => commentsContainer.Hide());
|
||||
setUpCommentsResponse(comments);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,7 +839,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
new BeatmapInfo
|
||||
{
|
||||
OnlineBeatmapID = id * 10,
|
||||
Path = "normal.osu",
|
||||
Version = "Normal",
|
||||
StarDifficulty = 2,
|
||||
BaseDifficulty = new BeatmapDifficulty
|
||||
@@ -850,7 +849,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
new BeatmapInfo
|
||||
{
|
||||
OnlineBeatmapID = id * 10 + 1,
|
||||
Path = "hard.osu",
|
||||
Version = "Hard",
|
||||
StarDifficulty = 5,
|
||||
BaseDifficulty = new BeatmapDifficulty
|
||||
@@ -861,7 +859,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
new BeatmapInfo
|
||||
{
|
||||
OnlineBeatmapID = id * 10 + 2,
|
||||
Path = "insane.osu",
|
||||
Version = "Insane",
|
||||
StarDifficulty = 6,
|
||||
BaseDifficulty = new BeatmapDifficulty
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -23,25 +23,15 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public class TestSceneFilterControl : OsuManualInputManagerTestScene
|
||||
{
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private readonly CollectionManager collectionManager;
|
||||
private 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)
|
||||
{
|
||||
@@ -49,13 +39,14 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
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;
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
collectionManager = new CollectionManager(LocalStorage),
|
||||
Content
|
||||
});
|
||||
|
||||
Dependencies.Cache(collectionManager);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
@@ -231,7 +222,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private IEnumerable<Dropdown<CollectionMenuItem>.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems()
|
||||
=> control.ChildrenOfType<CollectionFilterDropdown>().Single().ChildrenOfType<Dropdown<CollectionMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
|
||||
private IEnumerable<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems()
|
||||
=> control.ChildrenOfType<CollectionFilterDropdown>().Single().ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -879,7 +879,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
Ruleset = getRuleset(),
|
||||
OnlineBeatmapID = beatmapId,
|
||||
Path = "normal.osu",
|
||||
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
||||
Length = length,
|
||||
BPM = bpm,
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5),
|
||||
Origin = anchor,
|
||||
Anchor = anchor,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DrawableTeamHeader(colour)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
@@ -26,6 +27,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public IBeatmap Beatmap { get; }
|
||||
|
||||
private CancellationToken cancellationToken;
|
||||
|
||||
protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
@@ -36,14 +39,25 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
public abstract bool CanConvert();
|
||||
|
||||
/// <summary>
|
||||
/// Converts <see cref="Beatmap"/>.
|
||||
/// </summary>
|
||||
/// <returns>The converted Beatmap.</returns>
|
||||
public IBeatmap Convert()
|
||||
public IBeatmap Convert(CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.cancellationToken = cancellationToken;
|
||||
|
||||
// We always operate on a clone of the original beatmap, to not modify it game-wide
|
||||
return ConvertBeatmap(Beatmap.Clone());
|
||||
return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the conversion of a Beatmap using this Beatmap Converter.
|
||||
/// </summary>
|
||||
/// <param name="original">The un-converted Beatmap.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The converted Beatmap.</returns>
|
||||
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
#pragma warning disable 618
|
||||
return ConvertBeatmap(original);
|
||||
#pragma warning restore 618
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,19 +65,20 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
/// <param name="original">The un-converted Beatmap.</param>
|
||||
/// <returns>The converted Beatmap.</returns>
|
||||
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
|
||||
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original)
|
||||
{
|
||||
var beatmap = CreateBeatmap();
|
||||
|
||||
beatmap.BeatmapInfo = original.BeatmapInfo;
|
||||
beatmap.ControlPointInfo = original.ControlPointInfo;
|
||||
beatmap.HitObjects = convertHitObjects(original.HitObjects, original).OrderBy(s => s.StartTime).ToList();
|
||||
beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList();
|
||||
beatmap.Breaks = original.Breaks;
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
private List<T> convertHitObjects(IReadOnlyList<HitObject> hitObjects, IBeatmap beatmap)
|
||||
private List<T> convertHitObjects(IReadOnlyList<HitObject> hitObjects, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<T>(hitObjects.Count);
|
||||
|
||||
@@ -75,7 +90,7 @@ namespace osu.Game.Beatmaps
|
||||
continue;
|
||||
}
|
||||
|
||||
var converted = ConvertHitObject(obj, beatmap);
|
||||
var converted = ConvertHitObject(obj, beatmap, cancellationToken);
|
||||
|
||||
if (ObjectConverted != null)
|
||||
{
|
||||
@@ -104,7 +119,23 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
/// <param name="original">The hit object to convert.</param>
|
||||
/// <param name="beatmap">The un-converted Beatmap.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The converted hit object.</returns>
|
||||
protected abstract IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap);
|
||||
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
#pragma warning disable 618
|
||||
return ConvertHitObject(original, beatmap);
|
||||
#pragma warning restore 618
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the conversion of a hit object.
|
||||
/// This method is generally executed sequentially for all objects in a beatmap.
|
||||
/// </summary>
|
||||
/// <param name="original">The hit object to convert.</param>
|
||||
/// <param name="beatmap">The un-converted Beatmap.</param>
|
||||
/// <returns>The converted hit object.</returns>
|
||||
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
|
||||
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap) => Enumerable.Empty<T>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
@@ -76,7 +77,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public bool CanConvert() => true;
|
||||
|
||||
public IBeatmap Convert()
|
||||
public IBeatmap Convert(CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var obj in Beatmap.HitObjects)
|
||||
ObjectConverted?.Invoke(obj, obj.Yield());
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
@@ -30,6 +31,8 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Converts <see cref="Beatmap"/>.
|
||||
/// </summary>
|
||||
IBeatmap Convert();
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The converted Beatmap.</returns>
|
||||
IBeatmap Convert(CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
|
||||
// Convert
|
||||
IBeatmap converted = converter.Convert();
|
||||
IBeatmap converted = converter.Convert(cancellationSource.Token);
|
||||
|
||||
// Apply conversion mods to the result
|
||||
foreach (var mod in mods.OfType<IApplicableAfterBeatmapConversion>())
|
||||
|
||||
@@ -12,22 +12,26 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// A dropdown to select the <see cref="CollectionMenuItem"/> to filter beatmaps using.
|
||||
/// A dropdown to select the <see cref="CollectionFilterMenuItem"/> to filter beatmaps using.
|
||||
/// </summary>
|
||||
public class CollectionFilterDropdown : OsuDropdown<CollectionMenuItem>
|
||||
public class CollectionFilterDropdown : OsuDropdown<CollectionFilterMenuItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to show the "manage collections..." menu item in the dropdown.
|
||||
/// </summary>
|
||||
protected virtual bool ShowManageCollectionsItem => true;
|
||||
|
||||
private readonly IBindableList<BeatmapCollection> collections = new BindableList<BeatmapCollection>();
|
||||
private readonly IBindableList<BeatmapInfo> beatmaps = new BindableList<BeatmapInfo>();
|
||||
private readonly BindableList<CollectionMenuItem> filters = new BindableList<CollectionMenuItem>();
|
||||
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||
@@ -62,17 +66,19 @@ namespace osu.Game.Screens.Select
|
||||
var selectedItem = SelectedItem?.Value?.Collection;
|
||||
|
||||
filters.Clear();
|
||||
filters.Add(new AllBeatmapsCollectionMenuItem());
|
||||
filters.AddRange(collections.Select(c => new CollectionMenuItem(c)));
|
||||
filters.Add(new ManageCollectionsMenuItem());
|
||||
filters.Add(new AllBeatmapsCollectionFilterMenuItem());
|
||||
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c)));
|
||||
|
||||
if (ShowManageCollectionsItem)
|
||||
filters.Add(new ManageCollectionsFilterMenuItem());
|
||||
|
||||
Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the <see cref="CollectionMenuItem"/> selection has changed.
|
||||
/// Occurs when the <see cref="CollectionFilterMenuItem"/> selection has changed.
|
||||
/// </summary>
|
||||
private void filterChanged(ValueChangedEvent<CollectionMenuItem> filter)
|
||||
private void filterChanged(ValueChangedEvent<CollectionFilterMenuItem> filter)
|
||||
{
|
||||
// Binding the beatmaps will trigger a collection change event, which results in an infinite-loop. This is rebound later, when it's safe to do so.
|
||||
beatmaps.CollectionChanged -= filterBeatmapsChanged;
|
||||
@@ -87,7 +93,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
// Never select the manage collection filter - rollback to the previous filter.
|
||||
// This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value.
|
||||
if (filter.NewValue is ManageCollectionsMenuItem)
|
||||
if (filter.NewValue is ManageCollectionsFilterMenuItem)
|
||||
{
|
||||
Current.Value = filter.OldValue;
|
||||
manageCollectionsDialog?.Show();
|
||||
@@ -104,18 +110,22 @@ namespace osu.Game.Screens.Select
|
||||
Current.TriggerChange();
|
||||
}
|
||||
|
||||
protected override string GenerateItemText(CollectionMenuItem item) => item.CollectionName.Value;
|
||||
protected override string GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value;
|
||||
|
||||
protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader
|
||||
protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d =>
|
||||
{
|
||||
SelectedItem = { BindTarget = Current }
|
||||
};
|
||||
d.SelectedItem.BindTarget = Current;
|
||||
});
|
||||
|
||||
protected override DropdownMenu CreateMenu() => new CollectionDropdownMenu();
|
||||
protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu();
|
||||
|
||||
protected virtual CollectionDropdownHeader CreateCollectionHeader() => new CollectionDropdownHeader();
|
||||
|
||||
protected virtual CollectionDropdownMenu CreateCollectionMenu() => new CollectionDropdownMenu();
|
||||
|
||||
public class CollectionDropdownHeader : OsuDropdownHeader
|
||||
{
|
||||
public readonly Bindable<CollectionMenuItem> SelectedItem = new Bindable<CollectionMenuItem>();
|
||||
public readonly Bindable<CollectionFilterMenuItem> SelectedItem = new Bindable<CollectionFilterMenuItem>();
|
||||
private readonly Bindable<string> collectionName = new Bindable<string>();
|
||||
|
||||
protected override string Label
|
||||
@@ -152,7 +162,7 @@ namespace osu.Game.Screens.Select
|
||||
private void updateText() => base.Label = collectionName.Value;
|
||||
}
|
||||
|
||||
private class CollectionDropdownMenu : OsuDropdownMenu
|
||||
protected class CollectionDropdownMenu : OsuDropdownMenu
|
||||
{
|
||||
public CollectionDropdownMenu()
|
||||
{
|
||||
@@ -162,10 +172,10 @@ namespace osu.Game.Screens.Select
|
||||
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item);
|
||||
}
|
||||
|
||||
private class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
|
||||
protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
|
||||
{
|
||||
[NotNull]
|
||||
protected new CollectionMenuItem Item => ((DropdownMenuItem<CollectionMenuItem>)base.Item).Value;
|
||||
protected new CollectionFilterMenuItem Item => ((DropdownMenuItem<CollectionFilterMenuItem>)base.Item).Value;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
@@ -3,14 +3,13 @@
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Collections;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="BeatmapCollection"/> filter.
|
||||
/// </summary>
|
||||
public class CollectionMenuItem
|
||||
public class CollectionFilterMenuItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The collection to filter beatmaps from.
|
||||
@@ -26,27 +25,27 @@ namespace osu.Game.Screens.Select
|
||||
public readonly Bindable<string> CollectionName;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="CollectionMenuItem"/>.
|
||||
/// Creates a new <see cref="CollectionFilterMenuItem"/>.
|
||||
/// </summary>
|
||||
/// <param name="collection">The collection to filter beatmaps from.</param>
|
||||
public CollectionMenuItem([CanBeNull] BeatmapCollection collection)
|
||||
public CollectionFilterMenuItem([CanBeNull] BeatmapCollection collection)
|
||||
{
|
||||
Collection = collection;
|
||||
CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable<string>("All beatmaps");
|
||||
}
|
||||
}
|
||||
|
||||
public class AllBeatmapsCollectionMenuItem : CollectionMenuItem
|
||||
public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem
|
||||
{
|
||||
public AllBeatmapsCollectionMenuItem()
|
||||
public AllBeatmapsCollectionFilterMenuItem()
|
||||
: base(null)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class ManageCollectionsMenuItem : CollectionMenuItem
|
||||
public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem
|
||||
{
|
||||
public ManageCollectionsMenuItem()
|
||||
public ManageCollectionsFilterMenuItem()
|
||||
: base(null)
|
||||
{
|
||||
CollectionName.Value = "Manage collections...";
|
||||
@@ -230,7 +230,7 @@ namespace osu.Game.Collections
|
||||
public void DeleteAll()
|
||||
{
|
||||
Collections.Clear();
|
||||
PostNotification?.Invoke(new SimpleNotification { Text = "Deleted all collections!" });
|
||||
PostNotification?.Invoke(new ProgressCompletionNotification { Text = "Deleted all collections!" });
|
||||
}
|
||||
|
||||
private readonly object saveLock = new object();
|
||||
|
||||