mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 10:52:53 +08:00
Merge branch 'master' into argon-cursor
This commit is contained in:
commit
9f92925ecd
@ -21,7 +21,6 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
@ -250,11 +249,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Test]
|
||||
public void TestHitLightingColour()
|
||||
{
|
||||
var fruitColour = SkinConfiguration.DefaultComboColours[1];
|
||||
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
|
||||
AddStep("catch fruit", () => attemptCatch(new Fruit()));
|
||||
AddAssert("correct hit lighting colour", () =>
|
||||
catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == fruitColour);
|
||||
AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@ -88,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
if (!objects.Any())
|
||||
return false;
|
||||
|
||||
return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType<ShakeContainer>().First().Children.OfType<Container>().Single().Scale.X, target));
|
||||
return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType<Container>().First().Scale.X, target));
|
||||
}
|
||||
|
||||
private bool checkSomeHit()
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
@ -47,12 +48,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
private ShakeContainer shakeContainer;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
scaleContainer = new Container
|
||||
{
|
||||
@ -72,22 +75,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
return true;
|
||||
},
|
||||
},
|
||||
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
|
||||
shakeContainer = new ShakeContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
ShakeDuration = 30,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Scale = new Vector2(4),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Scale = new Vector2(4),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Size = HitArea.DrawSize;
|
||||
|
||||
@ -123,6 +134,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shake() => shakeContainer.Shake();
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
Debug.Assert(HitObject.HitWindows != null);
|
||||
@ -139,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
|
||||
{
|
||||
Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
|
||||
Shake();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -6,17 +6,16 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableOsuHitObject : DrawableHitObject<OsuHitObject>
|
||||
public abstract class DrawableOsuHitObject : DrawableHitObject<OsuHitObject>
|
||||
{
|
||||
public readonly IBindable<Vector2> PositionBindable = new Bindable<Vector2>();
|
||||
public readonly IBindable<int> StackHeightBindable = new Bindable<int>();
|
||||
@ -34,8 +33,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
public Func<DrawableHitObject, double, bool> CheckHittable;
|
||||
|
||||
private ShakeContainer shakeContainer;
|
||||
|
||||
protected DrawableOsuHitObject(OsuHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
@ -45,12 +42,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
private void load()
|
||||
{
|
||||
Alpha = 0;
|
||||
|
||||
base.AddInternal(shakeContainer = new ShakeContainer
|
||||
{
|
||||
ShakeDuration = 30,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
@ -73,18 +64,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
|
||||
}
|
||||
|
||||
// Forward all internal management to shakeContainer.
|
||||
// This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690)
|
||||
protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable);
|
||||
protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren);
|
||||
protected override bool RemoveInternal(Drawable drawable, bool disposeImmediately) => shakeContainer.Remove(drawable, disposeImmediately);
|
||||
|
||||
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
|
||||
|
||||
private OsuInputManager osuActionInputManager;
|
||||
internal OsuInputManager OsuActionInputManager => osuActionInputManager ??= GetContainingInputManager() as OsuInputManager;
|
||||
|
||||
public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
|
||||
/// <summary>
|
||||
/// Shake the hit object in case it was clicked far too early or late (aka "note lock").
|
||||
/// </summary>
|
||||
public virtual void Shake() { }
|
||||
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public SkinnableDrawable Body { get; private set; }
|
||||
|
||||
private ShakeContainer shakeContainer;
|
||||
|
||||
/// <summary>
|
||||
/// A target container which can be used to add top level elements to the slider's display.
|
||||
/// Intended to be used for proxy purposes only.
|
||||
@ -74,17 +77,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
|
||||
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
||||
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
|
||||
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
||||
shakeContainer = new ShakeContainer
|
||||
{
|
||||
ShakeDuration = 30,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
|
||||
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
||||
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
|
||||
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
||||
}
|
||||
},
|
||||
// slider head is not included in shake as it handles hit detection, and handles its own shaking.
|
||||
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
||||
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
|
||||
Ball,
|
||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
||||
};
|
||||
});
|
||||
|
||||
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
||||
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
||||
@ -109,6 +121,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
PathVersion.BindTo(HitObject.Path.Version);
|
||||
}
|
||||
|
||||
public override void Shake() => shakeContainer.Shake();
|
||||
|
||||
protected override void OnFree()
|
||||
{
|
||||
base.OnFree();
|
||||
|
@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
pathVersion.BindTo(DrawableSlider.PathVersion);
|
||||
|
||||
OnShake = DrawableSlider.Shake;
|
||||
CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
|
||||
}
|
||||
|
||||
@ -96,9 +95,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
|
||||
}
|
||||
|
||||
public Action<double> OnShake;
|
||||
|
||||
public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
|
||||
public override void Shake()
|
||||
{
|
||||
base.Shake();
|
||||
DrawableSlider.Shake();
|
||||
}
|
||||
|
||||
private void updatePosition()
|
||||
{
|
||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Origin = Anchor.Centre;
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
|
||||
InternalChild = scaleContainer = new Container
|
||||
AddInternal(scaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
},
|
||||
Arrow = new ReverseArrowPiece(),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Origin = Anchor.Centre;
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
scaleContainer = new Container
|
||||
{
|
||||
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = scaleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
|
||||
AddInternal(scaleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
|
||||
{
|
||||
Masking = true,
|
||||
Origin = Anchor.Centre,
|
||||
@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
});
|
||||
|
||||
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
|
||||
}
|
||||
|
@ -6,7 +6,9 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.PolygonExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@ -18,37 +20,52 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestFixture]
|
||||
public class TestSceneGameplayLeaderboard : OsuTestScene
|
||||
{
|
||||
private readonly TestGameplayLeaderboard leaderboard;
|
||||
private TestGameplayLeaderboard leaderboard;
|
||||
|
||||
private readonly BindableDouble playerScore = new BindableDouble();
|
||||
|
||||
public TestSceneGameplayLeaderboard()
|
||||
{
|
||||
Add(leaderboard = new TestGameplayLeaderboard
|
||||
AddStep("toggle expanded", () =>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(2),
|
||||
if (leaderboard != null)
|
||||
leaderboard.Expanded.Value = !leaderboard.Expanded.Value;
|
||||
});
|
||||
|
||||
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
[Test]
|
||||
public void TestLayoutWithManyScores()
|
||||
{
|
||||
AddStep("reset leaderboard", () =>
|
||||
createLeaderboard();
|
||||
|
||||
AddStep("add many scores in one go", () =>
|
||||
{
|
||||
leaderboard.Clear();
|
||||
playerScore.Value = 1222333;
|
||||
for (int i = 0; i < 32; i++)
|
||||
createRandomScore(new APIUser { Username = $"Player {i + 1}" });
|
||||
|
||||
// Add player at end to force an animation down the whole list.
|
||||
playerScore.Value = 0;
|
||||
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
||||
});
|
||||
|
||||
AddStep("add local player", () => createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true));
|
||||
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
|
||||
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
|
||||
// Gameplay leaderboard has custom scroll logic, which when coupled with LayoutDuration
|
||||
// has caused layout to not work in the past.
|
||||
|
||||
AddUntilStep("wait for fill flow layout",
|
||||
() => leaderboard.ChildrenOfType<FillFlowContainer<GameplayLeaderboardScore>>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad));
|
||||
|
||||
AddUntilStep("wait for some scores not masked away",
|
||||
() => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayerScore()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
var player2Score = new BindableDouble(1234567);
|
||||
var player3Score = new BindableDouble(1111111);
|
||||
|
||||
@ -73,6 +90,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestRandomScores()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
int playerNumber = 1;
|
||||
AddRepeatStep("add player with random score", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 10);
|
||||
}
|
||||
@ -80,6 +100,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestExistingUsers()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
AddStep("add peppy", () => createRandomScore(new APIUser { Username = "peppy", Id = 2 }));
|
||||
AddStep("add smoogipoo", () => createRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 }));
|
||||
AddStep("add flyte", () => createRandomScore(new APIUser { Username = "flyte", Id = 3103765 }));
|
||||
@ -89,6 +112,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestMaxHeight()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
int playerNumber = 1;
|
||||
AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3);
|
||||
checkHeight(4);
|
||||
@ -103,6 +129,28 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
|
||||
}
|
||||
|
||||
private void addLocalPlayer()
|
||||
{
|
||||
AddStep("add local player", () =>
|
||||
{
|
||||
playerScore.Value = 1222333;
|
||||
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
||||
});
|
||||
}
|
||||
|
||||
private void createLeaderboard()
|
||||
{
|
||||
AddStep("create leaderboard", () =>
|
||||
{
|
||||
Child = leaderboard = new TestGameplayLeaderboard
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(2),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user);
|
||||
|
||||
private void createLeaderboardScore(BindableDouble score, APIUser user, bool isTracked = false)
|
||||
|
@ -0,0 +1,72 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneSoloGameplayLeaderboard : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||
|
||||
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("clear scores", () => scores.Clear());
|
||||
|
||||
AddStep("create component", () =>
|
||||
{
|
||||
var trackingUser = new APIUser
|
||||
{
|
||||
Username = "local user",
|
||||
Id = 2,
|
||||
};
|
||||
|
||||
Child = new SoloGameplayLeaderboard(trackingUser)
|
||||
{
|
||||
Scores = { BindTarget = scores },
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Expanded = { Value = true },
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("add scores", () => scores.AddRange(createSampleScores()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalUser()
|
||||
{
|
||||
AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v);
|
||||
AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v);
|
||||
AddSliderStep("combo", 0, 1000, 0, v => scoreProcessor.Combo.Value = v);
|
||||
}
|
||||
|
||||
private static List<ScoreInfo> createSampleScores()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ScoreInfo { User = new APIUser { Username = @"peppy" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
new ScoreInfo { User = new APIUser { Username = @"smoogipoo" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
}.Concat(Enumerable.Range(0, 50).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@ -120,6 +120,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
=> AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
|
||||
|
||||
private void assertCombo(int userId, int expectedCombo)
|
||||
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
|
||||
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo);
|
||||
}
|
||||
}
|
||||
|
@ -522,7 +522,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
|
||||
|
||||
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId);
|
||||
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
|
||||
|
||||
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
|
||||
}
|
||||
|
@ -39,7 +39,9 @@ namespace osu.Game.Online.Leaderboards
|
||||
/// <summary>
|
||||
/// The currently displayed scores.
|
||||
/// </summary>
|
||||
public IEnumerable<TScoreInfo> Scores => scores;
|
||||
public IBindableList<TScoreInfo> Scores => scores;
|
||||
|
||||
private readonly BindableList<TScoreInfo> scores = new BindableList<TScoreInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current scope should refetch in response to changes in API connectivity state.
|
||||
@ -68,8 +70,6 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
|
||||
private ICollection<TScoreInfo> scores;
|
||||
|
||||
private TScope scope;
|
||||
|
||||
public TScope Scope
|
||||
@ -169,7 +169,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation.");
|
||||
}
|
||||
|
||||
Debug.Assert(scores?.Any() != true);
|
||||
Debug.Assert(!scores.Any());
|
||||
|
||||
setState(state);
|
||||
}
|
||||
@ -181,15 +181,26 @@ namespace osu.Game.Online.Leaderboards
|
||||
/// <param name="userScore">The user top score, if any.</param>
|
||||
protected void SetScores(IEnumerable<TScoreInfo> scores, TScoreInfo userScore = default)
|
||||
{
|
||||
this.scores = scores?.ToList();
|
||||
userScoreContainer.Score.Value = userScore;
|
||||
this.scores.Clear();
|
||||
if (scores != null)
|
||||
this.scores.AddRange(scores);
|
||||
|
||||
if (userScore == null)
|
||||
userScoreContainer.Hide();
|
||||
else
|
||||
userScoreContainer.Show();
|
||||
// Schedule needs to be non-delayed here for the weird logic in refetchScores to work.
|
||||
// If it is removed, the placeholder will be incorrectly updated to "no scores" rather than "retrieving".
|
||||
// This whole flow should be refactored in the future.
|
||||
Scheduler.Add(applyNewScores, false);
|
||||
|
||||
Scheduler.Add(updateScoresDrawables, false);
|
||||
void applyNewScores()
|
||||
{
|
||||
userScoreContainer.Score.Value = userScore;
|
||||
|
||||
if (userScore == null)
|
||||
userScoreContainer.Hide();
|
||||
else
|
||||
userScoreContainer.Show();
|
||||
|
||||
updateScoresDrawables();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -209,8 +220,8 @@ namespace osu.Game.Online.Leaderboards
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
cancelPendingWork();
|
||||
SetScores(null);
|
||||
|
||||
SetScores(null);
|
||||
setState(LeaderboardState.Retrieving);
|
||||
|
||||
currentFetchCancellationSource = new CancellationTokenSource();
|
||||
@ -247,7 +258,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
.Expire();
|
||||
scoreFlowContainer = null;
|
||||
|
||||
if (scores?.Any() != true)
|
||||
if (!scores.Any())
|
||||
{
|
||||
setState(LeaderboardState.NoScores);
|
||||
return;
|
||||
|
@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
{
|
||||
config.BindWith(OsuSetting.PositionalHitsoundsLevel, positionalHitsoundsLevel);
|
||||
|
||||
// Explicit non-virtual function call.
|
||||
// Explicit non-virtual function call in case a DrawableHitObject overrides AddInternal.
|
||||
base.AddInternal(Samples = new PausableSkinnableSound());
|
||||
|
||||
CurrentSkin = skinSource;
|
||||
@ -405,7 +405,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </summary>
|
||||
public event Action<DrawableHitObject, ArmedState> ApplyCustomUpdateState;
|
||||
|
||||
protected override void ClearInternal(bool disposeChildren = true) => throw new InvalidOperationException($"Should never clear a {nameof(DrawableHitObject)}");
|
||||
protected override void ClearInternal(bool disposeChildren = true) =>
|
||||
// See sample addition in load method.
|
||||
throw new InvalidOperationException(
|
||||
$"Should never clear a {nameof(DrawableHitObject)} as the base implementation adds components. If attempting to use {nameof(InternalChild)} or {nameof(InternalChildren)}, using {nameof(AddInternal)} or {nameof(AddRangeInternal)} instead.");
|
||||
|
||||
private void updateState(ArmedState newState, bool force = false)
|
||||
{
|
||||
|
@ -9,8 +9,6 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -21,7 +19,6 @@ using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
@ -41,14 +38,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>();
|
||||
|
||||
private MultiplayerGameplayLeaderboard leaderboard;
|
||||
|
||||
private readonly MultiplayerRoomUser[] users;
|
||||
|
||||
private readonly Bindable<bool> leaderboardExpanded = new BindableBool();
|
||||
|
||||
private LoadingLayer loadingDisplay;
|
||||
private FillFlowContainer leaderboardFlow;
|
||||
|
||||
private MultiplayerGameplayLeaderboard multiplayerLeaderboard;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a multiplayer player.
|
||||
@ -62,7 +56,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
AllowPause = false,
|
||||
AllowRestart = false,
|
||||
AllowSkipping = room.AutoSkip.Value,
|
||||
AutomaticallySkipIntro = room.AutoSkip.Value
|
||||
AutomaticallySkipIntro = room.AutoSkip.Value,
|
||||
AlwaysShowLeaderboard = true,
|
||||
})
|
||||
{
|
||||
this.users = users;
|
||||
@ -74,45 +69,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
HUDOverlay.Add(leaderboardFlow = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5)
|
||||
});
|
||||
|
||||
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
|
||||
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
|
||||
|
||||
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
|
||||
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(users), l =>
|
||||
{
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
leaderboard.Expanded.BindTo(leaderboardExpanded);
|
||||
|
||||
leaderboardFlow.Insert(0, l);
|
||||
|
||||
if (leaderboard.TeamScores.Count >= 2)
|
||||
{
|
||||
LoadComponentAsync(new GameplayMatchScoreDisplay
|
||||
{
|
||||
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
|
||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||
}, scoreDisplay => leaderboardFlow.Insert(1, scoreDisplay));
|
||||
}
|
||||
});
|
||||
|
||||
LoadComponentAsync(new GameplayChatDisplay(Room)
|
||||
{
|
||||
Expanded = { BindTarget = leaderboardExpanded },
|
||||
}, chat => leaderboardFlow.Insert(2, chat));
|
||||
Expanded = { BindTarget = LeaderboardExpandedState },
|
||||
}, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat));
|
||||
|
||||
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() => multiplayerLeaderboard = new MultiplayerGameplayLeaderboard(users);
|
||||
|
||||
protected override void AddLeaderboardToHUD(GameplayLeaderboard leaderboard)
|
||||
{
|
||||
Debug.Assert(leaderboard == multiplayerLeaderboard);
|
||||
|
||||
HUDOverlay.LeaderboardFlow.Insert(0, leaderboard);
|
||||
|
||||
if (multiplayerLeaderboard.TeamScores.Count >= 2)
|
||||
{
|
||||
LoadComponentAsync(new GameplayMatchScoreDisplay
|
||||
{
|
||||
Team1Score = { BindTarget = multiplayerLeaderboard.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = multiplayerLeaderboard.TeamScores.Last().Value },
|
||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||
}, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
@ -167,9 +150,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLeaderboardExpandedState() =>
|
||||
leaderboardExpanded.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
|
||||
|
||||
private void failAndBail(string message = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
@ -178,23 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Schedule(() => PerformExit(false));
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
adjustLeaderboardPosition();
|
||||
}
|
||||
|
||||
private void adjustLeaderboardPosition()
|
||||
{
|
||||
const float padding = 44; // enough margin to avoid the hit error display.
|
||||
|
||||
leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
|
||||
}
|
||||
|
||||
private void onGameplayStarted() => Scheduler.Add(() =>
|
||||
{
|
||||
if (!this.IsCurrentScreen())
|
||||
@ -232,8 +195,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
Debug.Assert(Room.RoomID.Value != null);
|
||||
|
||||
return leaderboard.TeamScores.Count == 2
|
||||
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, leaderboard.TeamScores)
|
||||
return multiplayerLeaderboard.TeamScores.Count == 2
|
||||
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, multiplayerLeaderboard.TeamScores)
|
||||
: new MultiplayerResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem);
|
||||
}
|
||||
|
||||
|
@ -1,11 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -13,15 +10,14 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class GameplayLeaderboard : CompositeDrawable
|
||||
public abstract class GameplayLeaderboard : CompositeDrawable
|
||||
{
|
||||
private readonly int maxPanels;
|
||||
private readonly Cached sorting = new Cached();
|
||||
|
||||
public Bindable<bool> Expanded = new Bindable<bool>();
|
||||
@ -31,16 +27,15 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private bool requiresScroll;
|
||||
private readonly OsuScrollContainer scroll;
|
||||
|
||||
private GameplayLeaderboardScore trackedScore;
|
||||
private GameplayLeaderboardScore? trackedScore;
|
||||
|
||||
private const int max_panels = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new leaderboard.
|
||||
/// </summary>
|
||||
/// <param name="maxPanels">The maximum panels to show at once. Defines the maximum height of this component.</param>
|
||||
public GameplayLeaderboard(int maxPanels = 8)
|
||||
protected GameplayLeaderboard()
|
||||
{
|
||||
this.maxPanels = maxPanels;
|
||||
|
||||
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
@ -77,7 +72,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
/// Whether the player should be tracked on the leaderboard.
|
||||
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
|
||||
/// </param>
|
||||
public ILeaderboardScore Add([CanBeNull] APIUser user, bool isTracked)
|
||||
public ILeaderboardScore Add(IUser? user, bool isTracked)
|
||||
{
|
||||
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
|
||||
|
||||
@ -93,9 +88,18 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
Flow.Add(drawable);
|
||||
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
|
||||
int displayCount = Math.Min(Flow.Count, maxPanels);
|
||||
int displayCount = Math.Min(Flow.Count, max_panels);
|
||||
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||
// Add extra margin space to flow equal to height of leaderboard.
|
||||
// This ensures the content is always on screen, but also accounts for the fact that scroll operations
|
||||
// without animation were actually forcing the local score to a location it can't usually reside at.
|
||||
//
|
||||
// Basically, the local score was in the scroll extension region (due to always trying to scroll the
|
||||
// local player to the middle of the display, but there being no other content below the local player
|
||||
// to scroll up by).
|
||||
Flow.Margin = new MarginPadding { Bottom = Height };
|
||||
requiresScroll = displayCount != Flow.Count;
|
||||
|
||||
return drawable;
|
||||
@ -108,7 +112,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
scroll.ScrollToStart(false);
|
||||
}
|
||||
|
||||
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked) =>
|
||||
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) =>
|
||||
new GameplayLeaderboardScore(user, isTracked);
|
||||
|
||||
protected override void Update()
|
||||
@ -118,7 +122,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (requiresScroll && trackedScore != null)
|
||||
{
|
||||
float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
|
||||
scroll.ScrollTo(scrollTarget, false);
|
||||
scroll.ScrollTo(scrollTarget);
|
||||
}
|
||||
|
||||
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
|
||||
@ -165,7 +169,10 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (sorting.IsValid)
|
||||
return;
|
||||
|
||||
var orderedByScore = Flow.OrderByDescending(i => i.TotalScore.Value).ToList();
|
||||
var orderedByScore = Flow
|
||||
.OrderByDescending(i => i.TotalScore.Value)
|
||||
.ThenBy(i => i.DisplayOrder.Value)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < Flow.Count; i++)
|
||||
{
|
||||
|
@ -12,7 +12,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
@ -55,6 +55,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
public BindableDouble Accuracy { get; } = new BindableDouble(1);
|
||||
public BindableInt Combo { get; } = new BindableInt();
|
||||
public BindableBool HasQuit { get; } = new BindableBool();
|
||||
public Bindable<long> DisplayOrder { get; } = new Bindable<long>();
|
||||
|
||||
public Color4? BackgroundColour { get; set; }
|
||||
|
||||
@ -81,7 +82,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
public APIUser User { get; }
|
||||
public IUser User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this score is the local user or a replay player (and should be focused / always visible).
|
||||
@ -103,7 +104,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
/// </summary>
|
||||
/// <param name="user">The score's player.</param>
|
||||
/// <param name="tracked">Whether the player is the local user or a replay player.</param>
|
||||
public GameplayLeaderboardScore([CanBeNull] APIUser user, bool tracked)
|
||||
public GameplayLeaderboardScore([CanBeNull] IUser user, bool tracked)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
|
@ -14,5 +14,11 @@ namespace osu.Game.Screens.Play.HUD
|
||||
BindableInt Combo { get; }
|
||||
|
||||
BindableBool HasQuit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional value to guarantee stable ordering.
|
||||
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
|
||||
/// </summary>
|
||||
Bindable<long> DisplayOrder { get; }
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
@ -125,11 +126,11 @@ namespace osu.Game.Screens.Play.HUD
|
||||
playingUserIds.BindCollectionChanged(playingUsersChanged);
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked)
|
||||
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser user, bool isTracked)
|
||||
{
|
||||
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
|
||||
|
||||
if (UserScores[user.Id].Team is int team)
|
||||
if (UserScores[user.OnlineID].Team is int team)
|
||||
{
|
||||
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
|
||||
leaderboardScore.TextColour = Color4.White;
|
||||
|
73
osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
Normal file
73
osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
Normal file
@ -0,0 +1,73 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class SoloGameplayLeaderboard : GameplayLeaderboard
|
||||
{
|
||||
private readonly IUser trackingUser;
|
||||
|
||||
public readonly IBindableList<ScoreInfo> Scores = new BindableList<ScoreInfo>();
|
||||
|
||||
// hold references to ensure bindables are updated.
|
||||
private readonly List<Bindable<long>> scoreBindables = new List<Bindable<long>>();
|
||||
|
||||
[Resolved]
|
||||
private ScoreProcessor scoreProcessor { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
public SoloGameplayLeaderboard(IUser trackingUser)
|
||||
{
|
||||
this.trackingUser = trackingUser;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true);
|
||||
}
|
||||
|
||||
private void showScores()
|
||||
{
|
||||
Clear();
|
||||
scoreBindables.Clear();
|
||||
|
||||
if (!Scores.Any())
|
||||
return;
|
||||
|
||||
foreach (var s in Scores)
|
||||
{
|
||||
var score = Add(s.User, false);
|
||||
|
||||
var bindableTotal = scoreManager.GetBindableTotalScore(s);
|
||||
|
||||
// Direct binding not possible due to differing types (see https://github.com/ppy/osu/issues/20298).
|
||||
bindableTotal.BindValueChanged(total => score.TotalScore.Value = total.NewValue, true);
|
||||
scoreBindables.Add(bindableTotal);
|
||||
|
||||
score.Accuracy.Value = s.Accuracy;
|
||||
score.Combo.Value = s.MaxCombo;
|
||||
score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
ILeaderboardScore local = Add(trackingUser, true);
|
||||
|
||||
local.TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
local.Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||
local.Combo.BindTarget = scoreProcessor.Combo;
|
||||
|
||||
// Local score should always show lower than any existing scores in cases of ties.
|
||||
local.DisplayOrder.Value = long.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@ -35,11 +34,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public const Easing FADE_EASING = Easing.OutQuint;
|
||||
|
||||
/// <summary>
|
||||
/// The total height of all the top of screen scoring elements.
|
||||
/// </summary>
|
||||
public float TopScoringElementsHeight { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total height of all the bottom of screen scoring elements.
|
||||
/// </summary>
|
||||
@ -80,9 +74,15 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly SkinnableTargetContainer mainComponents;
|
||||
|
||||
private IEnumerable<Drawable> hideTargets => new Drawable[] { mainComponents, KeyCounter, topRightElements };
|
||||
/// <summary>
|
||||
/// A flow which sits at the left side of the screen to house leaderboard (and related) components.
|
||||
/// Will automatically be positioned to avoid colliding with top scoring elements.
|
||||
/// </summary>
|
||||
public readonly FillFlowContainer LeaderboardFlow;
|
||||
|
||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
|
||||
private readonly List<Drawable> hideTargets;
|
||||
|
||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
|
||||
{
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
this.mods = mods;
|
||||
@ -127,8 +127,20 @@ namespace osu.Game.Screens.Play
|
||||
HoldToQuit = CreateHoldForMenuButton(),
|
||||
}
|
||||
},
|
||||
clicksPerSecondCalculator = new ClicksPerSecondCalculator()
|
||||
LeaderboardFlow = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding(44), // enough margin to avoid the hit error display
|
||||
Spacing = new Vector2(5)
|
||||
},
|
||||
clicksPerSecondCalculator = new ClicksPerSecondCalculator(),
|
||||
};
|
||||
|
||||
hideTargets = new List<Drawable> { mainComponents, KeyCounter, topRightElements };
|
||||
|
||||
if (!alwaysShowLeaderboard)
|
||||
hideTargets.Add(LeaderboardFlow);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
@ -177,22 +189,36 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Vector2? lowestTopScreenSpace = null;
|
||||
float? lowestTopScreenSpaceLeft = null;
|
||||
float? lowestTopScreenSpaceRight = null;
|
||||
|
||||
Vector2? highestBottomScreenSpace = null;
|
||||
|
||||
// LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
|
||||
foreach (var element in mainComponents.Components.Cast<Drawable>())
|
||||
{
|
||||
// for now align top-right components with the bottom-edge of the lowest top-anchored hud element.
|
||||
if (element.Anchor.HasFlagFast(Anchor.TopRight) || (element.Anchor.HasFlagFast(Anchor.y0) && element.RelativeSizeAxes == Axes.X))
|
||||
// for now align some top components with the bottom-edge of the lowest top-anchored hud element.
|
||||
if (element.Anchor.HasFlagFast(Anchor.y0))
|
||||
{
|
||||
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
|
||||
if (element is LegacyHealthDisplay)
|
||||
continue;
|
||||
|
||||
var bottomRight = element.ScreenSpaceDrawQuad.BottomRight;
|
||||
if (lowestTopScreenSpace == null || bottomRight.Y > lowestTopScreenSpace.Value.Y)
|
||||
lowestTopScreenSpace = bottomRight;
|
||||
float bottom = element.ScreenSpaceDrawQuad.BottomRight.Y;
|
||||
|
||||
bool isRelativeX = element.RelativeSizeAxes == Axes.X;
|
||||
|
||||
if (element.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX)
|
||||
{
|
||||
if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value)
|
||||
lowestTopScreenSpaceRight = bottom;
|
||||
}
|
||||
|
||||
if (element.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX)
|
||||
{
|
||||
if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value)
|
||||
lowestTopScreenSpaceLeft = bottom;
|
||||
}
|
||||
}
|
||||
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
|
||||
else if (element.Anchor.HasFlagFast(Anchor.BottomRight) || (element.Anchor.HasFlagFast(Anchor.y2) && element.RelativeSizeAxes == Axes.X))
|
||||
@ -203,11 +229,16 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
if (lowestTopScreenSpace.HasValue)
|
||||
topRightElements.Y = TopScoringElementsHeight = MathHelper.Clamp(ToLocalSpace(lowestTopScreenSpace.Value).Y, 0, DrawHeight - topRightElements.DrawHeight);
|
||||
if (lowestTopScreenSpaceRight.HasValue)
|
||||
topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight);
|
||||
else
|
||||
topRightElements.Y = 0;
|
||||
|
||||
if (lowestTopScreenSpaceLeft.HasValue)
|
||||
LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight);
|
||||
else
|
||||
LeaderboardFlow.Y = 0;
|
||||
|
||||
if (highestBottomScreenSpace.HasValue)
|
||||
bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight);
|
||||
else
|
||||
|
@ -9,6 +9,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -34,6 +35,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Users;
|
||||
@ -375,6 +377,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
if (Configuration.AutomaticallySkipIntro)
|
||||
skipIntroOverlay.SkipWhenReady();
|
||||
|
||||
loadLeaderboard();
|
||||
}
|
||||
|
||||
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
|
||||
@ -417,7 +421,7 @@ namespace osu.Game.Screens.Play
|
||||
// display the cursor above some HUD elements.
|
||||
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
|
||||
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
|
||||
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods)
|
||||
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard)
|
||||
{
|
||||
HoldToQuit =
|
||||
{
|
||||
@ -820,6 +824,41 @@ namespace osu.Game.Screens.Play
|
||||
return mouseWheelDisabled.Value && !e.AltPressed;
|
||||
}
|
||||
|
||||
#region Gameplay leaderboard
|
||||
|
||||
protected readonly Bindable<bool> LeaderboardExpandedState = new BindableBool();
|
||||
|
||||
private void loadLeaderboard()
|
||||
{
|
||||
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
|
||||
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
|
||||
|
||||
var gameplayLeaderboard = CreateGameplayLeaderboard();
|
||||
|
||||
if (gameplayLeaderboard != null)
|
||||
{
|
||||
LoadComponentAsync(gameplayLeaderboard, leaderboard =>
|
||||
{
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
leaderboard.Expanded.BindTo(LeaderboardExpandedState);
|
||||
|
||||
AddLeaderboardToHUD(leaderboard);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
protected virtual GameplayLeaderboard CreateGameplayLeaderboard() => null;
|
||||
|
||||
protected virtual void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) => HUDOverlay.LeaderboardFlow.Add(leaderboard);
|
||||
|
||||
private void updateLeaderboardExpandedState() =>
|
||||
LeaderboardExpandedState.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fail Logic
|
||||
|
||||
protected FailOverlay FailOverlay { get; private set; }
|
||||
|
@ -36,5 +36,10 @@ namespace osu.Game.Screens.Play
|
||||
/// Whether the intro should be skipped by default.
|
||||
/// </summary>
|
||||
public bool AutomaticallySkipIntro { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the gameplay leaderboard should always be shown (usually in a contracted state).
|
||||
/// </summary>
|
||||
public bool AlwaysShowLeaderboard { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -14,6 +15,7 @@ using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
@ -55,6 +57,14 @@ namespace osu.Game.Screens.Play
|
||||
// Don't re-import replay scores as they're already present in the database.
|
||||
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
||||
|
||||
public readonly BindableList<ScoreInfo> LeaderboardScores = new BindableList<ScoreInfo>();
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||
{
|
||||
Scores = { BindTarget = LeaderboardScores }
|
||||
};
|
||||
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
|
@ -5,12 +5,15 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
@ -40,8 +43,26 @@ namespace osu.Game.Screens.Play
|
||||
return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
|
||||
}
|
||||
|
||||
public readonly BindableList<ScoreInfo> LeaderboardScores = new BindableList<ScoreInfo>();
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||
{
|
||||
Scores = { BindTarget = LeaderboardScores }
|
||||
};
|
||||
|
||||
protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
|
||||
|
||||
protected override Task ImportScore(Score score)
|
||||
{
|
||||
// Before importing a score, stop binding the leaderboard with its score source.
|
||||
// This avoids a case where the imported score may cause a leaderboard refresh
|
||||
// (if the leaderboard's source is local).
|
||||
LeaderboardScores.UnbindBindings();
|
||||
|
||||
return base.ImportScore(score);
|
||||
}
|
||||
|
||||
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
|
||||
{
|
||||
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo;
|
||||
|
@ -41,6 +41,11 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
return;
|
||||
|
||||
beatmapInfo = value;
|
||||
|
||||
// Refetch is scheduled, which can cause scores to be outdated if the leaderboard is not currently updating.
|
||||
// As scores are potentially used by other components, clear them eagerly to ensure a more correct state.
|
||||
SetScores(null);
|
||||
|
||||
RefetchScores();
|
||||
}
|
||||
}
|
||||
@ -148,12 +153,10 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
|
||||
|
||||
req.Success += r => Schedule(() =>
|
||||
{
|
||||
SetScores(
|
||||
scoreManager.OrderByTotalScore(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))),
|
||||
r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo));
|
||||
});
|
||||
req.Success += r => SetScores(
|
||||
scoreManager.OrderByTotalScore(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))),
|
||||
r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)
|
||||
);
|
||||
|
||||
return req;
|
||||
}
|
||||
@ -208,7 +211,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
scores = scoreManager.OrderByTotalScore(scores.Detach());
|
||||
|
||||
Schedule(() => SetScores(scores));
|
||||
SetScores(scores);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@ -24,27 +22,38 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
public class PlaySongSelect : SongSelect
|
||||
{
|
||||
private OsuScreen playerLoader;
|
||||
private OsuScreen? playerLoader;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private INotificationOverlay notifications { get; set; }
|
||||
private INotificationOverlay? notifications { get; set; }
|
||||
|
||||
public override bool AllowExternalScreenChange => true;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap();
|
||||
|
||||
private PlayBeatmapDetailArea playBeatmapDetailArea = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit());
|
||||
|
||||
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
|
||||
}
|
||||
|
||||
protected void PresentScore(ScoreInfo score) =>
|
||||
FinaliseSelection(score.BeatmapInfo, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false)));
|
||||
|
||||
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
|
||||
protected override BeatmapDetailArea CreateBeatmapDetailArea()
|
||||
{
|
||||
playBeatmapDetailArea = new PlayBeatmapDetailArea
|
||||
{
|
||||
Leaderboard =
|
||||
{
|
||||
ScoreSelected = PresentScore
|
||||
}
|
||||
};
|
||||
|
||||
return playBeatmapDetailArea;
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
@ -61,9 +70,9 @@ namespace osu.Game.Screens.Select
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
private IReadOnlyList<Mod> modsAtGameplayStart;
|
||||
private IReadOnlyList<Mod>? modsAtGameplayStart;
|
||||
|
||||
private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
|
||||
private ModAutoplay? getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
|
||||
|
||||
protected override bool OnStart()
|
||||
{
|
||||
@ -100,14 +109,26 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
Player createPlayer()
|
||||
{
|
||||
Player player;
|
||||
|
||||
var replayGeneratingMod = Mods.Value.OfType<ICreateReplayData>().FirstOrDefault();
|
||||
|
||||
if (replayGeneratingMod != null)
|
||||
{
|
||||
return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods));
|
||||
player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))
|
||||
{
|
||||
LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores }
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
player = new SoloPlayer
|
||||
{
|
||||
LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores }
|
||||
};
|
||||
}
|
||||
|
||||
return new SoloPlayer();
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,28 @@ namespace osu.Game.Skinning
|
||||
: base(skin, resources)
|
||||
{
|
||||
this.resources = resources;
|
||||
|
||||
Configuration.CustomComboColours = new List<Color4>
|
||||
{
|
||||
// Standard combo progression order is green - blue - red - yellow.
|
||||
// But for whatever reason, this starts from index 1, not 0.
|
||||
//
|
||||
// We've added two new combo colours in argon, so to ensure the initial rotation matches,
|
||||
// this same progression is in slots 1 - 4.
|
||||
|
||||
// Orange
|
||||
new Color4(241, 116, 0, 255),
|
||||
// Green
|
||||
new Color4(0, 241, 53, 255),
|
||||
// Blue
|
||||
new Color4(0, 82, 241, 255),
|
||||
// Red
|
||||
new Color4(241, 0, 0, 255),
|
||||
// Yellow
|
||||
new Color4(232, 235, 0, 255),
|
||||
// Purple
|
||||
new Color4(92, 0, 241, 255),
|
||||
};
|
||||
}
|
||||
|
||||
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Textures?.Get(componentName, wrapModeS, wrapModeT);
|
||||
|
@ -14,13 +14,13 @@ namespace osu.Game.Users.Drawables
|
||||
[LongRunningLoad]
|
||||
public class DrawableAvatar : Sprite
|
||||
{
|
||||
private readonly APIUser user;
|
||||
private readonly IUser user;
|
||||
|
||||
/// <summary>
|
||||
/// A simple, non-interactable avatar sprite for the specified user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user. A null value will get a placeholder avatar.</param>
|
||||
public DrawableAvatar(APIUser user = null)
|
||||
public DrawableAvatar(IUser user = null)
|
||||
{
|
||||
this.user = user;
|
||||
|
||||
@ -33,10 +33,10 @@ namespace osu.Game.Users.Drawables
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LargeTextureStore textures)
|
||||
{
|
||||
if (user != null && user.Id > 1)
|
||||
if (user != null && user.OnlineID > 1)
|
||||
// TODO: The fallback here should not need to exist. Users should be looked up and populated via UserLookupCache or otherwise
|
||||
// in remaining cases where this is required (chat tabs, local leaderboard), at which point this should be removed.
|
||||
Texture = textures.Get(user.AvatarUrl ?? $@"https://a.ppy.sh/{user.Id}");
|
||||
Texture = textures.Get((user as APIUser)?.AvatarUrl ?? $@"https://a.ppy.sh/{user.OnlineID}");
|
||||
|
||||
Texture ??= textures.Get(@"Online/avatar-guest");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user