1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 00:47:24 +08:00

Merge branch 'master' into spinner-skinning

This commit is contained in:
Dean Herbert 2020-07-31 23:35:06 +09:00 committed by GitHub
commit f03bad6513
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1200 additions and 293 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.727.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.723.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.731.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.730.1" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.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
{
/// <summary>
/// Index of banana in current shower.
/// </summary>
public int BananaIndex;
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement();
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() };
public Banana()
{
Samples = samples;
}
private class BananaHitSampleInfo : HitSampleInfo
{
private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" };
public override IEnumerable<string> LookupNames => lookupNames;
}
}
}

View File

@ -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++;
}
}

View File

@ -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))

View File

@ -1,23 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using 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<DrawablePool<PoolableHitExplosion>> hitExplosionPools = new List<DrawablePool<PoolableHitExplosion>>();
public TestSceneHitExplosion()
{
int runcount = 0;
@ -29,28 +33,40 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (runcount % 15 > 12)
return;
CreatedDrawables.OfType<Container>().ForEach(c =>
int poolIndex = 0;
foreach (var c in CreatedDrawables.OfType<Container>())
{
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<PoolableHitExplosion>(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
};
});
}
}

View File

@ -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<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
@ -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);

View File

@ -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<ManiaAction> Action = new Bindable<ManiaAction>();
public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container TopLevelContainer;
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new[]
{
hitExplosionPool = new DrawablePool<PoolableHitExplosion>(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)

View File

@ -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<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
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<ScrollingDirection> 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);
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Mania.UI
{
/// <summary>
/// Common interface for all hit explosion bodies.
/// </summary>
public interface IHitExplosion
{
/// <summary>
/// Begins animating this <see cref="IHitExplosion"/>.
/// </summary>
/// <param name="result">The type of <see cref="JudgementResult"/> that caused this explosion.</param>
void Animate(JudgementResult result);
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.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();
}
}
}

View File

@ -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:

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<byte[]> 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);
}
}
}

View File

@ -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
/// </summary>
[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
}
/// <summary>
/// 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
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[Test]
public void TestUserSkinLookupIgnoresSampleBank()
{
const string unwanted_sample = "normal-hitnormal2";
SetupSkins(string.Empty, unwanted_sample);
CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
AssertNoLookup(unwanted_sample);
}
/// <summary>
/// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin.
/// </summary>
@ -145,6 +165,7 @@ namespace osu.Game.Tests.Gameplay
/// </summary>
[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]);

View File

@ -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<ScorePanel>().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<ScorePanel>().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<ScorePanel>().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().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<ScorePanel>().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<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().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<ScorePanel>().Count());
AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
}
}
private void createResults(Func<ScoreInfo> 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<MultiplayerScore>();
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<T>(APIRequest<T> 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<long>() ?? 1000000;
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "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, int>
{
{ 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<string, JToken>
{
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<string, JToken>
{
{ "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)
{
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -1,13 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.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<Drawable> 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);
}
}
}

View File

@ -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();
}

View File

@ -15,6 +15,6 @@ namespace osu.Game.Online.API.Requests
{
[UsedImplicitly]
[JsonExtensionData]
public IDictionary<string, JToken> Properties;
public IDictionary<string, JToken> Properties { get; set; } = new Dictionary<string, JToken>();
}
}

View File

@ -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;
}

View File

@ -1,29 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Online.API;
namespace osu.Game.Online.Multiplayer
{
public class GetRoomPlaylistScoresRequest : APIRequest<RoomPlaylistScores>
{
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<MultiplayerScore> Scores { get; set; }
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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
{
/// <summary>
/// Returns a list of scores for the specified playlist item.
/// </summary>
public class IndexPlaylistScoresRequest : APIRequest<IndexedMultiplayerScores>
{
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";
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// A collection of parameters which should be passed to the index endpoint to fetch the next page.
/// </summary>
public class IndexScoresParams
{
[UsedImplicitly]
[JsonExtensionData]
public IDictionary<string, JToken> Properties { get; set; } = new Dictionary<string, JToken>();
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// A <see cref="MultiplayerScores"/> object returned via a <see cref="IndexPlaylistScoresRequest"/>.
/// </summary>
public class IndexedMultiplayerScores : MultiplayerScores
{
/// <summary>
/// The total scores in the playlist item.
/// </summary>
[JsonProperty("total")]
public int? TotalScores { get; set; }
/// <summary>
/// The user's score, if any.
/// </summary>
[JsonProperty("user_score")]
[CanBeNull]
public MultiplayerScore UserScore { get; set; }
}
}

View File

@ -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; }
/// <summary>
/// The position of this score, starting at 1.
/// </summary>
[JsonProperty("position")]
public int? Position { get; set; }
/// <summary>
/// Any scores in the room around this score.
/// </summary>
[JsonProperty("scores_around")]
[CanBeNull]
public MultiplayerScoresAround ScoresAround { get; set; }
public ScoreInfo CreateScoreInfo(PlaylistItem playlistItem)
{
var rulesetInstance = playlistItem.Ruleset.Value.CreateInstance();

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Online.API.Requests;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// An object which contains scores and related data for fetching next pages.
/// </summary>
public class MultiplayerScores : ResponseWithCursor
{
/// <summary>
/// The scores.
/// </summary>
[JsonProperty("scores")]
public List<MultiplayerScore> Scores { get; set; } = new List<MultiplayerScore>();
/// <summary>
/// The parameters to be used to fetch the next page.
/// </summary>
[JsonProperty("params")]
public IndexScoresParams Params { get; set; } = new IndexScoresParams();
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// An object which stores scores higher and lower than the user's score.
/// </summary>
public class MultiplayerScoresAround
{
/// <summary>
/// Scores sorted "higher" than the user's score, depending on the sorting order.
/// </summary>
[JsonProperty("higher")]
[CanBeNull]
public MultiplayerScores Higher { get; set; }
/// <summary>
/// Scores sorted "lower" than the user's score, depending on the sorting order.
/// </summary>
[JsonProperty("lower")]
[CanBeNull]
public MultiplayerScores Lower { get; set; }
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Online.API;
namespace osu.Game.Online.Multiplayer
{
public class ShowPlaylistUserScoreRequest : APIRequest<MultiplayerScore>
{
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}";
}
}

View File

@ -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();
/// <summary>
/// MD5 representation of the game executable.
/// </summary>
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));

View File

@ -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);
}
}
}

