diff --git a/osu.Android.props b/osu.Android.props
index 7e6f1469f5..a384ad4c34 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 7c0b73e8c3..f9d56dfa78 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs
index 0b3d1d23e0..4ecfb7b16d 100644
--- a/osu.Game.Rulesets.Catch/Objects/Banana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
@@ -8,8 +10,27 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public class Banana : Fruit
{
+ ///
+ /// Index of banana in current shower.
+ ///
+ public int BananaIndex;
+
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement();
+
+ private static readonly List samples = new List { new BananaHitSampleInfo() };
+
+ public Banana()
+ {
+ Samples = samples;
+ }
+
+ private class BananaHitSampleInfo : HitSampleInfo
+ {
+ private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" };
+
+ public override IEnumerable LookupNames => lookupNames;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index 04a995c77e..89c51459a6 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -30,15 +30,21 @@ namespace osu.Game.Rulesets.Catch.Objects
if (spacing <= 0)
return;
- for (double i = StartTime; i <= EndTime; i += spacing)
+ double time = StartTime;
+ int i = 0;
+
+ while (time <= EndTime)
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new Banana
{
- Samples = Samples,
- StartTime = i
+ StartTime = time,
+ BananaIndex = i,
});
+
+ time += spacing;
+ i++;
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
index 01b76ceed9..a865984d45 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
@@ -40,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1);
}
+ public override void PlaySamples()
+ {
+ base.PlaySamples();
+ if (Samples != null)
+ Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f;
+ }
+
private Color4 getBananaColour()
{
switch (RNG.Next(0, 3))
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
index a692c0b697..0c56f7bcf4 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
@@ -1,23 +1,27 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Skinning;
+using osu.Game.Rulesets.Objects;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
[TestFixture]
public class TestSceneHitExplosion : ManiaSkinnableTestScene
{
+ private readonly List> hitExplosionPools = new List>();
+
public TestSceneHitExplosion()
{
int runcount = 0;
@@ -29,28 +33,40 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (runcount % 15 > 12)
return;
- CreatedDrawables.OfType().ForEach(c =>
+ int poolIndex = 0;
+
+ foreach (var c in CreatedDrawables.OfType())
{
- c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, 0),
- _ => new DefaultHitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }));
- });
+ c.Add(hitExplosionPools[poolIndex].Get(e =>
+ {
+ e.Apply(new JudgementResult(new HitObject(), runcount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
+
+ e.Anchor = Anchor.Centre;
+ e.Origin = Anchor.Centre;
+ }));
+
+ poolIndex++;
+ }
}, 100);
}
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new ColumnTestContainer(0, ManiaAction.Key1)
+ SetContents(() =>
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativePositionAxes = Axes.Y,
- Y = -0.25f,
- Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
+ var pool = new DrawablePool(5);
+ hitExplosionPools.Add(pool);
+
+ return new ColumnTestContainer(0, ManiaAction.Key1)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativePositionAxes = Axes.Y,
+ Y = -0.25f,
+ Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
+ Child = pool
+ };
});
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 972cbec4a2..ed00ed0b4c 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
index bc93bb2615..12747924de 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
@@ -6,13 +6,15 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning
{
- public class LegacyHitExplosion : LegacyManiaColumnElement
+ public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion
{
private readonly IBindable direction = new Bindable();
@@ -62,9 +64,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
explosion.Anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}
- protected override void LoadComplete()
+ public void Animate(JudgementResult result)
{
- base.LoadComplete();
+ (explosion as IFramedAnimation)?.GotoFrame(0);
explosion?.FadeInFromZero(80)
.Then().FadeOut(120);
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 642353bd0b..255ce4c064 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -9,9 +9,9 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable Action = new Bindable();
public readonly ColumnHitObjectArea HitObjectArea;
-
internal readonly Container TopLevelContainer;
+ private readonly DrawablePool hitExplosionPool;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
@@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new[]
{
+ hitExplosionPool = new DrawablePool(5),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
@@ -108,15 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, Index), _ =>
- new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick))
- {
- RelativeSizeAxes = Axes.Both
- };
-
- HitObjectArea.Explosions.Add(explosion);
-
- explosion.Delay(200).Expire(true);
+ HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
}
public bool OnPressed(ManiaAction action)
diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
index 7a047ed121..225269cf48 100644
--- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
@@ -8,6 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -15,35 +17,36 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI
{
- public class DefaultHitExplosion : CompositeDrawable
+ public class DefaultHitExplosion : CompositeDrawable, IHitExplosion
{
+ private const float default_large_faint_size = 0.8f;
+
public override bool RemoveWhenNotAlive => true;
+ [Resolved]
+ private Column column { get; set; }
+
private readonly IBindable direction = new Bindable();
- private readonly CircularContainer largeFaint;
- private readonly CircularContainer mainGlow1;
+ private CircularContainer largeFaint;
+ private CircularContainer mainGlow1;
- public DefaultHitExplosion(Color4 objectColour, bool isSmall = false)
+ public DefaultHitExplosion()
{
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.X;
Height = DefaultNotePiece.NOTE_HEIGHT;
+ }
- // scale roughly in-line with visual appearance of notes
- Scale = new Vector2(1f, 0.6f);
-
- if (isSmall)
- Scale *= 0.5f;
-
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
const float angle_variangle = 15; // should be less than 45
-
const float roundness = 80;
-
const float initial_height = 10;
- var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
+ var colour = Interpolation.ValueAt(0.4f, column.AccentColour, Color4.White, 0, 1);
InternalChildren = new Drawable[]
{
@@ -54,12 +57,12 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both,
Masking = true,
// we want our size to be very small so the glow dominates it.
- Size = new Vector2(0.8f),
+ Size = new Vector2(default_large_faint_size),
Blending = BlendingParameters.Additive,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+ Colour = Interpolation.ValueAt(0.1f, column.AccentColour, Color4.White, 0, 1).Opacity(0.3f),
Roundness = 160,
Radius = 200,
},
@@ -74,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.UI
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+ Colour = Interpolation.ValueAt(0.6f, column.AccentColour, Color4.White, 0, 1),
Roundness = 20,
Radius = 50,
},
@@ -114,30 +117,11 @@ namespace osu.Game.Rulesets.Mania.UI
},
}
};
- }
- [BackgroundDependencyLoader]
- private void load(IScrollingInfo scrollingInfo)
- {
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
}
- protected override void LoadComplete()
- {
- const double duration = 200;
-
- base.LoadComplete();
-
- largeFaint
- .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
- .FadeOut(duration * 2);
-
- mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint);
-
- this.FadeOut(duration, Easing.Out);
- }
-
private void onDirectionChanged(ValueChangedEvent direction)
{
if (direction.NewValue == ScrollingDirection.Up)
@@ -151,5 +135,29 @@ namespace osu.Game.Rulesets.Mania.UI
Y = -DefaultNotePiece.NOTE_HEIGHT / 2;
}
}
+
+ public void Animate(JudgementResult result)
+ {
+ // scale roughly in-line with visual appearance of notes
+ Vector2 scale = new Vector2(1, 0.6f);
+
+ if (result.Judgement is HoldNoteTickJudgement)
+ scale *= 0.5f;
+
+ this.ScaleTo(scale);
+
+ largeFaint
+ .ResizeTo(default_large_faint_size)
+ .Then()
+ .ResizeTo(default_large_faint_size * new Vector2(5, 1), PoolableHitExplosion.DURATION, Easing.OutQuint)
+ .FadeOut(PoolableHitExplosion.DURATION * 2);
+
+ mainGlow1
+ .ScaleTo(1)
+ .Then()
+ .ScaleTo(1.4f, PoolableHitExplosion.DURATION, Easing.OutQuint);
+
+ this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index 8797f014df..d99f6cb8d3 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -15,6 +15,10 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
+ public DrawableManiaJudgement()
+ {
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs
new file mode 100644
index 0000000000..3252dcc276
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Judgements;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// Common interface for all hit explosion bodies.
+ ///
+ public interface IHitExplosion
+ {
+ ///
+ /// Begins animating this .
+ ///
+ /// The type of that caused this explosion.
+ void Animate(JudgementResult result);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
new file mode 100644
index 0000000000..64b7d7d550
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ public class PoolableHitExplosion : PoolableDrawable
+ {
+ public const double DURATION = 200;
+
+ public JudgementResult Result { get; private set; }
+
+ [Resolved]
+ private Column column { get; set; }
+
+ private SkinnableDrawable skinnableExplosion;
+
+ public PoolableHitExplosion()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), _ => new DefaultHitExplosion())
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+
+ public void Apply(JudgementResult result)
+ {
+ Result = result;
+ }
+
+ protected override void PrepareForUse()
+ {
+ base.PrepareForUse();
+
+ (skinnableExplosion?.Drawable as IHitExplosion)?.Animate(Result);
+
+ this.Delay(DURATION).Then().Expire();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index faa04dea97..36780b0f80 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@@ -33,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI
public IReadOnlyList Columns => columnFlow.Children;
private readonly FillFlowContainer columnFlow;
- public Container Judgements => judgements;
private readonly JudgementContainer judgements;
+ private readonly DrawablePool judgementPool;
private readonly Drawable barLineContainer;
private readonly Container topLevelContainer;
@@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new Drawable[]
{
+ judgementPool = new DrawablePool(2),
new Container
{
Anchor = Anchor.TopCentre,
@@ -208,12 +210,14 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- judgements.Clear();
- judgements.Add(new DrawableManiaJudgement(result, judgedObject)
+ judgements.Clear(false);
+ judgements.Add(judgementPool.Get(j =>
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- });
+ j.Apply(result, judgedObject);
+
+ j.Anchor = Anchor.Centre;
+ j.Origin = Anchor.Centre;
+ }));
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
index a0a38fc47b..cad98185ce 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
@@ -1,12 +1,27 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
public abstract class OsuSkinnableTestScene : SkinnableTestScene
{
+ private Container content;
+
+ protected override Container Content
+ {
+ get
+ {
+ if (content == null)
+ base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
+
+ return content;
+ }
+ }
+
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png
new file mode 100644
index 0000000000..3811e5050f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png
new file mode 100644
index 0000000000..d84eab2f15
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png
new file mode 100644
index 0000000000..4dd4a6d319
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png
new file mode 100644
index 0000000000..c66f1c9309
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png
new file mode 100644
index 0000000000..33902186d9
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png
new file mode 100644
index 0000000000..6882a232e0
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png
new file mode 100644
index 0000000000..98a9991c2f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav
new file mode 100644
index 0000000000..5e583e77aa
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav
new file mode 100644
index 0000000000..bba19381f1
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index a9404f665a..6a689a1f80 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -26,19 +25,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture]
public class TestSceneSlider : OsuSkinnableTestScene
{
- private Container content;
-
- protected override Container Content
- {
- get
- {
- if (content == null)
- base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
-
- return content;
- }
- }
-
private int depthIndex;
public TestSceneSlider()
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index 67afc45e32..47b3926ceb 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -4,57 +4,76 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneSpinner : OsuTestScene
+ public class TestSceneSpinner : OsuSkinnableTestScene
{
- private readonly Container content;
- protected override Container Content => content;
-
private int depthIndex;
- public TestSceneSpinner()
- {
- base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
+ private TestDrawableSpinner drawableSpinner;
- AddStep("Miss Big", () => testSingle(2));
- AddStep("Miss Medium", () => testSingle(5));
- AddStep("Miss Small", () => testSingle(7));
- AddStep("Hit Big", () => testSingle(2, true));
- AddStep("Hit Medium", () => testSingle(5, true));
- AddStep("Hit Small", () => testSingle(7, true));
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestVariousSpinners(bool autoplay)
+ {
+ string term = autoplay ? "Hit" : "Miss";
+ AddStep($"{term} Big", () => SetContents(() => testSingle(2, autoplay)));
+ AddStep($"{term} Medium", () => SetContents(() => testSingle(5, autoplay)));
+ AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay)));
}
- private void testSingle(float circleSize, bool auto = false)
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestLongSpinner(bool autoplay)
{
- var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 };
+ AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 2000)));
+ AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
+ AddUntilStep("Check correct progress", () => drawableSpinner.Progress == (autoplay ? 1 : 0));
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestSuperShortSpinner(bool autoplay)
+ {
+ AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 200)));
+ AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
+ AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1);
+ }
+
+ private Drawable testSingle(float circleSize, bool auto = false, double length = 3000)
+ {
+ const double delay = 2000;
+
+ var spinner = new Spinner
+ {
+ StartTime = Time.Current + delay,
+ EndTime = Time.Current + delay + length
+ };
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
- var drawable = new TestDrawableSpinner(spinner, auto)
+ drawableSpinner = new TestDrawableSpinner(spinner, auto)
{
Anchor = Anchor.Centre,
Depth = depthIndex++
};
foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawable });
+ mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
- Add(drawable);
+ return drawableSpinner;
}
private class TestDrawableSpinner : DrawableSpinner
{
- private bool auto;
+ private readonly bool auto;
public TestDrawableSpinner(Spinner s, bool auto)
: base(s)
@@ -62,16 +81,11 @@ namespace osu.Game.Rulesets.Osu.Tests
this.auto = auto;
}
- protected override void CheckForResult(bool userTriggered, double timeOffset)
+ protected override void Update()
{
- if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1)
- {
- // force completion only once to not break human interaction
- Disc.CumulativeRotation = Spinner.SpinsRequired * 360;
- auto = false;
- }
-
- base.CheckForResult(userTriggered, timeOffset);
+ base.Update();
+ if (auto)
+ RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index c36bec391f..69857f8ef9 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
+using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
@@ -60,66 +62,75 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerRewindingRotation()
{
+ double trackerRotationTolerance = 0;
+
addSeekStep(5000);
- AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100));
- AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100));
+ AddStep("calculate rotation tolerance", () =>
+ {
+ trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
+ });
+ AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
+ AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
addSeekStep(0);
- AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100));
- AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100));
+ AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance));
+ AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
}
[Test]
public void TestSpinnerMiddleRewindingRotation()
{
- double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0;
+ double finalCumulativeTrackerRotation = 0;
+ double finalTrackerRotation = 0, trackerRotationTolerance = 0;
+ double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000);
- AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.Disc.Rotation);
- AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.Disc.CumulativeRotation);
- AddStep("retrieve spinner symbol rotation", () => finalSpinnerSymbolRotation = spinnerSymbol.Rotation);
+ AddStep("retrieve disc rotation", () =>
+ {
+ finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
+ trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
+ });
+ AddStep("retrieve spinner symbol rotation", () =>
+ {
+ finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
+ spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
+ });
+ AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.RateAdjustedRotation);
addSeekStep(2500);
- AddUntilStep("disc rotation rewound",
+ AddAssert("disc rotation rewound",
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
- () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation / 2, 100));
- AddUntilStep("symbol rotation rewound",
- () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, 100));
+ // due to the exponential damping applied we're allowing a larger margin of error of about 10%
+ // (5% relative to the final rotation value, but we're half-way through the spin).
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance));
+ AddAssert("symbol rotation rewound",
+ () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
+ AddAssert("is cumulative rotation rewound",
+ // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000);
AddAssert("is disc rotation almost same",
- () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation, 100));
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
- () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, 100));
- AddAssert("is disc rotation absolute almost same",
- () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, finalAbsoluteDiscRotation, 100));
+ () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
+ AddAssert("is cumulative rotation almost same",
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation, 100));
}
[Test]
public void TestRotationDirection([Values(true, false)] bool clockwise)
{
if (clockwise)
- {
- AddStep("flip replay", () =>
- {
- var drawableRuleset = this.ChildrenOfType().Single();
- var score = drawableRuleset.ReplayScore;
- var scoreWithFlippedReplay = new Score
- {
- ScoreInfo = score.ScoreInfo,
- Replay = flipReplay(score.Replay)
- };
- drawableRuleset.SetReplayScore(scoreWithFlippedReplay);
- });
- }
+ transformReplay(flip);
addSeekStep(5000);
- AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.Disc.Rotation > 0 : drawableSpinner.Disc.Rotation < 0);
+ AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.RotationTracker.Rotation > 0 : drawableSpinner.RotationTracker.Rotation < 0);
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
- private Replay flipReplay(Replay scoreReplay) => new Replay
+ private Replay flip(Replay scoreReplay) => new Replay
{
Frames = scoreReplay
.Frames
@@ -142,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
- return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK;
+ return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * SpinnerTick.SCORE_PER_TICK;
});
addSeekStep(0);
@@ -174,6 +185,49 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
}
+ [TestCase(0.5)]
+ [TestCase(2.0)]
+ public void TestSpinUnaffectedByClockRate(double rate)
+ {
+ double expectedProgress = 0;
+ double expectedSpm = 0;
+
+ addSeekStep(1000);
+ AddStep("retrieve spinner state", () =>
+ {
+ expectedProgress = drawableSpinner.Progress;
+ expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
+ });
+
+ addSeekStep(0);
+
+ AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
+ // autoplay replay frames use track time;
+ // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
+ // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
+ // as real time is what we care about for spinners
+ // (so we're making the spin take 1000ms in real time *always*, regardless of the track clock's rate).
+ transformReplay(replay => applyRateAdjustment(replay, rate));
+
+ addSeekStep(1000);
+ AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
+ AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
+ }
+
+ private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
+ {
+ Frames = scoreReplay
+ .Frames
+ .Cast()
+ .Select(replayFrame =>
+ {
+ var adjustedTime = replayFrame.Time * rate;
+ return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
+ })
+ .Cast()
+ .ToList()
+ };
+
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => track.Seek(time));
@@ -181,6 +235,18 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
+ private void transformReplay(Func replayTransformation) => AddStep("set replay", () =>
+ {
+ var drawableRuleset = this.ChildrenOfType().Single();
+ var score = drawableRuleset.ReplayScore;
+ var transformedScore = new Score
+ {
+ ScoreInfo = score.ScoreInfo,
+ Replay = replayTransformation.Invoke(score.Replay)
+ };
+ drawableRuleset.SetReplayScore(transformedScore);
+ });
+
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects = new List
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index d6a68abaf2..f3837ea6b1 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index fdba03f260..08fd13915d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -82,9 +82,7 @@ namespace osu.Game.Rulesets.Osu.Mods
case DrawableSpinner spinner:
// hide elements we don't care about.
- spinner.Disc.Hide();
- spinner.Ticks.Hide();
- spinner.Background.Hide();
+ // todo: hide background
using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true))
spinner.FadeOut(fadeOutDuration);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index 7b54baa99b..47d765fecd 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
var spinner = (DrawableSpinner)drawable;
- spinner.Disc.Tracking = true;
- spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
+ spinner.RotationTracker.Tracking = true;
+ spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index 774f9cf58b..d7582f3196 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods
var h = drawableOsu.HitObject;
+ //todo: expose and hide spinner background somehow
+
switch (drawable)
{
case DrawableHitCircle circle:
@@ -56,11 +58,6 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.Body.OnSkinChanged += () => applySliderState(slider);
applySliderState(slider);
break;
-
- case DrawableSpinner spinner:
- spinner.Disc.Hide();
- spinner.Background.Hide();
- break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 854fc4c91c..a438dc8be4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -14,8 +14,8 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
-using osuTK;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index ecf78efdd9..f10d11827b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -3,21 +3,19 @@
using System;
using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using osuTK;
-using osuTK.Graphics;
-using osu.Game.Graphics;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Rulesets.Objects;
-using osu.Framework.Utils;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
+using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -27,28 +25,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Container ticks;
- public readonly SpinnerDisc Disc;
- public readonly SpinnerTicks Ticks;
+ public readonly SpinnerRotationTracker RotationTracker;
public readonly SpinnerSpmCounter SpmCounter;
private readonly SpinnerBonusDisplay bonusDisplay;
- private readonly Container mainContainer;
-
- public readonly SpinnerBackground Background;
- private readonly Container circleContainer;
- private readonly CirclePiece circle;
- private readonly GlowPiece glow;
-
- private readonly SpriteIcon symbol;
-
- private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c");
- private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c");
-
private readonly IBindable positionBindable = new Bindable();
- private Color4 normalColour;
- private Color4 completeColour;
-
public DrawableSpinner(Spinner s)
: base(s)
{
@@ -57,66 +39,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RelativeSizeAxes = Axes.Both;
- // we are slightly bigger than our parent, to clip the top and bottom of the circle
- Height = 1.3f;
-
Spinner = s;
InternalChildren = new Drawable[]
{
ticks = new Container(),
- circleContainer = new Container
- {
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new Drawable[]
- {
- glow = new GlowPiece(),
- circle = new CirclePiece
- {
- Position = Vector2.Zero,
- Anchor = Anchor.Centre,
- },
- new RingPiece(),
- symbol = new SpriteIcon
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(48),
- Icon = FontAwesome.Solid.Asterisk,
- Shadow = false,
- },
- }
- },
- mainContainer = new AspectContainer
+ new AspectContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
- Children = new[]
+ Children = new Drawable[]
{
- Background = new SpinnerBackground
- {
- Disc =
- {
- Alpha = 0f,
- },
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- Disc = new SpinnerDisc(Spinner)
- {
- Scale = Vector2.Zero,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- circleContainer.CreateProxy(),
- Ticks = new SpinnerTicks
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
+ new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()),
+ RotationTracker = new SpinnerRotationTracker(Spinner)
}
},
SpmCounter = new SpinnerSpmCounter
@@ -135,6 +71,58 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
};
}
+ private Bindable isSpinning;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ isSpinning = RotationTracker.IsSpinning.GetBoundCopy();
+ isSpinning.BindValueChanged(updateSpinningSample);
+ }
+
+ private SkinnableSound spinningSample;
+
+ private const float minimum_volume = 0.0001f;
+
+ protected override void LoadSamples()
+ {
+ base.LoadSamples();
+
+ spinningSample?.Expire();
+ spinningSample = null;
+
+ var firstSample = HitObject.Samples.FirstOrDefault();
+
+ if (firstSample != null)
+ {
+ var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
+ clone.Name = "spinnerspin";
+
+ AddInternal(spinningSample = new SkinnableSound(clone)
+ {
+ Volume = { Value = minimum_volume },
+ Looping = true,
+ });
+ }
+ }
+
+ private void updateSpinningSample(ValueChangedEvent tracking)
+ {
+ // note that samples will not start playing if exiting a seek operation in the middle of a spinner.
+ // may be something we want to address at a later point, but not so easy to make happen right now
+ // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
+ if (tracking.NewValue && ShouldPlaySamples)
+ {
+ spinningSample?.Play();
+ spinningSample?.VolumeTo(1, 200);
+ }
+ else
+ {
+ spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop());
+ }
+ }
+
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
@@ -147,6 +135,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ base.UpdateStateTransforms(state);
+
+ using (BeginDelayedSequence(Spinner.Duration, true))
+ this.FadeOut(160);
+
+ // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
+ isSpinning?.TriggerChange();
+ }
+
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
@@ -170,31 +169,31 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- normalColour = baseColour;
- completeColour = colours.YellowLight;
-
- Background.AccentColour = normalColour;
- Ticks.AccentColour = normalColour;
-
- Disc.AccentColour = fillColour;
- circle.Colour = colours.BlueDark;
- glow.Colour = colours.BlueDark;
-
positionBindable.BindValueChanged(pos => Position = pos.NewValue);
positionBindable.BindTo(HitObject.PositionBindable);
}
- public float Progress => Math.Clamp(Disc.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
+ ///
+ /// The completion progress of this spinner from 0..1 (clamped).
+ ///
+ public float Progress
+ {
+ get
+ {
+ if (Spinner.SpinsRequired == 0)
+ // some spinners are so short they can't require an integer spin count.
+ // these become implicitly hit.
+ return 1;
+
+ return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / Spinner.SpinsRequired, 0, 1);
+ }
+ }
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (Time.Current < HitObject.StartTime) return;
- if (Progress >= 1 && !Disc.Complete)
- {
- Disc.Complete = true;
- transformFillColour(completeColour, 200);
- }
+ RotationTracker.Complete.Value = Progress >= 1;
if (userTriggered || Time.Current < Spinner.EndTime)
return;
@@ -219,29 +218,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update()
{
base.Update();
+
if (HandleUserInput)
- Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
+ RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
+
+ if (spinningSample != null)
+ // todo: implement SpinnerFrequencyModulate
+ spinningSample.Frequency.Value = 0.5f + Progress;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
- if (!SpmCounter.IsPresent && Disc.Tracking)
+ if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
-
- circle.Rotation = Disc.Rotation;
- Ticks.Rotation = Disc.Rotation;
-
- SpmCounter.SetRotation(Disc.CumulativeRotation);
+ SpmCounter.SetRotation(RotationTracker.RateAdjustedRotation);
updateBonusScore();
-
- float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
- float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress;
- Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
-
- symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
}
private int wholeSpins;
@@ -251,7 +245,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (ticks.Count == 0)
return;
- int spins = (int)(Disc.CumulativeRotation / 360);
+ int spins = (int)(RotationTracker.RateAdjustedRotation / 360);
if (spins < wholeSpins)
{
@@ -275,64 +269,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
wholeSpins++;
}
}
-
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- circleContainer.ScaleTo(0);
- mainContainer.ScaleTo(0);
-
- using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
- {
- float phaseOneScale = Spinner.Scale * 0.7f;
-
- circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 4, Easing.OutQuint);
-
- mainContainer
- .ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.6f, HitObject.TimePreempt / 4, Easing.OutQuint)
- .RotateTo((float)(25 * Spinner.Duration / 2000), HitObject.TimePreempt + Spinner.Duration);
-
- using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
- {
- circleContainer.ScaleTo(Spinner.Scale, 400, Easing.OutQuint);
- mainContainer.ScaleTo(1, 400, Easing.OutQuint);
- }
- }
- }
-
- protected override void UpdateStateTransforms(ArmedState state)
- {
- base.UpdateStateTransforms(state);
-
- using (BeginDelayedSequence(Spinner.Duration, true))
- {
- this.FadeOut(160);
-
- switch (state)
- {
- case ArmedState.Hit:
- transformFillColour(completeColour, 0);
- this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
- mainContainer.RotateTo(mainContainer.Rotation + 180, 320);
- break;
-
- case ArmedState.Miss:
- this.ScaleTo(Scale * 0.8f, 320, Easing.In);
- break;
- }
- }
- }
-
- private void transformFillColour(Colour4 colour, double duration)
- {
- Disc.FadeAccent(colour, duration);
-
- Background.FadeAccent(colour.Darken(1), duration);
- Ticks.FadeAccent(colour, duration);
-
- circle.FadeColour(colour, duration);
- glow.FadeColour(colour, duration);
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
new file mode 100644
index 0000000000..1476fe6010
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
@@ -0,0 +1,189 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
+{
+ public class DefaultSpinnerDisc : CompositeDrawable
+ {
+ private DrawableSpinner drawableSpinner;
+
+ private Spinner spinner;
+
+ private const float idle_alpha = 0.2f;
+ private const float tracking_alpha = 0.4f;
+
+ private Color4 normalColour;
+ private Color4 completeColour;
+
+ private SpinnerTicks ticks;
+
+ private int wholeRotationCount;
+
+ private SpinnerFill fill;
+ private Container mainContainer;
+ private SpinnerCentreLayer centre;
+ private SpinnerBackgroundLayer background;
+
+ public DefaultSpinnerDisc()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ // we are slightly bigger than our parent, to clip the top and bottom of the circle
+ // this should probably be revisited when scaled spinners are a thing.
+ Scale = new Vector2(1.3f);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, DrawableHitObject drawableHitObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableHitObject;
+ spinner = (Spinner)drawableSpinner.HitObject;
+
+ normalColour = colours.BlueDark;
+ completeColour = colours.YellowLight;
+
+ InternalChildren = new Drawable[]
+ {
+ mainContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ background = new SpinnerBackgroundLayer(),
+ fill = new SpinnerFill
+ {
+ Alpha = idle_alpha,
+ AccentColour = normalColour
+ },
+ ticks = new SpinnerTicks
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AccentColour = normalColour
+ },
+ }
+ },
+ centre = new SpinnerCentreLayer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
+ drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (drawableSpinner.RotationTracker.Complete.Value)
+ {
+ if (checkNewRotationCount)
+ {
+ fill.FinishTransforms(false, nameof(Alpha));
+ fill
+ .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
+ .Then()
+ .FadeTo(tracking_alpha, 250, Easing.OutQuint);
+ }
+ }
+ else
+ {
+ fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
+ }
+
+ const float initial_scale = 0.2f;
+ float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress;
+
+ fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
+ mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation;
+ }
+
+ private void updateStateTransforms(ValueChangedEvent state)
+ {
+ centre.ScaleTo(0);
+ mainContainer.ScaleTo(0);
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ {
+ // constant ambient rotation to give the spinner "spinning" character.
+ this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
+
+ centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
+ mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ {
+ centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
+ mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
+ }
+ }
+
+ // transforms we have from completing the spinner will be rolled back, so reapply immediately.
+ updateComplete(state.NewValue == ArmedState.Hit, 0);
+
+ using (BeginDelayedSequence(spinner.Duration, true))
+ {
+ switch (state.NewValue)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
+ this.RotateTo(mainContainer.Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ this.ScaleTo(Scale * 0.8f, 320, Easing.In);
+ break;
+ }
+ }
+ }
+
+ private void updateComplete(bool complete, double duration)
+ {
+ var colour = complete ? completeColour : normalColour;
+
+ ticks.FadeAccent(colour.Darken(1), duration);
+ fill.FadeAccent(colour.Darken(1), duration);
+
+ background.FadeAccent(colour, duration);
+ centre.FadeAccent(colour, duration);
+ }
+
+ private bool checkNewRotationCount
+ {
+ get
+ {
+ int rotations = (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360);
+
+ if (wholeRotationCount == rotations) return false;
+
+ wholeRotationCount = rotations;
+ return true;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs
similarity index 88%
rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs
rename to osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs
index 944354abca..043bc5618c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs
@@ -10,7 +10,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SpinnerBackground : CircularContainer, IHasAccentColour
+ public class SpinnerFill : CircularContainer, IHasAccentColour
{
public readonly Box Disc;
@@ -31,11 +31,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
}
- public SpinnerBackground()
+ public SpinnerFill()
{
RelativeSizeAxes = Axes.Both;
Masking = true;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
Children = new Drawable[]
{
Disc = new Box
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
similarity index 57%
rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
rename to osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
index 35819cd05e..f1a782cbb5 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
@@ -2,89 +2,62 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-using osuTK;
-using osuTK.Graphics;
using osu.Framework.Utils;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SpinnerDisc : CircularContainer, IHasAccentColour
+ public class SpinnerRotationTracker : CircularContainer
{
private readonly Spinner spinner;
- public Color4 AccentColour
- {
- get => background.AccentColour;
- set => background.AccentColour = value;
- }
-
- private readonly SpinnerBackground background;
-
- private const float idle_alpha = 0.2f;
- private const float tracking_alpha = 0.4f;
-
public override bool IsPresent => true; // handle input when hidden
- public SpinnerDisc(Spinner s)
+ public SpinnerRotationTracker(Spinner s)
{
spinner = s;
RelativeSizeAxes = Axes.Both;
-
- Children = new Drawable[]
- {
- background = new SpinnerBackground { Alpha = idle_alpha },
- };
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
- private bool tracking;
+ public bool Tracking { get; set; }
- public bool Tracking
- {
- get => tracking;
- set
- {
- if (value == tracking) return;
-
- tracking = value;
-
- background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100);
- }
- }
-
- private bool complete;
-
- public bool Complete
- {
- get => complete;
- set
- {
- if (value == complete) return;
-
- complete = value;
-
- updateCompleteTick();
- }
- }
+ public readonly BindableBool Complete = new BindableBool();
///
- /// The total rotation performed on the spinner disc, disregarding the spin direction.
+ /// The total rotation performed on the spinner disc, disregarding the spin direction,
+ /// adjusted for the track's playback rate.
///
///
+ ///
/// This value is always non-negative and is monotonically increasing with time
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
+ ///
+ ///
+ /// The rotation from each frame is multiplied by the clock's current playback rate.
+ /// The reason this is done is to ensure that spinners give the same score and require the same number of spins
+ /// regardless of whether speed-modifying mods are applied.
+ ///
///
///
- /// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
+ /// Assuming no speed-modifying mods are active,
+ /// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
/// this property will return the value of 720 (as opposed to 0 for ).
+ /// If Double Time is active instead (with a speed multiplier of 1.5x),
+ /// in the same scenario the property will return 720 * 1.5 = 1080.
///
- public float CumulativeRotation;
+ public float RateAdjustedRotation { get; private set; }
+
+ ///
+ /// Whether the spinning is spinning at a reasonable speed to be considered visually spinning.
+ ///
+ public readonly BindableBool IsSpinning = new BindableBool();
///
/// Whether currently in the correct time range to allow spinning.
@@ -101,9 +74,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private float lastAngle;
private float currentRotation;
- private int completeTick;
-
- private bool updateCompleteTick() => completeTick != (completeTick = (int)(CumulativeRotation / 360));
private bool rotationTransferred;
@@ -114,21 +84,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
var delta = thisAngle - lastAngle;
- if (tracking)
- Rotate(delta);
+ if (Tracking)
+ AddRotation(delta);
lastAngle = thisAngle;
- if (Complete && updateCompleteTick())
- {
- background.FinishTransforms(false, nameof(Alpha));
- background
- .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
- .Then()
- .FadeTo(tracking_alpha, 250, Easing.OutQuint);
- }
+ IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f;
- Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
+ Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed));
}
///
@@ -138,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
/// Will be a no-op if not a valid time to spin.
///
/// The delta angle.
- public void Rotate(float angle)
+ public void AddRotation(float angle)
{
if (!isSpinnableTime)
return;
@@ -161,7 +124,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
currentRotation += angle;
- CumulativeRotation += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
+ // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
+ // (see: ModTimeRamp)
+ RateAdjustedRotation += (float)(Math.Abs(angle) * Clock.Rate);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs
new file mode 100644
index 0000000000..3cd2454706
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . 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.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class SpinnerBackgroundLayer : SpinnerFill
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, DrawableHitObject drawableHitObject)
+ {
+ Disc.Alpha = 0;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs
new file mode 100644
index 0000000000..b62ce822f0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs
@@ -0,0 +1,71 @@
+// Copyright (c) ppy Pty Ltd . 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class SpinnerCentreLayer : CompositeDrawable, IHasAccentColour
+ {
+ private DrawableSpinner spinner;
+
+ private CirclePiece circle;
+ private GlowPiece glow;
+ private SpriteIcon symbol;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ spinner = (DrawableSpinner)drawableHitObject;
+
+ InternalChildren = new Drawable[]
+ {
+ glow = new GlowPiece(),
+ circle = new CirclePiece
+ {
+ Position = Vector2.Zero,
+ Anchor = Anchor.Centre,
+ },
+ new RingPiece(),
+ symbol = new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(48),
+ Icon = FontAwesome.Solid.Asterisk,
+ Shadow = false,
+ },
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, spinner.RotationTracker.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
+ }
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ accentColour = value;
+
+ circle.Colour = accentColour;
+ glow.Colour = accentColour;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 619b49926e..1658a4e7c2 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects
double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
- SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond));
+ SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
}
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index b2cdc8ccbf..5468764692 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -17,5 +17,6 @@ namespace osu.Game.Rulesets.Osu
SliderFollowCircle,
SliderBall,
SliderBody,
+ SpinnerBody
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
index e7486ef9b0..0ab3e8825b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
+using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Osu.Skinning
{
@@ -28,53 +29,67 @@ namespace osu.Game.Rulesets.Osu.Skinning
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
+ private Container circleSprites;
+ private Sprite hitCircleSprite;
+ private Sprite hitCircleOverlay;
+
+ private SkinnableSpriteText hitCircleText;
+
private readonly IBindable state = new Bindable();
private readonly Bindable accentColour = new Bindable();
private readonly IBindable indexInCurrentCombo = new Bindable();
+ [Resolved]
+ private ISkinSource skin { get; set; }
+
[BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject, ISkinSource skin)
+ private void load(DrawableHitObject drawableObject)
{
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
- Sprite hitCircleSprite;
- SkinnableSpriteText hitCircleText;
-
InternalChildren = new Drawable[]
{
- hitCircleSprite = new Sprite
+ circleSprites = new Container
{
- Texture = getTextureWithFallback(string.Empty),
- Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ hitCircleSprite = new Sprite
+ {
+ Texture = getTextureWithFallback(string.Empty),
+ Colour = drawableObject.AccentColour.Value,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ hitCircleOverlay = new Sprite
+ {
+ Texture = getTextureWithFallback("overlay"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ }
},
hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
- }, confineMode: ConfineMode.NoScaling),
- new Sprite
+ }, confineMode: ConfineMode.NoScaling)
{
- Texture = getTextureWithFallback("overlay"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- }
+ },
};
bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
- if (!overlayAboveNumber)
- ChangeInternalChildDepth(hitCircleText, -float.MaxValue);
+ if (overlayAboveNumber)
+ AddInternal(hitCircleOverlay.CreateProxy());
state.BindTo(drawableObject.State);
- state.BindValueChanged(updateState, true);
-
accentColour.BindTo(drawableObject.AccentColour);
- accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
-
indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
- indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
Texture getTextureWithFallback(string name)
{
@@ -87,6 +102,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ state.BindValueChanged(updateState, true);
+ accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
+ indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
+ }
+
private void updateState(ValueChangedEvent state)
{
const double legacy_fade_duration = 240;
@@ -94,8 +118,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
switch (state.NewValue)
{
case ArmedState.Hit:
- this.FadeOut(legacy_fade_duration, Easing.Out);
- this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
+ circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+
+ var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value;
+
+ if (legacyVersion >= 2.0m)
+ // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
+ hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
+ else
+ {
+ // old skins scale and fade it normally along other pieces.
+ hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
+ hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ }
+
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
new file mode 100644
index 0000000000..72bc3ddc9a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
@@ -0,0 +1,99 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ ///
+ /// Legacy skinned spinner with two main spinning layers, one fixed overlay and one final spinning overlay.
+ /// No background layer.
+ ///
+ public class LegacyNewStyleSpinner : CompositeDrawable
+ {
+ private Sprite discBottom;
+ private Sprite discTop;
+ private Sprite spinningMiddle;
+ private Sprite fixedMiddle;
+
+ private DrawableSpinner drawableSpinner;
+
+ private const float final_scale = 0.625f;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource source, DrawableHitObject drawableObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableObject;
+
+ Scale = new Vector2(final_scale);
+
+ InternalChildren = new Drawable[]
+ {
+ discBottom = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-bottom")
+ },
+ discTop = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-top")
+ },
+ fixedMiddle = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-middle")
+ },
+ spinningMiddle = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-middle2")
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.FadeOut();
+ drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+ }
+
+ private void updateStateTransforms(ValueChangedEvent state)
+ {
+ var spinner = (Spinner)drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ this.FadeInFromZero(spinner.TimePreempt / 2);
+
+ fixedMiddle.FadeColour(Color4.White);
+ using (BeginAbsoluteSequence(spinner.StartTime, true))
+ fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ spinningMiddle.Rotation = discTop.Rotation = drawableSpinner.RotationTracker.Rotation;
+ discBottom.Rotation = discTop.Rotation / 3;
+
+ Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
new file mode 100644
index 0000000000..0ae1d8f683
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
@@ -0,0 +1,114 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ ///
+ /// Legacy skinned spinner with one main spinning layer and a background layer.
+ ///
+ public class LegacyOldStyleSpinner : CompositeDrawable
+ {
+ private DrawableSpinner drawableSpinner;
+ private Sprite disc;
+ private Container metre;
+
+ private const float background_y_offset = 20;
+
+ private const float sprite_scale = 1 / 1.6f;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource source, DrawableHitObject drawableObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableObject;
+
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Sprite
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Texture = source.GetTexture("spinner-background"),
+ Y = background_y_offset,
+ Scale = new Vector2(sprite_scale)
+ },
+ disc = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-circle"),
+ Scale = new Vector2(sprite_scale)
+ },
+ metre = new Container
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Y = background_y_offset,
+ Masking = true,
+ Child = new Sprite
+ {
+ Texture = source.GetTexture("spinner-metre"),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ },
+ Scale = new Vector2(0.625f)
+ }
+ };
+ }
+
+ private Vector2 metreFinalSize;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.FadeOut();
+ drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+
+ metreFinalSize = metre.Size = metre.Child.Size;
+ }
+
+ private void updateStateTransforms(ValueChangedEvent state)
+ {
+ var spinner = drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ this.FadeInFromZero(spinner.TimePreempt / 2);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ disc.Rotation = drawableSpinner.RotationTracker.Rotation;
+ metre.Height = getMetreHeight(drawableSpinner.Progress);
+ }
+
+ private const int total_bars = 10;
+
+ private float getMetreHeight(float progress)
+ {
+ progress = Math.Min(99, progress * 100);
+
+ int barCount = (int)progress / 10;
+
+ // todo: add SpinnerNoBlink support
+ if (RNG.NextBool(((int)progress % 10) / 10f))
+ barCount++;
+
+ return (float)barCount / total_bars * metreFinalSize.Y;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 95ef2d58b1..81d1d05b66 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -102,6 +102,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
Scale = new Vector2(0.8f),
Spacing = new Vector2(-overlap, 0)
};
+
+ case OsuSkinComponents.SpinnerBody:
+ bool hasBackground = Source.GetTexture("spinner-background") != null;
+
+ if (Source.GetTexture("spinner-top") != null && !hasBackground)
+ return new LegacyNewStyleSpinner();
+ else if (hasBackground)
+ return new LegacyOldStyleSpinner();
+
+ return null;
}
return null;
diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
index b4d51d11c9..b2299398e1 100644
--- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.UI
protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo);
- public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer();
+ public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true };
protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay();
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 600efefca3..4ef9bbe091 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Osu.UI
{
public class OsuPlayfield : Playfield
{
- private readonly ApproachCircleProxyContainer approachCircles;
+ private readonly ProxyContainer approachCircles;
+ private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
private readonly FollowPointRenderer followPoints;
private readonly OrderedHitPolicy hitPolicy;
@@ -38,6 +39,10 @@ namespace osu.Game.Rulesets.Osu.UI
{
InternalChildren = new Drawable[]
{
+ spinnerProxies = new ProxyContainer
+ {
+ RelativeSizeAxes = Axes.Both
+ },
followPoints = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
@@ -54,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
Child = HitObjectContainer,
},
- approachCircles = new ApproachCircleProxyContainer
+ approachCircles = new ProxyContainer
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
@@ -76,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.UI
h.OnNewResult += onNewResult;
h.OnLoadComplete += d =>
{
+ if (d is DrawableSpinner)
+ spinnerProxies.Add(d.CreateProxy());
+
if (d is IDrawableHitObjectWithProxiedApproach c)
approachCircles.Add(c.ProxiedLayer.CreateProxy());
};
@@ -113,9 +121,9 @@ namespace osu.Game.Rulesets.Osu.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
- private class ApproachCircleProxyContainer : LifetimeManagementContainer
+ private class ProxyContainer : LifetimeManagementContainer
{
- public void Add(Drawable approachCircleProxy) => AddInternal(approachCircleProxy);
+ public void Add(Drawable proxy) => AddInternal(proxy);
}
private class DrawableJudgementPool : DrawablePool
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs
index 9c8be868b0..0d1a5a8304 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs
@@ -11,10 +11,19 @@ namespace osu.Game.Rulesets.Osu.UI
public class OsuPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
protected override Container Content => content;
- private readonly Container content;
+ private readonly ScalingContainer content;
private const float playfield_size_adjust = 0.8f;
+ ///
+ /// When true, an offset is applied to allow alignment with historical storyboards displayed in the same parent space.
+ /// This will shift the playfield downwards slightly.
+ ///
+ public bool AlignWithStoryboard
+ {
+ set => content.PlayfieldShift = value;
+ }
+
public OsuPlayfieldAdjustmentContainer()
{
Anchor = Anchor.Centre;
@@ -39,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.UI
///
private class ScalingContainer : Container
{
+ internal bool PlayfieldShift { get; set; }
+
protected override void Update()
{
base.Update();
@@ -55,6 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI
// Scale = 819.2 / 512
// Scale = 1.6
Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X);
+ Position = new Vector2(0, (PlayfieldShift ? 8f : 0f) * Scale.X);
// Size = 0.625
Size = Vector2.Divide(Vector2.One, Scale);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu b/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu
new file mode 100644
index 0000000000..f9755782c2
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu
@@ -0,0 +1,10 @@
+osu file format v14
+
+[General]
+Mode: 1
+
+[TimingPoints]
+0,300,4,1,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs
new file mode 100644
index 0000000000..7089ea6619
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Reflection;
+using NUnit.Framework;
+using osu.Framework.IO.Stores;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class TestSceneTaikoHitObjectSamples : HitObjectSampleTest
+ {
+ protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
+
+ protected override IResourceStore Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples)));
+
+ [TestCase("taiko-normal-hitnormal")]
+ [TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
+ public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
+ {
+ SetupSkins(expectedSample, expectedSample);
+
+ CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertBeatmapLookup(expectedSample);
+ }
+
+ [TestCase("taiko-normal-hitnormal")]
+ [TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
+ public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
+ {
+ SetupSkins(string.Empty, expectedSample);
+
+ CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertUserLookup(expectedSample);
+ }
+
+ [TestCase("taiko-normal-hitnormal2")]
+ [TestCase("normal-hitnormal2")]
+ public void TestUserSkinLookupIgnoresSampleBank(string unwantedSample)
+ {
+ SetupSkins(string.Empty, unwantedSample);
+
+ CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertNoLookup(unwantedSample);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index ada7ac5d74..e896606ee8 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
index 737946e1e0..6b95931b21 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
@@ -6,9 +6,9 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
-using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
+using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Tests.Gameplay
{
@@ -67,9 +67,11 @@ namespace osu.Game.Tests.Gameplay
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
/// normal-hitnormal2
/// normal-hitnormal
+ /// hitnormal
///
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
{
SetupSkins(expectedSample, expectedSample);
@@ -80,12 +82,13 @@ namespace osu.Game.Tests.Gameplay
}
///
- /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin when the beatmap does not contain the sample:
- /// normal-hitnormal2
+ /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin
+ /// (ignoring the custom sample set index) when the beatmap skin does not contain the sample:
/// normal-hitnormal
+ /// hitnormal
///
- [TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
{
SetupSkins(string.Empty, expectedSample);
@@ -95,6 +98,23 @@ namespace osu.Game.Tests.Gameplay
AssertUserLookup(expectedSample);
}
+ ///
+ /// Tests that a hitobject which provides a custom sample set of 2 does not retrieve a normal-hitnormal2 sample from the user skin
+ /// if the beatmap skin does not contain the sample.
+ /// User skins in stable ignore the custom sample set index when performing lookups.
+ ///
+ [Test]
+ public void TestUserSkinLookupIgnoresSampleBank()
+ {
+ const string unwanted_sample = "normal-hitnormal2";
+
+ SetupSkins(string.Empty, unwanted_sample);
+
+ CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
+
+ AssertNoLookup(unwanted_sample);
+ }
+
///
/// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin.
///
@@ -145,6 +165,7 @@ namespace osu.Game.Tests.Gameplay
///
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
public void TestControlPointCustomSampleFromBeatmap(string sampleName)
{
SetupSkins(sampleName, sampleName);
@@ -178,7 +199,7 @@ namespace osu.Game.Tests.Gameplay
string[] expectedSamples =
{
"normal-hitnormal2",
- "normal-hitwhistle2"
+ "normal-hitwhistle" // user skin lookups ignore custom sample set index
};
SetupSkins(expectedSamples[0], expectedSamples[1]);
@@ -190,7 +211,7 @@ namespace osu.Game.Tests.Gameplay
}
///
- /// Tests that when a custom sample bank is used, but is disabled,
+ /// Tests that when a custom sample bank is used, but is disabled,
/// only the additional sound will be looked up.
///
[Test]
@@ -209,7 +230,7 @@ namespace osu.Game.Tests.Gameplay
}
///
- /// Tests that when a normal sample bank is used and is disabled,
+ /// Tests that when a normal sample bank is used and is disabled,
/// the normal sound will be looked up anyway.
///
[Test]
@@ -226,6 +247,6 @@ namespace osu.Game.Tests.Gameplay
}
private void disableLayeredHitSounds()
- => AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[GlobalSkinConfiguration.LayeredHitSounds.ToString()] = "0");
+ => AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[LegacySetting.LayeredHitSounds.ToString()] = "0");
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
index 0023866124..03fd2b968c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
@@ -3,14 +3,24 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Net;
using System.Threading.Tasks;
+using JetBrains.Annotations;
+using Newtonsoft.Json.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Multi.Ranking;
+using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
@@ -18,43 +28,134 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneTimeshiftResultsScreen : ScreenTestScene
{
- private bool roomsReceived;
+ private const int scores_per_result = 10;
+
+ private TestResultsScreen resultsScreen;
+ private int currentScoreId;
+ private bool requestComplete;
[SetUp]
public void Setup() => Schedule(() =>
{
- roomsReceived = false;
+ currentScoreId = 0;
+ requestComplete = false;
bindHandler();
});
[Test]
- public void TestShowResultsWithScore()
+ public void TestShowWithUserScore()
{
- createResults(new TestScoreInfo(new OsuRuleset().RulesetInfo));
- AddWaitStep("wait for display", 5);
+ ScoreInfo userScore = null;
+
+ AddStep("bind user score info handler", () =>
+ {
+ userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ bindHandler(userScore: userScore);
+ });
+
+ createResults(() => userScore);
+ waitForDisplay();
+
+ AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
[Test]
- public void TestShowResultsNullScore()
+ public void TestShowNullUserScore()
{
- createResults(null);
- AddWaitStep("wait for display", 5);
+ createResults();
+ waitForDisplay();
+
+ AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
[Test]
- public void TestShowResultsNullScoreWithDelay()
+ public void TestShowUserScoreWithDelay()
+ {
+ ScoreInfo userScore = null;
+
+ AddStep("bind user score info handler", () =>
+ {
+ userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ bindHandler(3000, userScore);
+ });
+
+ createResults(() => userScore);
+ waitForDisplay();
+
+ AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1);
+ AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
+ }
+
+ [Test]
+ public void TestShowNullUserScoreWithDelay()
{
AddStep("bind delayed handler", () => bindHandler(3000));
- createResults(null);
- AddUntilStep("wait for rooms to be received", () => roomsReceived);
- AddWaitStep("wait for display", 5);
+
+ createResults();
+ waitForDisplay();
+
+ AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
- private void createResults(ScoreInfo score)
+ [Test]
+ public void TestFetchWhenScrolledToTheRight()
+ {
+ createResults();
+ waitForDisplay();
+
+ AddStep("bind delayed handler", () => bindHandler(3000));
+
+ for (int i = 0; i < 2; i++)
+ {
+ int beforePanelCount = 0;
+
+ AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count());
+ AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false));
+
+ AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
+ waitForDisplay();
+
+ AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result);
+ AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
+ }
+ }
+
+ [Test]
+ public void TestFetchWhenScrolledToTheLeft()
+ {
+ ScoreInfo userScore = null;
+
+ AddStep("bind user score info handler", () =>
+ {
+ userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ bindHandler(userScore: userScore);
+ });
+
+ createResults(() => userScore);
+ waitForDisplay();
+
+ AddStep("bind delayed handler", () => bindHandler(3000));
+
+ for (int i = 0; i < 2; i++)
+ {
+ int beforePanelCount = 0;
+
+ AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count());
+ AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToStart(false));
+
+ AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
+ waitForDisplay();
+
+ AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result);
+ AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
+ }
+ }
+
+ private void createResults(Func getScore = null)
{
AddStep("load results", () =>
{
- LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem
+ LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@@ -62,62 +163,214 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
- private void bindHandler(double delay = 0)
+ private void waitForDisplay()
{
- var roomScores = new List();
+ AddUntilStep("wait for request to complete", () => requestComplete);
+ AddWaitStep("wait for display", 5);
+ }
- for (int i = 0; i < 10; i++)
+ private void bindHandler(double delay = 0, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
+ {
+ requestComplete = false;
+
+ if (failRequests)
{
- roomScores.Add(new MultiplayerScore
+ triggerFail(request, delay);
+ return;
+ }
+
+ switch (request)
+ {
+ case ShowPlaylistUserScoreRequest s:
+ if (userScore == null)
+ triggerFail(s, delay);
+ else
+ triggerSuccess(s, createUserResponse(userScore), delay);
+ break;
+
+ case IndexPlaylistScoresRequest i:
+ triggerSuccess(i, createIndexResponse(i), delay);
+ break;
+ }
+ };
+
+ private void triggerSuccess(APIRequest req, T result, double delay)
+ where T : class
+ {
+ if (delay == 0)
+ success();
+ else
+ {
+ Task.Run(async () =>
{
- ID = i,
- Accuracy = 0.9 - 0.01 * i,
- EndedAt = DateTimeOffset.Now.Subtract(TimeSpan.FromHours(i)),
+ await Task.Delay(TimeSpan.FromMilliseconds(delay));
+ Schedule(success);
+ });
+ }
+
+ void success()
+ {
+ requestComplete = true;
+ req.TriggerSuccess(result);
+ }
+ }
+
+ private void triggerFail(APIRequest req, double delay)
+ {
+ if (delay == 0)
+ fail();
+ else
+ {
+ Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(delay));
+ Schedule(fail);
+ });
+ }
+
+ void fail()
+ {
+ requestComplete = true;
+ req.TriggerFailure(new WebException("Failed."));
+ }
+ }
+
+ private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore)
+ {
+ var multiplayerUserScore = new MultiplayerScore
+ {
+ ID = (int)(userScore.OnlineScoreID ?? currentScoreId++),
+ Accuracy = userScore.Accuracy,
+ EndedAt = userScore.Date,
+ Passed = userScore.Passed,
+ Rank = userScore.Rank,
+ Position = 200,
+ MaxCombo = userScore.MaxCombo,
+ TotalScore = userScore.TotalScore,
+ User = userScore.User,
+ Statistics = userScore.Statistics,
+ ScoresAround = new MultiplayerScoresAround
+ {
+ Higher = new MultiplayerScores(),
+ Lower = new MultiplayerScores()
+ }
+ };
+
+ for (int i = 1; i <= scores_per_result; i++)
+ {
+ multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ Accuracy = userScore.Accuracy,
+ EndedAt = userScore.Date,
Passed = true,
- Rank = ScoreRank.B,
- MaxCombo = 999,
- TotalScore = 999999 - i * 1000,
+ Rank = userScore.Rank,
+ MaxCombo = userScore.MaxCombo,
+ TotalScore = userScore.TotalScore - i,
User = new User
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
- Statistics =
+ Statistics = userScore.Statistics
+ });
+
+ multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ Accuracy = userScore.Accuracy,
+ EndedAt = userScore.Date,
+ Passed = true,
+ Rank = userScore.Rank,
+ MaxCombo = userScore.MaxCombo,
+ TotalScore = userScore.TotalScore + i,
+ User = new User
+ {
+ Id = 2,
+ Username = $"peppy{i}",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ },
+ Statistics = userScore.Statistics
+ });
+ }
+
+ addCursor(multiplayerUserScore.ScoresAround.Lower);
+ addCursor(multiplayerUserScore.ScoresAround.Higher);
+
+ return multiplayerUserScore;
+ }
+
+ private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req)
+ {
+ var result = new IndexedMultiplayerScores();
+
+ long startTotalScore = req.Cursor?.Properties["total_score"].ToObject() ?? 1000000;
+ string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc";
+
+ for (int i = 1; i <= scores_per_result; i++)
+ {
+ result.Scores.Add(new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ Accuracy = 1,
+ EndedAt = DateTimeOffset.Now,
+ Passed = true,
+ Rank = ScoreRank.X,
+ MaxCombo = 1000,
+ TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
+ User = new User
+ {
+ Id = 2,
+ Username = $"peppy{i}",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ },
+ Statistics = new Dictionary
{
{ HitResult.Miss, 1 },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
- { HitResult.Great, 300 },
+ { HitResult.Great, 300 }
}
});
}
- ((DummyAPIAccess)API).HandleRequest = request =>
+ addCursor(result);
+
+ return result;
+ }
+
+ private void addCursor(MultiplayerScores scores)
+ {
+ scores.Cursor = new Cursor
{
- switch (request)
+ Properties = new Dictionary
{
- case GetRoomPlaylistScoresRequest r:
- if (delay == 0)
- success();
- else
- {
- Task.Run(async () =>
- {
- await Task.Delay(TimeSpan.FromMilliseconds(delay));
- Schedule(success);
- });
- }
+ { "total_score", JToken.FromObject(scores.Scores[^1].TotalScore) },
+ { "score_id", JToken.FromObject(scores.Scores[^1].ID) },
+ }
+ };
- void success()
- {
- r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores });
- roomsReceived = true;
- }
-
- break;
+ scores.Params = new IndexScoresParams
+ {
+ Properties = new Dictionary
+ {
+ { "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") }
}
};
}
+
+ private class TestResultsScreen : TimeshiftResultsScreen
+ {
+ public new LoadingSpinner LeftSpinner => base.LeftSpinner;
+ public new LoadingSpinner CentreSpinner => base.CentreSpinner;
+ public new LoadingSpinner RightSpinner => base.RightSpinner;
+ public new ScorePanelList ScorePanelList => base.ScorePanelList;
+
+ public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
+ : base(score, roomId, playlistItem, allowRetry)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs
index 0446cadac9..17675bfbc0 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs
@@ -31,15 +31,16 @@ namespace osu.Game.Tests.Visual.Online
{
new NewsCard(new APINewsPost
{
- Title = "This post has an image which starts with \"/\" and has many authors!",
+ Title = "This post has an image which starts with \"/\" and has many authors! (clickable)",
Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
Author = "someone, someone1, someone2, someone3, someone4",
FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png",
- PublishedAt = DateTimeOffset.Now
+ PublishedAt = DateTimeOffset.Now,
+ Slug = "2020-07-16-summer-theme-park-2020-voting-open"
}),
new NewsCard(new APINewsPost
{
- Title = "This post has a full-url image! (HTML entity: &)",
+ Title = "This post has a full-url image! (HTML entity: &) (non-clickable)",
Preview = "boom (HTML entity: &)",
Author = "user (HTML entity: &)",
FirstImage = "https://assets.ppy.sh/artists/88/header.jpg",
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs
new file mode 100644
index 0000000000..78288bf6e4
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Overlays.News;
+using osu.Framework.Graphics;
+using osu.Game.Overlays;
+using osu.Framework.Allocation;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneNewsHeader : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ private TestHeader header;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Child = header = new TestHeader
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ };
+ });
+
+ [Test]
+ public void TestControl()
+ {
+ AddAssert("Front page selected", () => header.Current.Value == "frontpage");
+ AddAssert("1 tab total", () => header.TabCount == 1);
+
+ AddStep("Set article 1", () => header.SetArticle("1"));
+ AddAssert("Article 1 selected", () => header.Current.Value == "1");
+ AddAssert("2 tabs total", () => header.TabCount == 2);
+
+ AddStep("Set article 2", () => header.SetArticle("2"));
+ AddAssert("Article 2 selected", () => header.Current.Value == "2");
+ AddAssert("2 tabs total", () => header.TabCount == 2);
+
+ AddStep("Set front page", () => header.SetFrontPage());
+ AddAssert("Front page selected", () => header.Current.Value == "frontpage");
+ AddAssert("1 tab total", () => header.TabCount == 1);
+ }
+
+ private class TestHeader : NewsHeader
+ {
+ public int TabCount => TabControl.Items.Count;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
index d47c972564..37d51c16d2 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
@@ -2,65 +2,64 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Graphics;
+using NUnit.Framework;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
-using osu.Game.Overlays.News;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneNewsOverlay : OsuTestScene
{
- private TestNewsOverlay news;
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
- protected override void LoadComplete()
+ private NewsOverlay news;
+
+ [SetUp]
+ public void SetUp() => Schedule(() => Child = news = new NewsOverlay());
+
+ [Test]
+ public void TestRequest()
{
- base.LoadComplete();
- Add(news = new TestNewsOverlay());
- AddStep(@"Show", news.Show);
- AddStep(@"Hide", news.Hide);
-
- AddStep(@"Show front page", () => news.ShowFrontPage());
- AddStep(@"Custom article", () => news.Current.Value = "Test Article 101");
-
- AddStep(@"Article covers", () => news.LoadAndShowContent(new NewsCoverTest()));
+ setUpNewsResponse(responseExample);
+ AddStep("Show", () => news.Show());
+ AddStep("Show article", () => news.ShowArticle("article"));
}
- private class TestNewsOverlay : NewsOverlay
- {
- public new void LoadAndShowContent(NewsContent content) => base.LoadAndShowContent(content);
- }
-
- private class NewsCoverTest : NewsContent
- {
- public NewsCoverTest()
+ private void setUpNewsResponse(GetNewsResponse r)
+ => AddStep("set up response", () =>
{
- Spacing = new osuTK.Vector2(0, 10);
-
- var article = new NewsArticleCover.ArticleInfo
+ dummyAPI.HandleRequest = request =>
{
- Author = "Ephemeral",
- CoverUrl = "https://assets.ppy.sh/artists/58/header.jpg",
- Time = new DateTime(2019, 12, 4),
- Title = "New Featured Artist: Kurokotei"
- };
+ if (!(request is GetNewsRequest getNewsRequest))
+ return;
- Children = new Drawable[]
- {
- new NewsArticleCover(article)
- {
- Height = 200
- },
- new NewsArticleCover(article)
- {
- Height = 120
- },
- new NewsArticleCover(article)
- {
- RelativeSizeAxes = Axes.None,
- Size = new osuTK.Vector2(400, 200),
- }
+ getNewsRequest.TriggerSuccess(r);
};
+ });
+
+ private GetNewsResponse responseExample => new GetNewsResponse
+ {
+ NewsPosts = new[]
+ {
+ new APINewsPost
+ {
+ Title = "This post has an image which starts with \"/\" and has many authors!",
+ Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ Author = "someone, someone1, someone2, someone3, someone4",
+ FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png",
+ PublishedAt = DateTimeOffset.Now
+ },
+ new APINewsPost
+ {
+ Title = "This post has a full-url image! (HTML entity: &)",
+ Preview = "boom (HTML entity: &)",
+ Author = "user (HTML entity: &)",
+ FirstImage = "https://assets.ppy.sh/artists/88/header.jpg",
+ PublishedAt = DateTimeOffset.Now
+ }
}
- }
+ };
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
index 273f593c32..18ac415126 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
@@ -4,19 +4,22 @@
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Allocation;
-using osu.Game.Graphics;
+using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneShowMoreButton : OsuTestScene
{
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
public TestSceneShowMoreButton()
{
- TestButton button = null;
+ ShowMoreButton button = null;
int fireCount = 0;
- Add(button = new TestButton
+ Add(button = new ShowMoreButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -46,16 +49,5 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("action fired twice", () => fireCount == 2);
AddAssert("is in loading state", () => button.IsLoading);
}
-
- private class TestButton : ShowMoreButton
- {
- [BackgroundDependencyLoader]
- private void load(OsuColour colors)
- {
- IdleColour = colors.YellowDark;
- HoverColour = colors.Yellow;
- ChevronIconColour = colors.Red;
- }
- }
}
}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
index 3d335995ac..e06b3a8a7e 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
@@ -64,5 +64,77 @@ namespace osu.Game.Tests.Visual.Settings
}, 0, true);
});
}
+
+ [Test]
+ public void TestClearButtonOnBindings()
+ {
+ KeyBindingRow backBindingRow = null;
+
+ AddStep("click back binding row", () =>
+ {
+ backBindingRow = panel.ChildrenOfType().ElementAt(10);
+ InputManager.MoveMouseTo(backBindingRow);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ clickClearButton();
+
+ AddAssert("first binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().First().Text.Text));
+
+ AddStep("click second binding", () =>
+ {
+ var target = backBindingRow.ChildrenOfType().ElementAt(1);
+
+ InputManager.MoveMouseTo(target);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ clickClearButton();
+
+ AddAssert("second binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().ElementAt(1).Text.Text));
+
+ void clickClearButton()
+ {
+ AddStep("click clear button", () =>
+ {
+ var clearButton = backBindingRow.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(clearButton);
+ InputManager.Click(MouseButton.Left);
+ });
+ }
+ }
+
+ [Test]
+ public void TestClickRowSelectsFirstBinding()
+ {
+ KeyBindingRow backBindingRow = null;
+
+ AddStep("click back binding row", () =>
+ {
+ backBindingRow = panel.ChildrenOfType().ElementAt(10);
+ InputManager.MoveMouseTo(backBindingRow);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding);
+
+ AddStep("click second binding", () =>
+ {
+ var target = backBindingRow.ChildrenOfType().ElementAt(1);
+
+ InputManager.MoveMouseTo(target);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("click back binding row", () =>
+ {
+ backBindingRow = panel.ChildrenOfType().ElementAt(10);
+ InputManager.MoveMouseTo(backBindingRow);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
index 22ae5257e7..b347c39c1e 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
@@ -44,6 +44,7 @@ namespace osu.Game.Tests.Visual
typeof(NotificationOverlay),
typeof(BeatmapListingOverlay),
typeof(DashboardOverlay),
+ typeof(NewsOverlay),
typeof(ChannelManager),
typeof(ChatOverlay),
typeof(SettingsOverlay),
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs
new file mode 100644
index 0000000000..c51204eaba
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs
@@ -0,0 +1,150 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Overlays.Dashboard.Home;
+using osu.Game.Beatmaps;
+using osu.Game.Overlays;
+using osu.Framework.Allocation;
+using osu.Game.Users;
+using System;
+using osu.Framework.Graphics.Shapes;
+using System.Collections.Generic;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneDashboardBeatmapListing : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ private readonly Container content;
+
+ public TestSceneDashboardBeatmapListing()
+ {
+ Add(content = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Y,
+ Width = 300,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = 10 },
+ Child = new DashboardBeatmapListing(new_beatmaps, popular_beatmaps)
+ }
+ }
+ });
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AddStep("Set width to 500", () => content.ResizeWidthTo(500, 500));
+ AddStep("Set width to 300", () => content.ResizeWidthTo(300, 500));
+ }
+
+ private static readonly List new_beatmaps = new List
+ {
+ new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Very Long Title (TV size) [TATOE]",
+ Artist = "This artist has a really long name how is this possible",
+ Author = new User
+ {
+ Username = "author",
+ Id = 100
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers
+ {
+ Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
+ },
+ Ranked = DateTimeOffset.Now
+ }
+ },
+ new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Very Long Title (TV size) [TATOE]",
+ Artist = "This artist has a really long name how is this possible",
+ Author = new User
+ {
+ Username = "author",
+ Id = 100
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers
+ {
+ Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
+ },
+ Ranked = DateTimeOffset.MinValue
+ }
+ }
+ };
+
+ private static readonly List popular_beatmaps = new List
+ {
+ new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Title",
+ Artist = "Artist",
+ Author = new User
+ {
+ Username = "author",
+ Id = 100
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers
+ {
+ Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586",
+ },
+ FavouriteCount = 100
+ }
+ },
+ new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Title 2",
+ Artist = "Artist 2",
+ Author = new User
+ {
+ Username = "someone",
+ Id = 100
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers
+ {
+ Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586",
+ },
+ FavouriteCount = 10
+ }
+ }
+ };
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs
index 010e4330d7..5582cc6826 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -263,7 +264,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void moveLogoFacade()
{
- if (logoFacade?.Transforms.Count == 0 && transferContainer?.Transforms.Count == 0)
+ if (!(logoFacade?.Transforms).Any() && !(transferContainer?.Transforms).Any())
{
Random random = new Random();
trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300);
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 4b0506d818..d767973528 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index f256b8e4e9..95f5deb2cc 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
index 4aea7ff4c0..fa530ea2c4 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
{
}
- private class SettingsRoundDropdown : LadderSettingsDropdown
+ private class SettingsRoundDropdown : SettingsDropdown
{
public SettingsRoundDropdown(BindableList rounds)
{
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs
deleted file mode 100644
index 347e4d91e0..0000000000
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Overlays.Settings;
-
-namespace osu.Game.Tournament.Screens.Ladder.Components
-{
- public class LadderSettingsDropdown : SettingsDropdown
- {
- protected override OsuDropdown CreateDropdown() => new DropdownControl();
-
- private new class DropdownControl : SettingsDropdown.DropdownControl
- {
- protected override DropdownMenu CreateMenu() => new Menu();
-
- private new class Menu : OsuDropdownMenu
- {
- public Menu()
- {
- MaxHeight = 200;
- }
- }
- }
- }
-}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
index a630e51e44..6604e3a313 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
@@ -6,11 +6,12 @@ using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Ladder.Components
{
- public class SettingsTeamDropdown : LadderSettingsDropdown
+ public class SettingsTeamDropdown : SettingsDropdown
{
public SettingsTeamDropdown(BindableList teams)
{
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
index 5e644fbf1c..b80b4e45ed 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
@@ -109,25 +109,58 @@ namespace osu.Game.Beatmaps
}
private CancellationTokenSource trackedUpdateCancellationSource;
+ private readonly List linkedCancellationSources = new List();
///
/// Updates all tracked using the current ruleset and mods.
///
private void updateTrackedBindables()
{
- trackedUpdateCancellationSource?.Cancel();
+ cancelTrackedBindableUpdate();
trackedUpdateCancellationSource = new CancellationTokenSource();
foreach (var b in trackedBindables)
{
- if (trackedUpdateCancellationSource.IsCancellationRequested)
- break;
+ var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken);
+ linkedCancellationSources.Add(linkedSource);
- using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken))
- updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
+ updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
}
}
+ ///
+ /// Cancels the existing update of all tracked via .
+ ///
+ private void cancelTrackedBindableUpdate()
+ {
+ trackedUpdateCancellationSource?.Cancel();
+ trackedUpdateCancellationSource = null;
+
+ if (linkedCancellationSources != null)
+ {
+ foreach (var c in linkedCancellationSources)
+ c.Dispose();
+
+ linkedCancellationSources.Clear();
+ }
+ }
+
+ ///
+ /// Creates a new and triggers an initial value update.
+ ///
+ /// The that star difficulty should correspond to.
+ /// The initial to get the difficulty with.
+ /// The initial s to get the difficulty with.
+ /// An optional which stops updating the star difficulty for the given .
+ /// The .
+ private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable initialMods,
+ CancellationToken cancellationToken)
+ {
+ var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken);
+ updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken);
+ return bindable;
+ }
+
///
/// Updates the value of a with a given ruleset + mods.
///
@@ -148,22 +181,6 @@ namespace osu.Game.Beatmaps
}, cancellationToken);
}
- ///
- /// Creates a new and triggers an initial value update.
- ///
- /// The that star difficulty should correspond to.
- /// The initial to get the difficulty with.
- /// The initial s to get the difficulty with.
- /// An optional which stops updating the star difficulty for the given .
- /// The .
- private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable initialMods,
- CancellationToken cancellationToken)
- {
- var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken);
- updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken);
- return bindable;
- }
-
///
/// Computes the difficulty defined by a key, and stores it to the timed cache.
///
@@ -220,6 +237,14 @@ namespace osu.Game.Beatmaps
return difficultyCache.TryGetValue(key, out existingDifficulty);
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ cancelTrackedBindableUpdate();
+ updateScheduler?.Dispose();
+ }
+
private readonly struct DifficultyCacheLookup : IEquatable
{
public readonly int BeatmapId;
diff --git a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
index c9cd9f1158..924c7913f3 100644
--- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
+++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
@@ -1,13 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
using osu.Game.Graphics.Sprites;
+using osu.Game.Overlays;
using osuTK;
-using osuTK.Graphics;
using System.Collections.Generic;
namespace osu.Game.Graphics.UserInterface
@@ -16,14 +18,6 @@ namespace osu.Game.Graphics.UserInterface
{
private const int duration = 200;
- private Color4 chevronIconColour;
-
- protected Color4 ChevronIconColour
- {
- get => chevronIconColour;
- set => chevronIconColour = leftChevron.Colour = rightChevron.Colour = value;
- }
-
public string Text
{
get => text.Text;
@@ -32,22 +26,28 @@ namespace osu.Game.Graphics.UserInterface
protected override IEnumerable EffectTargets => new[] { background };
- private ChevronIcon leftChevron;
- private ChevronIcon rightChevron;
+ private ChevronIcon leftIcon;
+ private ChevronIcon rightIcon;
private SpriteText text;
private Box background;
private FillFlowContainer textContainer;
public ShowMoreButton()
{
- Height = 30;
- Width = 140;
+ AutoSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ IdleColour = colourProvider.Background2;
+ HoverColour = colourProvider.Background1;
}
protected override Drawable CreateContent() => new CircularContainer
{
Masking = true,
- RelativeSizeAxes = Axes.Both,
+ AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
@@ -56,22 +56,36 @@ namespace osu.Game.Graphics.UserInterface
},
textContainer = new FillFlowContainer
{
+ AlwaysPresent = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Spacing = new Vector2(7),
+ Spacing = new Vector2(10),
+ Margin = new MarginPadding
+ {
+ Horizontal = 20,
+ Vertical = 5
+ },
Children = new Drawable[]
{
- leftChevron = new ChevronIcon(),
+ leftIcon = new ChevronIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = "show more".ToUpper(),
},
- rightChevron = new ChevronIcon(),
+ rightIcon = new ChevronIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
}
}
}
@@ -81,17 +95,40 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnLoadFinished() => textContainer.FadeIn(duration, Easing.OutQuint);
- private class ChevronIcon : SpriteIcon
+ protected override bool OnHover(HoverEvent e)
{
- private const int icon_size = 8;
+ base.OnHover(e);
+ leftIcon.SetHoveredState(true);
+ rightIcon.SetHoveredState(true);
+ return true;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+ leftIcon.SetHoveredState(false);
+ rightIcon.SetHoveredState(false);
+ }
+
+ public class ChevronIcon : SpriteIcon
+ {
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
public ChevronIcon()
{
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
- Size = new Vector2(icon_size);
+ Size = new Vector2(7.5f);
Icon = FontAwesome.Solid.ChevronDown;
}
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Colour = colourProvider.Foreground1;
+ }
+
+ public void SetHoveredState(bool hovered) =>
+ this.FadeColour(hovered ? colourProvider.Light1 : colourProvider.Foreground1, 200, Easing.OutQuint);
}
}
}
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 2115326cc2..6912d9b629 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -136,6 +136,11 @@ namespace osu.Game.Online.API
Success?.Invoke();
}
+ internal void TriggerFailure(Exception e)
+ {
+ Failure?.Invoke(e);
+ }
+
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
@@ -166,7 +171,7 @@ namespace osu.Game.Online.API
}
Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network);
- pendingFailure = () => Failure?.Invoke(e);
+ pendingFailure = () => TriggerFailure(e);
checkAndScheduleFailure();
}
diff --git a/osu.Game/Online/API/Requests/Cursor.cs b/osu.Game/Online/API/Requests/Cursor.cs
index f21445ca32..3de8db770c 100644
--- a/osu.Game/Online/API/Requests/Cursor.cs
+++ b/osu.Game/Online/API/Requests/Cursor.cs
@@ -15,6 +15,6 @@ namespace osu.Game.Online.API.Requests
{
[UsedImplicitly]
[JsonExtensionData]
- public IDictionary Properties;
+ public IDictionary Properties { get; set; } = new Dictionary();
}
}
diff --git a/osu.Game/Online/API/Requests/GetNewsRequest.cs b/osu.Game/Online/API/Requests/GetNewsRequest.cs
new file mode 100644
index 0000000000..36d9dc0652
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetNewsRequest.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.IO.Network;
+using osu.Game.Extensions;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetNewsRequest : APIRequest
+ {
+ private readonly Cursor cursor;
+
+ public GetNewsRequest(Cursor cursor = null)
+ {
+ this.cursor = cursor;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+ req.AddCursor(cursor);
+ return req;
+ }
+
+ protected override string Target => "news";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/GetNewsResponse.cs b/osu.Game/Online/API/Requests/GetNewsResponse.cs
new file mode 100644
index 0000000000..835289a51d
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetNewsResponse.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetNewsResponse : ResponseWithCursor
+ {
+ [JsonProperty("news_posts")]
+ public IEnumerable NewsPosts;
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs
index f973f96b37..2d99b12519 100644
--- a/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs
+++ b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs
@@ -11,17 +11,20 @@ namespace osu.Game.Online.Multiplayer
{
private readonly int roomId;
private readonly int playlistItemId;
+ private readonly string versionHash;
- public CreateRoomScoreRequest(int roomId, int playlistItemId)
+ public CreateRoomScoreRequest(int roomId, int playlistItemId, string versionHash)
{
this.roomId = roomId;
this.playlistItemId = playlistItemId;
+ this.versionHash = versionHash;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
+ req.AddParameter("version_hash", versionHash);
return req;
}
diff --git a/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs
deleted file mode 100644
index 3d3bd20ff3..0000000000
--- a/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using Newtonsoft.Json;
-using osu.Game.Online.API;
-
-namespace osu.Game.Online.Multiplayer
-{
- public class GetRoomPlaylistScoresRequest : APIRequest
- {
- private readonly int roomId;
- private readonly int playlistItemId;
-
- public GetRoomPlaylistScoresRequest(int roomId, int playlistItemId)
- {
- this.roomId = roomId;
- this.playlistItemId = playlistItemId;
- }
-
- protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores";
- }
-
- public class RoomPlaylistScores
- {
- [JsonProperty("scores")]
- public List Scores { get; set; }
- }
-}
diff --git a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs
new file mode 100644
index 0000000000..684d0aecd8
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs
@@ -0,0 +1,59 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using JetBrains.Annotations;
+using osu.Framework.IO.Network;
+using osu.Game.Extensions;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// Returns a list of scores for the specified playlist item.
+ ///
+ public class IndexPlaylistScoresRequest : APIRequest
+ {
+ public readonly int RoomId;
+ public readonly int PlaylistItemId;
+
+ [CanBeNull]
+ public readonly Cursor Cursor;
+
+ [CanBeNull]
+ public readonly IndexScoresParams IndexParams;
+
+ public IndexPlaylistScoresRequest(int roomId, int playlistItemId)
+ {
+ RoomId = roomId;
+ PlaylistItemId = playlistItemId;
+ }
+
+ public IndexPlaylistScoresRequest(int roomId, int playlistItemId, [NotNull] Cursor cursor, [NotNull] IndexScoresParams indexParams)
+ : this(roomId, playlistItemId)
+ {
+ Cursor = cursor;
+ IndexParams = indexParams;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+
+ if (Cursor != null)
+ {
+ Debug.Assert(IndexParams != null);
+
+ req.AddCursor(Cursor);
+
+ foreach (var (key, value) in IndexParams.Properties)
+ req.AddParameter(key, value.ToString());
+ }
+
+ return req;
+ }
+
+ protected override string Target => $@"rooms/{RoomId}/playlist/{PlaylistItemId}/scores";
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/IndexScoresParams.cs b/osu.Game/Online/Multiplayer/IndexScoresParams.cs
new file mode 100644
index 0000000000..a511e9a780
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/IndexScoresParams.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . 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.Multiplayer
+{
+ ///
+ /// A collection of parameters which should be passed to the index endpoint to fetch the next page.
+ ///
+ public class IndexScoresParams
+ {
+ [UsedImplicitly]
+ [JsonExtensionData]
+ public IDictionary Properties { get; set; } = new Dictionary();
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs b/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs
new file mode 100644
index 0000000000..e237b7e3fb
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// A object returned via a .
+ ///
+ public class IndexedMultiplayerScores : MultiplayerScores
+ {
+ ///
+ /// The total scores in the playlist item.
+ ///
+ [JsonProperty("total")]
+ public int? TotalScores { get; set; }
+
+ ///
+ /// The user's score, if any.
+ ///
+ [JsonProperty("user_score")]
+ [CanBeNull]
+ public MultiplayerScore UserScore { get; set; }
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerScore.cs b/osu.Game/Online/Multiplayer/MultiplayerScore.cs
index 3bbf19b11f..8191003aad 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerScore.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerScore.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Game.Online.API;
@@ -47,6 +48,19 @@ namespace osu.Game.Online.Multiplayer
[JsonProperty("ended_at")]
public DateTimeOffset EndedAt { get; set; }
+ ///
+ /// The position of this score, starting at 1.
+ ///
+ [JsonProperty("position")]
+ public int? Position { get; set; }
+
+ ///
+ /// Any scores in the room around this score.
+ ///
+ [JsonProperty("scores_around")]
+ [CanBeNull]
+ public MultiplayerScoresAround ScoresAround { get; set; }
+
public ScoreInfo CreateScoreInfo(PlaylistItem playlistItem)
{
var rulesetInstance = playlistItem.Ruleset.Value.CreateInstance();
@@ -66,7 +80,8 @@ namespace osu.Game.Online.Multiplayer
Date = EndedAt,
Hash = string.Empty, // todo: temporary?
Rank = Rank,
- Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty()
+ Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty(),
+ Position = Position,
};
return scoreInfo;
diff --git a/osu.Game/Online/Multiplayer/MultiplayerScores.cs b/osu.Game/Online/Multiplayer/MultiplayerScores.cs
new file mode 100644
index 0000000000..7b9dcff828
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MultiplayerScores.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Game.Online.API.Requests;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// An object which contains scores and related data for fetching next pages.
+ ///
+ public class MultiplayerScores : ResponseWithCursor
+ {
+ ///
+ /// The scores.
+ ///
+ [JsonProperty("scores")]
+ public List Scores { get; set; } = new List();
+
+ ///
+ /// The parameters to be used to fetch the next page.
+ ///
+ [JsonProperty("params")]
+ public IndexScoresParams Params { get; set; } = new IndexScoresParams();
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs b/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs
new file mode 100644
index 0000000000..2ac62d0300
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// An object which stores scores higher and lower than the user's score.
+ ///
+ public class MultiplayerScoresAround
+ {
+ ///
+ /// Scores sorted "higher" than the user's score, depending on the sorting order.
+ ///
+ [JsonProperty("higher")]
+ [CanBeNull]
+ public MultiplayerScores Higher { get; set; }
+
+ ///
+ /// Scores sorted "lower" than the user's score, depending on the sorting order.
+ ///
+ [JsonProperty("lower")]
+ [CanBeNull]
+ public MultiplayerScores Lower { get; set; }
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs
new file mode 100644
index 0000000000..936b8bbe89
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Online.API;
+
+namespace osu.Game.Online.Multiplayer
+{
+ public class ShowPlaylistUserScoreRequest : APIRequest
+ {
+ private readonly int roomId;
+ private readonly int playlistItemId;
+ private readonly long userId;
+
+ public ShowPlaylistUserScoreRequest(int roomId, int playlistItemId, long userId)
+ {
+ this.roomId = roomId;
+ this.playlistItemId = playlistItemId;
+ this.userId = userId;
+ }
+
+ protected override string Target => $"rooms/{roomId}/playlist/{playlistItemId}/scores/users/{userId}";
+ }
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index d6a07651e2..053eb01dcd 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -67,12 +67,12 @@ namespace osu.Game
[NotNull]
private readonly NotificationOverlay notifications = new NotificationOverlay();
- private NowPlayingOverlay nowPlaying;
-
private BeatmapListingOverlay beatmapListing;
private DashboardOverlay dashboard;
+ private NewsOverlay news;
+
private UserProfileOverlay userProfile;
private BeatmapSetOverlay beatmapSetOverlay;
@@ -632,6 +632,7 @@ namespace osu.Game
// overlay elements
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
+ loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
@@ -647,7 +648,7 @@ namespace osu.Game
Origin = Anchor.TopRight,
}, rightFloatingOverlayContent.Add, true);
- loadComponentSingleFile(nowPlaying = new NowPlayingOverlay
+ loadComponentSingleFile(new NowPlayingOverlay
{
GetToolbarHeight = () => ToolbarOffset,
Anchor = Anchor.TopRight,
@@ -682,14 +683,13 @@ namespace osu.Game
{
overlay.State.ValueChanged += state =>
{
- if (state.NewValue == Visibility.Hidden) return;
-
- informationalOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
+ if (state.NewValue != Visibility.Hidden)
+ showOverlayAboveOthers(overlay, informationalOverlays);
};
}
// ensure only one of these overlays are open at once.
- var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, beatmapListing, changelogOverlay, rankingsOverlay };
+ var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay };
foreach (var overlay in singleDisplayOverlays)
{
@@ -698,9 +698,8 @@ namespace osu.Game
// informational overlays should be dismissed on a show or hide of a full overlay.
informationalOverlays.ForEach(o => o.Hide());
- if (state.NewValue == Visibility.Hidden) return;
-
- singleDisplayOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
+ if (state.NewValue != Visibility.Hidden)
+ showOverlayAboveOthers(overlay, singleDisplayOverlays);
};
}
@@ -725,6 +724,15 @@ namespace osu.Game
notifications.State.ValueChanged += _ => updateScreenOffset();
}
+ private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
+ {
+ otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
+
+ // show above others if not visible at all, else leave at current depth.
+ if (!overlay.IsPresent)
+ overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime);
+ }
+
public class GameIdleTracker : IdleTracker
{
private InputManager inputManager;
@@ -859,18 +867,6 @@ namespace osu.Game
switch (action)
{
- case GlobalAction.ToggleNowPlaying:
- nowPlaying.ToggleVisibility();
- return true;
-
- case GlobalAction.ToggleChat:
- chatOverlay.ToggleVisibility();
- return true;
-
- case GlobalAction.ToggleSocial:
- dashboard.ToggleVisibility();
- return true;
-
case GlobalAction.ResetInputSettings:
var sensitivity = frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity);
@@ -886,18 +882,6 @@ namespace osu.Game
Toolbar.ToggleVisibility();
return true;
- case GlobalAction.ToggleSettings:
- Settings.ToggleVisibility();
- return true;
-
- case GlobalAction.ToggleDirect:
- beatmapListing.ToggleVisibility();
- return true;
-
- case GlobalAction.ToggleNotifications:
- notifications.ToggleVisibility();
- return true;
-
case GlobalAction.ToggleGameplayMouseButtons:
LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get(OsuSetting.MouseDisableButtons));
return true;
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index fe5c0704b7..98f60d52d3 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Development;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.IO.Stores;
@@ -35,6 +36,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osuTK.Input;
+using RuntimeInfo = osu.Framework.RuntimeInfo;
namespace osu.Game
{
@@ -97,6 +99,11 @@ namespace osu.Game
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
+ ///
+ /// MD5 representation of the game executable.
+ ///
+ public string VersionHash { get; private set; }
+
public bool IsDeployedBuild => AssemblyVersion.Major > 0;
public virtual string Version
@@ -128,6 +135,18 @@ namespace osu.Game
[BackgroundDependencyLoader]
private void load()
{
+ try
+ {
+ using (var str = File.OpenRead(typeof(OsuGameBase).Assembly.Location))
+ VersionHash = str.ComputeMD5Hash();
+ }
+ catch
+ {
+ // special case for android builds, which can't read DLLs from a packed apk.
+ // should eventually be handled in a better way.
+ VersionHash = $"{Version}-{RuntimeInfo.OS}".ComputeMD5Hash();
+ }
+
Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly));
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
index 53438ca421..57bf2af4d2 100644
--- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
+++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
@@ -5,12 +5,12 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
+using static osu.Game.Graphics.UserInterface.ShowMoreButton;
namespace osu.Game.Overlays.Comments.Buttons
{
@@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Comments.Buttons
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
- private readonly SpriteIcon icon;
+ private readonly ChevronIcon icon;
private readonly Box background;
private readonly OsuSpriteText text;
@@ -68,12 +68,10 @@ namespace osu.Game.Overlays.Comments.Buttons
AlwaysPresent = true,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)
},
- icon = new SpriteIcon
+ icon = new ChevronIcon
{
Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Size = new Vector2(7.5f),
- Icon = FontAwesome.Solid.ChevronDown
+ Origin = Anchor.CentreLeft
}
}
}
@@ -88,7 +86,6 @@ namespace osu.Game.Overlays.Comments.Buttons
private void load()
{
background.Colour = colourProvider.Background2;
- icon.Colour = colourProvider.Foreground1;
}
protected void SetIconDirection(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1));
@@ -99,7 +96,7 @@ namespace osu.Game.Overlays.Comments.Buttons
{
base.OnHover(e);
background.FadeColour(colourProvider.Background1, 200, Easing.OutQuint);
- icon.FadeColour(colourProvider.Light1, 200, Easing.OutQuint);
+ icon.SetHoveredState(true);
return true;
}
@@ -107,7 +104,7 @@ namespace osu.Game.Overlays.Comments.Buttons
{
base.OnHoverLost(e);
background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint);
- icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint);
+ icon.SetHoveredState(false);
}
}
}
diff --git a/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowMoreRepliesButton.cs
similarity index 93%
rename from osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs
rename to osu.Game/Overlays/Comments/Buttons/ShowMoreRepliesButton.cs
index 2c363564d2..c115a8bb8f 100644
--- a/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs
+++ b/osu.Game/Overlays/Comments/Buttons/ShowMoreRepliesButton.cs
@@ -12,13 +12,13 @@ using osu.Framework.Allocation;
namespace osu.Game.Overlays.Comments.Buttons
{
- public class ShowMoreButton : LoadingButton
+ public class ShowMoreRepliesButton : LoadingButton
{
protected override IEnumerable EffectTargets => new[] { text };
private OsuSpriteText text;
- public ShowMoreButton()
+ public ShowMoreRepliesButton()
{
AutoSizeAxes = Axes.Both;
LoadingAnimationSize = new Vector2(8);
diff --git a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs
index d2ff7ecb1f..adf64eabb1 100644
--- a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs
+++ b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface;
@@ -11,16 +10,6 @@ namespace osu.Game.Overlays.Comments
{
public readonly BindableInt Current = new BindableInt();
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
- {
- Height = 20;
-
- IdleColour = colourProvider.Background2;
- HoverColour = colourProvider.Background1;
- ChevronIconColour = colourProvider.Foreground1;
- }
-
protected override void LoadComplete()
{
Current.BindValueChanged(onCurrentChanged, true);
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 9c0a48ec29..31aa41e967 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Comments
private FillFlowContainer childCommentsVisibilityContainer;
private FillFlowContainer childCommentsContainer;
private LoadRepliesButton loadRepliesButton;
- private ShowMoreButton showMoreButton;
+ private ShowMoreRepliesButton showMoreButton;
private ShowRepliesButton showRepliesButton;
private ChevronButton chevronButton;
private DeletedCommentsCounter deletedCommentsCounter;
@@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Comments
Top = 10
}
},
- showMoreButton = new ShowMoreButton
+ showMoreButton = new ShowMoreRepliesButton
{
Action = () => RepliesRequested(this, ++currentPage)
}
diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs
new file mode 100644
index 0000000000..4d96825353
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osuTK;
+
+namespace osu.Game.Overlays.Dashboard.Home
+{
+ public class DashboardBeatmapListing : CompositeDrawable
+ {
+ private readonly List newBeatmaps;
+ private readonly List popularBeatmaps;
+
+ public DashboardBeatmapListing(List newBeatmaps, List popularBeatmaps)
+ {
+ this.newBeatmaps = newBeatmaps;
+ this.popularBeatmaps = popularBeatmaps;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ InternalChild = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10),
+ Children = new DrawableBeatmapList[]
+ {
+ new DrawableNewBeatmapList(newBeatmaps),
+ new DrawablePopularBeatmapList(popularBeatmaps)
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs
new file mode 100644
index 0000000000..3badea155d
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs
@@ -0,0 +1,168 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Drawables;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+
+namespace osu.Game.Overlays.Dashboard.Home
+{
+ public abstract class DashboardBeatmapPanel : OsuClickableContainer
+ {
+ [Resolved]
+ protected OverlayColourProvider ColourProvider { get; private set; }
+
+ [Resolved(canBeNull: true)]
+ private BeatmapSetOverlay beatmapOverlay { get; set; }
+
+ protected readonly BeatmapSetInfo SetInfo;
+
+ private Box hoverBackground;
+ private SpriteIcon chevron;
+
+ protected DashboardBeatmapPanel(BeatmapSetInfo setInfo)
+ {
+ SetInfo = setInfo;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 60;
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Horizontal = -10 },
+ Child = hoverBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourProvider.Background3,
+ Alpha = 0
+ }
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[]
+ {
+ new Dimension(GridSizeMode.Absolute, 70),
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ RowDimensions = new[]
+ {
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = 6,
+ Child = new UpdateableBeatmapSetCover
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ BeatmapSet = SetInfo
+ }
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Horizontal = 10 },
+ Child = new FillFlowContainer
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ RelativeSizeAxes = Axes.X,
+ Truncate = true,
+ Font = OsuFont.GetFont(weight: FontWeight.Regular),
+ Text = SetInfo.Metadata.Title
+ },
+ new OsuSpriteText
+ {
+ RelativeSizeAxes = Axes.X,
+ Truncate = true,
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
+ Text = SetInfo.Metadata.Artist
+ },
+ new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular))
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Spacing = new Vector2(3),
+ Margin = new MarginPadding { Top = 2 }
+ }.With(c =>
+ {
+ c.AddText("by");
+ c.AddUserLink(SetInfo.Metadata.Author);
+ c.AddArbitraryDrawable(CreateInfo());
+ })
+ }
+ }
+ },
+ chevron = new SpriteIcon
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Size = new Vector2(16),
+ Icon = FontAwesome.Solid.ChevronRight,
+ Colour = ColourProvider.Foreground1
+ }
+ }
+ }
+ }
+ }
+ };
+
+ Action = () =>
+ {
+ if (SetInfo.OnlineBeatmapSetID.HasValue)
+ beatmapOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value);
+ };
+ }
+
+ protected abstract Drawable CreateInfo();
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ base.OnHover(e);
+ hoverBackground.FadeIn(200, Easing.OutQuint);
+ chevron.FadeColour(ColourProvider.Light1, 200, Easing.OutQuint);
+ return true;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+ hoverBackground.FadeOut(200, Easing.OutQuint);
+ chevron.FadeColour(ColourProvider.Foreground1, 200, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs
new file mode 100644
index 0000000000..b212eaf20a
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+
+namespace osu.Game.Overlays.Dashboard.Home
+{
+ public class DashboardNewBeatmapPanel : DashboardBeatmapPanel
+ {
+ public DashboardNewBeatmapPanel(BeatmapSetInfo setInfo)
+ : base(setInfo)
+ {
+ }
+
+ protected override Drawable CreateInfo() => new DrawableDate(SetInfo.OnlineInfo.Ranked ?? DateTimeOffset.Now, 10, false)
+ {
+ Colour = ColourProvider.Foreground1
+ };
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs
new file mode 100644
index 0000000000..e9066c0657
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+
+namespace osu.Game.Overlays.Dashboard.Home
+{
+ public class DashboardPopularBeatmapPanel : DashboardBeatmapPanel
+ {
+ public DashboardPopularBeatmapPanel(BeatmapSetInfo setInfo)
+ : base(setInfo)
+ {
+ }
+
+ protected override Drawable CreateInfo() => new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(3, 0),
+ Colour = ColourProvider.Foreground1,
+ Children = new Drawable[]
+ {
+ new SpriteIcon
+ {
+ Size = new Vector2(10),
+ Icon = FontAwesome.Solid.Heart
+ },
+ new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular),
+ Text = SetInfo.OnlineInfo.FavouriteCount.ToString()
+ }
+ }
+ };
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs
new file mode 100644
index 0000000000..f6535b7db3
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs
@@ -0,0 +1,56 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+
+namespace osu.Game.Overlays.Dashboard.Home
+{
+ public abstract class DrawableBeatmapList : CompositeDrawable
+ {
+ private readonly List beatmaps;
+
+ protected DrawableBeatmapList(List beatmaps)
+ {
+ this.beatmaps = beatmaps;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ FillFlowContainer flow;
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ InternalChild = flow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10),
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold),
+ Colour = colourProvider.Light1,
+ Text = Title
+ }
+ }
+ };
+
+ flow.AddRange(beatmaps.Select(CreateBeatmapPanel));
+ }
+
+ protected abstract string Title { get; }
+
+ protected abstract DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo);
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs
new file mode 100644
index 0000000000..75e8ca336d
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Overlays.Dashboard.Home
+{
+ public class DrawableNewBeatmapList : DrawableBeatmapList
+ {
+ public DrawableNewBeatmapList(List beatmaps)
+ : base(beatmaps)
+ {
+ }
+
+ protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardNewBeatmapPanel(setInfo);
+
+ protected override string Title => "New Ranked Beatmaps";
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs
new file mode 100644
index 0000000000..90bd00008c
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Overlays.Dashboard.Home
+{
+ public class DrawablePopularBeatmapList : DrawableBeatmapList
+ {
+ public DrawablePopularBeatmapList(List beatmaps)
+ : base(beatmaps)
+ {
+ }
+
+ protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardPopularBeatmapPanel(setInfo);
+
+ protected override string Title => "Popular Beatmaps";
+ }
+}
diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs
index a72c3f4fa5..e3a4b0e152 100644
--- a/osu.Game/Overlays/DashboardOverlay.cs
+++ b/osu.Game/Overlays/DashboardOverlay.cs
@@ -19,7 +19,6 @@ namespace osu.Game.Overlays
{
private CancellationTokenSource cancellationToken;
- private Box background;
private Container content;
private DashboardOverlayHeader header;
private LoadingLayer loading;
@@ -35,9 +34,10 @@ namespace osu.Game.Overlays
{
Children = new Drawable[]
{
- background = new Box
+ new Box
{
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourProvider.Background5
},
scrollFlow = new OverlayScrollContainer
{
@@ -66,8 +66,6 @@ namespace osu.Game.Overlays
},
loading = new LoadingLayer(content),
};
-
- background.Colour = ColourProvider.Background5;
}
protected override void LoadComplete()
diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
index eafb4572ca..b808d49fa2 100644
--- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
+++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
@@ -48,8 +48,7 @@ namespace osu.Game.Overlays.KeyBinding
public bool FilteringActive { get; set; }
private OsuSpriteText text;
- private Drawable pressAKey;
-
+ private FillFlowContainer cancelAndClearButtons;
private FillFlowContainer buttons;
public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text);
@@ -80,7 +79,7 @@ namespace osu.Game.Overlays.KeyBinding
Hollow = true,
};
- Children = new[]
+ Children = new Drawable[]
{
new Box
{
@@ -99,7 +98,7 @@ namespace osu.Game.Overlays.KeyBinding
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
},
- pressAKey = new FillFlowContainer
+ cancelAndClearButtons = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(padding) { Top = height + padding * 2 },
@@ -187,7 +186,8 @@ namespace osu.Game.Overlays.KeyBinding
if (bindTarget.IsHovered)
finalise();
- else
+ // prevent updating bind target before clear button's action
+ else if (!cancelAndClearButtons.Any(b => b.IsHovered))
updateBindTarget();
}
@@ -298,8 +298,8 @@ namespace osu.Game.Overlays.KeyBinding
if (HasFocus)
GetContainingInputManager().ChangeFocus(null);
- pressAKey.FadeOut(300, Easing.OutQuint);
- pressAKey.BypassAutoSizeAxes |= Axes.Y;
+ cancelAndClearButtons.FadeOut(300, Easing.OutQuint);
+ cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y;
}
protected override void OnFocus(FocusEvent e)
@@ -307,8 +307,8 @@ namespace osu.Game.Overlays.KeyBinding
AutoSizeDuration = 500;
AutoSizeEasing = Easing.OutQuint;
- pressAKey.FadeIn(300, Easing.OutQuint);
- pressAKey.BypassAutoSizeAxes &= ~Axes.Y;
+ cancelAndClearButtons.FadeIn(300, Easing.OutQuint);
+ cancelAndClearButtons.BypassAutoSizeAxes &= ~Axes.Y;
updateBindTarget();
base.OnFocus(e);
@@ -320,6 +320,9 @@ namespace osu.Game.Overlays.KeyBinding
base.OnFocusLost(e);
}
+ ///
+ /// Updates the bind target to the currently hovered key button or the first if clicked anywhere else.
+ ///
private void updateBindTarget()
{
if (bindTarget != null) bindTarget.IsBinding = false;
@@ -354,7 +357,7 @@ namespace osu.Game.Overlays.KeyBinding
}
}
- private class KeyButton : Container
+ public class KeyButton : Container
{
public readonly Framework.Input.Bindings.KeyBinding KeyBinding;
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 546f7a1ec4..a990f9a6ab 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -236,8 +236,8 @@ namespace osu.Game.Overlays
{
if (beatmap is Bindable working)
working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value);
- beatmap.Value.Track.Restart();
+ restartTrack();
return PreviousTrackResult.Previous;
}
@@ -262,13 +262,21 @@ namespace osu.Game.Overlays
{
if (beatmap is Bindable working)
working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value);
- beatmap.Value.Track.Restart();
+
+ restartTrack();
return true;
}
return false;
}
+ private void restartTrack()
+ {
+ // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase).
+ // we probably want to move this to a central method for switching to a new working beatmap in the future.
+ Schedule(() => beatmap.Value.Track.Restart());
+ }
+
private WorkingBeatmap current;
private TrackChangeDirection? queuedDirection;
diff --git a/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs
new file mode 100644
index 0000000000..0f177f151a
--- /dev/null
+++ b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs
@@ -0,0 +1,114 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using System.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osuTK;
+
+namespace osu.Game.Overlays.News.Displays
+{
+ public class FrontPageDisplay : CompositeDrawable
+ {
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ private FillFlowContainer content;
+ private ShowMoreButton showMore;
+
+ private GetNewsRequest request;
+ private Cursor lastCursor;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ Padding = new MarginPadding
+ {
+ Vertical = 20,
+ Left = 30,
+ Right = 50
+ };
+
+ InternalChild = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10),
+ Children = new Drawable[]
+ {
+ content = new FillFlowContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10)
+ },
+ showMore = new ShowMoreButton
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Margin = new MarginPadding
+ {
+ Top = 15
+ },
+ Action = performFetch,
+ Alpha = 0
+ }
+ }
+ };
+
+ performFetch();
+ }
+
+ private void performFetch()
+ {
+ request?.Cancel();
+
+ request = new GetNewsRequest(lastCursor);
+ request.Success += response => Schedule(() => onSuccess(response));
+ api.PerformAsync(request);
+ }
+
+ private CancellationTokenSource cancellationToken;
+
+ private void onSuccess(GetNewsResponse response)
+ {
+ cancellationToken?.Cancel();
+
+ lastCursor = response.Cursor;
+
+ var flow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10),
+ Children = response.NewsPosts.Select(p => new NewsCard(p)).ToList()
+ };
+
+ LoadComponentAsync(flow, loaded =>
+ {
+ content.Add(loaded);
+ showMore.IsLoading = false;
+ showMore.Show();
+ }, (cancellationToken = new CancellationTokenSource()).Token);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ request?.Cancel();
+ cancellationToken?.Cancel();
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs
deleted file mode 100644
index e3f5a8cea3..0000000000
--- a/osu.Game/Overlays/News/NewsArticleCover.cs
+++ /dev/null
@@ -1,174 +0,0 @@
-// Copyright (c) ppy Pty Ltd . 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.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Colour;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osuTK.Graphics;
-
-namespace osu.Game.Overlays.News
-{
- public class NewsArticleCover : Container
- {
- private const int hover_duration = 300;
-
- private readonly Box gradient;
-
- public NewsArticleCover(ArticleInfo info)
- {
- RelativeSizeAxes = Axes.X;
- Masking = true;
- CornerRadius = 4;
-
- NewsBackground bg;
-
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.1f))
- },
- new DelayedLoadWrapper(bg = new NewsBackground(info.CoverUrl)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fill,
- Alpha = 0
- })
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- },
- gradient = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.1f), Color4.Black.Opacity(0.7f)),
- Alpha = 0
- },
- new DateContainer(info.Time)
- {
- Margin = new MarginPadding
- {
- Right = 20,
- Top = 20,
- }
- },
- new OsuSpriteText
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding
- {
- Left = 25,
- Bottom = 50,
- },
- Font = OsuFont.GetFont(Typeface.Torus, 24, FontWeight.Bold),
- Text = info.Title,
- },
- new OsuSpriteText
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding
- {
- Left = 25,
- Bottom = 30,
- },
- Font = OsuFont.GetFont(Typeface.Torus, 16, FontWeight.Bold),
- Text = "by " + info.Author
- }
- };
-
- bg.OnLoadComplete += d => d.FadeIn(250, Easing.In);
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- gradient.FadeIn(hover_duration, Easing.OutQuint);
- return base.OnHover(e);
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- base.OnHoverLost(e);
- gradient.FadeOut(hover_duration, Easing.OutQuint);
- }
-
- [LongRunningLoad]
- private class NewsBackground : Sprite
- {
- private readonly string url;
-
- public NewsBackground(string coverUrl)
- {
- url = coverUrl ?? "Headers/news";
- }
-
- [BackgroundDependencyLoader]
- private void load(LargeTextureStore store)
- {
- Texture = store.Get(url);
- }
- }
-
- private class DateContainer : Container, IHasTooltip
- {
- private readonly DateTime date;
-
- public DateContainer(DateTime date)
- {
- this.date = date;
-
- Anchor = Anchor.TopRight;
- Origin = Anchor.TopRight;
- Masking = true;
- CornerRadius = 4;
- AutoSizeAxes = Axes.Both;
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black.Opacity(0.5f),
- },
- new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Bold, false, false),
- Text = date.ToString("d MMM yyy").ToUpper(),
- Margin = new MarginPadding
- {
- Vertical = 4,
- Horizontal = 8,
- }
- }
- };
- }
-
- public string TooltipText => date.ToString("dddd dd MMMM yyyy hh:mm:ss UTCz").ToUpper();
- }
-
- // fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now
- public class ArticleInfo
- {
- public string Title { get; set; }
- public string CoverUrl { get; set; }
- public DateTime Time { get; set; }
- public string Author { get; set; }
- }
- }
-}
diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs
index 9c478a7c1d..201c3ce826 100644
--- a/osu.Game/Overlays/News/NewsCard.cs
+++ b/osu.Game/Overlays/News/NewsCard.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -11,17 +12,17 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
+using osu.Framework.Platform;
using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.News
{
- public class NewsCard : CompositeDrawable
+ public class NewsCard : OsuHoverContainer
{
- [Resolved]
- private OverlayColourProvider colourProvider { get; set; }
+ protected override IEnumerable EffectTargets => new[] { background };
private readonly APINewsPost post;
@@ -31,24 +32,28 @@ namespace osu.Game.Overlays.News
public NewsCard(APINewsPost post)
{
this.post = post;
- }
- [BackgroundDependencyLoader]
- private void load()
- {
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 6;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider, GameHost host)
+ {
+ if (post.Slug != null)
+ {
+ TooltipText = "view in browser";
+ Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
+ }
NewsBackground bg;
-
- InternalChildren = new Drawable[]
+ AddRange(new Drawable[]
{
background = new Box
{
- RelativeSizeAxes = Axes.Both,
- Colour = colourProvider.Background4
+ RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
{
@@ -104,9 +109,11 @@ namespace osu.Game.Overlays.News
}
}
}
- },
- new HoverClickSounds()
- };
+ }
+ });
+
+ IdleColour = colourProvider.Background4;
+ HoverColour = colourProvider.Background3;
bg.OnLoadComplete += d => d.FadeIn(250, Easing.In);
@@ -116,18 +123,6 @@ namespace osu.Game.Overlays.News
main.AddText(post.Author, t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold));
}
- protected override bool OnHover(HoverEvent e)
- {
- background.FadeColour(colourProvider.Background3, 200, Easing.OutQuint);
- return true;
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- background.FadeColour(colourProvider.Background4, 200, Easing.OutQuint);
- base.OnHoverLost(e);
- }
-
[LongRunningLoad]
private class NewsBackground : Sprite
{
@@ -193,6 +188,8 @@ namespace osu.Game.Overlays.News
}
};
}
+
+ protected override bool OnClick(ClickEvent e) => true; // Protects the NewsCard from clicks while hovering DateContainer
}
}
}
diff --git a/osu.Game/Overlays/News/NewsContent.cs b/osu.Game/Overlays/News/NewsContent.cs
deleted file mode 100644
index 5ff210f9f5..0000000000
--- a/osu.Game/Overlays/News/NewsContent.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-
-namespace osu.Game.Overlays.News
-{
- public abstract class NewsContent : FillFlowContainer
- {
- protected NewsContent()
- {
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
- Direction = FillDirection.Vertical;
- Padding = new MarginPadding { Bottom = 100, Top = 20, Horizontal = 50 };
- }
- }
-}
diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs
index 8214c71b3a..ddada2bdaf 100644
--- a/osu.Game/Overlays/News/NewsHeader.cs
+++ b/osu.Game/Overlays/News/NewsHeader.cs
@@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using System;
namespace osu.Game.Overlays.News
{
@@ -11,24 +11,28 @@ namespace osu.Game.Overlays.News
{
private const string front_page_string = "frontpage";
- public readonly Bindable Post = new Bindable(null);
-
public Action ShowFrontPage;
+ private readonly Bindable article = new Bindable(null);
+
public NewsHeader()
{
TabControl.AddItem(front_page_string);
- Current.ValueChanged += e =>
+ Current.BindValueChanged(e =>
{
if (e.NewValue == front_page_string)
ShowFrontPage?.Invoke();
- };
+ });
- Post.ValueChanged += showPost;
+ article.BindValueChanged(onArticleChanged, true);
}
- private void showPost(ValueChangedEvent e)
+ public void SetFrontPage() => article.Value = null;
+
+ public void SetArticle(string slug) => article.Value = slug;
+
+ private void onArticleChanged(ValueChangedEvent e)
{
if (e.OldValue != null)
TabControl.RemoveItem(e.OldValue);
diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs
index 46d692d44d..09fb445b1f 100644
--- a/osu.Game/Overlays/NewsOverlay.cs
+++ b/osu.Game/Overlays/NewsOverlay.cs
@@ -7,18 +7,20 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.News;
+using osu.Game.Overlays.News.Displays;
namespace osu.Game.Overlays
{
public class NewsOverlay : FullscreenOverlay
{
+ private readonly Bindable article = new Bindable(null);
+
+ private Container content;
+ private LoadingLayer loading;
private NewsHeader header;
-
- private Container content;
-
- public readonly Bindable Current = new Bindable(null);
+ private OverlayScrollContainer scrollFlow;
public NewsOverlay()
: base(OverlayColourScheme.Purple)
@@ -26,18 +28,19 @@ namespace osu.Game.Overlays
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load()
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = colours.PurpleDarkAlternative
+ Colour = ColourProvider.Background5,
},
- new OverlayScrollContainer
+ scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
+ ScrollbarVisible = false,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
@@ -49,7 +52,7 @@ namespace osu.Game.Overlays
{
ShowFrontPage = ShowFrontPage
},
- content = new Container
+ content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -57,31 +60,81 @@ namespace osu.Game.Overlays
},
},
},
+ loading = new LoadingLayer(content),
};
-
- header.Post.BindTo(Current);
- Current.TriggerChange();
}
- private CancellationTokenSource loadContentCancellation;
-
- protected void LoadAndShowContent(NewsContent newContent)
+ protected override void LoadComplete()
{
- content.FadeTo(0.2f, 300, Easing.OutQuint);
+ base.LoadComplete();
- loadContentCancellation?.Cancel();
+ // should not be run until first pop-in to avoid requesting data before user views.
+ article.BindValueChanged(onArticleChanged);
+ }
- LoadComponentAsync(newContent, c =>
+ private bool displayUpdateRequired = true;
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+
+ if (displayUpdateRequired)
{
- content.Child = c;
- content.FadeIn(300, Easing.OutQuint);
- }, (loadContentCancellation = new CancellationTokenSource()).Token);
+ article.TriggerChange();
+ displayUpdateRequired = false;
+ }
+ }
+
+ protected override void PopOutComplete()
+ {
+ base.PopOutComplete();
+ displayUpdateRequired = true;
}
public void ShowFrontPage()
{
- Current.Value = null;
+ article.Value = null;
Show();
}
+
+ public void ShowArticle(string slug)
+ {
+ article.Value = slug;
+ Show();
+ }
+
+ private CancellationTokenSource cancellationToken;
+
+ private void onArticleChanged(ValueChangedEvent e)
+ {
+ cancellationToken?.Cancel();
+ loading.Show();
+
+ if (e.NewValue == null)
+ {
+ header.SetFrontPage();
+ LoadDisplay(new FrontPageDisplay());
+ return;
+ }
+
+ header.SetArticle(e.NewValue);
+ LoadDisplay(Empty());
+ }
+
+ protected void LoadDisplay(Drawable display)
+ {
+ scrollFlow.ScrollToStart();
+ LoadComponentAsync(display, loaded =>
+ {
+ content.Child = loaded;
+ loading.Hide();
+ }, (cancellationToken = new CancellationTokenSource()).Token);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ cancellationToken?.Cancel();
+ base.Dispose(isDisposing);
+ }
}
}
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 2e5f1071f2..55474c9d3e 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Profile
Masking = true,
Children = new Drawable[]
{
- coverContainer = new UserCoverBackground
+ coverContainer = new ProfileCoverBackground
{
RelativeSizeAxes = Axes.Both,
},
@@ -100,5 +100,10 @@ namespace osu.Game.Overlays.Profile
IconTexture = "Icons/profile";
}
}
+
+ private class ProfileCoverBackground : UserCoverBackground
+ {
+ protected override double LoadDelay => 0;
+ }
}
}
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index a30ff786fb..9720469548 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -14,12 +14,13 @@ using osu.Game.Users;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Profile.Sections
{
public abstract class PaginatedContainer : FillFlowContainer
{
- private readonly ProfileShowMoreButton moreButton;
+ private readonly ShowMoreButton moreButton;
private readonly OsuSpriteText missingText;
private APIRequest> retrievalRequest;
private CancellationTokenSource loadCancellation;
@@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Profile.Sections
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(0, 2),
},
- moreButton = new ProfileShowMoreButton
+ moreButton = new ShowMoreButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
diff --git a/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs b/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs
deleted file mode 100644
index 426ebeebe6..0000000000
--- a/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Game.Graphics.UserInterface;
-
-namespace osu.Game.Overlays.Profile.Sections
-{
- public class ProfileShowMoreButton : ShowMoreButton
- {
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
- {
- IdleColour = colourProvider.Background2;
- HoverColour = colourProvider.Background1;
- ChevronIconColour = colourProvider.Foreground1;
- }
- }
-}
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 04390a1193..596d3a9801 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -116,8 +116,6 @@ namespace osu.Game.Overlays.Settings.Sections
private class SkinDropdownControl : DropdownControl
{
protected override string GenerateItemText(SkinInfo item) => item.ToString();
-
- protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs
index 167061f485..1175ddaab8 100644
--- a/osu.Game/Overlays/Settings/SettingsDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs
@@ -38,6 +38,8 @@ namespace osu.Game.Overlays.Settings
Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
+
+ protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
}
}
}
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index de08b79f57..5bdd86c671 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -69,6 +69,7 @@ namespace osu.Game.Overlays.Toolbar
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
+ new ToolbarNewsButton(),
new ToolbarChangelogButton(),
new ToolbarRankingsButton(),
new ToolbarBeatmapListingButton(),
diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs
index eecb368ee9..cde305fffd 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Game.Graphics;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
@@ -11,6 +12,10 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarBeatmapListingButton()
{
SetIcon(OsuIcon.ChevronDownCircle);
+ TooltipMain = "Beatmap listing";
+ TooltipSub = "Browse for new beatmaps";
+
+ Hotkey = GlobalAction.ToggleDirect;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index 86a3f5d8aa..0afc6642b2 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -1,27 +1,34 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
+using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Input;
+using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Toolbar
{
- public class ToolbarButton : OsuClickableContainer
+ public abstract class ToolbarButton : OsuClickableContainer, IKeyBindingHandler
{
public const float WIDTH = Toolbar.HEIGHT * 1.4f;
+ protected GlobalAction? Hotkey { get; set; }
+
public void SetIcon(Drawable icon)
{
IconContainer.Icon = icon;
@@ -66,9 +73,13 @@ namespace osu.Game.Overlays.Toolbar
private readonly FillFlowContainer tooltipContainer;
private readonly SpriteText tooltip1;
private readonly SpriteText tooltip2;
+ private readonly SpriteText keyBindingTooltip;
protected FillFlowContainer Flow;
- public ToolbarButton()
+ [Resolved]
+ private KeyBindingStore keyBindings { get; set; }
+
+ protected ToolbarButton()
: base(HoverSampleSet.Loud)
{
Width = WIDTH;
@@ -123,7 +134,7 @@ namespace osu.Game.Overlays.Toolbar
Origin = TooltipAnchor,
Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5),
Alpha = 0,
- Children = new[]
+ Children = new Drawable[]
{
tooltip1 = new OsuSpriteText
{
@@ -132,17 +143,44 @@ namespace osu.Game.Overlays.Toolbar
Shadow = true,
Font = OsuFont.GetFont(size: 22, weight: FontWeight.Bold),
},
- tooltip2 = new OsuSpriteText
+ new FillFlowContainer
{
+ AutoSizeAxes = Axes.Both,
Anchor = TooltipAnchor,
Origin = TooltipAnchor,
- Shadow = true,
+ Direction = FillDirection.Horizontal,
+ Children = new[]
+ {
+ tooltip2 = new OsuSpriteText { Shadow = true },
+ keyBindingTooltip = new OsuSpriteText { Shadow = true }
+ }
}
}
}
};
}
+ private readonly Cached tooltipKeyBinding = new Cached();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ keyBindings.KeyBindingChanged += () => tooltipKeyBinding.Invalidate();
+ updateKeyBindingTooltip();
+ }
+
+ private void updateKeyBindingTooltip()
+ {
+ if (tooltipKeyBinding.IsValid)
+ return;
+
+ var binding = keyBindings.Query().Find(b => (GlobalAction)b.Action == Hotkey);
+ var keyBindingString = binding?.KeyCombination.ReadableString();
+ keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) ? $" ({keyBindingString})" : string.Empty;
+
+ tooltipKeyBinding.Validate();
+ }
+
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e)
@@ -154,6 +192,8 @@ namespace osu.Game.Overlays.Toolbar
protected override bool OnHover(HoverEvent e)
{
+ updateKeyBindingTooltip();
+
HoverBackground.FadeIn(200);
tooltipContainer.FadeIn(100);
return base.OnHover(e);
@@ -164,6 +204,21 @@ namespace osu.Game.Overlays.Toolbar
HoverBackground.FadeOut(200);
tooltipContainer.FadeOut(100);
}
+
+ public bool OnPressed(GlobalAction action)
+ {
+ if (action == Hotkey)
+ {
+ Click();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(GlobalAction action)
+ {
+ }
}
public class OpaqueBackground : Container
diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs
index 84210e27a4..c88b418853 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs
@@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarChangelogButton()
{
SetIcon(FontAwesome.Solid.Bullhorn);
+ TooltipMain = "Changelog";
+ TooltipSub = "Track recent dev updates in the osu! ecosystem";
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs
index ad0e5be551..dee4be0c1f 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
@@ -11,6 +12,10 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarChatButton()
{
SetIcon(FontAwesome.Solid.Comments);
+ TooltipMain = "Chat";
+ TooltipSub = "Join the real-time discussion";
+
+ Hotkey = GlobalAction.ToggleChat;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs
index e642f0c453..4845c9a99f 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs
@@ -2,33 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
- public class ToolbarHomeButton : ToolbarButton, IKeyBindingHandler
+ public class ToolbarHomeButton : ToolbarButton
{
public ToolbarHomeButton()
{
Icon = FontAwesome.Solid.Home;
TooltipMain = "Home";
TooltipSub = "Return to the main menu";
- }
- public bool OnPressed(GlobalAction action)
- {
- if (action == GlobalAction.Home)
- {
- Click();
- return true;
- }
-
- return false;
- }
-
- public void OnReleased(GlobalAction action)
- {
+ Hotkey = GlobalAction.Home;
}
}
}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs
index b29aec5842..f9aa2de497 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs
@@ -2,15 +2,23 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarMusicButton : ToolbarOverlayToggleButton
{
+ protected override Anchor TooltipAnchor => Anchor.TopRight;
+
public ToolbarMusicButton()
{
Icon = FontAwesome.Solid.Music;
+ TooltipMain = "Now playing";
+ TooltipSub = "Manage the currently playing track";
+
+ Hotkey = GlobalAction.ToggleNowPlaying;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs
new file mode 100644
index 0000000000..106c67a041
--- /dev/null
+++ b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . 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.Sprites;
+
+namespace osu.Game.Overlays.Toolbar
+{
+ public class ToolbarNewsButton : ToolbarOverlayToggleButton
+ {
+ public ToolbarNewsButton()
+ {
+ Icon = FontAwesome.Solid.Newspaper;
+ TooltipMain = "News";
+ TooltipSub = "Get up-to-date on community happenings";
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(NewsOverlay news)
+ {
+ StateContainer = news;
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs
index dbd6c557d3..a699fd907f 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
@@ -28,6 +29,8 @@ namespace osu.Game.Overlays.Toolbar
TooltipMain = "Notifications";
TooltipSub = "Waiting for 'ya";
+ Hotkey = GlobalAction.ToggleNotifications;
+
Add(countDisplay = new CountCircle
{
Alpha = 0,
diff --git a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs
index cbd097696d..c026ce99fe 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs
@@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarRankingsButton()
{
SetIcon(FontAwesome.Regular.ChartBar);
+ TooltipMain = "Ranking";
+ TooltipSub = "Find out who's the best right now";
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs
index 79942012f9..ed2a23ec2a 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
@@ -13,6 +14,8 @@ namespace osu.Game.Overlays.Toolbar
Icon = FontAwesome.Solid.Cog;
TooltipMain = "Settings";
TooltipSub = "Change your settings";
+
+ Hotkey = GlobalAction.ToggleSettings;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
index f6646eb81d..6faa58c559 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
@@ -11,6 +12,10 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarSocialButton()
{
Icon = FontAwesome.Solid.Users;
+ TooltipMain = "Friends";
+ TooltipSub = "Interact with those close to you";
+
+ Hotkey = GlobalAction.ToggleSocial;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 77075b2abe..9afc0ecaf4 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
///
///
/// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled
- /// using the skin config option.
+ /// using the skin config option.
///
public bool IsLayered { get; set; }
}
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 58a2ba056e..5d93f5186b 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -65,11 +65,15 @@ namespace osu.Game.Rulesets
// the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies.
// this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name
// already loaded in the AppDomain.
- foreach (var curAsm in AppDomain.CurrentDomain.GetAssemblies())
- {
- if (asm.Name.Equals(curAsm.GetName().Name, StringComparison.Ordinal))
- return curAsm;
- }
+ var domainAssembly = AppDomain.CurrentDomain.GetAssemblies()
+ // Given name is always going to be equally-or-more qualified than the assembly name.
+ .Where(a => args.Name.Contains(a.GetName().Name, StringComparison.Ordinal))
+ // Pick the greatest assembly version.
+ .OrderByDescending(a => a.GetName().Version)
+ .FirstOrDefault();
+
+ if (domainAssembly != null)
+ return domainAssembly;
return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName);
}
@@ -187,6 +191,11 @@ namespace osu.Game.Rulesets
if (loadedAssemblies.ContainsKey(assembly))
return;
+ // the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799).
+ // as a failsafe, also compare by FullName.
+ if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName))
+ return;
+
try
{
loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index 168e937256..83a1077d70 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -117,6 +117,8 @@ namespace osu.Game.Rulesets.UI
public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException();
+ public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException();
+
public BindableNumber Volume => throw new NotSupportedException();
public BindableNumber Balance => throw new NotSupportedException();
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 84c0d5b54e..efcf1737c9 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -179,6 +179,13 @@ namespace osu.Game.Scoring
[JsonIgnore]
public bool DeletePending { get; set; }
+ ///
+ /// The position of this score, starting at 1.
+ ///
+ [NotMapped]
+ [JsonProperty("position")]
+ public int? Position { get; set; }
+
[Serializable]
protected class DeserializedMod : IMod
{
diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs
index 269eab5772..4912df17b1 100644
--- a/osu.Game/Screens/Multi/Multiplayer.cs
+++ b/osu.Game/Screens/Multi/Multiplayer.cs
@@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
+using osu.Game.Overlays;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Lounge;
@@ -50,6 +51,9 @@ namespace osu.Game.Screens.Multi
[Cached]
private readonly Bindable currentFilter = new Bindable(new FilterCriteria());
+ [Resolved]
+ private MusicController music { get; set; }
+
[Cached(Type = typeof(IRoomManager))]
private RoomManager roomManager;
@@ -346,8 +350,7 @@ namespace osu.Game.Screens.Multi
track.RestartPoint = Beatmap.Value.Metadata.PreviewTime;
track.Looping = true;
- if (!track.IsRunning)
- track.Restart();
+ music.EnsurePlayingSomething();
}
}
else
diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
index c2381fe219..04da943a10 100644
--- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
+++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
@@ -58,13 +58,13 @@ namespace osu.Game.Screens.Multi.Play
if (!playlistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods");
- var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID);
+ var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID, Game.VersionHash);
req.Success += r => token = r.ID;
req.Failure += e =>
{
failed = true;
- Logger.Error(e, "Failed to retrieve a score submission token.");
+ Logger.Error(e, "Failed to retrieve a score submission token.\n\nThis may happen if you are running an old or non-official release of osu! (ie. you are self-compiling).");
Schedule(() =>
{
diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
index f367d44347..8da6a530a8 100644
--- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
+++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
@@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -20,7 +22,15 @@ namespace osu.Game.Screens.Multi.Ranking
private readonly int roomId;
private readonly PlaylistItem playlistItem;
- private LoadingSpinner loadingLayer;
+ protected LoadingSpinner LeftSpinner { get; private set; }
+ protected LoadingSpinner CentreSpinner { get; private set; }
+ protected LoadingSpinner RightSpinner { get; private set; }
+
+ private MultiplayerScores higherScores;
+ private MultiplayerScores lowerScores;
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
: base(score, allowRetry)
@@ -32,29 +42,209 @@ namespace osu.Game.Screens.Multi.Ranking
[BackgroundDependencyLoader]
private void load()
{
- AddInternal(loadingLayer = new LoadingLayer
+ AddInternal(new Container
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- X = -10,
- State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden },
- Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y }
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y },
+ Children = new Drawable[]
+ {
+ LeftSpinner = new PanelListLoadingSpinner(ScorePanelList)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.Centre,
+ },
+ CentreSpinner = new PanelListLoadingSpinner(ScorePanelList)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden },
+ },
+ RightSpinner = new PanelListLoadingSpinner(ScorePanelList)
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ },
+ }
});
}
protected override APIRequest FetchScores(Action> scoresCallback)
{
- var req = new GetRoomPlaylistScoresRequest(roomId, playlistItem.ID);
+ // This performs two requests:
+ // 1. A request to show the user's score (and scores around).
+ // 2. If that fails, a request to index the room starting from the highest score.
- req.Success += r =>
+ var userScoreReq = new ShowPlaylistUserScoreRequest(roomId, playlistItem.ID, api.LocalUser.Value.Id);
+
+ userScoreReq.Success += userScore =>
{
- scoresCallback?.Invoke(r.Scores.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem)));
- loadingLayer.Hide();
+ var allScores = new List { userScore };
+
+ if (userScore.ScoresAround?.Higher != null)
+ {
+ allScores.AddRange(userScore.ScoresAround.Higher.Scores);
+ higherScores = userScore.ScoresAround.Higher;
+
+ Debug.Assert(userScore.Position != null);
+ setPositions(higherScores, userScore.Position.Value, -1);
+ }
+
+ if (userScore.ScoresAround?.Lower != null)
+ {
+ allScores.AddRange(userScore.ScoresAround.Lower.Scores);
+ lowerScores = userScore.ScoresAround.Lower;
+
+ Debug.Assert(userScore.Position != null);
+ setPositions(lowerScores, userScore.Position.Value, 1);
+ }
+
+ performSuccessCallback(scoresCallback, allScores);
};
- req.Failure += _ => loadingLayer.Hide();
+ // On failure, fallback to a normal index.
+ userScoreReq.Failure += _ => api.Queue(createIndexRequest(scoresCallback));
- return req;
+ return userScoreReq;
+ }
+
+ protected override APIRequest FetchNextPage(int direction, Action> scoresCallback)
+ {
+ Debug.Assert(direction == 1 || direction == -1);
+
+ MultiplayerScores pivot = direction == -1 ? higherScores : lowerScores;
+
+ if (pivot?.Cursor == null)
+ return null;
+
+ if (pivot == higherScores)
+ LeftSpinner.Show();
+ else
+ RightSpinner.Show();
+
+ return createIndexRequest(scoresCallback, pivot);
+ }
+
+ ///
+ /// Creates a with an optional score pivot.
+ ///
+ /// Does not queue the request.
+ /// The callback to perform with the resulting scores.
+ /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score.
+ /// The indexing .
+ private APIRequest createIndexRequest(Action> scoresCallback, [CanBeNull] MultiplayerScores pivot = null)
+ {
+ var indexReq = pivot != null
+ ? new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params)
+ : new IndexPlaylistScoresRequest(roomId, playlistItem.ID);
+
+ indexReq.Success += r =>
+ {
+ if (pivot == lowerScores)
+ {
+ lowerScores = r;
+ setPositions(r, pivot, 1);
+ }
+ else
+ {
+ higherScores = r;
+ setPositions(r, pivot, -1);
+ }
+
+ performSuccessCallback(scoresCallback, r.Scores, r);
+ };
+
+ indexReq.Failure += _ => hideLoadingSpinners(pivot);
+
+ return indexReq;
+ }
+
+ ///
+ /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback.
+ ///
+ /// The callback to invoke with the final s.
+ /// The s that were retrieved from s.
+ /// An optional pivot around which the scores were retrieved.
+ private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null)
+ {
+ var scoreInfos = new List(scores.Select(s => s.CreateScoreInfo(playlistItem)));
+
+ // Select a score if we don't already have one selected.
+ // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
+ if (SelectedScore.Value == null)
+ {
+ Schedule(() =>
+ {
+ // Prefer selecting the local user's score, or otherwise default to the first visible score.
+ SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
+ });
+ }
+
+ // Invoke callback to add the scores. Exclude the user's current score which was added previously.
+ callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
+
+ hideLoadingSpinners(pivot);
+ }
+
+ private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)
+ {
+ CentreSpinner.Hide();
+
+ if (pivot == lowerScores)
+ RightSpinner.Hide();
+ else if (pivot == higherScores)
+ LeftSpinner.Hide();
+ }
+
+ ///
+ /// Applies positions to all s referenced to a given pivot.
+ ///
+ /// The to set positions on.
+ /// The pivot.
+ /// The amount to increment the pivot position by for each in .
+ private void setPositions([NotNull] MultiplayerScores scores, [CanBeNull] MultiplayerScores pivot, int increment)
+ => setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment);
+
+ ///
+ /// Applies positions to all s referenced to a given pivot.
+ ///
+ /// The to set positions on.
+ /// The pivot position.
+ /// The amount to increment the pivot position by for each in .
+ private void setPositions([NotNull] MultiplayerScores scores, int pivotPosition, int increment)
+ {
+ foreach (var s in scores.Scores)
+ {
+ pivotPosition += increment;
+ s.Position = pivotPosition;
+ }
+ }
+
+ private class PanelListLoadingSpinner : LoadingSpinner
+ {
+ private readonly ScorePanelList list;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The list to track.
+ /// Whether the spinner should have a surrounding black box for visibility.
+ public PanelListLoadingSpinner(ScorePanelList list, bool withBox = true)
+ : base(withBox)
+ {
+ this.list = list;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ float panelOffset = list.DrawWidth / 2 - ScorePanel.EXPANDED_WIDTH;
+
+ if ((Anchor & Anchor.x0) > 0)
+ X = (float)(panelOffset - list.Current);
+ else if ((Anchor & Anchor.x2) > 0)
+ X = (float)(list.ScrollableExtent - list.Current - panelOffset);
+ }
}
}
}
diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs
index fa917cda32..97f1d1c91d 100644
--- a/osu.Game/Screens/Play/PauseOverlay.cs
+++ b/osu.Game/Screens/Play/PauseOverlay.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Game.Audio;
using osu.Game.Graphics;
diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs
new file mode 100644
index 0000000000..0935ee7fb2
--- /dev/null
+++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Scoring;
+
+namespace osu.Game.Screens.Ranking.Contracted
+{
+ public class ContractedPanelTopContent : CompositeDrawable
+ {
+ private readonly ScoreInfo score;
+
+ public ContractedPanelTopContent(ScoreInfo score)
+ {
+ this.score = score;
+
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Y = 6,
+ Text = score.Position != null ? $"#{score.Position}" : string.Empty,
+ Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold)
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 44458d8c8e..c95cf1066e 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -37,7 +37,8 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable SelectedScore = new Bindable();
public readonly ScoreInfo Score;
- private readonly bool allowRetry;
+
+ protected ScorePanelList ScorePanelList { get; private set; }
[Resolved(CanBeNull = true)]
private Player player { get; set; }
@@ -47,9 +48,13 @@ namespace osu.Game.Screens.Ranking
private StatisticsPanel statisticsPanel;
private Drawable bottomPanel;
- private ScorePanelList scorePanelList;
private Container detachedPanelContainer;
+ private bool fetchedInitialScores;
+ private APIRequest nextPageRequest;
+
+ private readonly bool allowRetry;
+
protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
{
Score = score;
@@ -84,7 +89,7 @@ namespace osu.Game.Screens.Ranking
RelativeSizeAxes = Axes.Both,
Score = { BindTarget = SelectedScore }
},
- scorePanelList = new ScorePanelList
+ ScorePanelList = new ScorePanelList
{
RelativeSizeAxes = Axes.Both,
SelectedScore = { BindTarget = SelectedScore },
@@ -142,7 +147,7 @@ namespace osu.Game.Screens.Ranking
};
if (Score != null)
- scorePanelList.AddScore(Score);
+ ScorePanelList.AddScore(Score);
if (player != null && allowRetry)
{
@@ -164,11 +169,7 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
- var req = FetchScores(scores => Schedule(() =>
- {
- foreach (var s in scores)
- addScore(s);
- }));
+ var req = FetchScores(fetchScoresCallback);
if (req != null)
api.Queue(req);
@@ -176,6 +177,28 @@ namespace osu.Game.Screens.Ranking
statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
}
+ protected override void Update()
+ {
+ base.Update();
+
+ if (fetchedInitialScores && nextPageRequest == null)
+ {
+ if (ScorePanelList.IsScrolledToStart)
+ nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
+ else if (ScorePanelList.IsScrolledToEnd)
+ nextPageRequest = FetchNextPage(1, fetchScoresCallback);
+
+ if (nextPageRequest != null)
+ {
+ // Scheduled after children to give the list a chance to update its scroll position and not potentially trigger a second request too early.
+ nextPageRequest.Success += () => ScheduleAfterChildren(() => nextPageRequest = null);
+ nextPageRequest.Failure += _ => ScheduleAfterChildren(() => nextPageRequest = null);
+
+ api.Queue(nextPageRequest);
+ }
+ }
+ }
+
///
/// Performs a fetch/refresh of scores to be displayed.
///
@@ -183,6 +206,22 @@ namespace osu.Game.Screens.Ranking
/// An responsible for the fetch operation. This will be queued and performed automatically.
protected virtual APIRequest FetchScores(Action> scoresCallback) => null;
+ ///
+ /// Performs a fetch of the next page of scores. This is invoked every frame until a non-null is returned.
+ ///
+ /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list.
+ /// A callback which should be called when fetching is completed. Scheduling is not required.
+ /// An responsible for the fetch operation. This will be queued and performed automatically.
+ protected virtual APIRequest FetchNextPage(int direction, Action> scoresCallback) => null;
+
+ private void fetchScoresCallback(IEnumerable scores) => Schedule(() =>
+ {
+ foreach (var s in scores)
+ addScore(s);
+
+ fetchedInitialScores = true;
+ });
+
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
@@ -213,7 +252,7 @@ namespace osu.Game.Screens.Ranking
private void addScore(ScoreInfo score)
{
- var panel = scorePanelList.AddScore(score);
+ var panel = ScorePanelList.AddScore(score);
if (detachedPanel != null)
panel.Alpha = 0;
@@ -226,11 +265,11 @@ namespace osu.Game.Screens.Ranking
if (state.NewValue == Visibility.Visible)
{
// Detach the panel in its original location, and move into the desired location in the local container.
- var expandedPanel = scorePanelList.GetPanelForScore(SelectedScore.Value);
+ var expandedPanel = ScorePanelList.GetPanelForScore(SelectedScore.Value);
var screenSpacePos = expandedPanel.ScreenSpaceDrawQuad.TopLeft;
// Detach and move into the local container.
- scorePanelList.Detach(expandedPanel);
+ ScorePanelList.Detach(expandedPanel);
detachedPanelContainer.Add(expandedPanel);
// Move into its original location in the local container first, then to the final location.
@@ -240,9 +279,9 @@ namespace osu.Game.Screens.Ranking
.MoveTo(new Vector2(StatisticsPanel.SIDE_PADDING, origLocation.Y), 150, Easing.OutQuint);
// Hide contracted panels.
- foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
+ foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
contracted.FadeOut(150, Easing.OutQuint);
- scorePanelList.HandleInput = false;
+ ScorePanelList.HandleInput = false;
// Dim background.
Background.FadeTo(0.1f, 150);
@@ -255,7 +294,7 @@ namespace osu.Game.Screens.Ranking
// Remove from the local container and re-attach.
detachedPanelContainer.Remove(detachedPanel);
- scorePanelList.Attach(detachedPanel);
+ ScorePanelList.Attach(detachedPanel);
// Move into its original location in the attached container first, then to the final location.
var origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos);
@@ -264,9 +303,9 @@ namespace osu.Game.Screens.Ranking
.MoveTo(new Vector2(0, origLocation.Y), 150, Easing.OutQuint);
// Show contracted panels.
- foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
+ foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
contracted.FadeIn(150, Easing.OutQuint);
- scorePanelList.HandleInput = true;
+ ScorePanelList.HandleInput = true;
// Un-dim background.
Background.FadeTo(0.5f, 150);
diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs
index 5da432d5b2..1904da7094 100644
--- a/osu.Game/Screens/Ranking/ScorePanel.cs
+++ b/osu.Game/Screens/Ranking/ScorePanel.cs
@@ -151,7 +151,7 @@ namespace osu.Game.Screens.Ranking
RelativeSizeAxes = Axes.Both,
User = Score.User,
Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0))
- },
+ }
}
},
middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
@@ -203,8 +203,8 @@ namespace osu.Game.Screens.Ranking
topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint);
middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint);
- topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0));
- middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0));
+ topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0));
+ middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0));
break;
case PanelState.Contracted:
@@ -213,7 +213,8 @@ namespace osu.Game.Screens.Ranking
topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint);
middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint);
- middleLayerContentContainer.Add(topLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0));
+ topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent(Score).With(d => d.Alpha = 0));
+ middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0));
break;
}
diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index 0f8bc82ac0..10bd99c8ce 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -26,6 +26,31 @@ namespace osu.Game.Screens.Ranking
///
private const float expanded_panel_spacing = 15;
+ ///
+ /// Minimum distance from either end point of the list that the list can be considered scrolled to the end point.
+ ///
+ private const float scroll_endpoint_distance = 100;
+
+ ///
+ /// Whether this can be scrolled and is currently scrolled to the start.
+ ///
+ public bool IsScrolledToStart => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance;
+
+ ///
+ /// Whether this can be scrolled and is currently scrolled to the end.
+ ///
+ public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance);
+
+ ///
+ /// The current scroll position.
+ ///
+ public double Current => scroll.Current;
+
+ ///
+ /// The scrollable extent.
+ ///
+ public double ScrollableExtent => scroll.ScrollableExtent;
+
///
/// An action to be invoked if a is clicked while in an expanded state.
///
diff --git a/osu.Game/Skinning/GlobalSkinConfiguration.cs b/osu.Game/Skinning/GlobalSkinConfiguration.cs
deleted file mode 100644
index d405702ea5..0000000000
--- a/osu.Game/Skinning/GlobalSkinConfiguration.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Skinning
-{
- public enum GlobalSkinConfiguration
- {
- AnimationFramerate,
- LayeredHitSounds,
- }
-}
diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs
index 87bca856a3..d647bc4a2d 100644
--- a/osu.Game/Skinning/LegacyBeatmapSkin.cs
+++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Skinning
public class LegacyBeatmapSkin : LegacySkin
{
protected override bool AllowManiaSkin => false;
+ protected override bool UseCustomSampleBanks => true;
public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, AudioManager audioManager)
: base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), audioManager, beatmap.Path)
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 3bbeff9918..02d07eee45 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -38,6 +38,12 @@ namespace osu.Game.Skinning
protected virtual bool AllowManiaSkin => hasKeyTexture.Value;
+ ///
+ /// Whether this skin can use samples with a custom bank (custom sample set in stable terminology).
+ /// Added in order to match sample lookup logic from stable (in stable, only the beatmap skin could use samples with a custom sample bank).
+ ///
+ protected virtual bool UseCustomSampleBanks => false;
+
public new LegacySkinConfiguration Configuration
{
get => base.Configuration as LegacySkinConfiguration;
@@ -120,15 +126,6 @@ namespace osu.Game.Skinning
break;
- case LegacySkinConfiguration.LegacySetting legacy:
- switch (legacy)
- {
- case LegacySkinConfiguration.LegacySetting.Version:
- return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION));
- }
-
- break;
-
case SkinCustomColourLookup customColour:
return SkinUtils.As(getCustomColour(Configuration, customColour.Lookup.ToString()));
@@ -142,28 +139,11 @@ namespace osu.Game.Skinning
break;
+ case LegacySkinConfiguration.LegacySetting legacy:
+ return legacySettingLookup(legacy);
+
default:
- // handles lookups like GlobalSkinConfiguration
-
- try
- {
- if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out var val))
- {
- // special case for handling skins which use 1 or 0 to signify a boolean state.
- if (typeof(TValue) == typeof(bool))
- val = val == "1" ? "true" : "false";
-
- var bindable = new Bindable();
- if (val != null)
- bindable.Parse(val);
- return bindable;
- }
- }
- catch
- {
- }
-
- break;
+ return genericLookup(lookup);
}
return null;
@@ -286,6 +266,43 @@ namespace osu.Game.Skinning
private IBindable getManiaImage(LegacyManiaSkinConfiguration source, string lookup)
=> source.ImageLookups.TryGetValue(lookup, out var image) ? new Bindable(image) : null;
+ [CanBeNull]
+ private IBindable legacySettingLookup(LegacySkinConfiguration.LegacySetting legacySetting)
+ {
+ switch (legacySetting)
+ {
+ case LegacySkinConfiguration.LegacySetting.Version:
+ return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION));
+
+ default:
+ return genericLookup(legacySetting);
+ }
+ }
+
+ [CanBeNull]
+ private IBindable genericLookup(TLookup lookup)
+ {
+ try
+ {
+ if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out var val))
+ {
+ // special case for handling skins which use 1 or 0 to signify a boolean state.
+ if (typeof(TValue) == typeof(bool))
+ val = val == "1" ? "true" : "false";
+
+ var bindable = new Bindable();
+ if (val != null)
+ bindable.Parse(val);
+ return bindable;
+ }
+ }
+ catch
+ {
+ }
+
+ return null;
+ }
+
public override Drawable GetDrawableComponent(ISkinComponent component)
{
switch (component)
@@ -337,7 +354,12 @@ namespace osu.Game.Skinning
public override SampleChannel GetSample(ISampleInfo sampleInfo)
{
- foreach (var lookup in sampleInfo.LookupNames)
+ var lookupNames = sampleInfo.LookupNames;
+
+ if (sampleInfo is HitSampleInfo hitSample)
+ lookupNames = getLegacyLookupNames(hitSample);
+
+ foreach (var lookup in lookupNames)
{
var sample = Samples?.Get(lookup);
@@ -345,10 +367,6 @@ namespace osu.Game.Skinning
return sample;
}
- if (sampleInfo is HitSampleInfo hsi)
- // Try fallback to non-bank samples.
- return Samples?.Get(hsi.Name);
-
return null;
}
@@ -361,5 +379,23 @@ namespace osu.Game.Skinning
string lastPiece = componentName.Split('/').Last();
yield return componentName.StartsWith("Gameplay/taiko/") ? "taiko-" + lastPiece : lastPiece;
}
+
+ private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample)
+ {
+ var lookupNames = hitSample.LookupNames;
+
+ if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix))
+ // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin.
+ // using .EndsWith() is intentional as it ensures parity in all edge cases
+ // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not).
+ lookupNames = hitSample.LookupNames.Where(name => !name.EndsWith(hitSample.Suffix));
+
+ // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort.
+ // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable,
+ // which is why this is done locally here.
+ lookupNames = lookupNames.Append(hitSample.Name);
+
+ return lookupNames;
+ }
}
}
diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs
index 027f5b8883..41b7aea34b 100644
--- a/osu.Game/Skinning/LegacySkinConfiguration.cs
+++ b/osu.Game/Skinning/LegacySkinConfiguration.cs
@@ -15,6 +15,8 @@ namespace osu.Game.Skinning
public enum LegacySetting
{
Version,
+ AnimationFramerate,
+ LayeredHitSounds,
}
}
}
diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs
index 7cf41ef3c1..bb46dc8b9f 100644
--- a/osu.Game/Skinning/LegacySkinExtensions.cs
+++ b/osu.Game/Skinning/LegacySkinExtensions.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Skinning
{
@@ -89,7 +90,7 @@ namespace osu.Game.Skinning
{
if (applyConfigFrameRate)
{
- var iniRate = source.GetConfig(GlobalSkinConfiguration.AnimationFramerate);
+ var iniRate = source.GetConfig(LegacySetting.AnimationFramerate);
if (iniRate?.Value > 0)
return 1000f / iniRate.Value;
diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs
index 786056b932..ebc4757e75 100644
--- a/osu.Game/Skinning/LegacySkinTransformer.cs
+++ b/osu.Game/Skinning/LegacySkinTransformer.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Legacy;
+using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Skinning
{
@@ -38,7 +39,7 @@ namespace osu.Game.Skinning
if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample))
return Source.GetSample(sampleInfo);
- var playLayeredHitSounds = GetConfig(GlobalSkinConfiguration.LayeredHitSounds);
+ var playLayeredHitSounds = GetConfig(LegacySetting.LayeredHitSounds);
if (legacySample.IsLayered && playLayeredHitSounds?.Value == false)
return new SampleChannelVirtual();
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index 27f6c37895..11856fa581 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -4,19 +4,18 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Transforms;
using osu.Game.Audio;
using osu.Game.Screens.Play;
namespace osu.Game.Skinning
{
- public class SkinnableSound : SkinReloadableDrawable
+ public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent
{
private readonly ISampleInfo[] hitSamples;
@@ -143,36 +142,17 @@ namespace osu.Game.Skinning
public BindableNumber Tempo => samplesContainer.Tempo;
+ public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable)
+ => samplesContainer.AddAdjustment(type, adjustBindable);
+
+ public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable)
+ => samplesContainer.RemoveAdjustment(type, adjustBindable);
+
+ public void RemoveAllAdjustments(AdjustableProperty type)
+ => samplesContainer.RemoveAllAdjustments(type);
+
public bool IsPlaying => samplesContainer.Any(s => s.Playing);
- ///
- /// Smoothly adjusts over time.
- ///
- /// A to which further transforms can be added.
- public TransformSequence VolumeTo(double newVolume, double duration = 0, Easing easing = Easing.None) =>
- samplesContainer.VolumeTo(newVolume, duration, easing);
-
- ///
- /// Smoothly adjusts over time.
- ///
- /// A to which further transforms can be added.
- public TransformSequence BalanceTo(double newBalance, double duration = 0, Easing easing = Easing.None) =>
- samplesContainer.BalanceTo(newBalance, duration, easing);
-
- ///
- /// Smoothly adjusts over time.
- ///
- /// A to which further transforms can be added.
- public TransformSequence FrequencyTo(double newFrequency, double duration = 0, Easing easing = Easing.None) =>
- samplesContainer.FrequencyTo(newFrequency, duration, easing);
-
- ///
- /// Smoothly adjusts over time.
- ///
- /// A to which further transforms can be added.
- public TransformSequence TempoTo(double newTempo, double duration = 0, Easing easing = Easing.None) =>
- samplesContainer.TempoTo(newTempo, duration, easing);
-
#endregion
}
}
diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs
index 1193a29d70..31cced6ce4 100644
--- a/osu.Game/Tests/TestScoreInfo.cs
+++ b/osu.Game/Tests/TestScoreInfo.cs
@@ -37,6 +37,8 @@ namespace osu.Game.Tests
Statistics[HitResult.Meh] = 50;
Statistics[HitResult.Good] = 100;
Statistics[HitResult.Great] = 300;
+
+ Position = 1;
}
private class TestModHardRock : ModHardRock
diff --git a/osu.Game/Users/UserCoverBackground.cs b/osu.Game/Users/UserCoverBackground.cs
index 748d9bd939..34bbf6892e 100644
--- a/osu.Game/Users/UserCoverBackground.cs
+++ b/osu.Game/Users/UserCoverBackground.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -23,6 +24,16 @@ namespace osu.Game.Users
protected override Drawable CreateDrawable(User user) => new Cover(user);
+ protected override double LoadDelay => 300;
+
+ ///
+ /// Delay before the background is unloaded while off-screen.
+ ///
+ protected virtual double UnloadDelay => 5000;
+
+ protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad)
+ => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay);
+
[LongRunningLoad]
private class Cover : CompositeDrawable
{
diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs
index 94c0c31cfc..57a87a713d 100644
--- a/osu.Game/Users/UserPanel.cs
+++ b/osu.Game/Users/UserPanel.cs
@@ -4,7 +4,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@@ -25,7 +24,7 @@ namespace osu.Game.Users
protected Action ViewProfile { get; private set; }
- protected DelayedLoadUnloadWrapper Background { get; private set; }
+ protected Drawable Background { get; private set; }
protected UserPanel(User user)
{
@@ -56,17 +55,12 @@ namespace osu.Game.Users
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider?.Background5 ?? Colours.Gray1
},
- Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground
+ Background = new UserCoverBackground
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
User = User,
- }, 300, 5000)
- {
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- RelativeSizeAxes = Axes.Both,
},
CreateLayout()
});
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 5ac54a853f..b38ef38ec2 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,8 +24,8 @@
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 8b2d1346be..00ddd94d53 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -80,7 +80,7 @@
-
+