diff --git a/osu.Android.props b/osu.Android.props
index 7e6f1469f5..13b4b6ebbb 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,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/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/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.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.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
index 737946e1e0..c3acc2ebe7 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
@@ -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]);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
index 0023866124..628d08a314 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,213 @@ 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,
+ 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/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/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/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..1793ba72ef 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();
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/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index fe5c0704b7..278f2d849f 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;
@@ -97,6 +98,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 +134,9 @@ namespace osu.Game
[BackgroundDependencyLoader]
private void load()
{
+ using (var str = File.OpenRead(typeof(OsuGameBase).Assembly.Location))
+ VersionHash = str.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/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/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/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 58a2ba056e..dd43092c0d 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);
}
diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
index c2381fe219..da082692d7 100644
--- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
+++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
@@ -58,7 +58,7 @@ 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 =>
{
diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
index f367d44347..e212bd4a82 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,173 @@ 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;
+ }
+
+ if (userScore.ScoresAround?.Lower != null)
+ {
+ allScores.AddRange(userScore.ScoresAround.Lower.Scores);
+ lowerScores = userScore.ScoresAround.Lower;
+ }
+
+ 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;
+ else
+ higherScores = r;
+
+ 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();
+ }
+
+ 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/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..24d193e9a7 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 }
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/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..fc04383a64 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;
@@ -337,7 +343,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 +356,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 +368,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/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..745555e0e2 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..f1080f0c8b 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -80,7 +80,7 @@
-
+