mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Merge branch 'master' into reposition-taiko-playfield
This commit is contained in:
commit
f761c5e119
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.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
SetContents(() => new Catcher
|
||||
SetContents(() => new Catcher(new Container())
|
||||
{
|
||||
RelativePositionAxes = Axes.None,
|
||||
Anchor = Anchor.Centre,
|
||||
|
@ -26,6 +26,48 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Resolved]
|
||||
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]
|
||||
public void TestDefaultFruitColour()
|
||||
{
|
||||
@ -68,6 +110,38 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
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)
|
||||
{
|
||||
DrawableFruit drawableFruit = null;
|
||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
private readonly ISkin source;
|
||||
|
||||
public CatchLegacySkinTransformer(ISkinSource source)
|
||||
public CatchLegacySkinTransformer(ISkin source)
|
||||
{
|
||||
this.source = source;
|
||||
}
|
||||
|
@ -3,26 +3,37 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// The duration between transitioning to hyper-dash state.
|
||||
/// </summary>
|
||||
public const double HYPER_DASH_TRANSITION_DURATION = 180;
|
||||
|
||||
/// <summary>
|
||||
/// Whether we are hyper-dashing or not.
|
||||
/// </summary>
|
||||
@ -35,7 +46,10 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public Container ExplodingFruitTarget;
|
||||
|
||||
public Container AdditiveTarget;
|
||||
[NotNull]
|
||||
private readonly Container trailsTarget;
|
||||
|
||||
private CatcherTrailDisplay trails;
|
||||
|
||||
public CatcherAnimationState CurrentState { get; private set; }
|
||||
|
||||
@ -44,33 +58,23 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
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;
|
||||
set
|
||||
protected set
|
||||
{
|
||||
if (value == dashing) return;
|
||||
|
||||
dashing = value;
|
||||
|
||||
Trail |= dashing;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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();
|
||||
updateTrailVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,18 +91,19 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
private CatcherSprite currentCatcher;
|
||||
|
||||
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||
|
||||
private int currentDirection;
|
||||
|
||||
private bool dashing;
|
||||
|
||||
private bool trail;
|
||||
|
||||
private double hyperDashModifier = 1;
|
||||
private int hyperDashDirection;
|
||||
private float hyperDashTargetPosition;
|
||||
|
||||
public Catcher(BeatmapDifficulty difficulty = null)
|
||||
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
this.trailsTarget = trailsTarget;
|
||||
|
||||
RelativePositionAxes = Axes.X;
|
||||
X = 0.5f;
|
||||
|
||||
@ -114,7 +119,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
caughtFruit = new Container<DrawableHitObject>
|
||||
{
|
||||
@ -138,6 +143,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
}
|
||||
};
|
||||
|
||||
trailsTarget.Add(trails = new CatcherTrailDisplay(this));
|
||||
|
||||
updateCatcher();
|
||||
}
|
||||
|
||||
@ -185,7 +192,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
caughtFruit.Add(fruit);
|
||||
|
||||
Add(new HitExplosion(fruit)
|
||||
AddInternal(new HitExplosion(fruit)
|
||||
{
|
||||
X = fruit.X,
|
||||
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>
|
||||
public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
|
||||
{
|
||||
const float hyper_dash_transition_length = 180;
|
||||
|
||||
var wasHyperDashing = HyperDashing;
|
||||
|
||||
if (modifier <= 1 || X == targetPosition)
|
||||
@ -250,11 +255,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
hyperDashDirection = 0;
|
||||
|
||||
if (wasHyperDashing)
|
||||
{
|
||||
this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint);
|
||||
this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint);
|
||||
Trail &= Dashing;
|
||||
}
|
||||
runHyperDashStateTransition(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -264,20 +265,32 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (!wasHyperDashing)
|
||||
{
|
||||
this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint);
|
||||
this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint);
|
||||
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);
|
||||
trails.DisplayEndGlow();
|
||||
runHyperDashStateTransition(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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
base.Update();
|
||||
@ -411,22 +439,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
(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)
|
||||
{
|
||||
if (CurrentState == state)
|
||||
@ -436,25 +448,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
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)
|
||||
{
|
||||
if (ExplodingFruitTarget != null)
|
||||
|
@ -33,10 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = CATCHER_SIZE;
|
||||
Child = MovableCatcher = new Catcher(difficulty)
|
||||
{
|
||||
AdditiveTarget = this,
|
||||
};
|
||||
Child = MovableCatcher = new Catcher(this, 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.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
@ -49,6 +50,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left)
|
||||
return;
|
||||
|
||||
base.OnMouseUp(e);
|
||||
EndPlacement(true);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
@ -46,6 +47,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left)
|
||||
return false;
|
||||
|
||||
if (Column == null)
|
||||
return base.OnMouseDown(e);
|
||||
|
||||
|
@ -5,6 +5,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
@ -30,6 +31,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left)
|
||||
return false;
|
||||
|
||||
base.OnMouseDown(e);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
@ -20,10 +20,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
public TestSceneTaikoScroller()
|
||||
{
|
||||
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,
|
||||
}));
|
||||
|
||||
AddToggleStep("Toggle passing", passing => this.ChildrenOfType<LegacyTaikoScroller>().ForEach(s => s.LastResult.Value =
|
||||
new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss }));
|
||||
|
||||
|
@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
{
|
||||
public class LegacyTaikoScroller : CompositeDrawable
|
||||
{
|
||||
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
|
||||
|
||||
public LegacyTaikoScroller()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
@ -50,27 +52,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
}, true);
|
||||
}
|
||||
|
||||
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
bool wideEnough() =>
|
||||
InternalChildren.Any()
|
||||
&& InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count >= ScreenSpaceDrawQuad.Width * 2;
|
||||
|
||||
// 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 (!wideEnough())
|
||||
{
|
||||
ClearInternal();
|
||||
|
||||
while (!wideEnough())
|
||||
AddInternal(new ScrollerSprite { Passing = passing });
|
||||
}
|
||||
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();
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
@ -86,11 +87,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.TaikoScroller:
|
||||
case TaikoSkinComponents.Scroller:
|
||||
if (GetTexture("taiko-slider") != null)
|
||||
return new LegacyTaikoScroller();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.Mascot:
|
||||
if (GetTexture("pippidonclear0") != null)
|
||||
return new DrawableTaikoMascot();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.GetDrawableComponent(component);
|
||||
|
@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
TaikoExplosionMiss,
|
||||
TaikoExplosionGood,
|
||||
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)));
|
||||
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty())
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
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.Objects;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
@ -32,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
|
||||
private ScrollingHitObjectContainer drumRollHitContainer;
|
||||
internal Drawable HitTarget;
|
||||
private SkinnableDrawable mascot;
|
||||
|
||||
private ProxyContainer topLevelHitContainer;
|
||||
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
|
||||
{
|
||||
Name = "Top level hit objects",
|
||||
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.
|
||||
rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth };
|
||||
hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
|
||||
|
||||
mascot.Scale = new Vector2(DrawHeight / DEFAULT_HEIGHT);
|
||||
}
|
||||
|
||||
public override void Add(DrawableHitObject h)
|
||||
|
@ -8,14 +8,23 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.IO;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
[TestFixture]
|
||||
public class CustomDataDirectoryTest
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
if (Directory.Exists(customPath))
|
||||
Directory.Delete(customPath, true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
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)
|
||||
{
|
||||
var osu = new OsuGameBase();
|
||||
|
@ -152,11 +152,12 @@ namespace osu.Game.Tests.Skins
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSetBeatmapVersionNoFallback()
|
||||
public void TestSetBeatmapVersionFallsBackToUserSkin()
|
||||
{
|
||||
// completely ignoring beatmap versions for simplicity.
|
||||
AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m);
|
||||
AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m);
|
||||
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.7m);
|
||||
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 2.3m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -172,7 +173,6 @@ namespace osu.Game.Tests.Skins
|
||||
public void TestIniWithNoVersionFallsBackTo1()
|
||||
{
|
||||
AddStep("Parse skin with no version", () => userSource.Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(new MemoryStream())));
|
||||
AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null);
|
||||
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.0m);
|
||||
}
|
||||
|
||||
|
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.
|
||||
// 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.Platform;
|
||||
using osu.Game.Configuration;
|
||||
@ -9,17 +13,119 @@ namespace osu.Game.IO
|
||||
{
|
||||
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)
|
||||
: 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);
|
||||
|
||||
if (!string.IsNullOrEmpty(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
|
||||
{
|
||||
/// <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
|
||||
/// reconstruct the objects with their original types.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of objects contained in the <see cref="List{T}"/> this attribute is attached to.</typeparam>
|
||||
public class TypedListConverter<T> : JsonConverter
|
||||
/// <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<IReadOnlyList<T>>
|
||||
{
|
||||
private readonly bool requiresTypeVersion;
|
||||
|
||||
@ -36,9 +36,7 @@ namespace osu.Game.IO.Serialization.Converters
|
||||
this.requiresTypeVersion = requiresTypeVersion;
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(List<T>);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
public override IReadOnlyList<T> ReadJson(JsonReader reader, Type objectType, IReadOnlyList<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
var list = new List<T>();
|
||||
|
||||
@ -59,14 +57,12 @@ namespace osu.Game.IO.Serialization.Converters
|
||||
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 objects = new List<JObject>();
|
||||
|
||||
foreach (var item in list)
|
||||
foreach (var item in value)
|
||||
{
|
||||
var type = item.GetType();
|
||||
var assemblyName = type.Assembly.GetName();
|
||||
|
@ -11,26 +11,22 @@ namespace osu.Game.IO.Serialization.Converters
|
||||
/// <summary>
|
||||
/// A type of <see cref="JsonConverter"/> that serializes only the X and Y coordinates of a <see cref="Vector2"/>.
|
||||
/// </summary>
|
||||
public class Vector2Converter : JsonConverter
|
||||
public class Vector2Converter : JsonConverter<Vector2>
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(Vector2);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
var obj = JObject.Load(reader);
|
||||
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.WritePropertyName("x");
|
||||
writer.WriteValue(vector.X);
|
||||
writer.WriteValue(value.X);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(vector.Y);
|
||||
writer.WriteValue(value.Y);
|
||||
|
||||
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 void ChangeTargetStorage(Storage newStorage)
|
||||
protected virtual void ChangeTargetStorage(Storage 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
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of parameters which should be passed to the search endpoint to fetch the next page.
|
||||
/// </summary>
|
||||
[JsonProperty("cursor")]
|
||||
public dynamic CursorJson;
|
||||
public Cursor Cursor;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapListing;
|
||||
using osu.Game.Rulesets;
|
||||
@ -10,29 +11,31 @@ namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
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 RulesetInfo ruleset;
|
||||
private readonly Cursor cursor;
|
||||
|
||||
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.ruleset = ruleset;
|
||||
this.cursor = cursor;
|
||||
|
||||
SearchCategory = SearchCategory.Any;
|
||||
SortCriteria = SortCriteria.Ranked;
|
||||
SortDirection = SortDirection.Descending;
|
||||
SearchCategory = searchCategory;
|
||||
SortCriteria = sortCriteria;
|
||||
SortDirection = sortDirection;
|
||||
Genre = SearchGenre.Any;
|
||||
Language = SearchLanguage.Any;
|
||||
}
|
||||
@ -55,6 +58,8 @@ namespace osu.Game.Online.API.Requests
|
||||
|
||||
req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}");
|
||||
|
||||
req.AddCursor(cursor);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game
|
||||
|
||||
protected BackButton BackButton;
|
||||
|
||||
protected SettingsPanel Settings;
|
||||
protected SettingsOverlay Settings;
|
||||
|
||||
private VolumeOverlay volume;
|
||||
private OsuLogo osuLogo;
|
||||
@ -767,13 +767,20 @@ namespace osu.Game
|
||||
|
||||
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
|
||||
{
|
||||
if (cache)
|
||||
dependencies.Cache(d);
|
||||
dependencies.CacheAs(component);
|
||||
|
||||
if (d is OverlayContainer overlay)
|
||||
if (component is OverlayContainer 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).
|
||||
@ -791,12 +798,12 @@ namespace osu.Game
|
||||
|
||||
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
|
||||
// throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true
|
||||
Task task = null;
|
||||
var del = new ScheduledDelegate(() => task = LoadComponentAsync(d, add));
|
||||
var del = new ScheduledDelegate(() => task = LoadComponentAsync(component, loadCompleteAction));
|
||||
Scheduler.Add(del);
|
||||
|
||||
// The delegate won't complete if OsuGame has been disposed in the meantime
|
||||
@ -811,7 +818,7 @@ namespace osu.Game
|
||||
|
||||
await task;
|
||||
|
||||
Logger.Log($"Loaded {d}!", level: LogLevel.Debug);
|
||||
Logger.Log($"Loaded {component}!", level: LogLevel.Debug);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@ -819,7 +826,7 @@ namespace osu.Game
|
||||
});
|
||||
});
|
||||
|
||||
return d;
|
||||
return component;
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
|
@ -328,6 +328,8 @@ namespace osu.Game
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
RulesetStore?.Dispose();
|
||||
|
||||
contextFactory.FlushConnections();
|
||||
}
|
||||
|
||||
private class OsuUserInputManager : UserInputManager
|
||||
@ -355,5 +357,11 @@ namespace osu.Game
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when a search finishes. Contains only new items in the case of pagination.
|
||||
/// </summary>
|
||||
public Action<List<BeatmapSetInfo>> SearchFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when search criteria change.
|
||||
/// </summary>
|
||||
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]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
private readonly BeatmapListingSearchControl searchControl;
|
||||
private readonly BeatmapListingSortTabControl sortControl;
|
||||
private readonly Box sortControlBackground;
|
||||
|
||||
private SearchBeatmapSetsRequest getSetsRequest;
|
||||
|
||||
public BeatmapListingFilterControl()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
@ -114,51 +135,84 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
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)
|
||||
{
|
||||
SearchStarted?.Invoke();
|
||||
|
||||
getSetsRequest?.Cancel();
|
||||
resetSearch();
|
||||
|
||||
queryChangedDebounce?.Cancel();
|
||||
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
|
||||
queryChangedDebounce = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
resetSearch();
|
||||
FetchNextPage();
|
||||
}, queryTextChanged ? 500 : 100);
|
||||
}
|
||||
|
||||
private void updateSearch()
|
||||
private void performRequest()
|
||||
{
|
||||
getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value)
|
||||
{
|
||||
SearchCategory = searchControl.Category.Value,
|
||||
SortCriteria = sortControl.Current.Value,
|
||||
SortDirection = sortControl.SortDirection.Value,
|
||||
Genre = searchControl.Genre.Value,
|
||||
Language = searchControl.Language.Value
|
||||
};
|
||||
getSetsRequest = new SearchBeatmapSetsRequest(
|
||||
searchControl.Query.Value,
|
||||
searchControl.Ruleset.Value,
|
||||
lastResponse?.Cursor,
|
||||
searchControl.Category.Value,
|
||||
sortControl.Current.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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
getSetsRequest?.Cancel();
|
||||
queryChangedDebounce?.Cancel();
|
||||
resetSearch();
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
public void TakeFocus() => searchControl.TakeFocus();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -30,6 +32,10 @@ namespace osu.Game.Overlays
|
||||
private Drawable currentContent;
|
||||
private LoadingLayer loadingLayer;
|
||||
private Container panelTarget;
|
||||
private FillFlowContainer<BeatmapPanel> foundContent;
|
||||
private NotFoundDrawable notFoundContent;
|
||||
|
||||
private OverlayScrollContainer resultScrollContainer;
|
||||
|
||||
public BeatmapListingOverlay()
|
||||
: base(OverlayColourScheme.Blue)
|
||||
@ -48,7 +54,7 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourProvider.Background6
|
||||
},
|
||||
new OverlayScrollContainer
|
||||
resultScrollContainer = new OverlayScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
@ -80,9 +86,14 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Padding = new MarginPadding { Horizontal = 20 }
|
||||
},
|
||||
loadingLayer = new LoadingLayer(panelTarget)
|
||||
Padding = new MarginPadding { Horizontal = 20 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
foundContent = new FillFlowContainer<BeatmapPanel>(),
|
||||
notFoundContent = new NotFoundDrawable(),
|
||||
loadingLayer = new LoadingLayer(panelTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -110,34 +121,53 @@ namespace osu.Game.Overlays
|
||||
loadingLayer.Show();
|
||||
}
|
||||
|
||||
private Task panelLoadDelegate;
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
});
|
||||
|
||||
var newPanels = new FillFlowContainer<BeatmapPanel>
|
||||
if (filterControl.CurrentPage == 0)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(10),
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding { Vertical = 15 },
|
||||
ChildrenEnumerable = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
|
||||
//No matches case
|
||||
if (!newPanels.Any())
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
})
|
||||
};
|
||||
LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
|
||||
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)
|
||||
{
|
||||
loadingLayer.Hide();
|
||||
lastFetchDisplayedTime = Time.Current;
|
||||
|
||||
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.
|
||||
// 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.
|
||||
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);
|
||||
currentContent.FadeIn(200, Easing.OutQuint);
|
||||
if (!content.IsAlive)
|
||||
panelTarget.Add(content);
|
||||
content.FadeIn(200, Easing.OutQuint);
|
||||
|
||||
currentContent = content;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
api.Unregister(this);
|
||||
api?.Unregister(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,11 @@ namespace osu.Game.Skinning
|
||||
switch (lookup)
|
||||
{
|
||||
case LegacySkinConfiguration.LegacySetting s when s == LegacySkinConfiguration.LegacySetting.Version:
|
||||
if (Configuration.LegacyVersion is decimal version)
|
||||
return SkinUtils.As<TValue>(new Bindable<decimal>(version));
|
||||
// For lookup simplicity, ignore beatmap-level versioning completely.
|
||||
|
||||
// If it is decided that we need this due to beatmaps somehow using it, the default (1.0 specified in LegacySkinDecoder.CreateTemplateObject)
|
||||
// needs to be removed else it will cause incorrect skin behaviours. This is due to the config lookup having no context of which skin
|
||||
// it should be returning the version for.
|
||||
return null;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user