View File

@ -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<Drawable> EffectTargets => new[] { text };
private OsuSpriteText text;
public ShowMoreButton()
public ShowMoreRepliesButton()
{
AutoSizeAxes = Axes.Both;
LoadingAnimationSize = new Vector2(8);

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.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);

View File

@ -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)
}

View File

@ -236,8 +236,8 @@ namespace osu.Game.Overlays
{
if (beatmap is Bindable<WorkingBeatmap> 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<WorkingBeatmap> 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;

View File

@ -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;
}
}
}

View File

@ -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<TModel> : FillFlowContainer
{
private readonly ProfileShowMoreButton moreButton;
private readonly ShowMoreButton moreButton;
private readonly OsuSpriteText missingText;
private APIRequest<List<TModel>> 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,

View File

@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.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;
}
}
}

View File

@ -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);
}

View File

@ -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 =>
{

View File

@ -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<IEnumerable<ScoreInfo>> 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<MultiplayerScore> { 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<IEnumerable<ScoreInfo>> 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);
}
/// <summary>
/// Creates a <see cref="IndexPlaylistScoresRequest"/> with an optional score pivot.
/// </summary>
/// <remarks>Does not queue the request.</remarks>
/// <param name="scoresCallback">The callback to perform with the resulting scores.</param>
/// <param name="pivot">An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score.</param>
/// <returns>The indexing <see cref="APIRequest"/>.</returns>
private APIRequest createIndexRequest(Action<IEnumerable<ScoreInfo>> 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;
}
/// <summary>
/// Transforms returned <see cref="MultiplayerScores"/> into <see cref="ScoreInfo"/>s, ensure the <see cref="ScorePanelList"/> is put into a sane state, and invokes a given success callback.
/// </summary>
/// <param name="callback">The callback to invoke with the final <see cref="ScoreInfo"/>s.</param>
/// <param name="scores">The <see cref="MultiplayerScore"/>s that were retrieved from <see cref="APIRequest"/>s.</param>
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null)
{
var scoreInfos = new List<ScoreInfo>(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;
/// <summary>
/// Creates a new <see cref="PanelListLoadingSpinner"/>.
/// </summary>
/// <param name="list">The list to track.</param>
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
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);
}
}
}
}

View File

@ -37,7 +37,8 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
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<ScorePanel> 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);
}
}
}
/// <summary>
/// Performs a fetch/refresh of scores to be displayed.
/// </summary>
@ -183,6 +206,22 @@ namespace osu.Game.Screens.Ranking
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected virtual APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
/// <summary>
/// Performs a fetch of the next page of scores. This is invoked every frame until a non-null <see cref="APIRequest"/> is returned.
/// </summary>
/// <param name="direction">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.</param>
/// <param name="scoresCallback">A callback which should be called when fetching is completed. Scheduling is not required.</param>
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected virtual APIRequest FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
private void fetchScoresCallback(IEnumerable<ScoreInfo> 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);

View File

@ -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 }

View File

@ -26,6 +26,31 @@ namespace osu.Game.Screens.Ranking
/// </summary>
private const float expanded_panel_spacing = 15;
/// <summary>
/// Minimum distance from either end point of the list that the list can be considered scrolled to the end point.
/// </summary>
private const float scroll_endpoint_distance = 100;
/// <summary>
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the start.
/// </summary>
public bool IsScrolledToStart => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance;
/// <summary>
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the end.
/// </summary>
public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance);
/// <summary>
/// The current scroll position.
/// </summary>
public double Current => scroll.Current;
/// <summary>
/// The scrollable extent.
/// </summary>
public double ScrollableExtent => scroll.ScrollableExtent;
/// <summary>
/// An action to be invoked if a <see cref="ScorePanel"/> is clicked while in an expanded state.
/// </summary>

View File

@ -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<byte[]> storage, AudioManager audioManager)
: base(createSkinInfo(beatmap), new LegacySkinResourceStore<BeatmapSetFileInfo>(beatmap.BeatmapSet, storage), audioManager, beatmap.Path)

View File

@ -38,6 +38,12 @@ namespace osu.Game.Skinning
protected virtual bool AllowManiaSkin => hasKeyTexture.Value;
/// <summary>
/// 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).
/// </summary>
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<string> 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;
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.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;
/// <summary>
/// Delay before the background is unloaded while off-screen.
/// </summary>
protected virtual double UnloadDelay => 5000;
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay);
[LongRunningLoad]
private class Cover : CompositeDrawable
{

View File

@ -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()
});

View File

@ -24,8 +24,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.723.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.727.1" />
<PackageReference Include="ppy.osu.Framework" Version="2020.730.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.731.0" />
<PackageReference Include="Sentry" Version="2.1.5" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -70,8 +70,8 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.723.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.727.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.730.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.731.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies">
@ -80,7 +80,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.723.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.730.1" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />