mirror of
https://github.com/osukey/osukey.git
synced 2025-08-06 16:13:57 +09:00
Merge branch 'master' into fix-failure-logic
This commit is contained in:
@ -51,7 +51,7 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.511.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.511.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
36
osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
Normal file
36
osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// 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.IO.Stores;
|
||||||
|
using osu.Game.Rulesets.Catch.Skinning;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CatchSkinColourDecodingTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestCatchSkinColourDecoding()
|
||||||
|
{
|
||||||
|
var store = new NamespacedResourceStore<byte[]>(new DllResourceStore(GetType().Assembly), "Resources/special-skin");
|
||||||
|
var rawSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, store);
|
||||||
|
var skin = new CatchLegacySkinTransformer(rawSkin);
|
||||||
|
|
||||||
|
Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value);
|
||||||
|
Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value);
|
||||||
|
Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestLegacySkin : LegacySkin
|
||||||
|
{
|
||||||
|
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
|
||||||
|
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
|
||||||
|
: base(skin, storage, null, "skin.ini")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
[CatchTheBeat]
|
||||||
|
HyperDash: 232,185,35
|
||||||
|
HyperDashFruit: 0,255,255
|
||||||
|
HyperDashAfterImage: 232,74,35
|
@ -8,6 +8,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Tests
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
{
|
{
|
||||||
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
SetContents(() => new Catcher
|
SetContents(() => new Catcher(new Container())
|
||||||
{
|
{
|
||||||
RelativePositionAxes = Axes.None,
|
RelativePositionAxes = Axes.None,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
|
@ -26,6 +26,48 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private SkinManager skins { get; set; }
|
private SkinManager skins { get; set; }
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDefaultCatcherColour()
|
||||||
|
{
|
||||||
|
var skin = new TestSkin();
|
||||||
|
|
||||||
|
checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCustomCatcherColour()
|
||||||
|
{
|
||||||
|
var skin = new TestSkin
|
||||||
|
{
|
||||||
|
HyperDashColour = Color4.Goldenrod
|
||||||
|
};
|
||||||
|
|
||||||
|
checkHyperDashCatcherColour(skin, skin.HyperDashColour);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCustomEndGlowColour()
|
||||||
|
{
|
||||||
|
var skin = new TestSkin
|
||||||
|
{
|
||||||
|
HyperDashAfterImageColour = Color4.Lime
|
||||||
|
};
|
||||||
|
|
||||||
|
checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR, skin.HyperDashAfterImageColour);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCustomEndGlowColourPriority()
|
||||||
|
{
|
||||||
|
var skin = new TestSkin
|
||||||
|
{
|
||||||
|
HyperDashColour = Color4.Goldenrod,
|
||||||
|
HyperDashAfterImageColour = Color4.Lime
|
||||||
|
};
|
||||||
|
|
||||||
|
checkHyperDashCatcherColour(skin, skin.HyperDashColour, skin.HyperDashAfterImageColour);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDefaultFruitColour()
|
public void TestDefaultFruitColour()
|
||||||
{
|
{
|
||||||
@ -68,6 +110,38 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
checkHyperDashFruitColour(skin, skin.HyperDashColour);
|
checkHyperDashFruitColour(skin, skin.HyperDashColour);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
|
||||||
|
{
|
||||||
|
CatcherArea catcherArea = null;
|
||||||
|
CatcherTrailDisplay trails = null;
|
||||||
|
|
||||||
|
AddStep("create hyper-dashing catcher", () =>
|
||||||
|
{
|
||||||
|
Child = setupSkinHierarchy(catcherArea = new CatcherArea
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Scale = new Vector2(4f),
|
||||||
|
}, skin);
|
||||||
|
|
||||||
|
trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
|
||||||
|
catcherArea.MovableCatcher.SetHyperDashState(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("catcher colour is correct", () => catcherArea.MovableCatcher.Colour == expectedCatcherColour);
|
||||||
|
|
||||||
|
AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
|
||||||
|
AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour));
|
||||||
|
|
||||||
|
AddStep("finish hyper-dashing", () =>
|
||||||
|
{
|
||||||
|
catcherArea.MovableCatcher.SetHyperDashState(1);
|
||||||
|
catcherArea.MovableCatcher.FinishTransforms();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("catcher colour returned to white", () => catcherArea.MovableCatcher.Colour == Color4.White);
|
||||||
|
}
|
||||||
|
|
||||||
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
|
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
|
||||||
{
|
{
|
||||||
DrawableFruit drawableFruit = null;
|
DrawableFruit drawableFruit = null;
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
|||||||
{
|
{
|
||||||
private readonly ISkin source;
|
private readonly ISkin source;
|
||||||
|
|
||||||
public CatchLegacySkinTransformer(ISkinSource source)
|
public CatchLegacySkinTransformer(ISkin source)
|
||||||
{
|
{
|
||||||
this.source = source;
|
this.source = source;
|
||||||
}
|
}
|
||||||
|
@ -3,26 +3,37 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Animations;
|
using osu.Framework.Graphics.Animations;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Catch.Skinning;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.UI
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
{
|
{
|
||||||
public class Catcher : Container, IKeyBindingHandler<CatchAction>
|
public class Catcher : SkinReloadableDrawable, IKeyBindingHandler<CatchAction>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail
|
||||||
|
/// and end glow/after-image during a hyper-dash.
|
||||||
|
/// </summary>
|
||||||
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
|
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The duration between transitioning to hyper-dash state.
|
||||||
|
/// </summary>
|
||||||
|
public const double HYPER_DASH_TRANSITION_DURATION = 180;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether we are hyper-dashing or not.
|
/// Whether we are hyper-dashing or not.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -35,7 +46,10 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
public Container ExplodingFruitTarget;
|
public Container ExplodingFruitTarget;
|
||||||
|
|
||||||
public Container AdditiveTarget;
|
[NotNull]
|
||||||
|
private readonly Container trailsTarget;
|
||||||
|
|
||||||
|
private CatcherTrailDisplay trails;
|
||||||
|
|
||||||
public CatcherAnimationState CurrentState { get; private set; }
|
public CatcherAnimationState CurrentState { get; private set; }
|
||||||
|
|
||||||
@ -44,33 +58,23 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const float allowed_catch_range = 0.8f;
|
private const float allowed_catch_range = 0.8f;
|
||||||
|
|
||||||
protected bool Dashing
|
/// <summary>
|
||||||
|
/// The drawable catcher for <see cref="CurrentState"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal Drawable CurrentDrawableCatcher => currentCatcher.Drawable;
|
||||||
|
|
||||||
|
private bool dashing;
|
||||||
|
|
||||||
|
public bool Dashing
|
||||||
{
|
{
|
||||||
get => dashing;
|
get => dashing;
|
||||||
set
|
protected set
|
||||||
{
|
{
|
||||||
if (value == dashing) return;
|
if (value == dashing) return;
|
||||||
|
|
||||||
dashing = value;
|
dashing = value;
|
||||||
|
|
||||||
Trail |= dashing;
|
updateTrailVisibility();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Activate or deactivate the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met.
|
|
||||||
/// </summary>
|
|
||||||
protected bool Trail
|
|
||||||
{
|
|
||||||
get => trail;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value == trail || AdditiveTarget == null) return;
|
|
||||||
|
|
||||||
trail = value;
|
|
||||||
|
|
||||||
if (Trail)
|
|
||||||
beginTrail();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,18 +91,19 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
private CatcherSprite currentCatcher;
|
private CatcherSprite currentCatcher;
|
||||||
|
|
||||||
|
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||||
|
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||||
|
|
||||||
private int currentDirection;
|
private int currentDirection;
|
||||||
|
|
||||||
private bool dashing;
|
|
||||||
|
|
||||||
private bool trail;
|
|
||||||
|
|
||||||
private double hyperDashModifier = 1;
|
private double hyperDashModifier = 1;
|
||||||
private int hyperDashDirection;
|
private int hyperDashDirection;
|
||||||
private float hyperDashTargetPosition;
|
private float hyperDashTargetPosition;
|
||||||
|
|
||||||
public Catcher(BeatmapDifficulty difficulty = null)
|
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
|
||||||
{
|
{
|
||||||
|
this.trailsTarget = trailsTarget;
|
||||||
|
|
||||||
RelativePositionAxes = Axes.X;
|
RelativePositionAxes = Axes.X;
|
||||||
X = 0.5f;
|
X = 0.5f;
|
||||||
|
|
||||||
@ -114,7 +119,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
caughtFruit = new Container<DrawableHitObject>
|
caughtFruit = new Container<DrawableHitObject>
|
||||||
{
|
{
|
||||||
@ -138,6 +143,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
trailsTarget.Add(trails = new CatcherTrailDisplay(this));
|
||||||
|
|
||||||
updateCatcher();
|
updateCatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +192,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
caughtFruit.Add(fruit);
|
caughtFruit.Add(fruit);
|
||||||
|
|
||||||
Add(new HitExplosion(fruit)
|
AddInternal(new HitExplosion(fruit)
|
||||||
{
|
{
|
||||||
X = fruit.X,
|
X = fruit.X,
|
||||||
Scale = new Vector2(fruit.HitObject.Scale)
|
Scale = new Vector2(fruit.HitObject.Scale)
|
||||||
@ -240,8 +247,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyper-dashing.</param>
|
/// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyper-dashing.</param>
|
||||||
public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
|
public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
|
||||||
{
|
{
|
||||||
const float hyper_dash_transition_length = 180;
|
|
||||||
|
|
||||||
var wasHyperDashing = HyperDashing;
|
var wasHyperDashing = HyperDashing;
|
||||||
|
|
||||||
if (modifier <= 1 || X == targetPosition)
|
if (modifier <= 1 || X == targetPosition)
|
||||||
@ -250,11 +255,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
hyperDashDirection = 0;
|
hyperDashDirection = 0;
|
||||||
|
|
||||||
if (wasHyperDashing)
|
if (wasHyperDashing)
|
||||||
{
|
runHyperDashStateTransition(false);
|
||||||
this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint);
|
|
||||||
this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint);
|
|
||||||
Trail &= Dashing;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -264,20 +265,32 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
if (!wasHyperDashing)
|
if (!wasHyperDashing)
|
||||||
{
|
{
|
||||||
this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint);
|
trails.DisplayEndGlow();
|
||||||
this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint);
|
runHyperDashStateTransition(true);
|
||||||
Trail = true;
|
|
||||||
|
|
||||||
var hyperDashEndGlow = createAdditiveSprite();
|
|
||||||
|
|
||||||
hyperDashEndGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
|
|
||||||
hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.95f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In);
|
|
||||||
hyperDashEndGlow.FadeOut(1200);
|
|
||||||
hyperDashEndGlow.Expire(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void runHyperDashStateTransition(bool hyperDashing)
|
||||||
|
{
|
||||||
|
trails.HyperDashTrailsColour = hyperDashColour;
|
||||||
|
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
|
||||||
|
updateTrailVisibility();
|
||||||
|
|
||||||
|
if (hyperDashing)
|
||||||
|
{
|
||||||
|
this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||||
|
this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||||
|
this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
|
||||||
|
|
||||||
public bool OnPressed(CatchAction action)
|
public bool OnPressed(CatchAction action)
|
||||||
{
|
{
|
||||||
switch (action)
|
switch (action)
|
||||||
@ -366,6 +379,21 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
|
||||||
|
{
|
||||||
|
base.SkinChanged(skin, allowFallback);
|
||||||
|
|
||||||
|
hyperDashColour =
|
||||||
|
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
|
||||||
|
DEFAULT_HYPER_DASH_COLOUR;
|
||||||
|
|
||||||
|
hyperDashEndGlowColour =
|
||||||
|
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value ??
|
||||||
|
hyperDashColour;
|
||||||
|
|
||||||
|
runHyperDashStateTransition(HyperDashing);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
@ -411,22 +439,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
(currentCatcher.Drawable as IFramedAnimation)?.GotoFrame(0);
|
(currentCatcher.Drawable as IFramedAnimation)?.GotoFrame(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void beginTrail()
|
|
||||||
{
|
|
||||||
if (!dashing && !HyperDashing)
|
|
||||||
{
|
|
||||||
Trail = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var additive = createAdditiveSprite();
|
|
||||||
|
|
||||||
additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
|
|
||||||
additive.Expire(true);
|
|
||||||
|
|
||||||
Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateState(CatcherAnimationState state)
|
private void updateState(CatcherAnimationState state)
|
||||||
{
|
{
|
||||||
if (CurrentState == state)
|
if (CurrentState == state)
|
||||||
@ -436,25 +448,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
updateCatcher();
|
updateCatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
private CatcherTrailSprite createAdditiveSprite()
|
|
||||||
{
|
|
||||||
var tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture;
|
|
||||||
|
|
||||||
var sprite = new CatcherTrailSprite(tex)
|
|
||||||
{
|
|
||||||
Anchor = Anchor,
|
|
||||||
Scale = Scale,
|
|
||||||
Colour = HyperDashing ? Color4.Red : Color4.White,
|
|
||||||
Blending = BlendingParameters.Additive,
|
|
||||||
RelativePositionAxes = RelativePositionAxes,
|
|
||||||
Position = Position
|
|
||||||
};
|
|
||||||
|
|
||||||
AdditiveTarget?.Add(sprite);
|
|
||||||
|
|
||||||
return sprite;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeFromPlateWithTransform(DrawableHitObject fruit, Action<DrawableHitObject> action)
|
private void removeFromPlateWithTransform(DrawableHitObject fruit, Action<DrawableHitObject> action)
|
||||||
{
|
{
|
||||||
if (ExplodingFruitTarget != null)
|
if (ExplodingFruitTarget != null)
|
||||||
|
@ -33,10 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
Height = CATCHER_SIZE;
|
Height = CATCHER_SIZE;
|
||||||
Child = MovableCatcher = new Catcher(difficulty)
|
Child = MovableCatcher = new Catcher(this, difficulty);
|
||||||
{
|
|
||||||
AdditiveTarget = this,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float GetCatcherSize(BeatmapDifficulty difficulty)
|
public static float GetCatcherSize(BeatmapDifficulty difficulty)
|
||||||
|
135
osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
Normal file
135
osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Animations;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a component responsible for displaying
|
||||||
|
/// the appropriate catcher trails when requested to.
|
||||||
|
/// </summary>
|
||||||
|
public class CatcherTrailDisplay : CompositeDrawable
|
||||||
|
{
|
||||||
|
private readonly Catcher catcher;
|
||||||
|
|
||||||
|
private readonly Container<CatcherTrailSprite> dashTrails;
|
||||||
|
private readonly Container<CatcherTrailSprite> hyperDashTrails;
|
||||||
|
private readonly Container<CatcherTrailSprite> endGlowSprites;
|
||||||
|
|
||||||
|
private Color4 hyperDashTrailsColour;
|
||||||
|
|
||||||
|
public Color4 HyperDashTrailsColour
|
||||||
|
{
|
||||||
|
get => hyperDashTrailsColour;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (hyperDashTrailsColour == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
hyperDashTrailsColour = value;
|
||||||
|
hyperDashTrails.FadeColour(hyperDashTrailsColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color4 endGlowSpritesColour;
|
||||||
|
|
||||||
|
public Color4 EndGlowSpritesColour
|
||||||
|
{
|
||||||
|
get => endGlowSpritesColour;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (endGlowSpritesColour == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
endGlowSpritesColour = value;
|
||||||
|
endGlowSprites.FadeColour(endGlowSpritesColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool trail;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to start displaying trails following the catcher.
|
||||||
|
/// </summary>
|
||||||
|
public bool DisplayTrail
|
||||||
|
{
|
||||||
|
get => trail;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (trail == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
trail = value;
|
||||||
|
|
||||||
|
if (trail)
|
||||||
|
displayTrail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CatcherTrailDisplay([NotNull] Catcher catcher)
|
||||||
|
{
|
||||||
|
this.catcher = catcher ?? throw new ArgumentNullException(nameof(catcher));
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = new[]
|
||||||
|
{
|
||||||
|
dashTrails = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both },
|
||||||
|
hyperDashTrails = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||||
|
endGlowSprites = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displays a single end-glow catcher sprite.
|
||||||
|
/// </summary>
|
||||||
|
public void DisplayEndGlow()
|
||||||
|
{
|
||||||
|
var endGlow = createTrailSprite(endGlowSprites);
|
||||||
|
|
||||||
|
endGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
|
||||||
|
endGlow.ScaleTo(endGlow.Scale * 0.95f).ScaleTo(endGlow.Scale * 1.2f, 1200, Easing.In);
|
||||||
|
endGlow.FadeOut(1200);
|
||||||
|
endGlow.Expire(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayTrail()
|
||||||
|
{
|
||||||
|
if (!DisplayTrail)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var sprite = createTrailSprite(catcher.HyperDashing ? hyperDashTrails : dashTrails);
|
||||||
|
|
||||||
|
sprite.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
|
||||||
|
sprite.Expire(true);
|
||||||
|
|
||||||
|
Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CatcherTrailSprite createTrailSprite(Container<CatcherTrailSprite> target)
|
||||||
|
{
|
||||||
|
var texture = (catcher.CurrentDrawableCatcher as TextureAnimation)?.CurrentFrame ?? ((Sprite)catcher.CurrentDrawableCatcher).Texture;
|
||||||
|
|
||||||
|
var sprite = new CatcherTrailSprite(texture)
|
||||||
|
{
|
||||||
|
Anchor = catcher.Anchor,
|
||||||
|
Scale = catcher.Scale,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
RelativePositionAxes = catcher.RelativePositionAxes,
|
||||||
|
Position = catcher.Position
|
||||||
|
};
|
||||||
|
|
||||||
|
target.Add(sprite);
|
||||||
|
|
||||||
|
return sprite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||||
{
|
{
|
||||||
@ -49,6 +50,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
|
|
||||||
protected override void OnMouseUp(MouseUpEvent e)
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
{
|
{
|
||||||
|
if (e.Button != MouseButton.Left)
|
||||||
|
return;
|
||||||
|
|
||||||
base.OnMouseUp(e);
|
base.OnMouseUp(e);
|
||||||
EndPlacement(true);
|
EndPlacement(true);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
|
|||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||||
{
|
{
|
||||||
@ -46,6 +47,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
{
|
{
|
||||||
|
if (e.Button != MouseButton.Left)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (Column == null)
|
if (Column == null)
|
||||||
return base.OnMouseDown(e);
|
return base.OnMouseDown(e);
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||||
{
|
{
|
||||||
@ -30,6 +31,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
{
|
{
|
||||||
|
if (e.Button != MouseButton.Left)
|
||||||
|
return false;
|
||||||
|
|
||||||
base.OnMouseDown(e);
|
base.OnMouseDown(e);
|
||||||
|
|
||||||
// Place the note immediately.
|
// Place the note immediately.
|
||||||
|
@ -0,0 +1,223 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Taiko.Judgements;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Scoring;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TestSceneDrawableTaikoMascot : TaikoSkinnableTestScene
|
||||||
|
{
|
||||||
|
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
|
||||||
|
{
|
||||||
|
typeof(DrawableTaikoMascot),
|
||||||
|
typeof(TaikoMascotAnimation)
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
[Cached(typeof(IScrollingInfo))]
|
||||||
|
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
|
||||||
|
{
|
||||||
|
Direction = { Value = ScrollingDirection.Left },
|
||||||
|
TimeRange = { Value = 5000 },
|
||||||
|
};
|
||||||
|
|
||||||
|
private TaikoScoreProcessor scoreProcessor;
|
||||||
|
|
||||||
|
private IEnumerable<DrawableTaikoMascot> mascots => this.ChildrenOfType<DrawableTaikoMascot>();
|
||||||
|
private IEnumerable<TaikoPlayfield> playfields => this.ChildrenOfType<TaikoPlayfield>();
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
scoreProcessor = new TaikoScoreProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStateAnimations()
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => setBeatmap());
|
||||||
|
|
||||||
|
AddStep("clear state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Clear)));
|
||||||
|
AddStep("idle state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Idle)));
|
||||||
|
AddStep("kiai state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai)));
|
||||||
|
AddStep("fail state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Fail)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInitialState()
|
||||||
|
{
|
||||||
|
AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
|
||||||
|
|
||||||
|
AddAssert("mascot initially idle", () => allMascotsIn(TaikoMascotAnimationState.Idle));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClearStateTransition()
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => setBeatmap());
|
||||||
|
|
||||||
|
AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
|
||||||
|
|
||||||
|
AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
|
||||||
|
AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }));
|
||||||
|
AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear));
|
||||||
|
AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail));
|
||||||
|
|
||||||
|
AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
|
||||||
|
AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIdleState()
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => setBeatmap());
|
||||||
|
|
||||||
|
createDrawableRuleset();
|
||||||
|
|
||||||
|
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
|
||||||
|
assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestKiaiState()
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => setBeatmap(true));
|
||||||
|
|
||||||
|
createDrawableRuleset();
|
||||||
|
|
||||||
|
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Kiai);
|
||||||
|
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Kiai);
|
||||||
|
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMissState()
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => setBeatmap());
|
||||||
|
|
||||||
|
createDrawableRuleset();
|
||||||
|
|
||||||
|
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
|
||||||
|
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
|
||||||
|
assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Fail);
|
||||||
|
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(true)]
|
||||||
|
[TestCase(false)]
|
||||||
|
public void TestClearStateOnComboMilestone(bool kiai)
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => setBeatmap(kiai));
|
||||||
|
|
||||||
|
createDrawableRuleset();
|
||||||
|
|
||||||
|
AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49);
|
||||||
|
|
||||||
|
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(true, TaikoMascotAnimationState.Kiai)]
|
||||||
|
[TestCase(false, TaikoMascotAnimationState.Idle)]
|
||||||
|
public void TestClearStateOnClearedSwell(bool kiai, TaikoMascotAnimationState expectedStateAfterClear)
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => setBeatmap(kiai));
|
||||||
|
|
||||||
|
createDrawableRuleset();
|
||||||
|
|
||||||
|
assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear);
|
||||||
|
AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLower()}", () => allMascotsIn(expectedStateAfterClear));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setBeatmap(bool kiai = false)
|
||||||
|
{
|
||||||
|
var controlPointInfo = new ControlPointInfo();
|
||||||
|
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 90 });
|
||||||
|
|
||||||
|
if (kiai)
|
||||||
|
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
|
||||||
|
|
||||||
|
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
BaseDifficulty = new BeatmapDifficulty(),
|
||||||
|
Metadata = new BeatmapMetadata
|
||||||
|
{
|
||||||
|
Artist = "Unknown",
|
||||||
|
Title = "Sample Beatmap",
|
||||||
|
AuthorString = "Craftplacer",
|
||||||
|
},
|
||||||
|
Ruleset = new TaikoRuleset().RulesetInfo
|
||||||
|
},
|
||||||
|
ControlPointInfo = controlPointInfo
|
||||||
|
});
|
||||||
|
|
||||||
|
scoreProcessor.ApplyBeatmap(Beatmap.Value.Beatmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDrawableRuleset()
|
||||||
|
{
|
||||||
|
AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded);
|
||||||
|
|
||||||
|
AddStep("create drawable ruleset", () =>
|
||||||
|
{
|
||||||
|
Beatmap.Value.Track.Start();
|
||||||
|
|
||||||
|
SetContents(() =>
|
||||||
|
{
|
||||||
|
var ruleset = new TaikoRuleset();
|
||||||
|
return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState)
|
||||||
|
{
|
||||||
|
AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
|
||||||
|
() => applyNewResult(judgementResult));
|
||||||
|
|
||||||
|
AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyNewResult(JudgementResult judgementResult)
|
||||||
|
{
|
||||||
|
scoreProcessor.ApplyResult(judgementResult);
|
||||||
|
|
||||||
|
foreach (var playfield in playfields)
|
||||||
|
{
|
||||||
|
var hit = new DrawableTestHit(new Hit(), judgementResult.Type);
|
||||||
|
Add(hit);
|
||||||
|
|
||||||
|
playfield.OnNewResult(hit, judgementResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var mascot in mascots)
|
||||||
|
{
|
||||||
|
mascot.LastResult.Value = judgementResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state);
|
||||||
|
private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.Skinning;
|
using osu.Game.Rulesets.Taiko.Skinning;
|
||||||
@ -12,11 +13,30 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
|||||||
{
|
{
|
||||||
public class TestSceneTaikoScroller : TaikoSkinnableTestScene
|
public class TestSceneTaikoScroller : TaikoSkinnableTestScene
|
||||||
{
|
{
|
||||||
|
private readonly ManualClock clock = new ManualClock();
|
||||||
|
|
||||||
|
private bool reversed;
|
||||||
|
|
||||||
public TestSceneTaikoScroller()
|
public TestSceneTaikoScroller()
|
||||||
{
|
{
|
||||||
AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty())));
|
AddStep("Load scroller", () => SetContents(() =>
|
||||||
|
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
|
||||||
|
{
|
||||||
|
Clock = new FramedClock(clock),
|
||||||
|
Height = 0.4f,
|
||||||
|
}));
|
||||||
|
|
||||||
AddToggleStep("Toggle passing", passing => this.ChildrenOfType<LegacyTaikoScroller>().ForEach(s => s.LastResult.Value =
|
AddToggleStep("Toggle passing", passing => this.ChildrenOfType<LegacyTaikoScroller>().ForEach(s => s.LastResult.Value =
|
||||||
new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss }));
|
new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss }));
|
||||||
|
|
||||||
|
AddToggleStep("toggle playback direction", reversed => this.reversed = reversed);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
clock.CurrentTime += (reversed ? -1 : 1) * Clock.ElapsedFrameTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
|||||||
{
|
{
|
||||||
public class LegacyTaikoScroller : CompositeDrawable
|
public class LegacyTaikoScroller : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
|
||||||
|
|
||||||
public LegacyTaikoScroller()
|
public LegacyTaikoScroller()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -50,37 +52,38 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
|||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
|
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
while (true)
|
// store X before checking wide enough so if we perform layout there is no positional discrepancy.
|
||||||
|
float currentX = (InternalChildren?.FirstOrDefault()?.X ?? 0) - (float)Clock.ElapsedFrameTime * 0.1f;
|
||||||
|
|
||||||
|
// ensure we have enough sprites
|
||||||
|
if (!InternalChildren.Any()
|
||||||
|
|| InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count < ScreenSpaceDrawQuad.Width * 2)
|
||||||
|
AddInternal(new ScrollerSprite { Passing = passing });
|
||||||
|
|
||||||
|
var first = InternalChildren.First();
|
||||||
|
var last = InternalChildren.Last();
|
||||||
|
|
||||||
|
foreach (var sprite in InternalChildren)
|
||||||
{
|
{
|
||||||
float? additiveX = null;
|
// add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale.
|
||||||
|
sprite.X = currentX;
|
||||||
|
currentX += sprite.DrawWidth;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var sprite in InternalChildren)
|
if (first.ScreenSpaceDrawQuad.TopLeft.X >= ScreenSpaceDrawQuad.TopLeft.X)
|
||||||
{
|
{
|
||||||
// add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale.
|
foreach (var internalChild in InternalChildren)
|
||||||
sprite.X = additiveX ??= sprite.X - (float)Time.Elapsed * 0.1f;
|
internalChild.X -= first.DrawWidth;
|
||||||
|
}
|
||||||
|
|
||||||
additiveX += sprite.DrawWidth - 1;
|
if (last.ScreenSpaceDrawQuad.TopRight.X <= ScreenSpaceDrawQuad.TopRight.X)
|
||||||
|
{
|
||||||
if (sprite.X + sprite.DrawWidth < 0)
|
foreach (var internalChild in InternalChildren)
|
||||||
sprite.Expire();
|
internalChild.X += first.DrawWidth;
|
||||||
}
|
|
||||||
|
|
||||||
var last = InternalChildren.LastOrDefault();
|
|
||||||
|
|
||||||
// only break from this loop once we have saturated horizontal space completely.
|
|
||||||
if (last != null && last.ScreenSpaceDrawQuad.TopRight.X >= ScreenSpaceDrawQuad.TopRight.X)
|
|
||||||
break;
|
|
||||||
|
|
||||||
AddInternal(new ScrollerSprite
|
|
||||||
{
|
|
||||||
Passing = passing
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Skinning
|
namespace osu.Game.Rulesets.Taiko.Skinning
|
||||||
@ -86,11 +87,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case TaikoSkinComponents.TaikoScroller:
|
case TaikoSkinComponents.Scroller:
|
||||||
if (GetTexture("taiko-slider") != null)
|
if (GetTexture("taiko-slider") != null)
|
||||||
return new LegacyTaikoScroller();
|
return new LegacyTaikoScroller();
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
case TaikoSkinComponents.Mascot:
|
||||||
|
if (GetTexture("pippidonclear0") != null)
|
||||||
|
return new DrawableTaikoMascot();
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return source.GetDrawableComponent(component);
|
return source.GetDrawableComponent(component);
|
||||||
|
@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
TaikoExplosionMiss,
|
TaikoExplosionMiss,
|
||||||
TaikoExplosionGood,
|
TaikoExplosionGood,
|
||||||
TaikoExplosionGreat,
|
TaikoExplosionGreat,
|
||||||
TaikoScroller
|
Scroller,
|
||||||
|
Mascot,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
123
osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
Normal file
123
osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// 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.Audio.Track;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Taiko.Judgements;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.UI
|
||||||
|
{
|
||||||
|
public class DrawableTaikoMascot : BeatSyncedContainer
|
||||||
|
{
|
||||||
|
public readonly Bindable<TaikoMascotAnimationState> State;
|
||||||
|
public readonly Bindable<JudgementResult> LastResult;
|
||||||
|
|
||||||
|
private readonly Dictionary<TaikoMascotAnimationState, TaikoMascotAnimation> animations;
|
||||||
|
private TaikoMascotAnimation currentAnimation;
|
||||||
|
|
||||||
|
private bool lastObjectHit = true;
|
||||||
|
private bool kiaiMode;
|
||||||
|
|
||||||
|
public DrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle)
|
||||||
|
{
|
||||||
|
Origin = Anchor = Anchor.BottomLeft;
|
||||||
|
|
||||||
|
State = new Bindable<TaikoMascotAnimationState>(startingState);
|
||||||
|
LastResult = new Bindable<JudgementResult>();
|
||||||
|
|
||||||
|
animations = new Dictionary<TaikoMascotAnimationState, TaikoMascotAnimation>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader(true)]
|
||||||
|
private void load(TextureStore textures, GameplayBeatmap gameplayBeatmap)
|
||||||
|
{
|
||||||
|
InternalChildren = new[]
|
||||||
|
{
|
||||||
|
animations[TaikoMascotAnimationState.Idle] = new TaikoMascotAnimation(TaikoMascotAnimationState.Idle),
|
||||||
|
animations[TaikoMascotAnimationState.Clear] = new TaikoMascotAnimation(TaikoMascotAnimationState.Clear),
|
||||||
|
animations[TaikoMascotAnimationState.Kiai] = new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai),
|
||||||
|
animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (gameplayBeatmap != null)
|
||||||
|
((IBindable<JudgementResult>)LastResult).BindTo(gameplayBeatmap.LastJudgementResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
animations.Values.ForEach(animation => animation.Hide());
|
||||||
|
|
||||||
|
State.BindValueChanged(mascotStateChanged, true);
|
||||||
|
LastResult.BindValueChanged(onNewResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNewResult(ValueChangedEvent<JudgementResult> resultChangedEvent)
|
||||||
|
{
|
||||||
|
var result = resultChangedEvent.NewValue;
|
||||||
|
if (result == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO: missing support for clear/fail state transition at end of beatmap gameplay
|
||||||
|
|
||||||
|
if (triggerComboClear(result) || triggerSwellClear(result))
|
||||||
|
{
|
||||||
|
State.Value = TaikoMascotAnimationState.Clear;
|
||||||
|
// always consider a clear equivalent to a hit to avoid clear -> miss transitions
|
||||||
|
lastObjectHit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.Judgement.AffectsCombo)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastObjectHit = result.IsHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
|
||||||
|
{
|
||||||
|
kiaiMode = effectPoint.KiaiMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
State.Value = getNextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaikoMascotAnimationState getNextState()
|
||||||
|
{
|
||||||
|
// don't change state if current animation is still playing (and we haven't rewound before it).
|
||||||
|
// used for clear state - others are manually animated on new beats.
|
||||||
|
if (currentAnimation?.Completed == false && currentAnimation.DisplayTime <= Time.Current)
|
||||||
|
return State.Value;
|
||||||
|
|
||||||
|
if (!lastObjectHit)
|
||||||
|
return TaikoMascotAnimationState.Fail;
|
||||||
|
|
||||||
|
return kiaiMode ? TaikoMascotAnimationState.Kiai : TaikoMascotAnimationState.Idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mascotStateChanged(ValueChangedEvent<TaikoMascotAnimationState> state)
|
||||||
|
{
|
||||||
|
currentAnimation?.Hide();
|
||||||
|
currentAnimation = animations[state.NewValue];
|
||||||
|
currentAnimation.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool triggerComboClear(JudgementResult judgementResult)
|
||||||
|
=> (judgementResult.ComboAtJudgement + 1) % 50 == 0 && judgementResult.Judgement.AffectsCombo && judgementResult.IsHit;
|
||||||
|
|
||||||
|
private bool triggerSwellClear(JudgementResult judgementResult)
|
||||||
|
=> judgementResult.Judgement is TaikoSwellJudgement && judgementResult.IsHit;
|
||||||
|
}
|
||||||
|
}
|
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
{
|
{
|
||||||
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
|
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
|
||||||
|
|
||||||
AddInternal(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty())
|
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Depth = float.MaxValue
|
Depth = float.MaxValue
|
||||||
|
133
osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
Normal file
133
osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// 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.Audio.Track;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Animations;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.UI
|
||||||
|
{
|
||||||
|
public sealed class TaikoMascotAnimation : BeatSyncedContainer
|
||||||
|
{
|
||||||
|
private readonly TextureAnimation textureAnimation;
|
||||||
|
|
||||||
|
private int currentFrame;
|
||||||
|
|
||||||
|
public double DisplayTime;
|
||||||
|
|
||||||
|
public TaikoMascotAnimation(TaikoMascotAnimationState state)
|
||||||
|
{
|
||||||
|
InternalChild = textureAnimation = createTextureAnimation(state).With(animation =>
|
||||||
|
{
|
||||||
|
animation.Origin = animation.Anchor = Anchor.BottomLeft;
|
||||||
|
animation.Scale = new Vector2(0.51f); // close enough to stable
|
||||||
|
});
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
Origin = Anchor = Anchor.BottomLeft;
|
||||||
|
|
||||||
|
// needs to be always present to prevent the animation clock consuming time spent when not present.
|
||||||
|
AlwaysPresent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Completed => !textureAnimation.IsPlaying || textureAnimation.PlaybackPosition >= textureAnimation.Duration;
|
||||||
|
|
||||||
|
public override void Show()
|
||||||
|
{
|
||||||
|
base.Show();
|
||||||
|
DisplayTime = Time.Current;
|
||||||
|
textureAnimation.Seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
|
||||||
|
{
|
||||||
|
// assume that if the animation is playing on its own, it's independent from the beat and doesn't need to be touched.
|
||||||
|
if (textureAnimation.FrameCount == 0 || textureAnimation.IsPlaying)
|
||||||
|
return;
|
||||||
|
|
||||||
|
textureAnimation.GotoFrame(currentFrame);
|
||||||
|
currentFrame = (currentFrame + 1) % textureAnimation.FrameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextureAnimation createTextureAnimation(TaikoMascotAnimationState state)
|
||||||
|
{
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case TaikoMascotAnimationState.Clear:
|
||||||
|
return new ClearMascotTextureAnimation();
|
||||||
|
|
||||||
|
case TaikoMascotAnimationState.Idle:
|
||||||
|
case TaikoMascotAnimationState.Kiai:
|
||||||
|
case TaikoMascotAnimationState.Fail:
|
||||||
|
return new ManualMascotTextureAnimation(state);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(state), $"Mascot animations for state {state} are not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ManualMascotTextureAnimation : TextureAnimation
|
||||||
|
{
|
||||||
|
private readonly TaikoMascotAnimationState state;
|
||||||
|
|
||||||
|
public ManualMascotTextureAnimation(TaikoMascotAnimationState state)
|
||||||
|
{
|
||||||
|
this.state = state;
|
||||||
|
|
||||||
|
IsPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ISkinSource skin)
|
||||||
|
{
|
||||||
|
for (int frameIndex = 0; true; frameIndex++)
|
||||||
|
{
|
||||||
|
var texture = getAnimationFrame(skin, state, frameIndex);
|
||||||
|
|
||||||
|
if (texture == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
AddFrame(texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ClearMascotTextureAnimation : TextureAnimation
|
||||||
|
{
|
||||||
|
private const float clear_animation_speed = 1000 / 10f;
|
||||||
|
|
||||||
|
private static readonly int[] clear_animation_sequence = { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 };
|
||||||
|
|
||||||
|
public ClearMascotTextureAnimation()
|
||||||
|
{
|
||||||
|
DefaultFrameLength = clear_animation_speed;
|
||||||
|
Loop = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ISkinSource skin)
|
||||||
|
{
|
||||||
|
foreach (var frameIndex in clear_animation_sequence)
|
||||||
|
{
|
||||||
|
var texture = getAnimationFrame(skin, TaikoMascotAnimationState.Clear, frameIndex);
|
||||||
|
|
||||||
|
if (texture == null)
|
||||||
|
// as per https://osu.ppy.sh/help/wiki/Skinning/osu!taiko#pippidon
|
||||||
|
break;
|
||||||
|
|
||||||
|
AddFrame(texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
|
||||||
|
=> skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}");
|
||||||
|
}
|
||||||
|
}
|
13
osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs
Normal file
13
osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.UI
|
||||||
|
{
|
||||||
|
public enum TaikoMascotAnimationState
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Clear,
|
||||||
|
Kiai,
|
||||||
|
Fail
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Taiko.Judgements;
|
using osu.Game.Rulesets.Taiko.Judgements;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.UI
|
namespace osu.Game.Rulesets.Taiko.UI
|
||||||
{
|
{
|
||||||
@ -32,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
|
private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
|
||||||
private ScrollingHitObjectContainer drumRollHitContainer;
|
private ScrollingHitObjectContainer drumRollHitContainer;
|
||||||
internal Drawable HitTarget;
|
internal Drawable HitTarget;
|
||||||
|
private SkinnableDrawable mascot;
|
||||||
|
|
||||||
private ProxyContainer topLevelHitContainer;
|
private ProxyContainer topLevelHitContainer;
|
||||||
private ProxyContainer barlineContainer;
|
private ProxyContainer barlineContainer;
|
||||||
@ -125,12 +127,20 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mascot = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Mascot), _ => Empty())
|
||||||
|
{
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Anchor = Anchor.TopLeft,
|
||||||
|
RelativePositionAxes = Axes.Y,
|
||||||
|
RelativeSizeAxes = Axes.None,
|
||||||
|
Y = 0.2f
|
||||||
|
},
|
||||||
topLevelHitContainer = new ProxyContainer
|
topLevelHitContainer = new ProxyContainer
|
||||||
{
|
{
|
||||||
Name = "Top level hit objects",
|
Name = "Top level hit objects",
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
drumRollHitContainer.CreateProxy()
|
drumRollHitContainer.CreateProxy(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +152,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
// This is basically allowing for correct alignment as relative pieces move around them.
|
// This is basically allowing for correct alignment as relative pieces move around them.
|
||||||
rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth };
|
rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth };
|
||||||
hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
|
hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
|
||||||
|
|
||||||
|
mascot.Scale = new Vector2(DrawHeight / DEFAULT_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Add(DrawableHitObject h)
|
public override void Add(DrawableHitObject h)
|
||||||
|
@ -13,18 +13,16 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
|
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
|
||||||
private const float default_aspect = 16f / 9f;
|
private const float default_aspect = 16f / 9f;
|
||||||
|
|
||||||
public TaikoPlayfieldAdjustmentContainer()
|
|
||||||
{
|
|
||||||
Anchor = Anchor.CentreLeft;
|
|
||||||
Origin = Anchor.CentreLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
|
float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
|
||||||
Size = new Vector2(1, default_relative_height * aspectAdjust);
|
Size = new Vector2(1, default_relative_height * aspectAdjust);
|
||||||
|
|
||||||
|
// Position the taiko playfield exactly one playfield from the top of the screen.
|
||||||
|
RelativePositionAxes = Axes.Y;
|
||||||
|
Y = Size.Y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,23 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Configuration;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.IO;
|
||||||
|
|
||||||
namespace osu.Game.Tests.NonVisual
|
namespace osu.Game.Tests.NonVisual
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class CustomDataDirectoryTest
|
public class CustomDataDirectoryTest
|
||||||
{
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(customPath))
|
||||||
|
Directory.Delete(customPath, true);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDefaultDirectory()
|
public void TestDefaultDirectory()
|
||||||
{
|
{
|
||||||
@ -108,6 +117,109 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMigration()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigration)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = loadOsu(host);
|
||||||
|
var storage = osu.Dependencies.Get<Storage>();
|
||||||
|
|
||||||
|
// ensure we perform a save
|
||||||
|
host.Dependencies.Get<FrameworkConfigManager>().Save();
|
||||||
|
|
||||||
|
// ensure we "use" cache
|
||||||
|
host.Storage.GetStorageForDirectory("cache");
|
||||||
|
|
||||||
|
// for testing nested files are not ignored (only top level)
|
||||||
|
host.Storage.GetStorageForDirectory("test-nested").GetStorageForDirectory("cache");
|
||||||
|
|
||||||
|
string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, "headless", nameof(TestMigration));
|
||||||
|
|
||||||
|
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
|
||||||
|
|
||||||
|
osu.Migrate(customPath);
|
||||||
|
|
||||||
|
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
|
||||||
|
|
||||||
|
// ensure cache was not moved
|
||||||
|
Assert.That(host.Storage.ExistsDirectory("cache"));
|
||||||
|
|
||||||
|
// ensure nested cache was moved
|
||||||
|
Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
|
||||||
|
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
|
||||||
|
|
||||||
|
foreach (var file in OsuStorage.IGNORE_FILES)
|
||||||
|
{
|
||||||
|
Assert.That(host.Storage.Exists(file), Is.True);
|
||||||
|
Assert.That(storage.Exists(file), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
|
||||||
|
{
|
||||||
|
Assert.That(host.Storage.ExistsDirectory(dir), Is.True);
|
||||||
|
Assert.That(storage.ExistsDirectory(dir), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.That(new StreamReader(host.Storage.GetStream("storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMigrationBetweenTwoTargets()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = loadOsu(host);
|
||||||
|
|
||||||
|
string customPath2 = $"{customPath}-2";
|
||||||
|
|
||||||
|
const string database_filename = "client.db";
|
||||||
|
|
||||||
|
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||||
|
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
|
||||||
|
|
||||||
|
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
|
||||||
|
Assert.That(File.Exists(Path.Combine(customPath2, database_filename)));
|
||||||
|
|
||||||
|
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||||
|
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMigrationToSameTargetFails()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = loadOsu(host);
|
||||||
|
|
||||||
|
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||||
|
Assert.Throws<InvalidOperationException>(() => osu.Migrate(customPath));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private OsuGameBase loadOsu(GameHost host)
|
private OsuGameBase loadOsu(GameHost host)
|
||||||
{
|
{
|
||||||
var osu = new OsuGameBase();
|
var osu = new OsuGameBase();
|
||||||
|
19
osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
Normal file
19
osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 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.Platform;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Settings
|
||||||
|
{
|
||||||
|
public class TestSceneDirectorySelector : OsuTestScene
|
||||||
|
{
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(GameHost host)
|
||||||
|
{
|
||||||
|
Add(new DirectorySelector { RelativeSizeAxes = Axes.Both });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -160,5 +160,13 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void FlushConnections()
|
||||||
|
{
|
||||||
|
foreach (var context in threadContexts.Values)
|
||||||
|
context.Dispose();
|
||||||
|
|
||||||
|
recycleThreadContexts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
osu.Game/Extensions/WebRequestExtensions.cs
Normal file
23
osu.Game/Extensions/WebRequestExtensions.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.
|
||||||
|
|
||||||
|
using osu.Framework.IO.Network;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
|
||||||
|
namespace osu.Game.Extensions
|
||||||
|
{
|
||||||
|
public static class WebRequestExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Add a pagination cursor to the web request in the format required by osu-web.
|
||||||
|
/// </summary>
|
||||||
|
public static void AddCursor(this WebRequest webRequest, Cursor cursor)
|
||||||
|
{
|
||||||
|
cursor?.Properties.ForEach(x =>
|
||||||
|
{
|
||||||
|
webRequest.AddParameter("cursor[" + x.Key + "]", x.Value.ToString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
273
osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs
Normal file
273
osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics.UserInterfaceV2
|
||||||
|
{
|
||||||
|
public class DirectorySelector : CompositeDrawable
|
||||||
|
{
|
||||||
|
private FillFlowContainer directoryFlow;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private GameHost host { get; set; }
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly Bindable<DirectoryInfo> currentDirectory = new Bindable<DirectoryInfo>();
|
||||||
|
|
||||||
|
public DirectorySelector(string initialPath = null)
|
||||||
|
{
|
||||||
|
currentDirectory.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding(10);
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new CurrentDirectoryDisplay
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Height = 50,
|
||||||
|
},
|
||||||
|
new OsuScrollContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = directoryFlow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
currentDirectory.BindValueChanged(updateDisplay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDisplay(ValueChangedEvent<DirectoryInfo> directory)
|
||||||
|
{
|
||||||
|
directoryFlow.Clear();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (directory.NewValue == null)
|
||||||
|
{
|
||||||
|
var drives = DriveInfo.GetDrives();
|
||||||
|
|
||||||
|
foreach (var drive in drives)
|
||||||
|
directoryFlow.Add(new DirectoryPiece(drive.RootDirectory));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
directoryFlow.Add(new ParentDirectoryPiece(currentDirectory.Value.Parent));
|
||||||
|
|
||||||
|
foreach (var dir in currentDirectory.Value.GetDirectories().OrderBy(d => d.Name))
|
||||||
|
{
|
||||||
|
if ((dir.Attributes & FileAttributes.Hidden) == 0)
|
||||||
|
directoryFlow.Add(new DirectoryPiece(dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
currentDirectory.Value = directory.OldValue;
|
||||||
|
|
||||||
|
this.FlashColour(Color4.Red, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CurrentDirectoryDisplay : CompositeDrawable
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private Bindable<DirectoryInfo> currentDirectory { get; set; }
|
||||||
|
|
||||||
|
private FillFlowContainer flow;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
flow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Spacing = new Vector2(5),
|
||||||
|
Height = DirectoryPiece.HEIGHT,
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
currentDirectory.BindValueChanged(updateDisplay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDisplay(ValueChangedEvent<DirectoryInfo> dir)
|
||||||
|
{
|
||||||
|
flow.Clear();
|
||||||
|
|
||||||
|
List<DirectoryPiece> pathPieces = new List<DirectoryPiece>();
|
||||||
|
|
||||||
|
DirectoryInfo ptr = dir.NewValue;
|
||||||
|
|
||||||
|
while (ptr != null)
|
||||||
|
{
|
||||||
|
pathPieces.Insert(0, new CurrentDisplayPiece(ptr));
|
||||||
|
ptr = ptr.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
flow.ChildrenEnumerable = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText { Text = "Current Directory: ", Font = OsuFont.Default.With(size: DirectoryPiece.HEIGHT), },
|
||||||
|
new ComputerPiece(),
|
||||||
|
}.Concat(pathPieces);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ComputerPiece : CurrentDisplayPiece
|
||||||
|
{
|
||||||
|
protected override IconUsage? Icon => null;
|
||||||
|
|
||||||
|
public ComputerPiece()
|
||||||
|
: base(null, "Computer")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CurrentDisplayPiece : DirectoryPiece
|
||||||
|
{
|
||||||
|
public CurrentDisplayPiece(DirectoryInfo directory, string displayName = null)
|
||||||
|
: base(directory, displayName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Flow.Add(new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Icon = FontAwesome.Solid.ChevronRight,
|
||||||
|
Size = new Vector2(FONT_SIZE / 2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? base.Icon : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ParentDirectoryPiece : DirectoryPiece
|
||||||
|
{
|
||||||
|
protected override IconUsage? Icon => FontAwesome.Solid.Folder;
|
||||||
|
|
||||||
|
public ParentDirectoryPiece(DirectoryInfo directory)
|
||||||
|
: base(directory, "..")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DirectoryPiece : CompositeDrawable
|
||||||
|
{
|
||||||
|
public const float HEIGHT = 20;
|
||||||
|
|
||||||
|
protected const float FONT_SIZE = 16;
|
||||||
|
|
||||||
|
protected readonly DirectoryInfo Directory;
|
||||||
|
|
||||||
|
private readonly string displayName;
|
||||||
|
|
||||||
|
protected FillFlowContainer Flow;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private Bindable<DirectoryInfo> currentDirectory { get; set; }
|
||||||
|
|
||||||
|
public DirectoryPiece(DirectoryInfo directory, string displayName = null)
|
||||||
|
{
|
||||||
|
Directory = directory;
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
Masking = true;
|
||||||
|
CornerRadius = 5;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = colours.GreySeafoamDarker,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
Flow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Height = 20,
|
||||||
|
Margin = new MarginPadding { Vertical = 2, Horizontal = 5 },
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
Spacing = new Vector2(5),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Icon.HasValue)
|
||||||
|
{
|
||||||
|
Flow.Add(new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Icon = Icon.Value,
|
||||||
|
Size = new Vector2(FONT_SIZE)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow.Add(new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Text = displayName ?? Directory.Name,
|
||||||
|
Font = OsuFont.Default.With(size: FONT_SIZE)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
currentDirectory.Value = Directory;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar)
|
||||||
|
? FontAwesome.Solid.Database
|
||||||
|
: FontAwesome.Regular.Folder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
@ -9,17 +13,119 @@ namespace osu.Game.IO
|
|||||||
{
|
{
|
||||||
public class OsuStorage : WrappedStorage
|
public class OsuStorage : WrappedStorage
|
||||||
{
|
{
|
||||||
|
private readonly GameHost host;
|
||||||
|
private readonly StorageConfigManager storageConfig;
|
||||||
|
|
||||||
|
internal static readonly string[] IGNORE_DIRECTORIES = { "cache" };
|
||||||
|
|
||||||
|
internal static readonly string[] IGNORE_FILES =
|
||||||
|
{
|
||||||
|
"framework.ini",
|
||||||
|
"storage.ini"
|
||||||
|
};
|
||||||
|
|
||||||
public OsuStorage(GameHost host)
|
public OsuStorage(GameHost host)
|
||||||
: base(host.Storage, string.Empty)
|
: base(host.Storage, string.Empty)
|
||||||
{
|
{
|
||||||
var storageConfig = new StorageConfigManager(host.Storage);
|
this.host = host;
|
||||||
|
|
||||||
|
storageConfig = new StorageConfigManager(host.Storage);
|
||||||
|
|
||||||
var customStoragePath = storageConfig.Get<string>(StorageConfig.FullPath);
|
var customStoragePath = storageConfig.Get<string>(StorageConfig.FullPath);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(customStoragePath))
|
if (!string.IsNullOrEmpty(customStoragePath))
|
||||||
{
|
|
||||||
ChangeTargetStorage(host.GetStorage(customStoragePath));
|
ChangeTargetStorage(host.GetStorage(customStoragePath));
|
||||||
Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
|
}
|
||||||
|
|
||||||
|
protected override void ChangeTargetStorage(Storage newStorage)
|
||||||
|
{
|
||||||
|
base.ChangeTargetStorage(newStorage);
|
||||||
|
Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Migrate(string newLocation)
|
||||||
|
{
|
||||||
|
var source = new DirectoryInfo(GetFullPath("."));
|
||||||
|
var destination = new DirectoryInfo(newLocation);
|
||||||
|
|
||||||
|
// ensure the new location has no files present, else hard abort
|
||||||
|
if (destination.Exists)
|
||||||
|
{
|
||||||
|
if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
|
||||||
|
throw new InvalidOperationException("Migration destination already has files present");
|
||||||
|
|
||||||
|
deleteRecursive(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyRecursive(source, destination);
|
||||||
|
|
||||||
|
ChangeTargetStorage(host.GetStorage(newLocation));
|
||||||
|
|
||||||
|
storageConfig.Set(StorageConfig.FullPath, newLocation);
|
||||||
|
storageConfig.Save();
|
||||||
|
|
||||||
|
deleteRecursive(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
|
||||||
|
{
|
||||||
|
foreach (System.IO.FileInfo fi in target.GetFiles())
|
||||||
|
{
|
||||||
|
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
fi.Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (DirectoryInfo dir in target.GetDirectories())
|
||||||
|
{
|
||||||
|
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
dir.Delete(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
|
||||||
|
{
|
||||||
|
// based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo
|
||||||
|
Directory.CreateDirectory(destination.FullName);
|
||||||
|
|
||||||
|
foreach (System.IO.FileInfo fi in source.GetFiles())
|
||||||
|
{
|
||||||
|
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
attemptCopy(fi, Path.Combine(destination.FullName, fi.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (DirectoryInfo dir in source.GetDirectories())
|
||||||
|
{
|
||||||
|
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void attemptCopy(System.IO.FileInfo fileInfo, string destination)
|
||||||
|
{
|
||||||
|
int tries = 5;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fileInfo.CopyTo(destination, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
if (tries-- == 0)
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,12 @@ using Newtonsoft.Json.Linq;
|
|||||||
namespace osu.Game.IO.Serialization.Converters
|
namespace osu.Game.IO.Serialization.Converters
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A type of <see cref="JsonConverter"/> that serializes a <see cref="List{T}"/> alongside
|
/// A type of <see cref="JsonConverter"/> that serializes an <see cref="IReadOnlyList{T}"/> alongside
|
||||||
/// a lookup table for the types contained. The lookup table is used in deserialization to
|
/// a lookup table for the types contained. The lookup table is used in deserialization to
|
||||||
/// reconstruct the objects with their original types.
|
/// reconstruct the objects with their original types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of objects contained in the <see cref="List{T}"/> this attribute is attached to.</typeparam>
|
/// <typeparam name="T">The type of objects contained in the <see cref="IReadOnlyList{T}"/> this attribute is attached to.</typeparam>
|
||||||
public class TypedListConverter<T> : JsonConverter
|
public class TypedListConverter<T> : JsonConverter<IReadOnlyList<T>>
|
||||||
{
|
{
|
||||||
private readonly bool requiresTypeVersion;
|
private readonly bool requiresTypeVersion;
|
||||||
|
|
||||||
@ -36,9 +36,7 @@ namespace osu.Game.IO.Serialization.Converters
|
|||||||
this.requiresTypeVersion = requiresTypeVersion;
|
this.requiresTypeVersion = requiresTypeVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanConvert(Type objectType) => objectType == typeof(List<T>);
|
public override IReadOnlyList<T> ReadJson(JsonReader reader, Type objectType, IReadOnlyList<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||||
|
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
|
||||||
{
|
{
|
||||||
var list = new List<T>();
|
var list = new List<T>();
|
||||||
|
|
||||||
@ -59,14 +57,12 @@ namespace osu.Game.IO.Serialization.Converters
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, IReadOnlyList<T> value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var list = (IEnumerable<T>)value;
|
|
||||||
|
|
||||||
var lookupTable = new List<string>();
|
var lookupTable = new List<string>();
|
||||||
var objects = new List<JObject>();
|
var objects = new List<JObject>();
|
||||||
|
|
||||||
foreach (var item in list)
|
foreach (var item in value)
|
||||||
{
|
{
|
||||||
var type = item.GetType();
|
var type = item.GetType();
|
||||||
var assemblyName = type.Assembly.GetName();
|
var assemblyName = type.Assembly.GetName();
|
||||||
|
@ -11,26 +11,22 @@ namespace osu.Game.IO.Serialization.Converters
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A type of <see cref="JsonConverter"/> that serializes only the X and Y coordinates of a <see cref="Vector2"/>.
|
/// A type of <see cref="JsonConverter"/> that serializes only the X and Y coordinates of a <see cref="Vector2"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Vector2Converter : JsonConverter
|
public class Vector2Converter : JsonConverter<Vector2>
|
||||||
{
|
{
|
||||||
public override bool CanConvert(Type objectType) => objectType == typeof(Vector2);
|
public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||||
|
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
|
||||||
{
|
{
|
||||||
var obj = JObject.Load(reader);
|
var obj = JObject.Load(reader);
|
||||||
return new Vector2((float)obj["x"], (float)obj["y"]);
|
return new Vector2((float)obj["x"], (float)obj["y"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var vector = (Vector2)value;
|
|
||||||
|
|
||||||
writer.WriteStartObject();
|
writer.WriteStartObject();
|
||||||
|
|
||||||
writer.WritePropertyName("x");
|
writer.WritePropertyName("x");
|
||||||
writer.WriteValue(vector.X);
|
writer.WriteValue(value.X);
|
||||||
writer.WritePropertyName("y");
|
writer.WritePropertyName("y");
|
||||||
writer.WriteValue(vector.Y);
|
writer.WriteValue(value.Y);
|
||||||
|
|
||||||
writer.WriteEndObject();
|
writer.WriteEndObject();
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ namespace osu.Game.IO
|
|||||||
|
|
||||||
protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path;
|
protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path;
|
||||||
|
|
||||||
protected void ChangeTargetStorage(Storage newStorage)
|
protected virtual void ChangeTargetStorage(Storage newStorage)
|
||||||
{
|
{
|
||||||
UnderlyingStorage = newStorage;
|
UnderlyingStorage = newStorage;
|
||||||
}
|
}
|
||||||
|
20
osu.Game/Online/API/Requests/Cursor.cs
Normal file
20
osu.Game/Online/API/Requests/Cursor.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A collection of parameters which should be passed to the search endpoint to fetch the next page.
|
||||||
|
/// </summary>
|
||||||
|
public class Cursor
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, JToken> Properties;
|
||||||
|
}
|
||||||
|
}
|
@ -7,10 +7,7 @@ namespace osu.Game.Online.API.Requests
|
|||||||
{
|
{
|
||||||
public abstract class ResponseWithCursor
|
public abstract class ResponseWithCursor
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// A collection of parameters which should be passed to the search endpoint to fetch the next page.
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("cursor")]
|
[JsonProperty("cursor")]
|
||||||
public dynamic CursorJson;
|
public Cursor Cursor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
|
using osu.Game.Extensions;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.BeatmapListing;
|
using osu.Game.Overlays.BeatmapListing;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -10,29 +11,31 @@ namespace osu.Game.Online.API.Requests
|
|||||||
{
|
{
|
||||||
public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse>
|
public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse>
|
||||||
{
|
{
|
||||||
public SearchCategory SearchCategory { get; set; }
|
public SearchCategory SearchCategory { get; }
|
||||||
|
|
||||||
public SortCriteria SortCriteria { get; set; }
|
public SortCriteria SortCriteria { get; }
|
||||||
|
|
||||||
public SortDirection SortDirection { get; set; }
|
public SortDirection SortDirection { get; }
|
||||||
|
|
||||||
public SearchGenre Genre { get; set; }
|
public SearchGenre Genre { get; }
|
||||||
|
|
||||||
public SearchLanguage Language { get; set; }
|
public SearchLanguage Language { get; }
|
||||||
|
|
||||||
private readonly string query;
|
private readonly string query;
|
||||||
private readonly RulesetInfo ruleset;
|
private readonly RulesetInfo ruleset;
|
||||||
|
private readonly Cursor cursor;
|
||||||
|
|
||||||
private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc";
|
private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc";
|
||||||
|
|
||||||
public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset)
|
public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending)
|
||||||
{
|
{
|
||||||
this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
|
this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
|
||||||
this.ruleset = ruleset;
|
this.ruleset = ruleset;
|
||||||
|
this.cursor = cursor;
|
||||||
|
|
||||||
SearchCategory = SearchCategory.Any;
|
SearchCategory = searchCategory;
|
||||||
SortCriteria = SortCriteria.Ranked;
|
SortCriteria = sortCriteria;
|
||||||
SortDirection = SortDirection.Descending;
|
SortDirection = sortDirection;
|
||||||
Genre = SearchGenre.Any;
|
Genre = SearchGenre.Any;
|
||||||
Language = SearchLanguage.Any;
|
Language = SearchLanguage.Any;
|
||||||
}
|
}
|
||||||
@ -55,6 +58,8 @@ namespace osu.Game.Online.API.Requests
|
|||||||
|
|
||||||
req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}");
|
req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}");
|
||||||
|
|
||||||
|
req.AddCursor(cursor);
|
||||||
|
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
protected BackButton BackButton;
|
protected BackButton BackButton;
|
||||||
|
|
||||||
protected SettingsPanel Settings;
|
protected SettingsOverlay Settings;
|
||||||
|
|
||||||
private VolumeOverlay volume;
|
private VolumeOverlay volume;
|
||||||
private OsuLogo osuLogo;
|
private OsuLogo osuLogo;
|
||||||
@ -767,13 +767,20 @@ namespace osu.Game
|
|||||||
|
|
||||||
private Task asyncLoadStream;
|
private Task asyncLoadStream;
|
||||||
|
|
||||||
private T loadComponentSingleFile<T>(T d, Action<T> add, bool cache = false)
|
/// <summary>
|
||||||
|
/// Queues loading the provided component in sequential fashion.
|
||||||
|
/// This operation is limited to a single thread to avoid saturating all cores.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="component">The component to load.</param>
|
||||||
|
/// <param name="loadCompleteAction">An action to invoke on load completion (generally to add the component to the hierarchy).</param>
|
||||||
|
/// <param name="cache">Whether to cache the component as type <typeparamref name="T"/> into the game dependencies before any scheduling.</param>
|
||||||
|
private T loadComponentSingleFile<T>(T component, Action<T> loadCompleteAction, bool cache = false)
|
||||||
where T : Drawable
|
where T : Drawable
|
||||||
{
|
{
|
||||||
if (cache)
|
if (cache)
|
||||||
dependencies.Cache(d);
|
dependencies.CacheAs(component);
|
||||||
|
|
||||||
if (d is OverlayContainer overlay)
|
if (component is OverlayContainer overlay)
|
||||||
overlays.Add(overlay);
|
overlays.Add(overlay);
|
||||||
|
|
||||||
// schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached).
|
// schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached).
|
||||||
@ -791,12 +798,12 @@ namespace osu.Game
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.Log($"Loading {d}...", level: LogLevel.Debug);
|
Logger.Log($"Loading {component}...", level: LogLevel.Debug);
|
||||||
|
|
||||||
// Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called
|
// Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called
|
||||||
// throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true
|
// throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true
|
||||||
Task task = null;
|
Task task = null;
|
||||||
var del = new ScheduledDelegate(() => task = LoadComponentAsync(d, add));
|
var del = new ScheduledDelegate(() => task = LoadComponentAsync(component, loadCompleteAction));
|
||||||
Scheduler.Add(del);
|
Scheduler.Add(del);
|
||||||
|
|
||||||
// The delegate won't complete if OsuGame has been disposed in the meantime
|
// The delegate won't complete if OsuGame has been disposed in the meantime
|
||||||
@ -811,7 +818,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
await task;
|
await task;
|
||||||
|
|
||||||
Logger.Log($"Loaded {d}!", level: LogLevel.Debug);
|
Logger.Log($"Loaded {component}!", level: LogLevel.Debug);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@ -819,7 +826,7 @@ namespace osu.Game
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return d;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnScroll(ScrollEvent e)
|
protected override bool OnScroll(ScrollEvent e)
|
||||||
|
@ -328,6 +328,8 @@ namespace osu.Game
|
|||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
RulesetStore?.Dispose();
|
RulesetStore?.Dispose();
|
||||||
|
|
||||||
|
contextFactory.FlushConnections();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OsuUserInputManager : UserInputManager
|
private class OsuUserInputManager : UserInputManager
|
||||||
@ -355,5 +357,11 @@ namespace osu.Game
|
|||||||
public override bool ChangeFocusOnClick => false;
|
public override bool ChangeFocusOnClick => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Migrate(string path)
|
||||||
|
{
|
||||||
|
contextFactory.FlushConnections();
|
||||||
|
(Storage as OsuStorage)?.Migrate(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,25 +22,46 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
{
|
{
|
||||||
public class BeatmapListingFilterControl : CompositeDrawable
|
public class BeatmapListingFilterControl : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when a search finishes. Contains only new items in the case of pagination.
|
||||||
|
/// </summary>
|
||||||
public Action<List<BeatmapSetInfo>> SearchFinished;
|
public Action<List<BeatmapSetInfo>> SearchFinished;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when search criteria change.
|
||||||
|
/// </summary>
|
||||||
public Action SearchStarted;
|
public Action SearchStarted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when pagination has reached the end of available results.
|
||||||
|
/// </summary>
|
||||||
|
private bool noMoreResults;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current page fetched of results (zero index).
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentPage { get; private set; }
|
||||||
|
|
||||||
|
private readonly BeatmapListingSearchControl searchControl;
|
||||||
|
private readonly BeatmapListingSortTabControl sortControl;
|
||||||
|
private readonly Box sortControlBackground;
|
||||||
|
|
||||||
|
private ScheduledDelegate queryChangedDebounce;
|
||||||
|
|
||||||
|
private SearchBeatmapSetsRequest getSetsRequest;
|
||||||
|
private SearchBeatmapSetsResponse lastResponse;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesets { get; set; }
|
private RulesetStore rulesets { get; set; }
|
||||||
|
|
||||||
private readonly BeatmapListingSearchControl searchControl;
|
|
||||||
private readonly BeatmapListingSortTabControl sortControl;
|
|
||||||
private readonly Box sortControlBackground;
|
|
||||||
|
|
||||||
private SearchBeatmapSetsRequest getSetsRequest;
|
|
||||||
|
|
||||||
public BeatmapListingFilterControl()
|
public BeatmapListingFilterControl()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
|
|
||||||
InternalChild = new FillFlowContainer
|
InternalChild = new FillFlowContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
@ -114,51 +135,84 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
sortDirection.BindValueChanged(_ => queueUpdateSearch());
|
sortDirection.BindValueChanged(_ => queueUpdateSearch());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduledDelegate queryChangedDebounce;
|
public void TakeFocus() => searchControl.TakeFocus();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetch the next page of results. May result in a no-op if a fetch is already in progress, or if there are no results left.
|
||||||
|
/// </summary>
|
||||||
|
public void FetchNextPage()
|
||||||
|
{
|
||||||
|
// there may be no results left.
|
||||||
|
if (noMoreResults)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// there may already be an active request.
|
||||||
|
if (getSetsRequest != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (lastResponse != null)
|
||||||
|
CurrentPage++;
|
||||||
|
|
||||||
|
performRequest();
|
||||||
|
}
|
||||||
|
|
||||||
private void queueUpdateSearch(bool queryTextChanged = false)
|
private void queueUpdateSearch(bool queryTextChanged = false)
|
||||||
{
|
{
|
||||||
SearchStarted?.Invoke();
|
SearchStarted?.Invoke();
|
||||||
|
|
||||||
getSetsRequest?.Cancel();
|
resetSearch();
|
||||||
|
|
||||||
queryChangedDebounce?.Cancel();
|
queryChangedDebounce = Scheduler.AddDelayed(() =>
|
||||||
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
|
{
|
||||||
|
resetSearch();
|
||||||
|
FetchNextPage();
|
||||||
|
}, queryTextChanged ? 500 : 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSearch()
|
private void performRequest()
|
||||||
{
|
{
|
||||||
getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value)
|
getSetsRequest = new SearchBeatmapSetsRequest(
|
||||||
{
|
searchControl.Query.Value,
|
||||||
SearchCategory = searchControl.Category.Value,
|
searchControl.Ruleset.Value,
|
||||||
SortCriteria = sortControl.Current.Value,
|
lastResponse?.Cursor,
|
||||||
SortDirection = sortControl.SortDirection.Value,
|
searchControl.Category.Value,
|
||||||
Genre = searchControl.Genre.Value,
|
sortControl.Current.Value,
|
||||||
Language = searchControl.Language.Value
|
sortControl.SortDirection.Value);
|
||||||
};
|
|
||||||
|
|
||||||
getSetsRequest.Success += response => Schedule(() => onSearchFinished(response));
|
getSetsRequest.Success += response =>
|
||||||
|
{
|
||||||
|
var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList();
|
||||||
|
|
||||||
|
if (sets.Count == 0)
|
||||||
|
noMoreResults = true;
|
||||||
|
|
||||||
|
lastResponse = response;
|
||||||
|
getSetsRequest = null;
|
||||||
|
|
||||||
|
SearchFinished?.Invoke(sets);
|
||||||
|
};
|
||||||
|
|
||||||
api.Queue(getSetsRequest);
|
api.Queue(getSetsRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSearchFinished(SearchBeatmapSetsResponse response)
|
private void resetSearch()
|
||||||
{
|
{
|
||||||
var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
|
noMoreResults = false;
|
||||||
|
CurrentPage = 0;
|
||||||
|
|
||||||
searchControl.BeatmapSet = response.Total == 0 ? null : beatmaps.First();
|
lastResponse = null;
|
||||||
|
|
||||||
SearchFinished?.Invoke(beatmaps);
|
getSetsRequest?.Cancel();
|
||||||
|
getSetsRequest = null;
|
||||||
|
|
||||||
|
queryChangedDebounce?.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
getSetsRequest?.Cancel();
|
resetSearch();
|
||||||
queryChangedDebounce?.Cancel();
|
|
||||||
|
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TakeFocus() => searchControl.TakeFocus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -30,6 +32,10 @@ namespace osu.Game.Overlays
|
|||||||
private Drawable currentContent;
|
private Drawable currentContent;
|
||||||
private LoadingLayer loadingLayer;
|
private LoadingLayer loadingLayer;
|
||||||
private Container panelTarget;
|
private Container panelTarget;
|
||||||
|
private FillFlowContainer<BeatmapPanel> foundContent;
|
||||||
|
private NotFoundDrawable notFoundContent;
|
||||||
|
|
||||||
|
private OverlayScrollContainer resultScrollContainer;
|
||||||
|
|
||||||
public BeatmapListingOverlay()
|
public BeatmapListingOverlay()
|
||||||
: base(OverlayColourScheme.Blue)
|
: base(OverlayColourScheme.Blue)
|
||||||
@ -48,7 +54,7 @@ namespace osu.Game.Overlays
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = ColourProvider.Background6
|
Colour = ColourProvider.Background6
|
||||||
},
|
},
|
||||||
new OverlayScrollContainer
|
resultScrollContainer = new OverlayScrollContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
ScrollbarVisible = false,
|
ScrollbarVisible = false,
|
||||||
@ -80,9 +86,14 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Padding = new MarginPadding { Horizontal = 20 }
|
Padding = new MarginPadding { Horizontal = 20 },
|
||||||
},
|
Children = new Drawable[]
|
||||||
loadingLayer = new LoadingLayer(panelTarget)
|
{
|
||||||
|
foundContent = new FillFlowContainer<BeatmapPanel>(),
|
||||||
|
notFoundContent = new NotFoundDrawable(),
|
||||||
|
loadingLayer = new LoadingLayer(panelTarget)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -110,34 +121,53 @@ namespace osu.Game.Overlays
|
|||||||
loadingLayer.Show();
|
loadingLayer.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task panelLoadDelegate;
|
||||||
|
|
||||||
private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
|
private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
|
||||||
{
|
{
|
||||||
if (!beatmaps.Any())
|
var newPanels = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
|
||||||
{
|
{
|
||||||
LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
|
Anchor = Anchor.TopCentre,
|
||||||
return;
|
Origin = Anchor.TopCentre,
|
||||||
}
|
});
|
||||||
|
|
||||||
var newPanels = new FillFlowContainer<BeatmapPanel>
|
if (filterControl.CurrentPage == 0)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
//No matches case
|
||||||
AutoSizeAxes = Axes.Y,
|
if (!newPanels.Any())
|
||||||
Spacing = new Vector2(10),
|
|
||||||
Alpha = 0,
|
|
||||||
Margin = new MarginPadding { Vertical = 15 },
|
|
||||||
ChildrenEnumerable = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
|
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
|
||||||
Origin = Anchor.TopCentre,
|
return;
|
||||||
})
|
}
|
||||||
};
|
|
||||||
|
|
||||||
LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
|
// spawn new children with the contained so we only clear old content at the last moment.
|
||||||
|
var content = new FillFlowContainer<BeatmapPanel>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(10),
|
||||||
|
Alpha = 0,
|
||||||
|
Margin = new MarginPadding { Vertical = 15 },
|
||||||
|
ChildrenEnumerable = newPanels
|
||||||
|
};
|
||||||
|
|
||||||
|
panelLoadDelegate = LoadComponentAsync(foundContent = content, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
panelLoadDelegate = LoadComponentsAsync(newPanels, loaded =>
|
||||||
|
{
|
||||||
|
lastFetchDisplayedTime = Time.Current;
|
||||||
|
foundContent.AddRange(loaded);
|
||||||
|
loaded.ForEach(p => p.FadeIn(200, Easing.OutQuint));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addContentToPlaceholder(Drawable content)
|
private void addContentToPlaceholder(Drawable content)
|
||||||
{
|
{
|
||||||
loadingLayer.Hide();
|
loadingLayer.Hide();
|
||||||
|
lastFetchDisplayedTime = Time.Current;
|
||||||
|
|
||||||
var lastContent = currentContent;
|
var lastContent = currentContent;
|
||||||
|
|
||||||
@ -149,11 +179,14 @@ namespace osu.Game.Overlays
|
|||||||
// If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
|
// If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
|
||||||
// At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
|
// At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
|
||||||
// To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
|
// To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
|
||||||
lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y);
|
lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => panelTarget.Remove(lastContent));
|
||||||
}
|
}
|
||||||
|
|
||||||
panelTarget.Add(currentContent = content);
|
if (!content.IsAlive)
|
||||||
currentContent.FadeIn(200, Easing.OutQuint);
|
panelTarget.Add(content);
|
||||||
|
content.FadeIn(200, Easing.OutQuint);
|
||||||
|
|
||||||
|
currentContent = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
@ -203,5 +236,23 @@ namespace osu.Game.Overlays
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const double time_between_fetches = 500;
|
||||||
|
|
||||||
|
private double lastFetchDisplayedTime;
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
const int pagination_scroll_distance = 500;
|
||||||
|
|
||||||
|
bool shouldShowMore = panelLoadDelegate?.IsCompleted != false
|
||||||
|
&& Time.Current - lastFetchDisplayedTime > time_between_fetches
|
||||||
|
&& (resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance));
|
||||||
|
|
||||||
|
if (shouldShowMore)
|
||||||
|
filterControl.FetchNextPage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2020.511.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2020.511.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
|
||||||
<PackageReference Include="Sentry" Version="2.1.1" />
|
<PackageReference Include="Sentry" Version="2.1.1" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.25.0" />
|
<PackageReference Include="SharpCompress" Version="0.25.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.511.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.511.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
|
Reference in New Issue
Block a user