Merge branch 'master' of https://github.com/ppy/osu into scroll-to-center
@ -51,7 +51,7 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.403.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.411.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.412.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.417.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||
<PackageReference Include="nunit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
</ItemGroup>
|
||||
|
132
osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
Normal file
@ -0,0 +1,132 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public class TestSceneHyperDashColouring : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestDefaultFruitColour()
|
||||
{
|
||||
var skin = new TestSkin();
|
||||
|
||||
checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomFruitColour()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
HyperDashFruitColour = Color4.Cyan
|
||||
};
|
||||
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomFruitColourPriority()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
HyperDashColour = Color4.Goldenrod,
|
||||
HyperDashFruitColour = Color4.Cyan
|
||||
};
|
||||
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFruitColourFallback()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
HyperDashColour = Color4.Goldenrod
|
||||
};
|
||||
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashColour);
|
||||
}
|
||||
|
||||
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
|
||||
{
|
||||
DrawableFruit drawableFruit = null;
|
||||
|
||||
AddStep("create hyper-dash fruit", () =>
|
||||
{
|
||||
var fruit = new Fruit { HyperDashTarget = new Banana() };
|
||||
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(4f),
|
||||
}, skin);
|
||||
});
|
||||
|
||||
AddAssert("hyper-dash colour is correct", () => checkLegacyFruitHyperDashColour(drawableFruit, expectedColour));
|
||||
}
|
||||
|
||||
private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
|
||||
{
|
||||
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
|
||||
var testSkinProvider = new SkinProvidingContainer(skin);
|
||||
var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
|
||||
|
||||
return legacySkinProvider
|
||||
.WithChild(testSkinProvider
|
||||
.WithChild(legacySkinTransformer
|
||||
.WithChild(child)));
|
||||
}
|
||||
|
||||
private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) =>
|
||||
fruit.ChildrenOfType<SkinnableDrawable>().First().Drawable.ChildrenOfType<Sprite>().Any(c => c.Colour == expectedColour);
|
||||
|
||||
private class TestSkin : LegacySkin
|
||||
{
|
||||
public Color4 HyperDashColour
|
||||
{
|
||||
get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
|
||||
set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
|
||||
}
|
||||
|
||||
public Color4 HyperDashAfterImageColour
|
||||
{
|
||||
get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
|
||||
set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
|
||||
}
|
||||
|
||||
public Color4 HyperDashFruitColour
|
||||
{
|
||||
get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
|
||||
set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
|
||||
}
|
||||
|
||||
public TestSkin()
|
||||
: base(new SkinInfo(), null, null, string.Empty)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -70,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
|
||||
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
|
||||
|
||||
protected override float SamplePlaybackPosition => HitObject.X;
|
||||
|
||||
protected DrawableCatchHitObject(CatchHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
BorderColour = Color4.Red,
|
||||
BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||
BorderThickness = 12f * RADIUS_ADJUST,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
Alpha = 0.3f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Red,
|
||||
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -65,6 +65,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
|
||||
public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source.GetConfig<TLookup, TValue>(lookup);
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case CatchSkinColour colour:
|
||||
return source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
}
|
||||
|
||||
return source.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
public enum CatchSkinColour
|
||||
{
|
||||
/// <summary>
|
||||
/// The colour to be used for the catcher while in hyper-dashing state.
|
||||
/// </summary>
|
||||
HyperDash,
|
||||
|
||||
/// <summary>
|
||||
/// The colour to be used for fruits that grant the catcher the ability to hyper-dash.
|
||||
/// </summary>
|
||||
HyperDashFruit,
|
||||
|
||||
/// <summary>
|
||||
/// The colour to be used for the "exploding" catcher sprite on beginning of hyper-dashing.
|
||||
/// </summary>
|
||||
HyperDashAfterImage,
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -55,14 +56,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
var hyperDash = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture(lookupName),
|
||||
Colour = Color4.Red,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Depth = 1,
|
||||
Alpha = 0.7f,
|
||||
Scale = new Vector2(1.2f)
|
||||
Scale = new Vector2(1.2f),
|
||||
Texture = skin.GetTexture(lookupName),
|
||||
Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ??
|
||||
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
|
||||
Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||
};
|
||||
|
||||
AddInternal(hyperDash);
|
||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class Catcher : Container, IKeyBindingHandler<CatchAction>
|
||||
{
|
||||
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
|
||||
|
||||
/// <summary>
|
||||
/// Whether we are hyper-dashing or not.
|
||||
/// </summary>
|
||||
|
51
osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// 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.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManiaLegacyReplayTest
|
||||
{
|
||||
[TestCase(ManiaAction.Key1)]
|
||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||
[TestCase(ManiaAction.Special1)]
|
||||
[TestCase(ManiaAction.Key8)]
|
||||
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
|
||||
{
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 });
|
||||
|
||||
var frame = new ManiaReplayFrame(0, actions);
|
||||
var legacyFrame = frame.ToLegacy(beatmap);
|
||||
|
||||
var decodedFrame = new ManiaReplayFrame();
|
||||
decodedFrame.FromLegacy(legacyFrame, beatmap);
|
||||
|
||||
Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
|
||||
}
|
||||
|
||||
[TestCase(ManiaAction.Key1)]
|
||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||
[TestCase(ManiaAction.Special1)]
|
||||
[TestCase(ManiaAction.Special2)]
|
||||
[TestCase(ManiaAction.Special1, ManiaAction.Special2)]
|
||||
[TestCase(ManiaAction.Special1, ManiaAction.Key5)]
|
||||
[TestCase(ManiaAction.Key8)]
|
||||
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
|
||||
{
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 });
|
||||
beatmap.Stages.Add(new StageDefinition { Columns = 5 });
|
||||
|
||||
var frame = new ManiaReplayFrame(0, actions);
|
||||
var legacyFrame = frame.ToLegacy(beatmap);
|
||||
|
||||
var decodedFrame = new ManiaReplayFrame();
|
||||
decodedFrame.FromLegacy(legacyFrame, beatmap);
|
||||
|
||||
Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
@ -24,6 +25,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private ManiaPlayfield playfield { get; set; }
|
||||
|
||||
protected override float SamplePlaybackPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
if (playfield == null)
|
||||
return base.SamplePlaybackPosition;
|
||||
|
||||
return (float)HitObject.Column / playfield.TotalColumns;
|
||||
}
|
||||
}
|
||||
|
||||
protected DrawableManiaHitObject(ManiaHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
|
||||
while (activeColumns > 0)
|
||||
{
|
||||
var isSpecial = maniaBeatmap.Stages.First().IsSpecialColumn(counter);
|
||||
bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
|
||||
|
||||
if ((activeColumns & 1) > 0)
|
||||
Actions.Add(isSpecial ? specialAction : normalAction);
|
||||
@ -58,33 +58,87 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
|
||||
int keys = 0;
|
||||
|
||||
var specialColumns = new List<int>();
|
||||
|
||||
for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
|
||||
{
|
||||
if (maniaBeatmap.Stages.First().IsSpecialColumn(i))
|
||||
specialColumns.Add(i);
|
||||
}
|
||||
|
||||
foreach (var action in Actions)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case ManiaAction.Special1:
|
||||
keys |= 1 << specialColumns[0];
|
||||
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0);
|
||||
break;
|
||||
|
||||
case ManiaAction.Special2:
|
||||
keys |= 1 << specialColumns[1];
|
||||
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1);
|
||||
break;
|
||||
|
||||
default:
|
||||
keys |= 1 << (action - ManiaAction.Key1);
|
||||
// the index in lazer, which doesn't include special keys.
|
||||
int nonSpecialKeyIndex = action - ManiaAction.Key1;
|
||||
|
||||
// the index inclusive of special keys.
|
||||
int overallIndex = 0;
|
||||
|
||||
// iterate to find the index including special keys.
|
||||
for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++)
|
||||
{
|
||||
// skip over special columns.
|
||||
if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex))
|
||||
continue;
|
||||
// found a non-special column to use.
|
||||
if (nonSpecialKeyIndex == 0)
|
||||
break;
|
||||
// found a non-special column but not ours.
|
||||
nonSpecialKeyIndex--;
|
||||
}
|
||||
|
||||
keys |= 1 << overallIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the overall index (across all stages) for a specified special key.
|
||||
/// </summary>
|
||||
/// <param name="maniaBeatmap">The beatmap.</param>
|
||||
/// <param name="specialOffset">The special key offset (0 is S1).</param>
|
||||
/// <returns>The overall index for the special column.</returns>
|
||||
private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset)
|
||||
{
|
||||
for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
|
||||
{
|
||||
if (isColumnAtIndexSpecial(maniaBeatmap, i))
|
||||
{
|
||||
if (specialOffset == 0)
|
||||
return i;
|
||||
|
||||
specialOffset--;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException("Special key index is too high.", nameof(specialOffset));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the column at an overall index (across all stages) is a special column.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap.</param>
|
||||
/// <param name="index">The overall index to check.</param>
|
||||
private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index)
|
||||
{
|
||||
foreach (var stage in beatmap.Stages)
|
||||
{
|
||||
if (index >= stage.Columns)
|
||||
{
|
||||
index -= stage.Columns;
|
||||
continue;
|
||||
}
|
||||
|
||||
return stage.IsSpecialColumn(index);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Column index is too high.", nameof(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -14,6 +15,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
[Cached]
|
||||
public class ManiaPlayfield : ScrollingPlayfield
|
||||
{
|
||||
private readonly List<Stage> stages = new List<Stage>();
|
||||
|
@ -0,0 +1,64 @@
|
||||
// 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 Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestScenePathControlPointVisualiser : OsuTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(StringHumanizeExtensions),
|
||||
typeof(PathControlPointPiece),
|
||||
typeof(PathControlPointConnectionPiece)
|
||||
};
|
||||
|
||||
private Slider slider;
|
||||
private PathControlPointVisualiser visualiser;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
slider = new Slider();
|
||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestAddOverlappingControlPoints()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
addControlPointStep(new Vector2(200));
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(500, 300));
|
||||
|
||||
AddAssert("last connection displayed", () =>
|
||||
{
|
||||
var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position.Value == new Vector2(300));
|
||||
return lastConnection.DrawWidth > 50;
|
||||
});
|
||||
}
|
||||
|
||||
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
|
||||
private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position)));
|
||||
}
|
||||
}
|
@ -1,18 +1,285 @@
|
||||
// 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.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
HitObjectContainer.Clear();
|
||||
ResetPlacement();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBeginPlacementWithoutFinishing()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
assertPlaced(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceWithoutMovingMouse()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertLength(0);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceWithMouseMovement()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 200));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertLength(200);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceNormalControlPoint()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceTwoNormalControlPoints()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100, 100));
|
||||
assertControlPointType(0, PathType.Bezier);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceSegmentControlPoint()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
assertControlPointType(1, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMoveToPerfectCurveThenPlaceLinear()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
assertLength(100);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMoveToBezierThenPlacePerfectCurve()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMoveToFourthOrderBezierThenPlaceThirdOrderBezier()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400));
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointType(0, PathType.Bezier);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceLinearSegmentThenPlaceLinearSegment()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
assertControlPointType(1, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceLinearSegmentThenPlacePerfectCurveSegment()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
assertControlPointType(1, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacePerfectCurveSegmentThenPlacePerfectCurveSegment()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(5);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointPosition(3, new Vector2(200, 100));
|
||||
assertControlPointPosition(4, new Vector2(200));
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
assertControlPointType(2, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
||||
|
||||
private void addClickStep(MouseButton button)
|
||||
{
|
||||
AddStep($"press {button}", () => InputManager.PressButton(button));
|
||||
AddStep($"release {button}", () => InputManager.ReleaseButton(button));
|
||||
}
|
||||
|
||||
private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected);
|
||||
|
||||
private void assertLength(double expected) => AddAssert($"slider length is {expected}", () => Precision.AlmostEquals(expected, getSlider().Distance, 1));
|
||||
|
||||
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected);
|
||||
|
||||
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type.Value == type);
|
||||
|
||||
private void assertControlPointPosition(int index, Vector2 position) =>
|
||||
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position.Value, 1));
|
||||
|
||||
private Slider getSlider() => HitObjectContainer.Count > 0 ? (Slider)((DrawableSlider)HitObjectContainer[0]).HitObject : null;
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
|
||||
}
|
||||
|
@ -16,22 +16,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// </summary>
|
||||
public class PathControlPointConnectionPiece : CompositeDrawable
|
||||
{
|
||||
public PathControlPoint ControlPoint;
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
private readonly Path path;
|
||||
private readonly Slider slider;
|
||||
private readonly int controlPointIndex;
|
||||
|
||||
private IBindable<Vector2> sliderPosition;
|
||||
private IBindable<int> pathVersion;
|
||||
|
||||
public PathControlPointConnectionPiece(Slider slider, PathControlPoint controlPoint)
|
||||
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
|
||||
{
|
||||
this.slider = slider;
|
||||
ControlPoint = controlPoint;
|
||||
this.controlPointIndex = controlPointIndex;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
ControlPoint = slider.Path.ControlPoints[controlPointIndex];
|
||||
|
||||
InternalChild = path = new SmoothPath
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@ -61,13 +64,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
path.ClearVertices();
|
||||
|
||||
int index = slider.Path.ControlPoints.IndexOf(ControlPoint) + 1;
|
||||
|
||||
if (index == 0 || index == slider.Path.ControlPoints.Count)
|
||||
int nextIndex = controlPointIndex + 1;
|
||||
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
|
||||
return;
|
||||
|
||||
path.AddVertex(Vector2.Zero);
|
||||
path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value);
|
||||
path.AddVertex(slider.Path.ControlPoints[nextIndex].Position.Value - ControlPoint.Position.Value);
|
||||
|
||||
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -12,6 +13,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
@ -33,6 +35,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private readonly Container marker;
|
||||
private readonly Drawable markerRing;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider snapProvider { get; set; }
|
||||
|
||||
@ -47,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
this.slider = slider;
|
||||
ControlPoint = controlPoint;
|
||||
|
||||
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
@ -137,7 +144,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e) => e.Button == MouseButton.Left;
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
@ -158,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
ControlPoint.Position.Value += e.Delta;
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the state of the circular control point marker.
|
||||
/// </summary>
|
||||
@ -168,8 +186,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
markerRing.Alpha = IsSelected.Value ? 1 : 0;
|
||||
|
||||
Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
|
||||
|
||||
if (IsHovered || IsSelected.Value)
|
||||
colour = Color4.White;
|
||||
colour = colour.Lighten(1);
|
||||
|
||||
marker.Colour = colour;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Bindables;
|
||||
@ -24,17 +25,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
{
|
||||
internal readonly Container<PathControlPointPiece> Pieces;
|
||||
internal readonly Container<PathControlPointConnectionPiece> Connections;
|
||||
|
||||
private readonly Container<PathControlPointConnectionPiece> connections;
|
||||
|
||||
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
||||
private readonly Slider slider;
|
||||
|
||||
private readonly bool allowSelection;
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
private IBindableList<PathControlPoint> controlPoints;
|
||||
|
||||
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
|
||||
|
||||
public PathControlPointVisualiser(Slider slider, bool allowSelection)
|
||||
@ -46,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both },
|
||||
Connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both },
|
||||
Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both }
|
||||
};
|
||||
}
|
||||
@ -57,33 +55,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
|
||||
controlPoints = slider.Path.ControlPoints.GetBoundCopy();
|
||||
controlPoints.ItemsAdded += addControlPoints;
|
||||
controlPoints.ItemsRemoved += removeControlPoints;
|
||||
|
||||
addControlPoints(controlPoints);
|
||||
controlPoints.CollectionChanged += onControlPointsChanged;
|
||||
controlPoints.BindTo(slider.Path.ControlPoints);
|
||||
}
|
||||
|
||||
private void addControlPoints(IEnumerable<PathControlPoint> controlPoints)
|
||||
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
foreach (var point in controlPoints)
|
||||
switch (e.Action)
|
||||
{
|
||||
Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
|
||||
{
|
||||
if (allowSelection)
|
||||
d.RequestSelection = selectPiece;
|
||||
}));
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
{
|
||||
var point = (PathControlPoint)e.NewItems[i];
|
||||
|
||||
connections.Add(new PathControlPointConnectionPiece(slider, point));
|
||||
}
|
||||
}
|
||||
Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
|
||||
{
|
||||
if (allowSelection)
|
||||
d.RequestSelection = selectPiece;
|
||||
}));
|
||||
|
||||
private void removeControlPoints(IEnumerable<PathControlPoint> controlPoints)
|
||||
{
|
||||
foreach (var point in controlPoints)
|
||||
{
|
||||
Pieces.RemoveAll(p => p.ControlPoint == point);
|
||||
connections.RemoveAll(c => c.ControlPoint == point);
|
||||
Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (var point in e.OldItems.Cast<PathControlPoint>())
|
||||
{
|
||||
Pieces.RemoveAll(p => p.ControlPoint == point);
|
||||
Connections.RemoveAll(c => c.ControlPoint == point);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
@ -23,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private SliderBodyPiece bodyPiece;
|
||||
private HitCirclePiece headCirclePiece;
|
||||
private HitCirclePiece tailCirclePiece;
|
||||
private PathControlPointVisualiser controlPointVisualiser;
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
@ -51,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
bodyPiece = new SliderBodyPiece(),
|
||||
headCirclePiece = new HitCirclePiece(),
|
||||
tailCirclePiece = new HitCirclePiece(),
|
||||
new PathControlPointVisualiser(HitObject, false)
|
||||
controlPointVisualiser = new PathControlPointVisualiser(HitObject, false)
|
||||
};
|
||||
|
||||
setState(PlacementState.Initial);
|
||||
@ -73,11 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
break;
|
||||
|
||||
case PlacementState.Body:
|
||||
ensureCursor();
|
||||
|
||||
// The given screen-space position may have been externally snapped, but the unsnapped position from the input manager
|
||||
// is used instead since snapping control points doesn't make much sense
|
||||
cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||
updateCursor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -91,17 +91,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
break;
|
||||
|
||||
case PlacementState.Body:
|
||||
switch (e.Button)
|
||||
{
|
||||
case MouseButton.Left:
|
||||
ensureCursor();
|
||||
if (e.Button != MouseButton.Left)
|
||||
break;
|
||||
|
||||
// Detatch the cursor
|
||||
cursor = null;
|
||||
break;
|
||||
if (canPlaceNewControlPoint(out var lastPoint))
|
||||
{
|
||||
// Place a new point by detatching the current cursor.
|
||||
updateCursor();
|
||||
cursor = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Transform the last point into a new segment.
|
||||
Debug.Assert(lastPoint != null);
|
||||
|
||||
segmentStart = lastPoint;
|
||||
segmentStart.Type.Value = PathType.Linear;
|
||||
|
||||
currentSegmentLength = 1;
|
||||
}
|
||||
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -114,16 +124,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override bool OnDoubleClick(DoubleClickEvent e)
|
||||
{
|
||||
// Todo: This should all not occur on double click, but rather if the previous control point is hovered.
|
||||
segmentStart = HitObject.Path.ControlPoints[^1];
|
||||
segmentStart.Type.Value = PathType.Linear;
|
||||
|
||||
currentSegmentLength = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void beginCurve()
|
||||
{
|
||||
BeginPlacement(commitStart: true);
|
||||
@ -161,17 +161,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureCursor()
|
||||
private void updateCursor()
|
||||
{
|
||||
if (cursor == null)
|
||||
if (canPlaceNewControlPoint(out _))
|
||||
{
|
||||
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
|
||||
currentSegmentLength++;
|
||||
// The cursor does not overlap a previous control point, so it can be added if not already existing.
|
||||
if (cursor == null)
|
||||
{
|
||||
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
|
||||
|
||||
// The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier).
|
||||
currentSegmentLength++;
|
||||
updatePathType();
|
||||
}
|
||||
|
||||
// Update the cursor position.
|
||||
cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||
}
|
||||
else if (cursor != null)
|
||||
{
|
||||
// The cursor overlaps a previous control point, so it's removed.
|
||||
HitObject.Path.ControlPoints.Remove(cursor);
|
||||
cursor = null;
|
||||
|
||||
// The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear).
|
||||
currentSegmentLength--;
|
||||
updatePathType();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a new control point can be placed at the current mouse position.
|
||||
/// </summary>
|
||||
/// <param name="lastPoint">The last-placed control point. May be null, but is not null if <c>false</c> is returned.</param>
|
||||
/// <returns>Whether a new control point can be placed at the current position.</returns>
|
||||
private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint)
|
||||
{
|
||||
// We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point.
|
||||
var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor);
|
||||
var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
|
||||
|
||||
lastPoint = last;
|
||||
return lastPiece?.IsHovered != true;
|
||||
}
|
||||
|
||||
private void updateSlider()
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
|
@ -38,6 +38,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[Resolved(CanBeNull = true)]
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
public SliderSelectionBlueprint(DrawableSlider slider)
|
||||
: base(slider)
|
||||
{
|
||||
@ -92,7 +95,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private int? placementControlPointIndex;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e) => placementControlPointIndex != null;
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (placementControlPointIndex != null)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
@ -103,7 +115,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
placementControlPointIndex = null;
|
||||
if (placementControlPointIndex != null)
|
||||
{
|
||||
placementControlPointIndex = null;
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
}
|
||||
|
||||
private BindableList<PathControlPoint> controlPoints => HitObject.Path.ControlPoints;
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects.
|
||||
public override bool HandlePositionalInput => true;
|
||||
|
||||
protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit.
|
||||
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
|
||||
|
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
70
osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs
Normal file
@ -0,0 +1,70 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneDrawableHit : TaikoSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
|
||||
{
|
||||
typeof(DrawableHit),
|
||||
typeof(DrawableCentreHit),
|
||||
typeof(DrawableRimHit),
|
||||
typeof(LegacyHit),
|
||||
}).ToList();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddStep("Centre hit", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}));
|
||||
|
||||
AddStep("Centre hit (strong)", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime(true))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}));
|
||||
|
||||
AddStep("Rim hit", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}));
|
||||
|
||||
AddStep("Rim hit (strong)", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime(true))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}));
|
||||
}
|
||||
|
||||
private Hit createHitAtCurrentTime(bool strong = false)
|
||||
{
|
||||
var hit = new Hit
|
||||
{
|
||||
IsStrong = strong,
|
||||
StartTime = Time.Current + 3000,
|
||||
};
|
||||
|
||||
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -14,13 +14,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
public DrawableCentreHit(Hit hit)
|
||||
: base(hit)
|
||||
{
|
||||
MainPiece.Add(new CentreHitSymbolPiece());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
MainPiece.AccentColour = colours.PinkDarker;
|
||||
}
|
||||
protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit),
|
||||
_ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
|
||||
}
|
||||
}
|
||||
|
@ -34,17 +34,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private Color4 colourIdle;
|
||||
private Color4 colourEngaged;
|
||||
|
||||
private ElongatedCirclePiece elongatedPiece;
|
||||
|
||||
public DrawableDrumRoll(DrumRoll drumRoll)
|
||||
: base(drumRoll)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
MainPiece.Add(tickContainer = new Container<DrawableDrumRollTick> { RelativeSizeAxes = Axes.Both });
|
||||
elongatedPiece.Add(tickContainer = new Container<DrawableDrumRollTick> { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
MainPiece.AccentColour = colourIdle = colours.YellowDark;
|
||||
elongatedPiece.AccentColour = colourIdle = colours.YellowDark;
|
||||
colourEngaged = colours.YellowDarker;
|
||||
}
|
||||
|
||||
@ -84,7 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
return base.CreateNestedHitObject(hitObject);
|
||||
}
|
||||
|
||||
protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece();
|
||||
protected override CompositeDrawable CreateMainPiece() => elongatedPiece = new ElongatedCirclePiece();
|
||||
|
||||
public override bool OnPressed(TaikoAction action) => false;
|
||||
|
||||
@ -101,7 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
|
||||
|
||||
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
|
||||
MainPiece.FadeAccent(newColour, 100);
|
||||
(MainPiece as IHasAccentColour)?.FadeAccent(newColour, 100);
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
protected override TaikoPiece CreateMainPiece() => new TickPiece
|
||||
protected override CompositeDrawable CreateMainPiece() => new TickPiece
|
||||
{
|
||||
Filled = HitObject.FirstTick
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -14,13 +14,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
public DrawableRimHit(Hit hit)
|
||||
: base(hit)
|
||||
{
|
||||
MainPiece.Add(new RimHitSymbolPiece());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
MainPiece.AccentColour = colours.BlueDarker;
|
||||
}
|
||||
protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit),
|
||||
_ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,11 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private readonly CircularContainer targetRing;
|
||||
private readonly CircularContainer expandingRing;
|
||||
|
||||
private readonly SwellSymbolPiece symbol;
|
||||
|
||||
public DrawableSwell(Swell swell)
|
||||
: base(swell)
|
||||
{
|
||||
@ -107,18 +105,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
});
|
||||
|
||||
AddInternal(ticks = new Container<DrawableSwellTick> { RelativeSizeAxes = Axes.Both });
|
||||
|
||||
MainPiece.Add(symbol = new SwellSymbolPiece());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
MainPiece.AccentColour = colours.YellowDark;
|
||||
expandingRing.Colour = colours.YellowLight;
|
||||
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
|
||||
}
|
||||
|
||||
protected override CompositeDrawable CreateMainPiece() => new SwellCirclePiece
|
||||
{
|
||||
// to allow for rotation transform
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -182,7 +184,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
.Then()
|
||||
.FadeTo(completion / 8, 2000, Easing.OutQuint);
|
||||
|
||||
symbol.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint);
|
||||
MainPiece.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint);
|
||||
|
||||
expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint);
|
||||
|
||||
|
@ -2,7 +2,9 @@
|
||||
// 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.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -28,5 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
}
|
||||
|
||||
public override bool OnPressed(TaikoAction action) => false;
|
||||
|
||||
protected override CompositeDrawable CreateMainPiece() => new TickPiece();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
using osuTK;
|
||||
using System.Linq;
|
||||
using osu.Game.Audio;
|
||||
@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
/// <summary>
|
||||
/// Moves <see cref="Content"/> to a layer proxied above the playfield.
|
||||
/// Does nothing is content is already proxied.
|
||||
/// Does nothing if content is already proxied.
|
||||
/// </summary>
|
||||
protected void ProxyContent()
|
||||
{
|
||||
@ -108,19 +107,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class DrawableTaikoHitObject<TTaikoHit> : DrawableTaikoHitObject
|
||||
where TTaikoHit : TaikoHitObject
|
||||
public abstract class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject
|
||||
where TObject : TaikoHitObject
|
||||
{
|
||||
public override Vector2 OriginPosition => new Vector2(DrawHeight / 2);
|
||||
|
||||
public new TTaikoHit HitObject;
|
||||
public new TObject HitObject;
|
||||
|
||||
protected readonly Vector2 BaseSize;
|
||||
protected readonly TaikoPiece MainPiece;
|
||||
protected readonly CompositeDrawable MainPiece;
|
||||
|
||||
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
|
||||
|
||||
protected DrawableTaikoHitObject(TTaikoHit hitObject)
|
||||
protected DrawableTaikoHitObject(TObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
HitObject = hitObject;
|
||||
@ -132,7 +131,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
|
||||
|
||||
Content.Add(MainPiece = CreateMainPiece());
|
||||
MainPiece.KiaiMode = HitObject.Kiai;
|
||||
|
||||
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
|
||||
}
|
||||
@ -169,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
// Normal and clap samples are handled by the drum
|
||||
protected override IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
|
||||
|
||||
protected virtual TaikoPiece CreateMainPiece() => new CirclePiece();
|
||||
protected abstract CompositeDrawable CreateMainPiece();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongHitObject"/>.
|
||||
|
@ -0,0 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
{
|
||||
public class CentreHitCirclePiece : CirclePiece
|
||||
{
|
||||
public CentreHitCirclePiece()
|
||||
{
|
||||
Add(new CentreHitSymbolPiece());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
AccentColour = colours.PinkDarker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The symbol used for centre hit pieces.
|
||||
/// </summary>
|
||||
public class CentreHitSymbolPiece : Container
|
||||
{
|
||||
public CentreHitSymbolPiece()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = new Vector2(SYMBOL_SIZE);
|
||||
Padding = new MarginPadding(SYMBOL_BORDER);
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new[] { new Box { RelativeSizeAxes = Axes.Both } }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
{
|
||||
/// <summary>
|
||||
/// The symbol used for centre hit pieces.
|
||||
/// </summary>
|
||||
public class CentreHitSymbolPiece : Container
|
||||
{
|
||||
public CentreHitSymbolPiece()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = new Vector2(CirclePiece.SYMBOL_SIZE);
|
||||
Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER);
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new[] { new Box { RelativeSizeAxes = Axes.Both } }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ using osuTK.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
{
|
||||
@ -20,21 +21,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
/// for a usage example.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class CirclePiece : TaikoPiece
|
||||
public abstract class CirclePiece : BeatSyncedContainer
|
||||
{
|
||||
public const float SYMBOL_SIZE = 0.45f;
|
||||
public const float SYMBOL_BORDER = 8;
|
||||
private const double pre_beat_transition_time = 80;
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
/// <summary>
|
||||
/// The colour of the inner circle and outer glows.
|
||||
/// </summary>
|
||||
public override Color4 AccentColour
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => base.AccentColour;
|
||||
get => accentColour;
|
||||
set
|
||||
{
|
||||
base.AccentColour = value;
|
||||
accentColour = value;
|
||||
|
||||
background.Colour = AccentColour;
|
||||
|
||||
@ -42,15 +45,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
}
|
||||
}
|
||||
|
||||
private bool kiaiMode;
|
||||
|
||||
/// <summary>
|
||||
/// Whether Kiai mode effects are enabled for this circle piece.
|
||||
/// </summary>
|
||||
public override bool KiaiMode
|
||||
public bool KiaiMode
|
||||
{
|
||||
get => base.KiaiMode;
|
||||
get => kiaiMode;
|
||||
set
|
||||
{
|
||||
base.KiaiMode = value;
|
||||
kiaiMode = value;
|
||||
|
||||
resetEdgeEffects();
|
||||
}
|
||||
@ -64,8 +69,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
|
||||
public Box FlashBox;
|
||||
|
||||
public CirclePiece()
|
||||
protected CirclePiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
EarlyActivationMilliseconds = pre_beat_transition_time;
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
|
@ -0,0 +1,55 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
{
|
||||
public class RimHitCirclePiece : CirclePiece
|
||||
{
|
||||
public RimHitCirclePiece()
|
||||
{
|
||||
Add(new RimHitSymbolPiece());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
AccentColour = colours.BlueDarker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The symbol used for rim hit pieces.
|
||||
/// </summary>
|
||||
public class RimHitSymbolPiece : CircularContainer
|
||||
{
|
||||
public RimHitSymbolPiece()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = new Vector2(SYMBOL_SIZE);
|
||||
|
||||
BorderThickness = SYMBOL_BORDER;
|
||||
BorderColour = Color4.White;
|
||||
Masking = true;
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
{
|
||||
/// <summary>
|
||||
/// The symbol used for rim hit pieces.
|
||||
/// </summary>
|
||||
public class RimHitSymbolPiece : CircularContainer
|
||||
{
|
||||
public RimHitSymbolPiece()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = new Vector2(CirclePiece.SYMBOL_SIZE);
|
||||
|
||||
BorderThickness = CirclePiece.SYMBOL_BORDER;
|
||||
BorderColour = Color4.White;
|
||||
Masking = true;
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osuTK;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
{
|
||||
/// <summary>
|
||||
/// The symbol used for swell pieces.
|
||||
/// </summary>
|
||||
public class SwellSymbolPiece : Container
|
||||
public class SwellCirclePiece : CirclePiece
|
||||
{
|
||||
public SwellSymbolPiece()
|
||||
public SwellCirclePiece()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
Add(new SwellSymbolPiece());
|
||||
}
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = new Vector2(CirclePiece.SYMBOL_SIZE);
|
||||
Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER);
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
AccentColour = colours.YellowDark;
|
||||
}
|
||||
|
||||
Children = new[]
|
||||
/// <summary>
|
||||
/// The symbol used for swell pieces.
|
||||
/// </summary>
|
||||
public class SwellSymbolPiece : Container
|
||||
{
|
||||
public SwellSymbolPiece()
|
||||
{
|
||||
new SpriteIcon
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = new Vector2(SYMBOL_SIZE);
|
||||
Padding = new MarginPadding(SYMBOL_BORDER);
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Icon = FontAwesome.Solid.Asterisk,
|
||||
Shadow = false
|
||||
}
|
||||
};
|
||||
new SpriteIcon
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Icon = FontAwesome.Solid.Asterisk,
|
||||
Shadow = false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
{
|
||||
public class TaikoPiece : BeatSyncedContainer, IHasAccentColour
|
||||
{
|
||||
/// <summary>
|
||||
/// The colour of the inner circle and outer glows.
|
||||
/// </summary>
|
||||
public virtual Color4 AccentColour { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether Kiai mode effects are enabled for this circle piece.
|
||||
/// </summary>
|
||||
public virtual bool KiaiMode { get; set; }
|
||||
|
||||
public TaikoPiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
{
|
||||
public class TickPiece : TaikoPiece
|
||||
public class TickPiece : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Any tick that is not the first for a drumroll is not filled, but is instead displayed
|
||||
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
FillMode = FillMode.Fit;
|
||||
Size = new Vector2(tick_size);
|
||||
|
||||
Add(new CircularContainer
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
AlwaysPresent = true
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
91
osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs
Normal file
@ -0,0 +1,91 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
{
|
||||
public class LegacyHit : CompositeDrawable, IHasAccentColour
|
||||
{
|
||||
private readonly TaikoSkinComponents component;
|
||||
|
||||
private Drawable backgroundLayer;
|
||||
|
||||
public LegacyHit(TaikoSkinComponents component)
|
||||
{
|
||||
this.component = component;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
|
||||
{
|
||||
Drawable getDrawableFor(string lookup)
|
||||
{
|
||||
const string normal_hit = "taikohit";
|
||||
const string big_hit = "taikobig";
|
||||
|
||||
string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit;
|
||||
|
||||
return skin.GetAnimation($"{prefix}{lookup}", true, false) ??
|
||||
// fallback to regular size if "big" version doesn't exist.
|
||||
skin.GetAnimation($"{normal_hit}{lookup}", true, false);
|
||||
}
|
||||
|
||||
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
|
||||
AddInternal(backgroundLayer = getDrawableFor("circle"));
|
||||
|
||||
var foregroundLayer = getDrawableFor("circleoverlay");
|
||||
if (foregroundLayer != null)
|
||||
AddInternal(foregroundLayer);
|
||||
|
||||
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
||||
// For now just stop at first frame for sanity.
|
||||
foreach (var c in InternalChildren)
|
||||
{
|
||||
(c as IFramedAnimation)?.Stop();
|
||||
|
||||
c.Anchor = Anchor.Centre;
|
||||
c.Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
AccentColour = component == TaikoSkinComponents.CentreHit
|
||||
? new Color4(235, 69, 44, 255)
|
||||
: new Color4(67, 142, 172, 255);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay".
|
||||
// This ensures they are scaled relative to each other but also match the expected DrawableHit size.
|
||||
foreach (var c in InternalChildren)
|
||||
c.Scale = new Vector2(DrawWidth / 128);
|
||||
}
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => accentColour;
|
||||
set
|
||||
{
|
||||
if (value == accentColour)
|
||||
return;
|
||||
|
||||
backgroundLayer.Colour = accentColour = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -32,6 +32,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
return new LegacyInputDrum();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.CentreHit:
|
||||
case TaikoSkinComponents.RimHit:
|
||||
|
||||
if (GetTexture("taikohitcircle") != null)
|
||||
return new LegacyHit(taikoComponent.Component);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.GetDrawableComponent(component);
|
||||
|
@ -6,5 +6,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
public enum TaikoSkinComponents
|
||||
{
|
||||
InputDrum,
|
||||
CentreHit,
|
||||
RimHit
|
||||
}
|
||||
}
|
||||
|
74
osu.Game.Tests/Editor/EditorChangeHandlerTest.cs
Normal file
@ -0,0 +1,74 @@
|
||||
// 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.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
|
||||
namespace osu.Game.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class EditorChangeHandlerTest
|
||||
{
|
||||
[Test]
|
||||
public void TestSaveRestoreState()
|
||||
{
|
||||
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
Assert.That(handler.CanRedo.Value, Is.False);
|
||||
|
||||
handler.SaveState();
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
Assert.That(handler.CanRedo.Value, Is.False);
|
||||
|
||||
handler.RestoreState(-1);
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
Assert.That(handler.CanRedo.Value, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaxStatesSaved()
|
||||
{
|
||||
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
|
||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
|
||||
handler.SaveState();
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
|
||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
|
||||
{
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
handler.RestoreState(-1);
|
||||
}
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaxStatesExceeded()
|
||||
{
|
||||
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
|
||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++)
|
||||
handler.SaveState();
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
|
||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
|
||||
{
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
handler.RestoreState(-1);
|
||||
}
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
342
osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs
Normal file
@ -0,0 +1,342 @@
|
||||
// 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.IO;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
|
||||
|
||||
namespace osu.Game.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class LegacyEditorBeatmapPatcherTest
|
||||
{
|
||||
private LegacyEditorBeatmapPatcher patcher;
|
||||
private EditorBeatmap current;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
patcher = new LegacyEditorBeatmapPatcher(current = new EditorBeatmap(new OsuBeatmap
|
||||
{
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddHitObject()
|
||||
{
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 1000 }
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInsertHitObject()
|
||||
{
|
||||
current.AddRange(new[]
|
||||
{
|
||||
new HitCircle { StartTime = 1000 },
|
||||
new HitCircle { StartTime = 3000 },
|
||||
});
|
||||
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
(OsuHitObject)current.HitObjects[0],
|
||||
new HitCircle { StartTime = 2000 },
|
||||
(OsuHitObject)current.HitObjects[1],
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeleteHitObject()
|
||||
{
|
||||
current.AddRange(new[]
|
||||
{
|
||||
new HitCircle { StartTime = 1000 },
|
||||
new HitCircle { StartTime = 2000 },
|
||||
new HitCircle { StartTime = 3000 },
|
||||
});
|
||||
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
(OsuHitObject)current.HitObjects[0],
|
||||
(OsuHitObject)current.HitObjects[2],
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeStartTime()
|
||||
{
|
||||
current.AddRange(new[]
|
||||
{
|
||||
new HitCircle { StartTime = 1000 },
|
||||
new HitCircle { StartTime = 2000 },
|
||||
new HitCircle { StartTime = 3000 },
|
||||
});
|
||||
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 500 },
|
||||
(OsuHitObject)current.HitObjects[1],
|
||||
(OsuHitObject)current.HitObjects[2],
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeSample()
|
||||
{
|
||||
current.AddRange(new[]
|
||||
{
|
||||
new HitCircle { StartTime = 1000 },
|
||||
new HitCircle { StartTime = 2000 },
|
||||
new HitCircle { StartTime = 3000 },
|
||||
});
|
||||
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
(OsuHitObject)current.HitObjects[0],
|
||||
new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } },
|
||||
(OsuHitObject)current.HitObjects[2],
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeSliderPath()
|
||||
{
|
||||
current.AddRange(new OsuHitObject[]
|
||||
{
|
||||
new HitCircle { StartTime = 1000 },
|
||||
new Slider
|
||||
{
|
||||
StartTime = 2000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero),
|
||||
new PathControlPoint(Vector2.One),
|
||||
new PathControlPoint(new Vector2(2), PathType.Bezier),
|
||||
new PathControlPoint(new Vector2(3)),
|
||||
}, 50)
|
||||
},
|
||||
new HitCircle { StartTime = 3000 },
|
||||
});
|
||||
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
(OsuHitObject)current.HitObjects[0],
|
||||
new Slider
|
||||
{
|
||||
StartTime = 2000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero, PathType.Bezier),
|
||||
new PathControlPoint(new Vector2(4)),
|
||||
new PathControlPoint(new Vector2(5)),
|
||||
}, 100)
|
||||
},
|
||||
(OsuHitObject)current.HitObjects[2],
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddMultipleHitObjects()
|
||||
{
|
||||
current.AddRange(new[]
|
||||
{
|
||||
new HitCircle { StartTime = 1000 },
|
||||
new HitCircle { StartTime = 2000 },
|
||||
new HitCircle { StartTime = 3000 },
|
||||
});
|
||||
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 500 },
|
||||
(OsuHitObject)current.HitObjects[0],
|
||||
new HitCircle { StartTime = 1500 },
|
||||
(OsuHitObject)current.HitObjects[1],
|
||||
new HitCircle { StartTime = 2250 },
|
||||
new HitCircle { StartTime = 2500 },
|
||||
(OsuHitObject)current.HitObjects[2],
|
||||
new HitCircle { StartTime = 3500 },
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeleteMultipleHitObjects()
|
||||
{
|
||||
current.AddRange(new[]
|
||||
{
|
||||
new HitCircle { StartTime = 500 },
|
||||
new HitCircle { StartTime = 1000 },
|
||||
new HitCircle { StartTime = 1500 },
|
||||
new HitCircle { StartTime = 2000 },
|
||||
new HitCircle { StartTime = 2250 },
|
||||
new HitCircle { StartTime = 2500 },
|
||||
new HitCircle { StartTime = 3000 },
|
||||
new HitCircle { StartTime = 3500 },
|
||||
});
|
||||
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
(OsuHitObject)current.HitObjects[1],
|
||||
(OsuHitObject)current.HitObjects[3],
|
||||
(OsuHitObject)current.HitObjects[6],
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeSamplesOfMultipleHitObjects()
|
||||
{
|
||||
current.AddRange(new[]
|
||||
{
|
||||
new HitCircle { StartTime = 500 },
|
||||
new HitCircle { StartTime = 1000 },
|
||||
new HitCircle { StartTime = 1500 },
|
||||
new HitCircle { StartTime = 2000 },
|
||||
new HitCircle { StartTime = 2250 },
|
||||
new HitCircle { StartTime = 2500 },
|
||||
new HitCircle { StartTime = 3000 },
|
||||
new HitCircle { StartTime = 3500 },
|
||||
});
|
||||
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
(OsuHitObject)current.HitObjects[0],
|
||||
new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } },
|
||||
(OsuHitObject)current.HitObjects[2],
|
||||
(OsuHitObject)current.HitObjects[3],
|
||||
new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE } } },
|
||||
(OsuHitObject)current.HitObjects[5],
|
||||
new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP } } },
|
||||
(OsuHitObject)current.HitObjects[7],
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddAndDeleteHitObjects()
|
||||
{
|
||||
current.AddRange(new[]
|
||||
{
|
||||
new HitCircle { StartTime = 500 },
|
||||
new HitCircle { StartTime = 1000 },
|
||||
new HitCircle { StartTime = 1500 },
|
||||
new HitCircle { StartTime = 2000 },
|
||||
new HitCircle { StartTime = 2250 },
|
||||
new HitCircle { StartTime = 2500 },
|
||||
new HitCircle { StartTime = 3000 },
|
||||
new HitCircle { StartTime = 3500 },
|
||||
});
|
||||
|
||||
var patch = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 750 },
|
||||
(OsuHitObject)current.HitObjects[1],
|
||||
(OsuHitObject)current.HitObjects[4],
|
||||
(OsuHitObject)current.HitObjects[5],
|
||||
new HitCircle { StartTime = 2650 },
|
||||
new HitCircle { StartTime = 2750 },
|
||||
new HitCircle { StartTime = 4000 },
|
||||
}
|
||||
};
|
||||
|
||||
runTest(patch);
|
||||
}
|
||||
|
||||
private void runTest(IBeatmap patch)
|
||||
{
|
||||
// Due to the method of testing, "patch" comes in without having been decoded via a beatmap decoder.
|
||||
// This causes issues because the decoder adds various default properties (e.g. new combo on first object, default samples).
|
||||
// To resolve "patch" into a sane state it is encoded and then re-decoded.
|
||||
patch = decode(encode(patch));
|
||||
|
||||
// Apply the patch.
|
||||
patcher.Patch(encode(current), encode(patch));
|
||||
|
||||
// Convert beatmaps to strings for assertion purposes.
|
||||
string currentStr = Encoding.ASCII.GetString(encode(current));
|
||||
string patchStr = Encoding.ASCII.GetString(encode(patch));
|
||||
|
||||
Assert.That(currentStr, Is.EqualTo(patchStr));
|
||||
}
|
||||
|
||||
private byte[] encode(IBeatmap beatmap)
|
||||
{
|
||||
using (var encoded = new MemoryStream())
|
||||
{
|
||||
using (var sw = new StreamWriter(encoded))
|
||||
new LegacyBeatmapEncoder(beatmap).Encode(sw);
|
||||
|
||||
return encoded.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private IBeatmap decode(byte[] state)
|
||||
{
|
||||
using (var stream = new MemoryStream(state))
|
||||
using (var reader = new LineBufferedReader(stream))
|
||||
return Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
|
||||
}
|
||||
}
|
||||
}
|
346
osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
Normal file
@ -0,0 +1,346 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Gameplay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneHitObjectSamples : PlayerTestScene
|
||||
{
|
||||
private readonly SkinInfo userSkinInfo = new SkinInfo();
|
||||
|
||||
private readonly BeatmapInfo beatmapInfo = new BeatmapInfo
|
||||
{
|
||||
BeatmapSet = new BeatmapSetInfo(),
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = User.SYSTEM_USER
|
||||
}
|
||||
};
|
||||
|
||||
private readonly TestResourceStore userSkinResourceStore = new TestResourceStore();
|
||||
private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore();
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
public TestSceneHitObjectSamples()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
private SkinSourceDependencyContainer dependencies;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
=> new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent)));
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides no custom sample set retrieves samples from the user skin.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestDefaultSampleFromUserSkin()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("hitobject-skin-sample.osu");
|
||||
|
||||
assertUserLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a sample set of 1 retrieves samples from the beatmap skin.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestDefaultSampleFromBeatmap()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-sample.osu");
|
||||
|
||||
assertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a sample set of 1 retrieves samples from the user skin when the beatmap does not contain the sample.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestDefaultSampleFromUserSkinFallback()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(null, expected_sample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-sample.osu");
|
||||
|
||||
assertUserLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
|
||||
/// normal-hitnormal2
|
||||
/// normal-hitnormal
|
||||
/// </summary>
|
||||
[TestCase("normal-hitnormal2")]
|
||||
[TestCase("normal-hitnormal")]
|
||||
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
|
||||
{
|
||||
setupSkins(expectedSample, expectedSample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
|
||||
|
||||
assertBeatmapLookup(expectedSample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin when the beatmap does not contain the sample:
|
||||
/// normal-hitnormal2
|
||||
/// normal-hitnormal
|
||||
/// </summary>
|
||||
[TestCase("normal-hitnormal2")]
|
||||
[TestCase("normal-hitnormal")]
|
||||
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
|
||||
{
|
||||
setupSkins(string.Empty, expectedSample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
|
||||
|
||||
assertUserLookup(expectedSample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestFileSampleFromBeatmap()
|
||||
{
|
||||
const string expected_sample = "hit_1.wav";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("file-beatmap-sample.osu");
|
||||
|
||||
assertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a default hitobject and control point causes <see cref="TestDefaultSampleFromUserSkin"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestControlPointSampleFromSkin()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("controlpoint-skin-sample.osu");
|
||||
|
||||
assertUserLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a control point that provides a custom sample set of 1 causes <see cref="TestDefaultSampleFromBeatmap"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestControlPointSampleFromBeatmap()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("controlpoint-beatmap-sample.osu");
|
||||
|
||||
assertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a control point that provides a custom sample of 2 causes <see cref="TestDefaultCustomSampleFromBeatmap"/>.
|
||||
/// </summary>
|
||||
[TestCase("normal-hitnormal2")]
|
||||
[TestCase("normal-hitnormal")]
|
||||
public void TestControlPointCustomSampleFromBeatmap(string sampleName)
|
||||
{
|
||||
setupSkins(sampleName, sampleName);
|
||||
|
||||
createTestWithBeatmap("controlpoint-beatmap-custom-sample.osu");
|
||||
|
||||
assertBeatmapLookup(sampleName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject's custom sample overrides the control point's.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitObjectCustomSampleOverride()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal3";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-custom-sample-override.osu");
|
||||
|
||||
assertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap;
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
=> new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio);
|
||||
|
||||
private IBeatmap currentTestBeatmap;
|
||||
|
||||
private void createTestWithBeatmap(string filename)
|
||||
{
|
||||
CreateTest(() =>
|
||||
{
|
||||
AddStep("clear performed lookups", () =>
|
||||
{
|
||||
userSkinResourceStore.PerformedLookups.Clear();
|
||||
beatmapSkinResourceStore.PerformedLookups.Clear();
|
||||
});
|
||||
|
||||
AddStep($"load {filename}", () =>
|
||||
{
|
||||
using (var reader = new LineBufferedReader(TestResources.OpenResource($"SampleLookups/{filename}")))
|
||||
currentTestBeatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void setupSkins(string beatmapFile, string userFile)
|
||||
{
|
||||
AddStep("setup skins", () =>
|
||||
{
|
||||
userSkinInfo.Files = new List<SkinFileInfo>
|
||||
{
|
||||
new SkinFileInfo
|
||||
{
|
||||
Filename = userFile,
|
||||
FileInfo = new IO.FileInfo { Hash = userFile }
|
||||
}
|
||||
};
|
||||
|
||||
beatmapInfo.BeatmapSet.Files = new List<BeatmapSetFileInfo>
|
||||
{
|
||||
new BeatmapSetFileInfo
|
||||
{
|
||||
Filename = beatmapFile,
|
||||
FileInfo = new IO.FileInfo { Hash = beatmapFile }
|
||||
}
|
||||
};
|
||||
|
||||
// Need to refresh the cached skin source to refresh the skin resource store.
|
||||
dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertBeatmapLookup(string name) => AddAssert($"\"{name}\" looked up from beatmap skin",
|
||||
() => !userSkinResourceStore.PerformedLookups.Contains(name) && beatmapSkinResourceStore.PerformedLookups.Contains(name));
|
||||
|
||||
private void assertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin",
|
||||
() => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name));
|
||||
|
||||
private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer
|
||||
{
|
||||
public ISkinSource SkinSource;
|
||||
|
||||
private readonly IReadOnlyDependencyContainer fallback;
|
||||
|
||||
public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback)
|
||||
{
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
public object Get(Type type)
|
||||
{
|
||||
if (type == typeof(ISkinSource))
|
||||
return SkinSource;
|
||||
|
||||
return fallback.Get(type);
|
||||
}
|
||||
|
||||
public object Get(Type type, CacheInfo info)
|
||||
{
|
||||
if (type == typeof(ISkinSource))
|
||||
return SkinSource;
|
||||
|
||||
return fallback.Get(type, info);
|
||||
}
|
||||
|
||||
public void Inject<T>(T instance) where T : class
|
||||
{
|
||||
// Never used directly
|
||||
}
|
||||
}
|
||||
|
||||
private class TestResourceStore : IResourceStore<byte[]>
|
||||
{
|
||||
public readonly List<string> PerformedLookups = new List<string>();
|
||||
|
||||
public byte[] Get(string name)
|
||||
{
|
||||
markLookup(name);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
public Task<byte[]> GetAsync(string name)
|
||||
{
|
||||
markLookup(name);
|
||||
return Task.FromResult(Array.Empty<byte>());
|
||||
}
|
||||
|
||||
public Stream GetStream(string name)
|
||||
{
|
||||
markLookup(name);
|
||||
return new MemoryStream();
|
||||
}
|
||||
|
||||
private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf(Path.DirectorySeparatorChar) + 1));
|
||||
|
||||
public IEnumerable<string> GetAvailableResources() => Enumerable.Empty<string>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap
|
||||
{
|
||||
private readonly BeatmapInfo skinBeatmapInfo;
|
||||
private readonly IResourceStore<byte[]> resourceStore;
|
||||
|
||||
public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio,
|
||||
double length = 60000)
|
||||
: base(beatmap, storyboard, referenceClock, audio, length)
|
||||
{
|
||||
this.skinBeatmapInfo = skinBeatmapInfo;
|
||||
this.resourceStore = resourceStore;
|
||||
}
|
||||
|
||||
protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager);
|
||||
}
|
||||
}
|
||||
}
|
@ -29,11 +29,17 @@ namespace osu.Game.Tests.NonVisual
|
||||
var cpi = new ControlPointInfo();
|
||||
|
||||
cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point.
|
||||
cpi.Add(1000, new TimingControlPoint()); // is redundant
|
||||
cpi.Add(1000, new TimingControlPoint()); // is also not redundant, due to change of offset
|
||||
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
|
||||
|
||||
cpi.Add(1000, new TimingControlPoint()); //is redundant
|
||||
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -86,11 +92,12 @@ namespace osu.Game.Tests.NonVisual
|
||||
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
|
||||
|
||||
cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
|
||||
cpi.Add(1000, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // is not redundant
|
||||
cpi.Add(1400, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // same settings, but is not redundant
|
||||
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
117
osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
Normal file
@ -0,0 +1,117 @@
|
||||
// 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.Testing;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Online
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestDummyAPIRequestHandling : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestGenericRequestHandling()
|
||||
{
|
||||
AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case CommentVoteRequest cRequest:
|
||||
cRequest.TriggerSuccess(new CommentBundle());
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
CommentVoteRequest request = null;
|
||||
CommentBundle response = null;
|
||||
|
||||
AddStep("fire request", () =>
|
||||
{
|
||||
response = null;
|
||||
request = new CommentVoteRequest(1, CommentVoteAction.Vote);
|
||||
request.Success += res => response = res;
|
||||
API.Queue(request);
|
||||
});
|
||||
|
||||
AddAssert("response event fired", () => response != null);
|
||||
|
||||
AddAssert("request has response", () => request.Result == response);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQueueRequestHandling()
|
||||
{
|
||||
registerHandler();
|
||||
|
||||
LeaveChannelRequest request;
|
||||
bool gotResponse = false;
|
||||
|
||||
AddStep("fire request", () =>
|
||||
{
|
||||
gotResponse = false;
|
||||
request = new LeaveChannelRequest(new Channel(), new User());
|
||||
request.Success += () => gotResponse = true;
|
||||
API.Queue(request);
|
||||
});
|
||||
|
||||
AddAssert("response event fired", () => gotResponse);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerformRequestHandling()
|
||||
{
|
||||
registerHandler();
|
||||
|
||||
LeaveChannelRequest request;
|
||||
bool gotResponse = false;
|
||||
|
||||
AddStep("fire request", () =>
|
||||
{
|
||||
gotResponse = false;
|
||||
request = new LeaveChannelRequest(new Channel(), new User());
|
||||
request.Success += () => gotResponse = true;
|
||||
API.Perform(request);
|
||||
});
|
||||
|
||||
AddAssert("response event fired", () => gotResponse);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerformAsyncRequestHandling()
|
||||
{
|
||||
registerHandler();
|
||||
|
||||
LeaveChannelRequest request;
|
||||
bool gotResponse = false;
|
||||
|
||||
AddStep("fire request", () =>
|
||||
{
|
||||
gotResponse = false;
|
||||
request = new LeaveChannelRequest(new Channel(), new User());
|
||||
request.Success += () => gotResponse = true;
|
||||
API.PerformAsync(request);
|
||||
});
|
||||
|
||||
AddAssert("response event fired", () => gotResponse);
|
||||
}
|
||||
|
||||
private void registerHandler()
|
||||
{
|
||||
AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case LeaveChannelRequest cRequest:
|
||||
cRequest.TriggerSuccess();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:0:0:
|
@ -0,0 +1,7 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,1,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:0:0:
|
@ -0,0 +1,7 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:0:0:
|
@ -0,0 +1,4 @@
|
||||
osu file format v14
|
||||
|
||||
[HitObjects]
|
||||
255,193,2170,1,0,0:0:0:0:hit_1.wav
|
@ -0,0 +1,7 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:3:0:
|
@ -0,0 +1,4 @@
|
||||
osu file format v14
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:2:0:
|
@ -0,0 +1,4 @@
|
||||
osu file format v14
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:1:0:
|
@ -0,0 +1,4 @@
|
||||
osu file format v14
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:0:0:
|
57
osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets.Scoring
|
||||
{
|
||||
public class ScoreProcessorTest
|
||||
{
|
||||
private ScoreProcessor scoreProcessor;
|
||||
private IBeatmap beatmap;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
scoreProcessor = new ScoreProcessor();
|
||||
beatmap = new TestBeatmap(new RulesetInfo())
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Good, 800_000)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, 50)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Good, 100)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, 300)]
|
||||
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
|
||||
{
|
||||
scoreProcessor.Mode.Value = scoringMode;
|
||||
scoreProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
|
||||
{
|
||||
Type = hitResult
|
||||
};
|
||||
scoreProcessor.ApplyResult(judgementResult);
|
||||
|
||||
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
|
||||
}
|
||||
}
|
||||
}
|
195
osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs
Normal file
@ -0,0 +1,195 @@
|
||||
// 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.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editor
|
||||
{
|
||||
public class TestSceneEditorChangeStates : ScreenTestScene
|
||||
{
|
||||
private EditorBeatmap editorBeatmap;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
Screens.Edit.Editor editor = null;
|
||||
|
||||
AddStep("load editor", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
LoadScreen(editor = new Screens.Edit.Editor());
|
||||
});
|
||||
|
||||
AddUntilStep("wait for editor to load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
|
||||
&& editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true);
|
||||
AddStep("get beatmap", () => editorBeatmap = editor.ChildrenOfType<EditorBeatmap>().Single());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUndoFromInitialState()
|
||||
{
|
||||
int hitObjectCount = 0;
|
||||
|
||||
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
|
||||
|
||||
addUndoSteps();
|
||||
|
||||
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRedoFromInitialState()
|
||||
{
|
||||
int hitObjectCount = 0;
|
||||
|
||||
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
|
||||
|
||||
addRedoSteps();
|
||||
|
||||
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddObjectAndUndo()
|
||||
{
|
||||
HitObject addedObject = null;
|
||||
HitObject removedObject = null;
|
||||
HitObject expectedObject = null;
|
||||
|
||||
AddStep("bind removal", () =>
|
||||
{
|
||||
editorBeatmap.HitObjectAdded += h => addedObject = h;
|
||||
editorBeatmap.HitObjectRemoved += h => removedObject = h;
|
||||
});
|
||||
|
||||
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
||||
AddAssert("hitobject added", () => addedObject == expectedObject);
|
||||
|
||||
addUndoSteps();
|
||||
AddAssert("hitobject removed", () => removedObject == expectedObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddObjectThenUndoThenRedo()
|
||||
{
|
||||
HitObject addedObject = null;
|
||||
HitObject removedObject = null;
|
||||
HitObject expectedObject = null;
|
||||
|
||||
AddStep("bind removal", () =>
|
||||
{
|
||||
editorBeatmap.HitObjectAdded += h => addedObject = h;
|
||||
editorBeatmap.HitObjectRemoved += h => removedObject = h;
|
||||
});
|
||||
|
||||
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
||||
addUndoSteps();
|
||||
|
||||
AddStep("reset variables", () =>
|
||||
{
|
||||
addedObject = null;
|
||||
removedObject = null;
|
||||
});
|
||||
|
||||
addRedoSteps();
|
||||
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
|
||||
AddAssert("no hitobject removed", () => removedObject == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveObjectThenUndo()
|
||||
{
|
||||
HitObject addedObject = null;
|
||||
HitObject removedObject = null;
|
||||
HitObject expectedObject = null;
|
||||
|
||||
AddStep("bind removal", () =>
|
||||
{
|
||||
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("reset variables", () =>
|
||||
{
|
||||
addedObject = null;
|
||||
removedObject = null;
|
||||
});
|
||||
|
||||
addUndoSteps();
|
||||
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
|
||||
AddAssert("no hitobject removed", () => removedObject == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveObjectThenUndoThenRedo()
|
||||
{
|
||||
HitObject addedObject = null;
|
||||
HitObject removedObject = null;
|
||||
HitObject expectedObject = null;
|
||||
|
||||
AddStep("bind removal", () =>
|
||||
{
|
||||
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));
|
||||
addUndoSteps();
|
||||
|
||||
AddStep("reset variables", () =>
|
||||
{
|
||||
addedObject = null;
|
||||
removedObject = null;
|
||||
});
|
||||
|
||||
addRedoSteps();
|
||||
AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo)
|
||||
AddAssert("no hitobject added", () => addedObject == null);
|
||||
}
|
||||
|
||||
private void addUndoSteps()
|
||||
{
|
||||
AddStep("press undo", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.PressKey(Key.Z);
|
||||
});
|
||||
|
||||
AddStep("release keys", () =>
|
||||
{
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
InputManager.ReleaseKey(Key.Z);
|
||||
});
|
||||
}
|
||||
|
||||
private void addRedoSteps()
|
||||
{
|
||||
AddStep("press redo", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.PressKey(Key.Z);
|
||||
});
|
||||
|
||||
AddStep("release keys", () =>
|
||||
{
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
InputManager.ReleaseKey(Key.Z);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
73
osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
Normal file
@ -0,0 +1,73 @@
|
||||
// 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.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneFailingLayer : OsuTestScene
|
||||
{
|
||||
private FailingLayer layer;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create layer", () =>
|
||||
{
|
||||
Child = layer = new FailingLayer();
|
||||
layer.BindHealthProcessor(new DrainingHealthProcessor(1));
|
||||
});
|
||||
|
||||
AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
|
||||
AddUntilStep("layer is visible", () => layer.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLayerFading()
|
||||
{
|
||||
AddSliderStep("current health", 0.0, 1.0, 1.0, val =>
|
||||
{
|
||||
if (layer != null)
|
||||
layer.Current.Value = val;
|
||||
});
|
||||
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddUntilStep("layer fade is visible", () => layer.Child.Alpha > 0.1f);
|
||||
AddStep("set health to 1", () => layer.Current.Value = 1f);
|
||||
AddUntilStep("layer fade is invisible", () => !layer.Child.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLayerDisabledViaConfig()
|
||||
{
|
||||
AddStep("disable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddUntilStep("layer is not visible", () => !layer.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLayerVisibilityWithAccumulatingProcessor()
|
||||
{
|
||||
AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new AccumulatingHealthProcessor(1)));
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddUntilStep("layer is not visible", () => !layer.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLayerVisibilityWithDrainingProcessor()
|
||||
{
|
||||
AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new DrainingHealthProcessor(1)));
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddWaitStep("wait for potential fade", 10);
|
||||
AddAssert("layer is still visible", () => layer.IsPresent);
|
||||
}
|
||||
}
|
||||
}
|
@ -20,26 +20,30 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
public void TestFromMainMenu()
|
||||
{
|
||||
var firstImport = importBeatmap(1);
|
||||
var secondimport = importBeatmap(3);
|
||||
|
||||
presentAndConfirm(firstImport);
|
||||
|
||||
AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
|
||||
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||
|
||||
var secondimport = importBeatmap(2);
|
||||
returnToMenu();
|
||||
presentAndConfirm(secondimport);
|
||||
returnToMenu();
|
||||
presentSecondDifficultyAndConfirm(firstImport, 1);
|
||||
returnToMenu();
|
||||
presentSecondDifficultyAndConfirm(secondimport, 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFromMainMenuDifferentRuleset()
|
||||
{
|
||||
var firstImport = importBeatmap(1);
|
||||
var secondimport = importBeatmap(3, new ManiaRuleset().RulesetInfo);
|
||||
|
||||
presentAndConfirm(firstImport);
|
||||
|
||||
AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
|
||||
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||
|
||||
var secondimport = importBeatmap(2, new ManiaRuleset().RulesetInfo);
|
||||
returnToMenu();
|
||||
presentAndConfirm(secondimport);
|
||||
returnToMenu();
|
||||
presentSecondDifficultyAndConfirm(firstImport, 1);
|
||||
returnToMenu();
|
||||
presentSecondDifficultyAndConfirm(secondimport, 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -48,8 +52,11 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
var firstImport = importBeatmap(1);
|
||||
presentAndConfirm(firstImport);
|
||||
|
||||
var secondimport = importBeatmap(2);
|
||||
var secondimport = importBeatmap(3);
|
||||
presentAndConfirm(secondimport);
|
||||
|
||||
presentSecondDifficultyAndConfirm(firstImport, 1);
|
||||
presentSecondDifficultyAndConfirm(secondimport, 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -58,8 +65,17 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
var firstImport = importBeatmap(1);
|
||||
presentAndConfirm(firstImport);
|
||||
|
||||
var secondimport = importBeatmap(2, new ManiaRuleset().RulesetInfo);
|
||||
var secondimport = importBeatmap(3, new ManiaRuleset().RulesetInfo);
|
||||
presentAndConfirm(secondimport);
|
||||
|
||||
presentSecondDifficultyAndConfirm(firstImport, 1);
|
||||
presentSecondDifficultyAndConfirm(secondimport, 3);
|
||||
}
|
||||
|
||||
private void returnToMenu()
|
||||
{
|
||||
AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
|
||||
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||
}
|
||||
|
||||
private Func<BeatmapSetInfo> importBeatmap(int i, RulesetInfo ruleset = null)
|
||||
@ -89,6 +105,13 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
BaseDifficulty = difficulty,
|
||||
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
|
||||
},
|
||||
new BeatmapInfo
|
||||
{
|
||||
OnlineBeatmapID = i * 2048,
|
||||
Metadata = metadata,
|
||||
BaseDifficulty = difficulty,
|
||||
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
|
||||
},
|
||||
}
|
||||
}).Result;
|
||||
});
|
||||
@ -106,5 +129,15 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.ID == getImport().ID);
|
||||
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID);
|
||||
}
|
||||
|
||||
private void presentSecondDifficultyAndConfirm(Func<BeatmapSetInfo> getImport, int importedID)
|
||||
{
|
||||
Predicate<BeatmapInfo> pred = b => b.OnlineBeatmapID == importedID * 2048;
|
||||
AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
|
||||
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
|
||||
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID * 2048);
|
||||
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
typeof(ChangelogListing),
|
||||
typeof(ChangelogSingleBuild),
|
||||
typeof(ChangelogBuild),
|
||||
typeof(Comments),
|
||||
};
|
||||
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
@ -149,8 +149,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
public DownloadState DownloadState => State.Value;
|
||||
|
||||
public TestDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
|
||||
: base(beatmapSet, noVideo)
|
||||
public TestDownloadButton(BeatmapSetInfo beatmapSet)
|
||||
: base(beatmapSet)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,35 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
this.rulesets = rulesets;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRecommendedSelection()
|
||||
{
|
||||
loadBeatmaps();
|
||||
|
||||
AddStep("set recommendation function", () => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault());
|
||||
|
||||
// check recommended was selected
|
||||
advanceSelection(direction: 1, diff: false);
|
||||
waitForSelection(1, 3);
|
||||
|
||||
// change away from recommended
|
||||
advanceSelection(direction: -1, diff: true);
|
||||
waitForSelection(1, 2);
|
||||
|
||||
// next set, check recommended
|
||||
advanceSelection(direction: 1, diff: false);
|
||||
waitForSelection(2, 3);
|
||||
|
||||
// next set, check recommended
|
||||
advanceSelection(direction: 1, diff: false);
|
||||
waitForSelection(3, 3);
|
||||
|
||||
// go back to first set and ensure user selection was retained
|
||||
advanceSelection(direction: -1, diff: false);
|
||||
advanceSelection(direction: -1, diff: false);
|
||||
waitForSelection(1, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test keyboard traversal
|
||||
/// </summary>
|
||||
|
91
osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs
Normal file
@ -0,0 +1,91 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public class TestSceneOsuMenu : OsuManualInputManagerTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(OsuMenu),
|
||||
typeof(DrawableOsuMenuItem)
|
||||
};
|
||||
|
||||
private OsuMenu menu;
|
||||
private bool actionPerformed;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
actionPerformed = false;
|
||||
|
||||
Child = menu = new OsuMenu(Direction.Vertical, true)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Items = new[]
|
||||
{
|
||||
new OsuMenuItem("standard", MenuItemType.Standard, performAction),
|
||||
new OsuMenuItem("highlighted", MenuItemType.Highlighted, performAction),
|
||||
new OsuMenuItem("destructive", MenuItemType.Destructive, performAction),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestClickEnabledMenuItem()
|
||||
{
|
||||
AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType<DrawableOsuMenuItem>().First()));
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("action performed", () => actionPerformed);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisableMenuItemsAndClick()
|
||||
{
|
||||
AddStep("disable menu items", () =>
|
||||
{
|
||||
foreach (var item in menu.Items)
|
||||
((OsuMenuItem)item).Action.Disabled = true;
|
||||
});
|
||||
|
||||
AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType<DrawableOsuMenuItem>().First()));
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("action not performed", () => !actionPerformed);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEnableMenuItemsAndClick()
|
||||
{
|
||||
AddStep("disable menu items", () =>
|
||||
{
|
||||
foreach (var item in menu.Items)
|
||||
((OsuMenuItem)item).Action.Disabled = true;
|
||||
});
|
||||
|
||||
AddStep("enable menu items", () =>
|
||||
{
|
||||
foreach (var item in menu.Items)
|
||||
((OsuMenuItem)item).Action.Disabled = false;
|
||||
});
|
||||
|
||||
AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType<DrawableOsuMenuItem>().First()));
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("action performed", () => actionPerformed);
|
||||
}
|
||||
|
||||
private void performAction() => actionPerformed = true;
|
||||
}
|
||||
}
|
@ -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 osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK.Graphics;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public class TestSceneOverlayScrollContainer : OsuManualInputManagerTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(OverlayScrollContainer)
|
||||
};
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
private TestScrollContainer scroll;
|
||||
|
||||
private int invocationCount;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = scroll = new TestScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
{
|
||||
Height = 3000,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Gray
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
invocationCount = 0;
|
||||
|
||||
scroll.Button.Action += () => invocationCount++;
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestButtonVisibility()
|
||||
{
|
||||
AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden);
|
||||
|
||||
AddStep("scroll to end", () => scroll.ScrollToEnd(false));
|
||||
AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible);
|
||||
|
||||
AddStep("scroll to start", () => scroll.ScrollToStart(false));
|
||||
AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden);
|
||||
|
||||
AddStep("scroll to 500", () => scroll.ScrollTo(500));
|
||||
AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f));
|
||||
AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonAction()
|
||||
{
|
||||
AddStep("scroll to end", () => scroll.ScrollToEnd(false));
|
||||
|
||||
AddStep("invoke action", () => scroll.Button.Action.Invoke());
|
||||
|
||||
AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClick()
|
||||
{
|
||||
AddStep("scroll to end", () => scroll.ScrollToEnd(false));
|
||||
|
||||
AddStep("click button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(scroll.Button);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleClicks()
|
||||
{
|
||||
AddStep("scroll to end", () => scroll.ScrollToEnd(false));
|
||||
|
||||
AddAssert("invocation count is 0", () => invocationCount == 0);
|
||||
|
||||
AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button));
|
||||
AddRepeatStep("click button", () => InputManager.Click(MouseButton.Left), 3);
|
||||
|
||||
AddAssert("invocation count is 1", () => invocationCount == 1);
|
||||
}
|
||||
|
||||
private class TestScrollContainer : OverlayScrollContainer
|
||||
{
|
||||
public new ScrollToTopButton Button => base.Button;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ using System;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
public abstract class ControlPoint : IComparable<ControlPoint>, IEquatable<ControlPoint>
|
||||
public abstract class ControlPoint : IComparable<ControlPoint>
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the control point takes effect.
|
||||
@ -19,12 +19,10 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this control point is equivalent to another, ignoring time.
|
||||
/// Determines whether this <see cref="ControlPoint"/> results in a meaningful change when placed alongside another.
|
||||
/// </summary>
|
||||
/// <param name="other">Another control point to compare with.</param>
|
||||
/// <returns>Whether equivalent.</returns>
|
||||
public abstract bool EquivalentTo(ControlPoint other);
|
||||
|
||||
public bool Equals(ControlPoint other) => Time == other?.Time && EquivalentTo(other);
|
||||
/// <param name="existing">An existing control point to compare with.</param>
|
||||
/// <returns>Whether this <see cref="ControlPoint"/> is redundant when placed alongside <paramref name="existing"/>.</returns>
|
||||
public abstract bool IsRedundant(ControlPoint existing);
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +247,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
break;
|
||||
}
|
||||
|
||||
return existing?.EquivalentTo(newPoint) == true;
|
||||
return newPoint?.IsRedundant(existing) == true;
|
||||
}
|
||||
|
||||
private void groupItemAdded(ControlPoint controlPoint)
|
||||
|
@ -27,7 +27,8 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
set => SpeedMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
public override bool EquivalentTo(ControlPoint other) =>
|
||||
other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier);
|
||||
public override bool IsRedundant(ControlPoint existing)
|
||||
=> existing is DifficultyControlPoint existingDifficulty
|
||||
&& SpeedMultiplier == existingDifficulty.SpeedMultiplier;
|
||||
}
|
||||
}
|
||||
|
@ -35,8 +35,10 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
set => KiaiModeBindable.Value = value;
|
||||
}
|
||||
|
||||
public override bool EquivalentTo(ControlPoint other) =>
|
||||
other is EffectControlPoint otherTyped &&
|
||||
KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine;
|
||||
public override bool IsRedundant(ControlPoint existing)
|
||||
=> !OmitFirstBarLine
|
||||
&& existing is EffectControlPoint existingEffect
|
||||
&& KiaiMode == existingEffect.KiaiMode
|
||||
&& OmitFirstBarLine == existingEffect.OmitFirstBarLine;
|
||||
}
|
||||
}
|
||||
|
@ -68,8 +68,9 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
return newSampleInfo;
|
||||
}
|
||||
|
||||
public override bool EquivalentTo(ControlPoint other) =>
|
||||
other is SampleControlPoint otherTyped &&
|
||||
SampleBank == otherTyped.SampleBank && SampleVolume == otherTyped.SampleVolume;
|
||||
public override bool IsRedundant(ControlPoint existing)
|
||||
=> existing is SampleControlPoint existingSample
|
||||
&& SampleBank == existingSample.SampleBank
|
||||
&& SampleVolume == existingSample.SampleVolume;
|
||||
}
|
||||
}
|
||||
|
@ -48,8 +48,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
public double BPM => 60000 / BeatLength;
|
||||
|
||||
public override bool EquivalentTo(ControlPoint other) =>
|
||||
other is TimingControlPoint otherTyped
|
||||
&& TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
|
||||
// Timing points are never redundant as they can change the time signature.
|
||||
public override bool IsRedundant(ControlPoint existing) => false;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Logging;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.Formats
|
||||
@ -149,7 +150,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
HitObjects,
|
||||
Variables,
|
||||
Fonts,
|
||||
Mania
|
||||
CatchTheBeat,
|
||||
Mania,
|
||||
}
|
||||
|
||||
internal class LegacyDifficultyControlPoint : DifficultyControlPoint
|
||||
@ -168,15 +170,19 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
var baseInfo = base.ApplyTo(hitSampleInfo);
|
||||
|
||||
if (string.IsNullOrEmpty(baseInfo.Suffix) && CustomSampleBank > 1)
|
||||
baseInfo.Suffix = CustomSampleBank.ToString();
|
||||
if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy
|
||||
&& legacy.CustomSampleBank == 0)
|
||||
{
|
||||
legacy.CustomSampleBank = CustomSampleBank;
|
||||
}
|
||||
|
||||
return baseInfo;
|
||||
}
|
||||
|
||||
public override bool EquivalentTo(ControlPoint other) =>
|
||||
base.EquivalentTo(other) && other is LegacySampleControlPoint otherTyped &&
|
||||
CustomSampleBank == otherTyped.CustomSampleBank;
|
||||
public override bool IsRedundant(ControlPoint existing)
|
||||
=> base.IsRedundant(existing)
|
||||
&& existing is LegacySampleControlPoint existingSample
|
||||
&& CustomSampleBank == existingSample.CustomSampleBank;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ namespace osu.Game.Configuration
|
||||
};
|
||||
|
||||
Set(OsuSetting.ExternalLinkWarning, true);
|
||||
Set(OsuSetting.PreferNoVideo, false);
|
||||
|
||||
// Audio
|
||||
Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
|
||||
@ -87,7 +88,9 @@ namespace osu.Game.Configuration
|
||||
Set(OsuSetting.ShowInterface, true);
|
||||
Set(OsuSetting.ShowProgressGraph, true);
|
||||
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
|
||||
Set(OsuSetting.FadePlayfieldWhenHealthLow, true);
|
||||
Set(OsuSetting.KeyOverlay, false);
|
||||
Set(OsuSetting.PositionalHitSounds, true);
|
||||
Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
|
||||
|
||||
Set(OsuSetting.FloatingComments, false);
|
||||
@ -176,11 +179,13 @@ namespace osu.Game.Configuration
|
||||
LightenDuringBreaks,
|
||||
ShowStoryboard,
|
||||
KeyOverlay,
|
||||
PositionalHitSounds,
|
||||
ScoreMeter,
|
||||
FloatingComments,
|
||||
ShowInterface,
|
||||
ShowProgressGraph,
|
||||
ShowHealthDisplayWhenCantFail,
|
||||
FadePlayfieldWhenHealthLow,
|
||||
MouseDisableButtons,
|
||||
MouseDisableWheel,
|
||||
AudioOffset,
|
||||
@ -212,6 +217,7 @@ namespace osu.Game.Configuration
|
||||
IncreaseFirstObjectVisibility,
|
||||
ScoreDisplayMode,
|
||||
ExternalLinkWarning,
|
||||
PreferNoVideo,
|
||||
Scaling,
|
||||
ScalingPositionX,
|
||||
ScalingPositionY,
|
||||
|
@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
|
||||
|
||||
if (timingPoint.Equals(lastTimingPoint) && beatIndex == lastBeat)
|
||||
if (timingPoint == lastTimingPoint && beatIndex == lastBeat)
|
||||
return;
|
||||
|
||||
using (BeginDelayedSequence(-TimeSinceLastBeat, true))
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -16,12 +17,7 @@ namespace osu.Game.Graphics.Containers
|
||||
public class SectionsContainer<T> : Container<T>
|
||||
where T : Drawable
|
||||
{
|
||||
private Drawable expandableHeader, fixedHeader, footer, headerBackground;
|
||||
private readonly OsuScrollContainer scrollContainer;
|
||||
private readonly Container headerBackgroundContainer;
|
||||
private readonly FlowContainer<T> scrollContentContainer;
|
||||
|
||||
protected override Container<T> Content => scrollContentContainer;
|
||||
public Bindable<T> SelectedSection { get; } = new Bindable<T>();
|
||||
|
||||
public Drawable ExpandableHeader
|
||||
{
|
||||
@ -83,6 +79,7 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
headerBackgroundContainer.Clear();
|
||||
headerBackground = value;
|
||||
|
||||
if (value == null) return;
|
||||
|
||||
headerBackgroundContainer.Add(headerBackground);
|
||||
@ -91,15 +88,37 @@ namespace osu.Game.Graphics.Containers
|
||||
}
|
||||
}
|
||||
|
||||
public Bindable<T> SelectedSection { get; } = new Bindable<T>();
|
||||
protected override Container<T> Content => scrollContentContainer;
|
||||
|
||||
protected virtual FlowContainer<T> CreateScrollContentContainer()
|
||||
=> new FillFlowContainer<T>
|
||||
private readonly OsuScrollContainer scrollContainer;
|
||||
private readonly Container headerBackgroundContainer;
|
||||
private readonly MarginPadding originalSectionsMargin;
|
||||
private Drawable expandableHeader, fixedHeader, footer, headerBackground;
|
||||
private FlowContainer<T> scrollContentContainer;
|
||||
|
||||
private float headerHeight, footerHeight;
|
||||
|
||||
private float lastKnownScroll;
|
||||
|
||||
public SectionsContainer()
|
||||
{
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
};
|
||||
scrollContainer = CreateScrollContainer().With(s =>
|
||||
{
|
||||
s.RelativeSizeAxes = Axes.Both;
|
||||
s.Masking = true;
|
||||
s.ScrollbarVisible = false;
|
||||
s.Child = scrollContentContainer = CreateScrollContentContainer();
|
||||
}),
|
||||
headerBackgroundContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
}
|
||||
});
|
||||
|
||||
originalSectionsMargin = scrollContentContainer.Margin;
|
||||
}
|
||||
|
||||
public override void Add(T drawable)
|
||||
{
|
||||
@ -109,40 +128,23 @@ namespace osu.Game.Graphics.Containers
|
||||
footerHeight = float.NaN;
|
||||
}
|
||||
|
||||
private float headerHeight, footerHeight;
|
||||
private readonly MarginPadding originalSectionsMargin;
|
||||
|
||||
private void updateSectionsMargin()
|
||||
{
|
||||
if (!Children.Any()) return;
|
||||
|
||||
var newMargin = originalSectionsMargin;
|
||||
newMargin.Top += headerHeight;
|
||||
newMargin.Bottom += footerHeight;
|
||||
|
||||
scrollContentContainer.Margin = newMargin;
|
||||
}
|
||||
|
||||
public SectionsContainer()
|
||||
{
|
||||
AddInternal(scrollContainer = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
ScrollbarVisible = false,
|
||||
Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() }
|
||||
});
|
||||
AddInternal(headerBackgroundContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
});
|
||||
originalSectionsMargin = scrollContentContainer.Margin;
|
||||
}
|
||||
|
||||
public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0));
|
||||
public void ScrollTo(Drawable section) =>
|
||||
scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0));
|
||||
|
||||
public void ScrollToTop() => scrollContainer.ScrollTo(0);
|
||||
|
||||
[NotNull]
|
||||
protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer();
|
||||
|
||||
[NotNull]
|
||||
protected virtual FlowContainer<T> CreateScrollContentContainer() =>
|
||||
new FillFlowContainer<T>
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
};
|
||||
|
||||
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
|
||||
{
|
||||
var result = base.OnInvalidate(invalidation, source);
|
||||
@ -156,8 +158,6 @@ namespace osu.Game.Graphics.Containers
|
||||
return result;
|
||||
}
|
||||
|
||||
private float lastKnownScroll;
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
@ -208,5 +208,16 @@ namespace osu.Game.Graphics.Containers
|
||||
SelectedSection.Value = bestMatch;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSectionsMargin()
|
||||
{
|
||||
if (!Children.Any()) return;
|
||||
|
||||
var newMargin = originalSectionsMargin;
|
||||
newMargin.Top += headerHeight;
|
||||
newMargin.Bottom += footerHeight;
|
||||
|
||||
scrollContentContainer.Margin = newMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
BackgroundColourHover = Color4Extensions.FromHex(@"172023");
|
||||
|
||||
updateTextColour();
|
||||
|
||||
Item.Action.BindDisabledChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
private void updateTextColour()
|
||||
@ -65,19 +67,33 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
sampleHover.Play();
|
||||
text.BoldText.FadeIn(transition_length, Easing.OutQuint);
|
||||
text.NormalText.FadeOut(transition_length, Easing.OutQuint);
|
||||
updateState();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
text.BoldText.FadeOut(transition_length, Easing.OutQuint);
|
||||
text.NormalText.FadeIn(transition_length, Easing.OutQuint);
|
||||
updateState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Alpha = Item.Action.Disabled ? 0.2f : 1;
|
||||
|
||||
if (IsHovered && !Item.Action.Disabled)
|
||||
{
|
||||
sampleHover.Play();
|
||||
text.BoldText.FadeIn(transition_length, Easing.OutQuint);
|
||||
text.NormalText.FadeOut(transition_length, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
text.BoldText.FadeOut(transition_length, Easing.OutQuint);
|
||||
text.NormalText.FadeIn(transition_length, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
sampleClick.Play();
|
||||
|
@ -16,20 +16,27 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest<T>(Uri);
|
||||
|
||||
public T Result => ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
|
||||
public T Result { get; private set; }
|
||||
|
||||
protected APIRequest()
|
||||
{
|
||||
base.Success += onSuccess;
|
||||
base.Success += () => TriggerSuccess(((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject);
|
||||
}
|
||||
|
||||
private void onSuccess() => Success?.Invoke(Result);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked on successful completion of an API request.
|
||||
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
|
||||
/// </summary>
|
||||
public new event APISuccessHandler<T> Success;
|
||||
|
||||
internal void TriggerSuccess(T result)
|
||||
{
|
||||
if (Result != null)
|
||||
throw new InvalidOperationException("Attempted to trigger success more than once");
|
||||
|
||||
Result = result;
|
||||
Success?.Invoke(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -96,10 +103,15 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
if (cancelled) return;
|
||||
|
||||
Success?.Invoke();
|
||||
TriggerSuccess();
|
||||
});
|
||||
}
|
||||
|
||||
internal void TriggerSuccess()
|
||||
{
|
||||
Success?.Invoke();
|
||||
}
|
||||
|
||||
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
|
||||
|
||||
public void Fail(Exception e)
|
||||
|
@ -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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -30,6 +31,11 @@ namespace osu.Game.Online.API
|
||||
|
||||
private readonly List<IOnlineComponent> components = new List<IOnlineComponent>();
|
||||
|
||||
/// <summary>
|
||||
/// Provide handling logic for an arbitrary API request.
|
||||
/// </summary>
|
||||
public Action<APIRequest> HandleRequest;
|
||||
|
||||
public APIState State
|
||||
{
|
||||
get => state;
|
||||
@ -55,11 +61,16 @@ namespace osu.Game.Online.API
|
||||
|
||||
public virtual void Queue(APIRequest request)
|
||||
{
|
||||
HandleRequest?.Invoke(request);
|
||||
}
|
||||
|
||||
public void Perform(APIRequest request) { }
|
||||
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
|
||||
|
||||
public Task PerformAsync(APIRequest request) => Task.CompletedTask;
|
||||
public Task PerformAsync(APIRequest request)
|
||||
{
|
||||
HandleRequest?.Invoke(request);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Register(IOnlineComponent component)
|
||||
{
|
||||
|
@ -315,8 +315,15 @@ namespace osu.Game
|
||||
/// The user should have already requested this interactively.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to select.</param>
|
||||
public void PresentBeatmap(BeatmapSetInfo beatmap)
|
||||
/// <param name="difficultyCriteria">
|
||||
/// Optional predicate used to try and find a difficulty to select.
|
||||
/// If omitted, this will try to present the first beatmap from the current ruleset.
|
||||
/// In case of failure the first difficulty of the set will be presented, ignoring the predicate.
|
||||
/// </param>
|
||||
public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate<BeatmapInfo> difficultyCriteria = null)
|
||||
{
|
||||
difficultyCriteria ??= b => b.Ruleset.Equals(Ruleset.Value);
|
||||
|
||||
var databasedSet = beatmap.OnlineBeatmapSetID != null
|
||||
? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID)
|
||||
: BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash);
|
||||
@ -334,13 +341,13 @@ namespace osu.Game
|
||||
menuScreen.LoadToSolo();
|
||||
|
||||
// we might even already be at the song
|
||||
if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash)
|
||||
if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && difficultyCriteria(Beatmap.Value.BeatmapInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use first beatmap available for current ruleset, else switch ruleset.
|
||||
var first = databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First();
|
||||
// Find first beatmap that matches our predicate.
|
||||
var first = databasedSet.Beatmaps.Find(difficultyCriteria) ?? databasedSet.Beatmaps.First();
|
||||
|
||||
Ruleset.Value = first.Ruleset;
|
||||
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first);
|
||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourProvider.Background6
|
||||
},
|
||||
new BasicScrollContainer
|
||||
new OverlayScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
|
@ -277,7 +277,8 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value)
|
||||
{
|
||||
Width = 50,
|
||||
RelativeSizeAxes = Axes.Y
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
SelectedBeatmap = { BindTarget = Picker.Beatmap }
|
||||
};
|
||||
break;
|
||||
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Overlays
|
||||
public BeatmapSetOverlay()
|
||||
: base(OverlayColourScheme.Blue)
|
||||
{
|
||||
OsuScrollContainer scroll;
|
||||
OverlayScrollContainer scroll;
|
||||
Info info;
|
||||
CommentsSection comments;
|
||||
|
||||
@ -49,7 +49,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
scroll = new OsuScrollContainer
|
||||
scroll = new OverlayScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
|
@ -1,79 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Changelog
|
||||
{
|
||||
public class Comments : CompositeDrawable
|
||||
{
|
||||
private readonly APIChangelogBuild build;
|
||||
|
||||
public Comments(APIChangelogBuild build)
|
||||
{
|
||||
this.build = build;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Horizontal = 50,
|
||||
Vertical = 20,
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
LinkFlowContainer text;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.GreyVioletDarker
|
||||
},
|
||||
},
|
||||
text = new LinkFlowContainer(t =>
|
||||
{
|
||||
t.Colour = colours.PinkLighter;
|
||||
t.Font = OsuFont.Default.With(size: 14);
|
||||
})
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
}
|
||||
};
|
||||
|
||||
text.AddParagraph("Got feedback?", t =>
|
||||
{
|
||||
t.Colour = Color4.White;
|
||||
t.Font = OsuFont.Default.With(italics: true, size: 20);
|
||||
t.Padding = new MarginPadding { Bottom = 20 };
|
||||
});
|
||||
|
||||
text.AddParagraph("We would love to hear what you think of this update! ");
|
||||
text.AddIcon(FontAwesome.Regular.GrinHearts);
|
||||
|
||||
text.AddParagraph("Please visit the ");
|
||||
text.AddLink("web version", $"{build.Url}#comments");
|
||||
text.AddText(" of this changelog to leave any comments.");
|
||||
}
|
||||
}
|
||||
}
|
@ -50,7 +50,7 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourProvider.Background4,
|
||||
},
|
||||
new OsuScrollContainer
|
||||
new OverlayScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
|
@ -1,9 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online;
|
||||
@ -14,16 +17,18 @@ namespace osu.Game.Overlays.Direct
|
||||
{
|
||||
protected bool DownloadEnabled => button.Enabled.Value;
|
||||
|
||||
private readonly bool noVideo;
|
||||
/// <summary>
|
||||
/// Currently selected beatmap. Used to present the correct difficulty after completing a download.
|
||||
/// </summary>
|
||||
public readonly IBindable<BeatmapInfo> SelectedBeatmap = new Bindable<BeatmapInfo>();
|
||||
|
||||
private readonly ShakeContainer shakeContainer;
|
||||
private readonly DownloadButton button;
|
||||
private Bindable<bool> noVideoSetting;
|
||||
|
||||
public PanelDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
|
||||
public PanelDownloadButton(BeatmapSetInfo beatmapSet)
|
||||
: base(beatmapSet)
|
||||
{
|
||||
this.noVideo = noVideo;
|
||||
|
||||
InternalChild = shakeContainer = new ShakeContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -43,7 +48,7 @@ namespace osu.Game.Overlays.Direct
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuGame game, BeatmapManager beatmaps)
|
||||
private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig)
|
||||
{
|
||||
if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
|
||||
{
|
||||
@ -52,6 +57,8 @@ namespace osu.Game.Overlays.Direct
|
||||
return;
|
||||
}
|
||||
|
||||
noVideoSetting = osuConfig.GetBindable<bool>(OsuSetting.PreferNoVideo);
|
||||
|
||||
button.Action = () =>
|
||||
{
|
||||
switch (State.Value)
|
||||
@ -62,11 +69,15 @@ namespace osu.Game.Overlays.Direct
|
||||
break;
|
||||
|
||||
case DownloadState.LocallyAvailable:
|
||||
game?.PresentBeatmap(BeatmapSet.Value);
|
||||
Predicate<BeatmapInfo> findPredicate = null;
|
||||
if (SelectedBeatmap.Value != null)
|
||||
findPredicate = b => b.OnlineBeatmapID == SelectedBeatmap.Value.OnlineBeatmapID;
|
||||
|
||||
game?.PresentBeatmap(BeatmapSet.Value, findPredicate);
|
||||
break;
|
||||
|
||||
default:
|
||||
beatmaps.Download(BeatmapSet.Value, noVideo);
|
||||
beatmaps.Download(BeatmapSet.Value, noVideoSetting.Value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays.News;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
@ -36,7 +35,7 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.PurpleDarkAlternative
|
||||
},
|
||||
new OsuScrollContainer
|
||||
new OverlayScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
|
149
osu.Game/Overlays/OverlayScrollContainer.cs
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="OsuScrollContainer"/> which provides <see cref="ScrollToTopButton"/>. Mostly used in <see cref="FullscreenOverlay"/>.
|
||||
/// </summary>
|
||||
public class OverlayScrollContainer : OsuScrollContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Scroll position at which the <see cref="ScrollToTopButton"/> will be shown.
|
||||
/// </summary>
|
||||
private const int button_scroll_position = 200;
|
||||
|
||||
protected readonly ScrollToTopButton Button;
|
||||
|
||||
public OverlayScrollContainer()
|
||||
{
|
||||
AddInternal(Button = new ScrollToTopButton
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding(20),
|
||||
Action = () =>
|
||||
{
|
||||
ScrollToStart();
|
||||
Button.State = Visibility.Hidden;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight)
|
||||
{
|
||||
Button.State = Visibility.Hidden;
|
||||
return;
|
||||
}
|
||||
|
||||
Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
|
||||
public class ScrollToTopButton : OsuHoverContainer
|
||||
{
|
||||
private const int fade_duration = 500;
|
||||
|
||||
private Visibility state;
|
||||
|
||||
public Visibility State
|
||||
{
|
||||
get => state;
|
||||
set
|
||||
{
|
||||
if (value == state)
|
||||
return;
|
||||
|
||||
state = value;
|
||||
Enabled.Value = state == Visibility.Visible;
|
||||
this.FadeTo(state == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<Drawable> EffectTargets => new[] { background };
|
||||
|
||||
private Color4 flashColour;
|
||||
|
||||
private readonly Container content;
|
||||
private readonly Box background;
|
||||
|
||||
public ScrollToTopButton()
|
||||
{
|
||||
Size = new Vector2(50);
|
||||
Alpha = 0;
|
||||
Add(content = new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Offset = new Vector2(0f, 1f),
|
||||
Radius = 3f,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(15),
|
||||
Icon = FontAwesome.Solid.ChevronUp
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TooltipText = "Scroll to top";
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
IdleColour = colourProvider.Background6;
|
||||
HoverColour = colourProvider.Background5;
|
||||
flashColour = colourProvider.Light1;
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
background.FlashColour(flashColour, 800, Easing.OutQuint);
|
||||
return base.OnClick(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
content.ScaleTo(0.75f, 2000, Easing.OutQuint);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
content.ScaleTo(1, 1000, Easing.OutElastic);
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected Bindable<RankingsScope> Scope => header.Current;
|
||||
|
||||
private readonly BasicScrollContainer scrollFlow;
|
||||
private readonly OverlayScrollContainer scrollFlow;
|
||||
private readonly Container contentContainer;
|
||||
private readonly LoadingLayer loading;
|
||||
private readonly Box background;
|
||||
@ -44,7 +44,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
scrollFlow = new BasicScrollContainer
|
||||
scrollFlow = new OverlayScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
|
||||
namespace osu.Game.Overlays.SearchableList
|
||||
@ -72,7 +71,7 @@ namespace osu.Game.Overlays.SearchableList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Child = new OsuScrollContainer
|
||||
Child = new OverlayScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
|
@ -53,10 +53,20 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
||||
Keywords = new[] { "hp", "bar" }
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Fade playfield to red when health is low",
|
||||
Bindable = config.GetBindable<bool>(OsuSetting.FadePlayfieldWhenHealthLow),
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Always show key overlay",
|
||||
Bindable = config.GetBindable<bool>(OsuSetting.KeyOverlay)
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Positional hitsounds",
|
||||
Bindable = config.GetBindable<bool>(OsuSetting.PositionalHitSounds)
|
||||
},
|
||||
new SettingsEnumDropdown<ScoreMeterType>
|
||||
{
|
||||
LabelText = "Score meter type",
|
||||
|
@ -21,6 +21,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online
|
||||
LabelText = "Warn about opening external links",
|
||||
Bindable = config.GetBindable<bool>(OsuSetting.ExternalLinkWarning)
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Prefer downloads without video",
|
||||
Keywords = new[] { "no-video" },
|
||||
Bindable = config.GetBindable<bool>(OsuSetting.PreferNoVideo)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -195,6 +195,8 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override OsuScrollContainer CreateScrollContainer() => new OverlayScrollContainer();
|
||||
|
||||
protected override FlowContainer<ProfileSection> CreateScrollContentContainer() => new FillFlowContainer<ProfileSection>
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
|
@ -106,6 +106,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
case ScrollEvent _:
|
||||
return false;
|
||||
|
||||
case DoubleClickEvent _:
|
||||
return false;
|
||||
|
||||
case MouseButtonEvent _:
|
||||
return true;
|
||||
|
||||
|
@ -12,11 +12,13 @@ using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Configuration;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Drawables
|
||||
@ -84,8 +86,20 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </summary>
|
||||
public JudgementResult Result { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The relative X position of this hit object for sample playback balance adjustment.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a range of 0..1 (0 for far-left, 0.5 for centre, 1 for far-right).
|
||||
/// Dampening is post-applied to ensure the effect is not too intense.
|
||||
/// </remarks>
|
||||
protected virtual float SamplePlaybackPosition => 0.5f;
|
||||
|
||||
private readonly BindableDouble balanceAdjust = new BindableDouble();
|
||||
|
||||
private BindableList<HitSampleInfo> samplesBindable;
|
||||
private Bindable<double> startTimeBindable;
|
||||
private Bindable<bool> userPositionalHitSounds;
|
||||
private Bindable<int> comboIndexBindable;
|
||||
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
@ -104,8 +118,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
userPositionalHitSounds = config.GetBindable<bool>(OsuSetting.PositionalHitSounds);
|
||||
var judgement = HitObject.CreateJudgement();
|
||||
|
||||
Result = CreateResult(judgement);
|
||||
@ -156,7 +171,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
|
||||
}
|
||||
|
||||
AddInternal(Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))));
|
||||
Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
|
||||
Samples.AddAdjustment(AdjustableProperty.Balance, balanceAdjust);
|
||||
AddInternal(Samples);
|
||||
}
|
||||
|
||||
private void onDefaultsApplied() => apply(HitObject);
|
||||
@ -353,7 +370,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
|
||||
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
|
||||
/// </summary>
|
||||
public virtual void PlaySamples() => Samples?.Play();
|
||||
public virtual void PlaySamples()
|
||||
{
|
||||
const float balance_adjust_amount = 0.4f;
|
||||
|
||||
balanceAdjust.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0);
|
||||
Samples?.Play();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
|
@ -409,22 +409,34 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
|
||||
}
|
||||
|
||||
private class LegacyHitSampleInfo : HitSampleInfo
|
||||
internal class LegacyHitSampleInfo : HitSampleInfo
|
||||
{
|
||||
private int customSampleBank;
|
||||
|
||||
public int CustomSampleBank
|
||||
{
|
||||
get => customSampleBank;
|
||||
set
|
||||
{
|
||||
if (value > 1)
|
||||
customSampleBank = value;
|
||||
|
||||
if (value >= 2)
|
||||
Suffix = value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FileHitSampleInfo : HitSampleInfo
|
||||
private class FileHitSampleInfo : LegacyHitSampleInfo
|
||||
{
|
||||
public string Filename;
|
||||
|
||||
public FileHitSampleInfo()
|
||||
{
|
||||
// Make sure that the LegacyBeatmapSkin does not fall back to the user skin.
|
||||
// Note that this does not change the lookup names, as they are overridden locally.
|
||||
CustomSampleBank = 1;
|
||||
}
|
||||
|
||||
public override IEnumerable<string> LookupNames => new[]
|
||||
{
|
||||
Filename,
|
||||
|