mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:17:51 +08:00
Merge branch 'master' into fix-taiko-scroller-time-control
This commit is contained in:
commit
4bf5477252
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -20,11 +20,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
|||||||
public TestSceneTaikoScroller()
|
public TestSceneTaikoScroller()
|
||||||
{
|
{
|
||||||
AddStep("Load scroller", () => SetContents(() =>
|
AddStep("Load scroller", () => SetContents(() =>
|
||||||
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty())
|
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
|
||||||
{
|
{
|
||||||
Clock = new FramedClock(clock),
|
Clock = new FramedClock(clock),
|
||||||
Height = 0.4f,
|
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 }));
|
||||||
|
|
||||||
|
@ -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)));
|
||||||
|
|
||||||
FrameStableComponents.Add(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)
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user