mirror of
https://github.com/ppy/osu.git
synced 2026-05-21 05:09:57 +08:00
Merge branch 'master' into open-playlist-link
This commit is contained in:
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
|
||||
public BindableBool HardRockOffsets { get; } = new BindableBool();
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!CircleSize.IsDefault) return format("CS", CircleSize);
|
||||
if (!ApproachRate.IsDefault) return format("AR", ApproachRate);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -68,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
// needs to be scaled down to remain playable.
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float aspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
scaleContainer.Scale = new Vector2(base_aspect_ratio / aspectRatio);
|
||||
scaleContainer.Scale = new Vector2(Math.Min(1, base_aspect_ratio / aspectRatio));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,12 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() => toggleTouchControls(false));
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
InputManager.EndTouch(new Touch(TouchSource.Touch1, Vector2.Zero));
|
||||
InputManager.EndTouch(new Touch(TouchSource.Touch2, Vector2.Zero));
|
||||
toggleTouchControls(false);
|
||||
});
|
||||
|
||||
#region Without touch controls
|
||||
|
||||
@@ -71,6 +76,35 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBetweenTwoColumns()
|
||||
{
|
||||
AddStep("touch after column 0", () =>
|
||||
{
|
||||
var column = getColumn(0);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(column.LayoutSize.X + 0.5f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
AddAssert("column 0 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(0).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 0 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
AddStep("touch before column 1", () =>
|
||||
{
|
||||
var column = getColumn(1);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(-0.5f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
AddAssert("column 1 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(1).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 1 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(1).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region With touch controls
|
||||
@@ -132,6 +166,38 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchControlBetweenTwoColumns()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
|
||||
AddStep("touch after receptor 0", () =>
|
||||
{
|
||||
var column = getReceptor(0);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(column.LayoutSize.X + 1f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
|
||||
AddAssert("column 0 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(0).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 0 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getReceptor(0).Action.Value));
|
||||
AddStep("touch before receptor 1", () =>
|
||||
{
|
||||
var column = getReceptor(1);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(-1f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
AddAssert("column 1 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(1).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 1 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getReceptor(1).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void toggleTouchControls(bool enabled)
|
||||
|
||||
@@ -131,8 +131,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
switch (maniaLookup.Lookup)
|
||||
{
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnSpacing:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(2));
|
||||
case LegacyManiaSkinConfigurationLookups.LeftColumnSpacing:
|
||||
case LegacyManiaSkinConfigurationLookups.RightColumnSpacing:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(1));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.StagePaddingBottom:
|
||||
case LegacyManiaSkinConfigurationLookups.StagePaddingTop:
|
||||
@@ -146,7 +147,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(width));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
|
||||
|
||||
var colour = getColourForLayout(columnIndex, stage);
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<Color4>(colour));
|
||||
|
||||
@@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private IBindable<ManiaMobileLayout> mobilePlayStyle = null!;
|
||||
|
||||
private float leftColumnSpacing;
|
||||
private float rightColumnSpacing;
|
||||
|
||||
public Column(int index, bool isSpecial)
|
||||
{
|
||||
Index = index;
|
||||
@@ -126,6 +129,14 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private void onSourceChanged()
|
||||
{
|
||||
AccentColour.Value = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, Index)?.Value ?? Color4.Black;
|
||||
|
||||
leftColumnSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.LeftColumnSpacing, Index))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
rightColumnSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.RightColumnSpacing, Index))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -187,8 +198,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
|
||||
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||
{
|
||||
// Extend input coverage to the gaps close to this column.
|
||||
var spacingInflation = new MarginPadding { Left = leftColumnSpacing, Right = rightColumnSpacing };
|
||||
return DrawRectangle.Inflate(spacingInflation).Contains(ToLocalSpace(screenSpacePos));
|
||||
}
|
||||
|
||||
#region Touch Input
|
||||
|
||||
|
||||
@@ -124,14 +124,15 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
float spacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
|
||||
float leftSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.LeftColumnSpacing, i))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = spacing };
|
||||
}
|
||||
float rightSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.RightColumnSpacing, i))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = leftSpacing, Right = rightSpacing };
|
||||
|
||||
float? width = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
|
||||
|
||||
@@ -74,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
receptorGridContent.Add(new ColumnInputReceptor
|
||||
{
|
||||
Action = { BindTarget = column.Action },
|
||||
Spacing = { BindTarget = Spacing },
|
||||
});
|
||||
receptorGridDimensions.Add(new Dimension());
|
||||
|
||||
@@ -122,6 +123,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
public partial class ColumnInputReceptor : CompositeDrawable
|
||||
{
|
||||
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||
public readonly IBindable<float> Spacing = new BindableFloat();
|
||||
|
||||
private readonly Box highlightOverlay;
|
||||
|
||||
@@ -159,6 +161,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
};
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
// Extend input coverage to the gaps close to this receptor.
|
||||
=> DrawRectangle.Inflate(new Vector2(Spacing.Value / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
updateButton(true);
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!CircleSize.IsDefault) return format("CS", CircleSize);
|
||||
if (!ApproachRate.IsDefault) return format("AR", ApproachRate);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonSliderBody : PlaySliderBody
|
||||
{
|
||||
// Eventually this would be a user setting.
|
||||
public float BodyAlpha { get; init; } = 1;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
const float path_radius = ArgonMainCirclePiece.OUTER_GRADIENT_SIZE / 2;
|
||||
@@ -26,6 +30,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
|
||||
protected override Default.DrawableSliderPath CreateSliderPath() => new DrawableSliderPath();
|
||||
|
||||
protected override Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour)
|
||||
{
|
||||
return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(BodyAlpha);
|
||||
}
|
||||
|
||||
private partial class DrawableSliderPath : Default.DrawableSliderPath
|
||||
{
|
||||
protected override Color4 ColourAt(float position)
|
||||
|
||||
@@ -16,13 +16,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
{
|
||||
bool isPro = Skin is ArgonProSkin;
|
||||
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
HitResult result = resultComponent.Component;
|
||||
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
if (Skin is ArgonProSkin && (result == HitResult.Great || result == HitResult.Perfect))
|
||||
if (isPro && (result == HitResult.Great || result == HitResult.Perfect))
|
||||
return Drawable.Empty();
|
||||
|
||||
switch (result)
|
||||
@@ -46,7 +48,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
return new ArgonMainCirclePiece(false);
|
||||
|
||||
case OsuSkinComponents.SliderBody:
|
||||
return new ArgonSliderBody();
|
||||
return new ArgonSliderBody
|
||||
{
|
||||
BodyAlpha = isPro ? 0.92f : 0.98f
|
||||
};
|
||||
|
||||
case OsuSkinComponents.SliderBall:
|
||||
return new ArgonSliderBall();
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
@@ -20,6 +21,23 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
ReadCurrentFromDifficulty = _ => 1,
|
||||
};
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -77,33 +77,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayerScore()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
var player2Score = new BindableLong(1234567);
|
||||
var player3Score = new BindableLong(1111111);
|
||||
|
||||
AddStep("add player 2", () => createLeaderboardScore(player2Score, new APIUser { Username = "Player 2" }));
|
||||
AddStep("add player 3", () => createLeaderboardScore(player3Score, new APIUser { Username = "Player 3" }));
|
||||
|
||||
AddUntilStep("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1));
|
||||
AddUntilStep("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2));
|
||||
AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
|
||||
|
||||
AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500);
|
||||
AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
|
||||
AddUntilStep("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2));
|
||||
AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
|
||||
|
||||
AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456);
|
||||
AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
|
||||
AddUntilStep("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2));
|
||||
AddUntilStep("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomScores()
|
||||
{
|
||||
@@ -183,30 +156,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
() => Does.Contain("#FF549A"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTrackedScorePosition([Values] bool partial)
|
||||
{
|
||||
createLeaderboard(partial);
|
||||
|
||||
AddStep("add many scores in one go", () =>
|
||||
{
|
||||
for (int i = 0; i < 49; 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);
|
||||
});
|
||||
|
||||
if (partial)
|
||||
AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null);
|
||||
else
|
||||
AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50));
|
||||
|
||||
AddStep("move tracked player to top", () => leaderboard.TrackedScore!.TotalScore.Value = 8_000_000);
|
||||
AddUntilStep("all players have non-null position", () => leaderboard.AllScores.Select(s => s.ScorePosition), () => Does.Not.Contain(null));
|
||||
}
|
||||
|
||||
private void addLocalPlayer()
|
||||
{
|
||||
AddStep("add local player", () =>
|
||||
@@ -216,12 +165,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
private void createLeaderboard(bool partial = false)
|
||||
private void createLeaderboard()
|
||||
{
|
||||
AddStep("create leaderboard", () =>
|
||||
{
|
||||
leaderboardProvider.Scores.Clear();
|
||||
leaderboardProvider.IsPartial = partial;
|
||||
Child = leaderboard = new TestDrawableGameplayLeaderboard
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@@ -243,24 +191,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public float Spacing => Flow.Spacing.Y;
|
||||
|
||||
public bool CheckPositionByUsername(string username, int? expectedPosition)
|
||||
{
|
||||
var scoreItem = Flow.FirstOrDefault(i => i.User?.Username == username);
|
||||
|
||||
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
|
||||
}
|
||||
|
||||
public IEnumerable<DrawableGameplayLeaderboardScore> GetAllScoresForUsername(string username)
|
||||
=> Flow.Where(i => i.User?.Username == username);
|
||||
|
||||
public IEnumerable<DrawableGameplayLeaderboardScore> AllScores => Flow;
|
||||
}
|
||||
|
||||
private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider
|
||||
{
|
||||
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
|
||||
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
|
||||
public bool IsPartial { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneSoloGameplayLeaderboardProvider : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestLocalLeaderboardHasPositionsAutofilled()
|
||||
{
|
||||
SoloGameplayLeaderboardProvider provider = null!;
|
||||
|
||||
var leaderboardManager = new LeaderboardManager();
|
||||
LoadComponent(leaderboardManager);
|
||||
var gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
AddStep("fetch local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(Beatmap.Value.BeatmapInfo, Ruleset.Value, BeatmapLeaderboardScope.Local, null)));
|
||||
AddStep("set scores", () =>
|
||||
{
|
||||
// this is dodgy but anything less dodgy is a lot of work
|
||||
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(
|
||||
Enumerable.Range(1, 100).Select(i => new ScoreInfo
|
||||
{
|
||||
TotalScore = 10_000 * (100 - i),
|
||||
Position = i,
|
||||
}).ToArray(),
|
||||
null
|
||||
);
|
||||
});
|
||||
AddStep("create content", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(LeaderboardManager), leaderboardManager),
|
||||
(typeof(GameplayState), gameplayState)
|
||||
],
|
||||
Children = new Drawable[]
|
||||
{
|
||||
leaderboardManager,
|
||||
provider = new SoloGameplayLeaderboardProvider()
|
||||
}
|
||||
});
|
||||
AddUntilStep("tracked score shows #101", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(101));
|
||||
AddUntilStep("tracked score ordered #101", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(101));
|
||||
AddStep("move score to #20", () => gameplayState.ScoreProcessor.TotalScore.Value = 802_000);
|
||||
AddUntilStep("tracked score shows #20", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(20));
|
||||
AddUntilStep("tracked score ordered #20", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(20));
|
||||
AddStep("move score to #1", () => gameplayState.ScoreProcessor.TotalScore.Value = 1_002_000);
|
||||
AddUntilStep("tracked score shows #1", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(1));
|
||||
AddUntilStep("tracked score ordered #1", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFullGlobalLeaderboard()
|
||||
{
|
||||
SoloGameplayLeaderboardProvider provider = null!;
|
||||
|
||||
var leaderboardManager = new LeaderboardManager();
|
||||
LoadComponent(leaderboardManager);
|
||||
var gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
AddStep("fetch local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(Beatmap.Value.BeatmapInfo, Ruleset.Value, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set scores", () =>
|
||||
{
|
||||
// this is dodgy but anything less dodgy is a lot of work
|
||||
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(
|
||||
Enumerable.Range(1, 40).Select(i => new ScoreInfo
|
||||
{
|
||||
TotalScore = 600_000 + 10_000 * (40 - i),
|
||||
Position = i,
|
||||
}).ToArray(),
|
||||
null
|
||||
);
|
||||
});
|
||||
AddStep("create content", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(LeaderboardManager), leaderboardManager),
|
||||
(typeof(GameplayState), gameplayState)
|
||||
],
|
||||
Children = new Drawable[]
|
||||
{
|
||||
leaderboardManager,
|
||||
provider = new SoloGameplayLeaderboardProvider()
|
||||
}
|
||||
});
|
||||
AddUntilStep("tracked score shows #41", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(41));
|
||||
AddUntilStep("tracked score ordered #41", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(41));
|
||||
AddStep("move score to #20", () => gameplayState.ScoreProcessor.TotalScore.Value = 802_000);
|
||||
AddUntilStep("tracked score shows #20", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(20));
|
||||
AddUntilStep("tracked score ordered #20", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(20));
|
||||
AddStep("move score to #1", () => gameplayState.ScoreProcessor.TotalScore.Value = 1_002_000);
|
||||
AddUntilStep("tracked score shows #1", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(1));
|
||||
AddUntilStep("tracked score ordered #1", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPartialGlobalLeaderboard()
|
||||
{
|
||||
SoloGameplayLeaderboardProvider provider = null!;
|
||||
|
||||
var leaderboardManager = new LeaderboardManager();
|
||||
LoadComponent(leaderboardManager);
|
||||
var gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
AddStep("fetch local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(Beatmap.Value.BeatmapInfo, Ruleset.Value, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set scores", () =>
|
||||
{
|
||||
// this is dodgy but anything less dodgy is a lot of work
|
||||
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(
|
||||
Enumerable.Range(1, 50).Select(i => new ScoreInfo
|
||||
{
|
||||
TotalScore = 500_000 + 10_000 * (50 - i),
|
||||
Position = i
|
||||
}).ToArray(),
|
||||
new ScoreInfo { TotalScore = 200_000 }
|
||||
);
|
||||
});
|
||||
AddStep("create content", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(LeaderboardManager), leaderboardManager),
|
||||
(typeof(GameplayState), gameplayState)
|
||||
],
|
||||
Children = new Drawable[]
|
||||
{
|
||||
leaderboardManager,
|
||||
provider = new SoloGameplayLeaderboardProvider()
|
||||
}
|
||||
});
|
||||
AddUntilStep("tracked score shows no position", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.Null);
|
||||
AddUntilStep("tracked score ordered #52", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(52));
|
||||
AddStep("move score above user best", () => gameplayState.ScoreProcessor.TotalScore.Value = 202_000);
|
||||
AddUntilStep("tracked score shows no position", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.Null);
|
||||
AddUntilStep("tracked score ordered #51", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(51));
|
||||
AddStep("move score to #20", () => gameplayState.ScoreProcessor.TotalScore.Value = 802_000);
|
||||
AddUntilStep("tracked score shows #20", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(20));
|
||||
AddUntilStep("tracked score ordered #20", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(20));
|
||||
AddStep("move score to #1", () => gameplayState.ScoreProcessor.TotalScore.Value = 1_002_000);
|
||||
AddUntilStep("tracked score shows #1", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(1));
|
||||
AddUntilStep("tracked score ordered #1", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Overlays.Comments;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[Ignore("This test hits online resources (and online retrieval can fail at any time), and also performs network calls to the production instance of the website. Un-ignore this test when it's actually actively needed.")]
|
||||
public partial class TestSceneImageProxying : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
// 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.Audio;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public partial class TestSceneSoloResultsScreen : ScreenTestScene
|
||||
{
|
||||
private ScoreManager scoreManager = null!;
|
||||
private RulesetStore rulesetStore = null!;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
|
||||
private LeaderboardManager leaderboardManager = null!;
|
||||
private BeatmapInfo importedBeatmap = null!;
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
|
||||
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
|
||||
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
|
||||
dependencies.Cache(leaderboardManager = new LeaderboardManager());
|
||||
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("load leaderboard manager", () => LoadComponent(leaderboardManager));
|
||||
|
||||
AddStep(@"set beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
Realm.Write(r =>
|
||||
{
|
||||
foreach (var set in r.All<BeatmapSetInfo>())
|
||||
set.Status = BeatmapOnlineStatus.Ranked;
|
||||
|
||||
foreach (var b in r.All<BeatmapInfo>())
|
||||
b.Status = BeatmapOnlineStatus.Ranked;
|
||||
});
|
||||
importedBeatmap = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
});
|
||||
AddStep("clear all scores", () => Realm.Write(r => r.RemoveAll<ScoreInfo>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalLeaderboardWithOfflineScore()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Local, null)));
|
||||
AddStep("import some local scores", () =>
|
||||
{
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 10_000 * (30 - i);
|
||||
scoreManager.Import(score);
|
||||
}
|
||||
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.Position = null;
|
||||
scoreManager.Import(localScore);
|
||||
localScore = localScore.Detach();
|
||||
});
|
||||
|
||||
AddStep("show results", () => LoadScreen(new SoloResultsScreen(localScore)));
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalLeaderboardWithOnlineScore()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Local, null)));
|
||||
AddStep("import some local scores", () =>
|
||||
{
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.OnlineID = i;
|
||||
score.TotalScore = 10_000 * (30 - i);
|
||||
scoreManager.Import(score);
|
||||
}
|
||||
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.OnlineID = 30;
|
||||
localScore.Position = null;
|
||||
scoreManager.Import(localScore);
|
||||
localScore = localScore.Detach();
|
||||
});
|
||||
|
||||
AddStep("show results", () => LoadScreen(new SoloResultsScreen(localScore)));
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardWithLessThan50Scores()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 10_000 * (30 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection { Scores = scores });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardWithLessThan50Scores_UserIsLast()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 300_000 + 10_000 * (30 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection { Scores = scores });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #31", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(31));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardWithMoreThan50Scores_UserOutsideOfTop50()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 500_000 + 10_000 * (50 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
var userBest = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
userBest.TotalScore = 50_000;
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||
{
|
||||
Scores = scores,
|
||||
UserScore = new APIScoreWithPosition
|
||||
{
|
||||
Score = SoloScoreInfo.ForSubmission(userBest),
|
||||
Position = 133_337,
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score has no position", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.Null);
|
||||
AddAssert("user best position preserved", () => this.ChildrenOfType<ScorePanel>().Any(p => p.ScorePosition.Value == 133_337));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardWithMoreThan50Scores_UserInTop50()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 500_000 + 10_000 * (50 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
var userBest = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
userBest.TotalScore = 50_000;
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||
{
|
||||
Scores = scores,
|
||||
UserScore = new APIScoreWithPosition
|
||||
{
|
||||
Score = SoloScoreInfo.ForSubmission(userBest),
|
||||
Position = 133_337,
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 651_000;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #36", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(36));
|
||||
AddAssert("user best position incremented by 1", () => this.ChildrenOfType<ScorePanel>().Any(p => p.ScorePosition.Value == 133_338));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardDeduplication()
|
||||
{
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 500_000 + 10_000 * (50 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
var userBest = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo(importedBeatmap));
|
||||
userBest.TotalScore = 151_000;
|
||||
userBest.ID = 12345;
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||
{
|
||||
Scores = scores,
|
||||
UserScore = new APIScoreWithPosition
|
||||
{
|
||||
Score = userBest,
|
||||
Position = 133_337,
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
var localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.OnlineID = 12345;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("only one score with ID 12345", () => this.ChildrenOfType<ScorePanel>().Count(s => s.Score.OnlineID == 12345), () => Is.EqualTo(1));
|
||||
AddAssert("user best position preserved", () => this.ChildrenOfType<ScorePanel>().Any(p => p.ScorePosition.Value == 133_337));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,8 +163,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180")]
|
||||
[TestCase(120, 120.6, "DT", "180-181 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180-181 (mostly 180)")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
|
||||
@@ -1277,12 +1277,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nonono");
|
||||
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||
AddStep("press ctrl-x", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.X);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut));
|
||||
|
||||
AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text, () => Is.Empty);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
};
|
||||
|
||||
private Container? resizeContainer;
|
||||
private float relativeWidth;
|
||||
|
||||
protected virtual Anchor ComponentAnchor => Anchor.TopLeft;
|
||||
protected virtual float InitialRelativeWidth => 0.5f;
|
||||
@@ -40,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
Origin = ComponentAnchor,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = relativeWidth,
|
||||
Width = InitialRelativeWidth,
|
||||
Child = Content
|
||||
}
|
||||
};
|
||||
@@ -49,8 +48,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
if (resizeContainer != null)
|
||||
resizeContainer.Width = v;
|
||||
|
||||
relativeWidth = v;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapFilterControl : SongSelectComponentsTestScene
|
||||
{
|
||||
protected override Anchor ComponentAnchor => Anchor.TopRight;
|
||||
protected override float InitialRelativeWidth => 0.7f;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Child = new FilterControl
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
+50
-33
@@ -7,6 +7,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
@@ -28,7 +29,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneLeaderboardScore : SongSelectComponentsTestScene
|
||||
public partial class TestSceneBeatmapLeaderboardScore : SongSelectComponentsTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
@@ -44,18 +45,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var scoreInfo in getTestScores())
|
||||
@@ -78,22 +84,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var scoreInfo in getTestScores())
|
||||
{
|
||||
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo)
|
||||
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo, sheared: false)
|
||||
{
|
||||
Rank = scoreInfo.Position,
|
||||
IsPersonalBest = scoreInfo.User.Id == 2,
|
||||
@@ -112,18 +123,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
var scoreInfo = new ScoreInfo
|
||||
@@ -260,9 +276,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
scores[2].TotalScore = RNG.Next(120_000, 400_000);
|
||||
scores[2].MaximumStatistics[HitResult.Great] = 3000;
|
||||
|
||||
scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight() };
|
||||
scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 2 } }, new OsuModHardRock(), new OsuModFlashlight() };
|
||||
scores[2].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic() };
|
||||
scores[3].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic(), new OsuModDifficultyAdjust() };
|
||||
scores[3].Mods = new Mod[]
|
||||
{ new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight { ComboBasedSize = { Value = false } }, new OsuModClassic(), new OsuModDifficultyAdjust { CircleSize = { Value = 3.2f } } };
|
||||
scores[4].Mods = new ManiaRuleset().CreateAllMods().ToArray();
|
||||
|
||||
return scores;
|
||||
@@ -0,0 +1,370 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.SongSelect;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapLeaderboardWedge : SongSelectComponentsTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
private TestBeatmapLeaderboardWedge leaderboard = null!;
|
||||
private ScoreManager scoreManager = null!;
|
||||
private RulesetStore rulesetStore = null!;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private OsuContextMenuContainer contentContainer = null!;
|
||||
private DialogOverlay dialogOverlay = null!;
|
||||
|
||||
private LeaderboardManager leaderboardManager = null!;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
|
||||
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
|
||||
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
|
||||
dependencies.Cache(leaderboardManager = new LeaderboardManager());
|
||||
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LoadComponent(dialogOverlay = new DialogOverlay
|
||||
{
|
||||
Depth = -1
|
||||
});
|
||||
|
||||
LoadComponent(leaderboardManager);
|
||||
|
||||
Child = contentContainer = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 500,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
dialogOverlay,
|
||||
}
|
||||
};
|
||||
|
||||
AddSliderStep("change relative height", 0f, 1f, 0.65f, v => Schedule(() =>
|
||||
{
|
||||
contentContainer.Height = v * DrawHeight;
|
||||
}));
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
if (leaderboard.IsNotNull())
|
||||
contentContainer.Remove(leaderboard, false);
|
||||
|
||||
contentContainer.Add(leaderboard = new TestBeatmapLeaderboardWedge
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
});
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPersonalBest()
|
||||
{
|
||||
AddStep(@"Show personal best", showPersonalBest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGlobalScoresDisplay()
|
||||
{
|
||||
setScope(BeatmapLeaderboardScope.Global);
|
||||
|
||||
AddStep(@"New Scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo())));
|
||||
AddStep(@"New Scores with teams", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()).Select(s =>
|
||||
{
|
||||
s.User.Team = new APITeam();
|
||||
return s;
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPersonalBestWithNullPosition()
|
||||
{
|
||||
AddStep("null personal best position", showPersonalBestWithNullPosition);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderStates()
|
||||
{
|
||||
AddStep("ensure no scores displayed", () => leaderboard.SetScores(Array.Empty<ScoreInfo>()));
|
||||
|
||||
AddStep(@"Retrieving", () => leaderboard.SetState(LeaderboardState.Retrieving));
|
||||
AddStep(@"Network failure", () => leaderboard.SetState(LeaderboardState.NetworkFailure));
|
||||
AddStep(@"No team", () => leaderboard.SetState(LeaderboardState.NoTeam));
|
||||
AddStep(@"No supporter", () => leaderboard.SetState(LeaderboardState.NotSupporter));
|
||||
AddStep(@"Not logged in", () => leaderboard.SetState(LeaderboardState.NotLoggedIn));
|
||||
AddStep(@"Ruleset unavailable", () => leaderboard.SetState(LeaderboardState.RulesetUnavailable));
|
||||
AddStep(@"Beatmap unavailable", () => leaderboard.SetState(LeaderboardState.BeatmapUnavailable));
|
||||
AddStep(@"None selected", () => leaderboard.SetState(LeaderboardState.NoneSelected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUseTheseModsDoesNotCopySystemMods()
|
||||
{
|
||||
AddStep(@"set scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
|
||||
{
|
||||
OnlineID = 1337,
|
||||
Position = 999,
|
||||
Rank = ScoreRank.XH,
|
||||
Accuracy = 1,
|
||||
MaxCombo = 244,
|
||||
TotalScore = 1707827,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), },
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
Username = @"waaiiru",
|
||||
CountryCode = CountryCode.ES,
|
||||
}
|
||||
}));
|
||||
AddUntilStep("wait for scores", () => this.ChildrenOfType<BeatmapLeaderboardScore>().Count(), () => Is.GreaterThan(0));
|
||||
AddStep("right click panel", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapLeaderboardScore>().Last());
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddStep("click use these mods", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableOsuMenuItem>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("received HD", () => this.ChildrenOfType<BeatmapLeaderboardScore>().Last().SelectedMods.Value.Any(m => m is OsuModHidden));
|
||||
AddAssert("did not receive SV2", () => !this.ChildrenOfType<BeatmapLeaderboardScore>().Last().SelectedMods.Value.Any(m => m is ModScoreV2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplay()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
|
||||
setScope(BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Set beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(10);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(20);
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplayWorksWhenStartingOffline()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
|
||||
AddStep("Log out", () => API.Logout());
|
||||
setScope(BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Import beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
clearScores();
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplayOnBeatmapEdit()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
string originalHash = string.Empty;
|
||||
|
||||
setScope(BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Import beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
|
||||
AddStep(@"Perform initial save to guarantee stable hash", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
|
||||
originalHash = beatmapInfo.Hash;
|
||||
});
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
|
||||
checkDisplayedCount(10);
|
||||
checkStoredCount(10);
|
||||
|
||||
AddStep(@"Save with changes", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmap.Difficulty.ApproachRate = 12;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
});
|
||||
|
||||
AddAssert("Hash changed", () => beatmapInfo.Hash, () => Is.Not.EqualTo(originalHash));
|
||||
checkDisplayedCount(0);
|
||||
checkStoredCount(10);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(20);
|
||||
checkStoredCount(30);
|
||||
|
||||
AddStep(@"Revert changes", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmap.Difficulty.ApproachRate = 8;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
});
|
||||
|
||||
AddAssert("Hash restored", () => beatmapInfo.Hash, () => Is.EqualTo(originalHash));
|
||||
checkDisplayedCount(10);
|
||||
checkStoredCount(30);
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
checkStoredCount(0);
|
||||
}
|
||||
|
||||
private void showPersonalBestWithNullPosition()
|
||||
{
|
||||
leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
|
||||
{
|
||||
OnlineID = 1337,
|
||||
Rank = ScoreRank.XH,
|
||||
Accuracy = 1,
|
||||
MaxCombo = 244,
|
||||
TotalScore = 1707827,
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
Username = @"waaiiru",
|
||||
CountryCode = CountryCode.ES,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private void showPersonalBest()
|
||||
{
|
||||
leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
|
||||
{
|
||||
OnlineID = 1337,
|
||||
Position = 999,
|
||||
Rank = ScoreRank.XH,
|
||||
Accuracy = 1,
|
||||
MaxCombo = 244,
|
||||
TotalScore = 1707827,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
Username = @"waaiiru",
|
||||
CountryCode = CountryCode.ES,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setScope(BeatmapLeaderboardScope scope)
|
||||
{
|
||||
AddStep(@"Set scope", () => ((Bindable<BeatmapLeaderboardScope>)leaderboard.Scope).Value = scope);
|
||||
}
|
||||
|
||||
private void importMoreScores(Func<BeatmapInfo> beatmapInfo)
|
||||
{
|
||||
AddStep(@"Import new scores", () =>
|
||||
{
|
||||
foreach (var score in TestSceneBeatmapLeaderboard.GenerateSampleScores(beatmapInfo()))
|
||||
scoreManager.Import(score);
|
||||
});
|
||||
}
|
||||
|
||||
private void clearScores()
|
||||
{
|
||||
AddStep("Clear all scores", () => scoreManager.Delete());
|
||||
}
|
||||
|
||||
private void checkDisplayedCount(int expected) =>
|
||||
AddUntilStep($"{expected} scores displayed", () => leaderboard.ChildrenOfType<BeatmapLeaderboardScore>().Count(), () => Is.EqualTo(expected));
|
||||
|
||||
private void checkStoredCount(int expected) =>
|
||||
AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All<ScoreInfo>().Count(s => !s.DeletePending)), () => Is.EqualTo(expected));
|
||||
|
||||
private partial class TestBeatmapLeaderboardWedge : BeatmapLeaderboardWedge
|
||||
{
|
||||
public new void SetState(LeaderboardState state) => base.SetState(state);
|
||||
public new void SetScores(IEnumerable<ScoreInfo> scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,63 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
working.BeatmapInfo.Metadata.Tags = string.Join(' ', Enumerable.Repeat(working.BeatmapInfo.Metadata.Tags, 3));
|
||||
onlineSet.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" };
|
||||
onlineSet.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" };
|
||||
onlineSet.Beatmaps.Single().TopTags = Enumerable.Repeat(onlineSet.Beatmaps.Single().TopTags, 3).SelectMany(t => t!).ToArray();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineAvailability()
|
||||
{
|
||||
AddStep("online beatmapset", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddUntilStep("rating wedge visible", () => wedge.RatingsVisible);
|
||||
AddUntilStep("fail time wedge visible", () => wedge.FailRetryVisible);
|
||||
AddStep("online beatmapset with local diff", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
working.BeatmapInfo.ResetOnlineInfo();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddUntilStep("rating wedge hidden", () => !wedge.RatingsVisible);
|
||||
AddUntilStep("fail time wedge hidden", () => !wedge.FailRetryVisible);
|
||||
AddStep("local beatmap", () =>
|
||||
{
|
||||
var (working, _) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = null;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("rating wedge still hidden", () => !wedge.RatingsVisible);
|
||||
AddAssert("fail time wedge still hidden", () => !wedge.FailRetryVisible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserTags()
|
||||
{
|
||||
AddStep("user tags", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddStep("no user tags", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
onlineSet.Beatmaps.Single().TopTags = null;
|
||||
onlineSet.RelatedTags = null;
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
@@ -164,13 +221,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
OnlineID = working.BeatmapInfo.OnlineID,
|
||||
PlayCount = 10000,
|
||||
PassCount = 4567,
|
||||
TopTags =
|
||||
[
|
||||
new APIBeatmapTag { TagId = 4, VoteCount = 1 },
|
||||
new APIBeatmapTag { TagId = 2, VoteCount = 1 },
|
||||
new APIBeatmapTag { TagId = 23, VoteCount = 5 },
|
||||
],
|
||||
FailTimes = new APIFailTimes
|
||||
{
|
||||
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
RelatedTags =
|
||||
[
|
||||
new APITag
|
||||
{
|
||||
Id = 2,
|
||||
Name = "song representation/simple",
|
||||
Description = "Accessible and straightforward map design."
|
||||
},
|
||||
new APITag
|
||||
{
|
||||
Id = 4,
|
||||
Name = "style/clean",
|
||||
Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects."
|
||||
},
|
||||
new APITag
|
||||
{
|
||||
Id = 23,
|
||||
Name = "aim/aim control",
|
||||
Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual.SongSelect;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
@@ -26,6 +38,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
private BeatmapTitleWedge titleWedge = null!;
|
||||
private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType<BeatmapTitleWedge.DifficultyDisplay>().Single();
|
||||
|
||||
private APIBeatmapSet? currentOnlineSet;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
@@ -36,11 +50,30 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
((DummyAPIAccess)API).HandleRequest = request =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case GetBeatmapSetRequest set:
|
||||
if (set.ID == currentOnlineSet?.OnlineID)
|
||||
{
|
||||
set.TriggerSuccess(currentOnlineSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
titleWedge = new BeatmapTitleWedge
|
||||
@@ -115,11 +148,45 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddAssert("check visibility", () => titleWedge.Alpha > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineAvailability()
|
||||
{
|
||||
AddStep("online beatmapset", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = 10000", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "10,000");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "2,345");
|
||||
AddStep("online beatmapset with local diff", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
working.BeatmapInfo.ResetOnlineInfo();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "-");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "2,345");
|
||||
AddStep("local beatmapset", () =>
|
||||
{
|
||||
var (working, _) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = null;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "-");
|
||||
AddAssert("favourites count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "-");
|
||||
}
|
||||
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180")]
|
||||
[TestCase(120, 120.6, "DT", "180-181 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180-181 (mostly 180)")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
@@ -134,6 +201,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
checkDisplayedBPM(expectedDisplay);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestPerformanceWithLongBeatmap()
|
||||
{
|
||||
AddStep("select heavy beatmap", () => Beatmap.Value = new HeavyWorkingBeatmap(Audio));
|
||||
|
||||
foreach (var rulesetInfo in rulesets.AvailableRulesets)
|
||||
setRuleset(rulesetInfo);
|
||||
}
|
||||
|
||||
private void setRuleset(RulesetInfo rulesetInfo)
|
||||
{
|
||||
AddStep("set ruleset", () => Ruleset.Value = rulesetInfo);
|
||||
@@ -155,5 +232,73 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
return label.Text == target;
|
||||
});
|
||||
}
|
||||
|
||||
private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap()
|
||||
{
|
||||
var working = CreateWorkingBeatmap(Ruleset.Value);
|
||||
var onlineSet = new APIBeatmapSet
|
||||
{
|
||||
OnlineID = working.BeatmapSetInfo.OnlineID,
|
||||
FavouriteCount = 2345,
|
||||
Beatmaps = new[]
|
||||
{
|
||||
new APIBeatmap
|
||||
{
|
||||
OnlineID = working.BeatmapInfo.OnlineID,
|
||||
PlayCount = 10000,
|
||||
PassCount = 4567,
|
||||
UserPlayCount = 123,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
|
||||
working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now;
|
||||
return (working, onlineSet);
|
||||
}
|
||||
|
||||
private class TestHitObject : ConvertHitObject;
|
||||
|
||||
private class HeavyWorkingBeatmap : WorkingBeatmap
|
||||
{
|
||||
private static readonly BeatmapInfo beatmap_info = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = { Username = "osuAuthor" },
|
||||
Artist = "osuArtist",
|
||||
Source = "osuSource",
|
||||
Title = "osuTitle"
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
StarRating = 6,
|
||||
DifficultyName = "osuVersion",
|
||||
Difficulty = new BeatmapDifficulty()
|
||||
};
|
||||
|
||||
public HeavyWorkingBeatmap(AudioManager audioManager)
|
||||
: base(beatmap_info, audioManager)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IBeatmap GetBeatmap()
|
||||
{
|
||||
List<HitObject> objects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 200_000; i++)
|
||||
objects.Add(new TestHitObject { StartTime = i * 1000 });
|
||||
|
||||
return new Beatmap
|
||||
{
|
||||
BeatmapInfo = beatmap_info,
|
||||
HitObjects = objects
|
||||
};
|
||||
}
|
||||
|
||||
public override Texture? GetBackground() => null;
|
||||
public override Stream? GetStream(string storagePath) => null;
|
||||
protected override Track? GetBeatmapTrack() => null;
|
||||
protected internal override ISkin? GetSkin() => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneDifficultyRangeSlider : ThemeComparisonTestScene
|
||||
{
|
||||
private readonly BindableNumber<double> customStart = new BindableNumber<double>
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f
|
||||
};
|
||||
|
||||
private readonly BindableNumber<double> customEnd = new BindableNumber<double>(10)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f
|
||||
};
|
||||
|
||||
public TestSceneDifficultyRangeSlider()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
CreateThemedContent(OverlayColourScheme.Aquamarine);
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
},
|
||||
new FilterControl.DifficultyRangeSlider
|
||||
{
|
||||
Width = 600,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1),
|
||||
LowerBound = customStart,
|
||||
UpperBound = customEnd,
|
||||
TooltipSuffix = "suffix",
|
||||
NubWidth = 32,
|
||||
MinRange = 0.1f,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,22 +12,81 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneModIcon : OsuTestScene
|
||||
{
|
||||
private FillFlowContainer spreadOutFlow = null!;
|
||||
private ModDisplay modDisplay = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create flows", () =>
|
||||
{
|
||||
Child = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.Relative, 0.5f),
|
||||
new Dimension(GridSizeMode.Relative, 0.5f),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
modDisplay = new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
spreadOutFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Full,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void addRange(IEnumerable<IMod> mods)
|
||||
{
|
||||
spreadOutFlow.AddRange(mods.Select(m => new ModIcon(m)));
|
||||
modDisplay.Current.Value = modDisplay.Current.Value.Concat(mods.OfType<Mod>()).ToList();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowAllMods()
|
||||
{
|
||||
AddStep("create mod icons", () =>
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
addRange(Ruleset.Value.CreateInstance().CreateAllMods().Select(m =>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods().Select(m => new ModIcon(m)),
|
||||
};
|
||||
if (m is OsuModFlashlight fl)
|
||||
fl.FollowDelay.Value = 1245;
|
||||
|
||||
if (m is OsuModDaycore dc)
|
||||
dc.SpeedChange.Value = 0.74f;
|
||||
|
||||
if (m is OsuModDifficultyAdjust da)
|
||||
da.CircleSize.Value = 8.2f;
|
||||
|
||||
if (m is ModAdaptiveSpeed ad)
|
||||
ad.AdjustPitch.Value = false;
|
||||
|
||||
return m;
|
||||
}));
|
||||
});
|
||||
|
||||
AddStep("toggle selected", () =>
|
||||
@@ -42,26 +101,22 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
AddStep("create mod icons", () =>
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
var rateAdjustMods = Ruleset.Value.CreateInstance().CreateAllMods()
|
||||
.OfType<ModRateAdjust>();
|
||||
|
||||
addRange(rateAdjustMods.SelectMany(m =>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods()
|
||||
.OfType<ModRateAdjust>()
|
||||
.SelectMany(m =>
|
||||
{
|
||||
List<ModIcon> icons = new List<ModIcon> { new ModIcon(m) };
|
||||
List<Mod> mods = new List<Mod> { m };
|
||||
|
||||
for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10)
|
||||
{
|
||||
m = (ModRateAdjust)m.DeepClone();
|
||||
m.SpeedChange.Value = i;
|
||||
icons.Add(new ModIcon(m));
|
||||
}
|
||||
for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10)
|
||||
{
|
||||
m = (ModRateAdjust)m.DeepClone();
|
||||
m.SpeedChange.Value = i;
|
||||
mods.Add(m);
|
||||
}
|
||||
|
||||
return icons;
|
||||
}),
|
||||
};
|
||||
return mods;
|
||||
}));
|
||||
});
|
||||
|
||||
AddStep("adjust rates", () =>
|
||||
@@ -81,21 +136,50 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Test]
|
||||
public void TestChangeModType()
|
||||
{
|
||||
ModIcon icon = null!;
|
||||
|
||||
AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime()));
|
||||
AddStep("change mod", () => icon.Mod = new OsuModEasy());
|
||||
AddStep("create mod icon", () => addRange([new OsuModDoubleTime()]));
|
||||
AddStep("change mod", () =>
|
||||
{
|
||||
foreach (var modIcon in this.ChildrenOfType<ModIcon>())
|
||||
modIcon.Mod = new OsuModEasy();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInterfaceModType()
|
||||
{
|
||||
ModIcon icon = null!;
|
||||
|
||||
var ruleset = new OsuRuleset();
|
||||
|
||||
AddStep("create mod icon", () => Child = icon = new ModIcon(ruleset.AllMods.First(m => m.Acronym == "DT")));
|
||||
AddStep("change mod", () => icon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ"));
|
||||
AddStep("create mod icon", () => addRange([ruleset.AllMods.First(m => m.Acronym == "DT")]));
|
||||
AddStep("change mod", () =>
|
||||
{
|
||||
foreach (var modIcon in this.ChildrenOfType<ModIcon>())
|
||||
modIcon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDifficultyAdjust()
|
||||
{
|
||||
AddStep("create icons", () =>
|
||||
{
|
||||
addRange([
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
CircleSize = { Value = 8 }
|
||||
},
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
CircleSize = { Value = 5.5f }
|
||||
},
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
CircleSize = { Value = 8 },
|
||||
ApproachRate = { Value = 8 },
|
||||
OverallDifficulty = { Value = 8 },
|
||||
DrainRate = { Value = 8 },
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
@@ -183,32 +182,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(2.5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ShearedButton(120)
|
||||
new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = "Test",
|
||||
Text = "Button",
|
||||
Action = () => { },
|
||||
Padding = new MarginPadding(),
|
||||
Height = 30,
|
||||
},
|
||||
new ShearedButton(120, 40)
|
||||
new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = "Test",
|
||||
Text = "Button",
|
||||
Action = () => { },
|
||||
Padding = new MarginPadding { Left = -1f },
|
||||
Height = 30,
|
||||
},
|
||||
new ShearedButton(120, 70)
|
||||
new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = "Test",
|
||||
Text = "Button",
|
||||
Action = () => { },
|
||||
Padding = new MarginPadding { Left = 3f },
|
||||
Height = 30,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneShearedRangeSlider : ThemeComparisonTestScene
|
||||
{
|
||||
private readonly BindableNumber<double> customStart = new BindableNumber<double>
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f
|
||||
};
|
||||
|
||||
private readonly BindableNumber<double> customEnd = new BindableNumber<double>(10)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f
|
||||
};
|
||||
|
||||
private ShearedRangeSlider shearedRangeSlider = null!;
|
||||
|
||||
public TestSceneShearedRangeSlider()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
CreateThemedContent(OverlayColourScheme.Aquamarine);
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
},
|
||||
shearedRangeSlider = new ShearedRangeSlider("Test")
|
||||
{
|
||||
Width = 600,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1),
|
||||
LowerBound = customStart,
|
||||
UpperBound = customEnd,
|
||||
TooltipSuffix = "suffix",
|
||||
NubWidth = 32,
|
||||
DefaultStringLowerBound = "0.0",
|
||||
DefaultStringUpperBound = "∞",
|
||||
MinRange = 0.1f,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset range", () =>
|
||||
{
|
||||
customStart.SetDefault();
|
||||
customEnd.SetDefault();
|
||||
});
|
||||
|
||||
AddAssert("Initial lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.1f));
|
||||
AddAssert("Initial upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(10).Within(0.1f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustRange()
|
||||
{
|
||||
AddStep("Adjust range", () =>
|
||||
{
|
||||
customStart.Value = 5;
|
||||
customEnd.Value = 7.5;
|
||||
});
|
||||
|
||||
AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(5).Within(0.1f));
|
||||
AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.1f));
|
||||
|
||||
AddStep("Test nub pushing", () =>
|
||||
{
|
||||
customStart.Value = 9;
|
||||
});
|
||||
|
||||
AddAssert("Pushed lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(9).Within(0.1f));
|
||||
AddAssert("Pushed upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(9.1).Within(0.1f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustRangeClickOutsideNub()
|
||||
{
|
||||
Vector2 lowerBoundNub = Vector2.Zero;
|
||||
Vector2 upperBoundNub = Vector2.Zero;
|
||||
|
||||
AddStep("click 75%", () =>
|
||||
{
|
||||
// save out original positions so we can use as absolute selection range.
|
||||
lowerBoundNub = shearedRangeSlider.ChildrenOfType<ShearedNub>().Last().ScreenSpaceDrawQuad.Centre - OsuGame.SHEAR * 2;
|
||||
upperBoundNub = shearedRangeSlider.ChildrenOfType<ShearedNub>().First().ScreenSpaceDrawQuad.Centre - OsuGame.SHEAR * 2;
|
||||
|
||||
InputManager.MoveMouseTo(lowerBoundNub + new Vector2((upperBoundNub.X - lowerBoundNub.X) * 0.75f, 0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.11f));
|
||||
AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.11f));
|
||||
|
||||
AddStep("click 30%", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(lowerBoundNub + new Vector2((upperBoundNub.X - lowerBoundNub.X) * 0.3f, 0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(3.0).Within(0.11f));
|
||||
AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.11f));
|
||||
|
||||
AddStep("click 0%", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(lowerBoundNub);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.11f));
|
||||
AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.11f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
@@ -30,16 +32,32 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
(typeof(OverlayColourProvider), colourProvider)
|
||||
},
|
||||
Children = new Drawable[]
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
new ShearedSearchTextBox
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f
|
||||
new ShearedSearchTextBox
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f
|
||||
},
|
||||
new ShearedFilterTextBox
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
FilterText = "12345 matches",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,38 +3,50 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneShearedSliderBar : OsuManualInputManagerTestScene
|
||||
public partial class TestSceneShearedSliderBar : ThemeComparisonTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
private TestSliderBar slider = null!;
|
||||
|
||||
private ShearedSliderBar<double> slider = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
protected override Drawable CreateContent() => slider = new TestSliderBar
|
||||
{
|
||||
AddStep("create slider", () => Child = slider = new ShearedSliderBar<double>
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Current = new BindableDouble(5)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Current = new BindableDouble(5)
|
||||
Precision = 0.1,
|
||||
MinValue = 0,
|
||||
MaxValue = 15
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void TestNubDisplay()
|
||||
{
|
||||
AddSliderStep("nub width", 20, 80, 50, v =>
|
||||
{
|
||||
if (slider.IsNotNull())
|
||||
{
|
||||
Precision = 0.1,
|
||||
MinValue = 0,
|
||||
MaxValue = 15
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f
|
||||
slider.Nub.Width = v;
|
||||
slider.RangePadding = v / 2f;
|
||||
}
|
||||
});
|
||||
AddToggleStep("nub shadow", v =>
|
||||
{
|
||||
if (slider.IsNotNull())
|
||||
slider.NubShadowColour = v ? Color4.Black.Opacity(0.2f) : Color4.Black.Opacity(0f);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,6 +81,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
});
|
||||
|
||||
AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1));
|
||||
AddStep("enable slider", () => slider.Current.Disabled = false);
|
||||
}
|
||||
|
||||
public partial class TestSliderBar : ShearedSliderBar<double>
|
||||
{
|
||||
public new ShearedNub Nub => base.Nub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,10 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.ScalingPositionX, 0.5f, 0f, 1f, 0.01f);
|
||||
SetDefault(OsuSetting.ScalingPositionY, 0.5f, 0f, 1f, 0.01f);
|
||||
|
||||
SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f);
|
||||
if (RuntimeInfo.IsMobile)
|
||||
SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.1f, 0.01f);
|
||||
else
|
||||
SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f);
|
||||
|
||||
SetDefault(OsuSetting.UIHoldActivationDelay, 200.0, 0.0, 500.0, 50.0);
|
||||
|
||||
@@ -222,6 +225,8 @@ namespace osu.Game.Configuration
|
||||
|
||||
SetDefault(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, true);
|
||||
SetDefault(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, true);
|
||||
|
||||
SetDefault(OsuSetting.WasSupporter, false);
|
||||
}
|
||||
|
||||
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
|
||||
@@ -463,5 +468,11 @@ namespace osu.Game.Configuration
|
||||
EditorShowStoryboard,
|
||||
EditorSubmissionNotifyOnDiscussionReplies,
|
||||
EditorSubmissionLoadInBrowserAfterSubmission,
|
||||
|
||||
/// <summary>
|
||||
/// Cached state of whether local user is a supporter.
|
||||
/// Used to allow early checks (ie for startup samples) to be in the correct state, even if the API authentication process has not completed.
|
||||
/// </summary>
|
||||
WasSupporter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Extensions
|
||||
/// <param name="maxDecimalDigits">The maximum number of decimals to be considered in the original value.</param>
|
||||
/// <param name="asPercentage">Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%.</param>
|
||||
/// <returns>The formatted output.</returns>
|
||||
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage = false) where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
double floatValue = double.CreateTruncating(value);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Layout;
|
||||
using osuTK;
|
||||
|
||||
@@ -18,6 +19,10 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
private readonly LayoutValue layout = new LayoutValue(Invalidation.MiscGeometry);
|
||||
|
||||
// Sheared components regularly end up off the side of the screen due to padding considerations.
|
||||
// If we use this class in places where performance is important, we should reconsider the handling of this.
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
|
||||
public ShearAligningWrapper(Drawable drawable)
|
||||
{
|
||||
RelativeSizeAxes = drawable.RelativeSizeAxes;
|
||||
|
||||
@@ -88,12 +88,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT)
|
||||
{
|
||||
Height = height;
|
||||
Padding = new MarginPadding { Horizontal = OsuGame.SHEAR.X * height };
|
||||
|
||||
Content.CornerRadius = CORNER_RADIUS;
|
||||
Content.Shear = OsuGame.SHEAR;
|
||||
Content.Masking = true;
|
||||
Shear = OsuGame.SHEAR;
|
||||
|
||||
Content.Anchor = Content.Origin = Anchor.Centre;
|
||||
Content.CornerRadius = CORNER_RADIUS;
|
||||
Content.Masking = true;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class ShearedFilterTextBox : ShearedSearchTextBox
|
||||
{
|
||||
private const float filter_text_size = 12;
|
||||
|
||||
public LocalisableString FilterText
|
||||
{
|
||||
get => ((InnerFilterTextBox)TextBox).FilterText.Text;
|
||||
set => Schedule(() => ((InnerFilterTextBox)TextBox).FilterText.Text = value);
|
||||
}
|
||||
|
||||
public ShearedFilterTextBox()
|
||||
{
|
||||
Height += filter_text_size;
|
||||
}
|
||||
|
||||
protected override InnerSearchTextBox CreateInnerTextBox() => new InnerFilterTextBox();
|
||||
|
||||
protected partial class InnerFilterTextBox : InnerSearchTextBox
|
||||
{
|
||||
public OsuSpriteText FilterText { get; private set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
TextContainer.Add(FilterText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Font = OsuFont.Torus.With(size: filter_text_size, weight: FontWeight.SemiBold),
|
||||
Margin = new MarginPadding { Top = 2, Left = -1 },
|
||||
Colour = colours.Yellow
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
TextContainer.Height *= (DrawHeight - filter_text_size) / DrawHeight;
|
||||
TextContainer.Margin = new MarginPadding { Bottom = filter_text_size };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,37 +21,54 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public Action? OnDoubleClicked { get; init; }
|
||||
|
||||
protected const float BORDER_WIDTH = 3;
|
||||
|
||||
public const int HEIGHT = 30;
|
||||
public const float EXPANDED_SIZE = 50;
|
||||
public const float CORNER_RADIUS = 5;
|
||||
|
||||
private readonly Box fill;
|
||||
private readonly Container main;
|
||||
private readonly Container shadow;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the shape for the nub, allowing for any type of container to be used.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ShearedNub()
|
||||
{
|
||||
Size = new Vector2(EXPANDED_SIZE, HEIGHT);
|
||||
InternalChild = main = new Container
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
Shear = OsuGame.SHEAR,
|
||||
BorderColour = Colour4.White,
|
||||
BorderThickness = BORDER_WIDTH,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Child = fill = new Box
|
||||
shadow = new Container
|
||||
{
|
||||
Shear = OsuGame.SHEAR,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 20f,
|
||||
},
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
},
|
||||
main = new Container
|
||||
{
|
||||
Shear = OsuGame.SHEAR,
|
||||
BorderColour = Colour4.White,
|
||||
BorderThickness = 8f,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Child = fill = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +93,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(onCurrentValueChanged, true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private bool glowing;
|
||||
@@ -89,22 +107,22 @@ namespace osu.Game.Graphics.UserInterface
|
||||
return;
|
||||
|
||||
glowing = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
if (value)
|
||||
{
|
||||
main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeColour(GlowingAccentColour, 800, Easing.OutQuint);
|
||||
private Color4 shadowColour = Color4.Black.Opacity(0f);
|
||||
|
||||
main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint);
|
||||
main.FadeColour(AccentColour, 800, Easing.OutQuint);
|
||||
}
|
||||
public Color4 ShadowColour
|
||||
{
|
||||
get => shadowColour;
|
||||
set
|
||||
{
|
||||
if (shadowColour == value)
|
||||
return;
|
||||
|
||||
shadowColour = value;
|
||||
shadow.FadeEdgeEffectTo(value, 800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,8 +148,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set
|
||||
{
|
||||
accentColour = value;
|
||||
if (!Glowing)
|
||||
main.Colour = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +160,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set
|
||||
{
|
||||
glowingAccentColour = value;
|
||||
if (Glowing)
|
||||
main.Colour = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,10 +172,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set
|
||||
{
|
||||
glowColour = value;
|
||||
|
||||
var effect = main.EdgeEffect;
|
||||
effect.Colour = Glowing ? value : value.Opacity(0);
|
||||
main.EdgeEffect = effect;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +190,26 @@ namespace osu.Game.Graphics.UserInterface
|
||||
else
|
||||
{
|
||||
main.ResizeWidthTo(0.75f, duration, Easing.OutQuint);
|
||||
main.TransformTo(nameof(BorderThickness), BORDER_WIDTH, duration, Easing.OutQuint);
|
||||
main.TransformTo(nameof(BorderThickness), 8f, duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (Glowing)
|
||||
{
|
||||
main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeColour(GlowingAccentColour, 800, Easing.OutQuint);
|
||||
|
||||
main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint);
|
||||
main.FadeColour(AccentColour, 800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class ShearedRangeSlider : CompositeDrawable
|
||||
{
|
||||
private readonly LocalisableString label;
|
||||
|
||||
private readonly BindableNumberWithCurrent<double> lowerBound = new BindableNumberWithCurrent<double>();
|
||||
|
||||
/// <summary>
|
||||
/// The lower limiting value.
|
||||
/// </summary>
|
||||
public Bindable<double> LowerBound
|
||||
{
|
||||
get => lowerBound.Current;
|
||||
set => lowerBound.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableNumberWithCurrent<double> upperBound = new BindableNumberWithCurrent<double>();
|
||||
|
||||
/// <summary>
|
||||
/// The upper limiting value.
|
||||
/// </summary>
|
||||
public Bindable<double> UpperBound
|
||||
{
|
||||
get => upperBound.Current;
|
||||
set => upperBound.Current = value;
|
||||
}
|
||||
|
||||
public float NubWidth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum difference between the lower bound and higher bound
|
||||
/// </summary>
|
||||
public float MinRange
|
||||
{
|
||||
set => minRange = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lower bound display for when it is set to its default value.
|
||||
/// </summary>
|
||||
public string DefaultStringLowerBound { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Upper bound display for when it is set to its default value.
|
||||
/// </summary>
|
||||
public string DefaultStringUpperBound { get; init; } = string.Empty;
|
||||
|
||||
public LocalisableString DefaultTooltipLowerBound { get; init; } = string.Empty;
|
||||
|
||||
public LocalisableString DefaultTooltipUpperBound { get; init; } = string.Empty;
|
||||
|
||||
public string TooltipSuffix { get; init; } = string.Empty;
|
||||
|
||||
private float minRange = 0.1f;
|
||||
|
||||
protected Container SliderContainer { get; private set; } = null!;
|
||||
|
||||
protected BoundSliderBar LowerBoundSlider { get; private set; } = null!;
|
||||
protected BoundSliderBar UpperBoundSlider { get; private set; } = null!;
|
||||
|
||||
protected Vector2 ScreenSpaceHalfwayPoint
|
||||
{
|
||||
get
|
||||
{
|
||||
var lowerSS = LowerBoundSlider.Nub.ScreenSpaceDrawQuad.TopLeft;
|
||||
var upperSS = UpperBoundSlider.Nub.ScreenSpaceDrawQuad.TopLeft;
|
||||
|
||||
return lowerSS + (upperSS - lowerSS) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
public ShearedRangeSlider(LocalisableString label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Height = ShearedNub.HEIGHT;
|
||||
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Masking = true,
|
||||
CornerRadius = 5f,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background3,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = label,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Margin = new MarginPadding { Horizontal = 12, Vertical = 5 },
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
},
|
||||
},
|
||||
SliderContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = -10 },
|
||||
Children = new[]
|
||||
{
|
||||
UpperBoundSlider = CreateBoundSlider(true).With(d =>
|
||||
{
|
||||
d.KeyboardStep = 0.1f;
|
||||
d.RelativeSizeAxes = Axes.X;
|
||||
d.TooltipSuffix = TooltipSuffix;
|
||||
d.DefaultString = DefaultStringUpperBound;
|
||||
d.DefaultTooltip = DefaultTooltipUpperBound;
|
||||
d.NubWidth = NubWidth;
|
||||
d.Current = upperBound;
|
||||
}),
|
||||
LowerBoundSlider = CreateBoundSlider(false).With(d =>
|
||||
{
|
||||
d.KeyboardStep = 0.1f;
|
||||
d.RelativeSizeAxes = Axes.X;
|
||||
d.TooltipSuffix = TooltipSuffix;
|
||||
d.DefaultString = DefaultStringLowerBound;
|
||||
d.DefaultTooltip = DefaultTooltipLowerBound;
|
||||
d.NubWidth = NubWidth;
|
||||
d.Current = lowerBound;
|
||||
}),
|
||||
UpperBoundSlider.Nub.CreateProxy(),
|
||||
LowerBoundSlider.Nub.CreateProxy(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
LowerBoundSlider.Current.ValueChanged += min => UpperBoundSlider.Current.Value = Math.Max(min.NewValue + minRange, UpperBoundSlider.Current.Value);
|
||||
UpperBoundSlider.Current.ValueChanged += max => LowerBoundSlider.Current.Value = Math.Min(max.NewValue - minRange, LowerBoundSlider.Current.Value);
|
||||
}
|
||||
|
||||
protected virtual BoundSliderBar CreateBoundSlider(bool isUpper) => new BoundSliderBar(this, isUpper);
|
||||
|
||||
protected partial class BoundSliderBar : ShearedSliderBar<double>
|
||||
{
|
||||
private readonly ShearedRangeSlider rangeSlider;
|
||||
private readonly bool isUpper;
|
||||
|
||||
public new float NormalizedValue => base.NormalizedValue;
|
||||
|
||||
public new ShearedNub Nub => base.Nub;
|
||||
|
||||
public string? DefaultString;
|
||||
public LocalisableString? DefaultTooltip;
|
||||
public string? TooltipSuffix;
|
||||
|
||||
public float NubWidth { get; set; } = ShearedNub.HEIGHT;
|
||||
|
||||
public override LocalisableString TooltipText =>
|
||||
(Current.IsDefault ? DefaultTooltip : Current.Value.ToString($@"0.## {TooltipSuffix}")) ?? Current.Value.ToString($@"0.## {TooltipSuffix}");
|
||||
|
||||
protected OsuSpriteText NubText { get; private set; } = null!;
|
||||
|
||||
public override bool AcceptsFocus => false;
|
||||
|
||||
public BoundSliderBar(ShearedRangeSlider rangeSlider, bool isUpper)
|
||||
{
|
||||
this.rangeSlider = rangeSlider;
|
||||
this.isUpper = isUpper;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Nub.Width = NubWidth;
|
||||
RangePadding = Nub.Width / 2;
|
||||
|
||||
Nub.Add(NubText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
X = -3,
|
||||
UseFullGlyphHeight = false,
|
||||
Colour = OsuColour.ForegroundTextColourFor(colourProvider.Light1),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
});
|
||||
|
||||
AccentColour = colourProvider.Highlight1.Darken(0.1f);
|
||||
Nub.AccentColour = colourProvider.Highlight1;
|
||||
Nub.GlowingAccentColour = colourProvider.Highlight1;
|
||||
Nub.GlowColour = colourProvider.Highlight1;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (!isUpper)
|
||||
{
|
||||
AccentColour = BackgroundColour;
|
||||
BackgroundColour = Color4.Transparent;
|
||||
}
|
||||
|
||||
Current.BindValueChanged(current => UpdateDisplay(current.NewValue), true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
protected virtual void UpdateDisplay(double value)
|
||||
{
|
||||
string defaultString = DefaultString ?? value.ToString("N1");
|
||||
NubText.Text = Current.IsDefault ? defaultString : value.ToString("N1");
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
if (isUpper)
|
||||
return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X > rangeSlider.ScreenSpaceHalfwayPoint.X;
|
||||
|
||||
return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X <= rangeSlider.ScreenSpaceHalfwayPoint.X;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (isUpper)
|
||||
{
|
||||
// Only draw left box where required to avoid masking bleed issues.
|
||||
LeftBox.X = ToParentSpace(ToLocalSpace(rangeSlider.LowerBoundSlider.Nub.ScreenSpaceDrawQuad.Centre)).X;
|
||||
LeftBox.Size -= new Vector2(LeftBox.X, 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
base.OnHover(e);
|
||||
return true; // Make sure only one nub shows hover effect at once.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,33 +21,33 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private const float corner_radius = 7;
|
||||
|
||||
private readonly Box background;
|
||||
private readonly SearchTextBox textBox;
|
||||
protected readonly InnerSearchTextBox TextBox;
|
||||
|
||||
public Bindable<string> Current
|
||||
{
|
||||
get => textBox.Current;
|
||||
set => textBox.Current = value;
|
||||
get => TextBox.Current;
|
||||
set => TextBox.Current = value;
|
||||
}
|
||||
|
||||
public bool HoldFocus
|
||||
{
|
||||
get => textBox.HoldFocus;
|
||||
set => textBox.HoldFocus = value;
|
||||
get => TextBox.HoldFocus;
|
||||
set => TextBox.HoldFocus = value;
|
||||
}
|
||||
|
||||
public LocalisableString PlaceholderText
|
||||
{
|
||||
get => textBox.PlaceholderText;
|
||||
set => textBox.PlaceholderText = value;
|
||||
get => TextBox.PlaceholderText;
|
||||
set => TextBox.PlaceholderText = value;
|
||||
}
|
||||
|
||||
public new bool HasFocus => textBox.HasFocus;
|
||||
public new bool HasFocus => TextBox.HasFocus;
|
||||
|
||||
public void TakeFocus() => textBox.TakeFocus();
|
||||
public void TakeFocus() => TextBox.TakeFocus();
|
||||
|
||||
public void KillFocus() => textBox.KillFocus();
|
||||
public void KillFocus() => TextBox.KillFocus();
|
||||
|
||||
public bool SelectAll() => textBox.SelectAll();
|
||||
public bool SelectAll() => TextBox.SelectAll();
|
||||
|
||||
public ShearedSearchTextBox()
|
||||
{
|
||||
@@ -69,13 +69,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
textBox = new InnerSearchTextBox
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One
|
||||
},
|
||||
TextBox = CreateInnerTextBox(),
|
||||
new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Solid.Search,
|
||||
@@ -101,10 +95,20 @@ namespace osu.Game.Graphics.UserInterface
|
||||
background.Colour = colourProvider.Background3;
|
||||
}
|
||||
|
||||
public override bool HandleNonPositionalInput => textBox.HandleNonPositionalInput;
|
||||
public override bool HandleNonPositionalInput => TextBox.HandleNonPositionalInput;
|
||||
|
||||
private partial class InnerSearchTextBox : SearchTextBox
|
||||
protected virtual InnerSearchTextBox CreateInnerTextBox() => new InnerSearchTextBox();
|
||||
|
||||
protected partial class InnerSearchTextBox : SearchTextBox
|
||||
{
|
||||
public InnerSearchTextBox()
|
||||
{
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.CentreLeft;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = Vector2.One;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Overlays;
|
||||
using static osu.Game.Graphics.UserInterface.ShearedNub;
|
||||
using Vector2 = osuTK.Vector2;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@@ -29,6 +28,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private readonly Container mainContent;
|
||||
|
||||
protected virtual bool FocusIndicator => true;
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
public Color4 AccentColour
|
||||
@@ -56,43 +57,41 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
public Color4 NubShadowColour
|
||||
{
|
||||
get => Nub.ShadowColour;
|
||||
set => Nub.ShadowColour = value;
|
||||
}
|
||||
|
||||
public ShearedSliderBar()
|
||||
{
|
||||
Shear = OsuGame.SHEAR;
|
||||
Height = HEIGHT;
|
||||
RangePadding = EXPANDED_SIZE / 2;
|
||||
Height = ShearedNub.HEIGHT;
|
||||
RangePadding = ShearedNub.EXPANDED_SIZE / 2;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
mainContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Child = new Container
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Children = new Drawable[]
|
||||
LeftBox = new Box
|
||||
{
|
||||
LeftBox = new Box
|
||||
{
|
||||
EdgeSmoothness = new Vector2(0, 0.5f),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
RightBox = new Box
|
||||
{
|
||||
EdgeSmoothness = new Vector2(0, 0.5f),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
EdgeSmoothness = new Vector2(0, 0.5f),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
RightBox = new Box
|
||||
{
|
||||
EdgeSmoothness = new Vector2(0, 0.5f),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -102,7 +101,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = Nub = new ShearedNub
|
||||
{
|
||||
X = -OsuGame.SHEAR.X * HEIGHT / 2f,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativePositionAxes = Axes.X,
|
||||
Current = { Value = true },
|
||||
@@ -146,13 +144,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
base.OnFocus(e);
|
||||
|
||||
mainContent.EdgeEffect = new EdgeEffectParameters
|
||||
if (FocusIndicator)
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = AccentColour.Darken(1),
|
||||
Hollow = true,
|
||||
Radius = 2,
|
||||
};
|
||||
mainContent.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = AccentColour.Darken(1),
|
||||
Hollow = true,
|
||||
Radius = 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
@@ -191,8 +192,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
|
||||
LeftBox.Size = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
RightBox.Size = new Vector2(Math.Clamp(DrawWidth - RangePadding - Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
}
|
||||
|
||||
protected override void UpdateValue(float value)
|
||||
|
||||
@@ -84,6 +84,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString RightMouseScroll => new TranslatableString(getKey(@"right_mouse_scroll"), @"Right mouse drag to absolute scroll");
|
||||
|
||||
/// <summary>
|
||||
/// "Show converts"
|
||||
/// </summary>
|
||||
public static LocalisableString ShowConverts => new TranslatableString(getKey(@"show_converts"), @"Show converts");
|
||||
|
||||
/// <summary>
|
||||
/// "Show converted beatmaps"
|
||||
/// </summary>
|
||||
|
||||
@@ -72,6 +72,8 @@ namespace osu.Game.Online.API
|
||||
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
||||
|
||||
private readonly Bindable<UserStatus> configStatus = new Bindable<UserStatus>();
|
||||
private readonly Bindable<bool> configSupporter = new Bindable<bool>();
|
||||
|
||||
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||
private readonly Logger log;
|
||||
|
||||
@@ -104,6 +106,7 @@ namespace osu.Game.Online.API
|
||||
authentication.Token.ValueChanged += onTokenChanged;
|
||||
|
||||
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
|
||||
config.BindWith(OsuSetting.WasSupporter, configSupporter);
|
||||
|
||||
if (HasLogin)
|
||||
{
|
||||
@@ -333,6 +336,7 @@ namespace osu.Game.Online.API
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
localUser.Value = me;
|
||||
configSupporter.Value = me.IsSupporter;
|
||||
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
||||
failureCount = 0;
|
||||
};
|
||||
@@ -368,7 +372,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
localUser.Value = new APIUser
|
||||
{
|
||||
Username = ProvidedUsername
|
||||
Username = ProvidedUsername,
|
||||
IsSupporter = configSupporter.Value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -607,6 +612,7 @@ namespace osu.Game.Online.API
|
||||
Schedule(() =>
|
||||
{
|
||||
localUser.Value = createGuestUser();
|
||||
configSupporter.Value = false;
|
||||
friends.Clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -125,7 +125,14 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
var result = LeaderboardScores.Success
|
||||
(
|
||||
response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore().ToArray(),
|
||||
response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap))
|
||||
.OrderByTotalScore()
|
||||
.Select((s, idx) =>
|
||||
{
|
||||
s.Position = idx + 1;
|
||||
return s;
|
||||
})
|
||||
.ToArray(),
|
||||
response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap)
|
||||
);
|
||||
inFlightOnlineRequest = null;
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Mods
|
||||
Height = ModSelectPanel.HEIGHT;
|
||||
|
||||
// shear will be applied at a higher level in `ModPresetColumn`.
|
||||
Content.Shear = Vector2.Zero;
|
||||
Shear = Vector2.Zero;
|
||||
Padding = new MarginPadding();
|
||||
|
||||
Text = "+";
|
||||
|
||||
@@ -40,11 +40,13 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
public AddPresetPopover(AddPresetButton addPresetButton)
|
||||
{
|
||||
const float content_width = 300;
|
||||
|
||||
button = addPresetButton;
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 300,
|
||||
Width = content_width,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
@@ -63,12 +65,24 @@ namespace osu.Game.Overlays.Mods
|
||||
Label = CommonStrings.Description,
|
||||
TabbableContentContainer = this
|
||||
},
|
||||
createButton = new ShearedButton
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = ModSelectOverlayStrings.AddPreset,
|
||||
Action = createPreset
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
createButton = new ShearedButton(content_width)
|
||||
{
|
||||
// todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = ModSelectOverlayStrings.AddPreset,
|
||||
Action = createPreset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "One or more values are being adjusted by mods that change speed.",
|
||||
Text = "One or more values are being adjusted by mods.",
|
||||
},
|
||||
attributesFillFlow = new FillFlowContainer
|
||||
{
|
||||
|
||||
@@ -52,9 +52,11 @@ namespace osu.Game.Overlays.Mods
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const float content_width = 300;
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 300,
|
||||
Width = content_width,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
@@ -107,25 +109,27 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
useCurrentModsButton = new ShearedButton
|
||||
useCurrentModsButton = new ShearedButton(content_width)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = ModSelectOverlayStrings.UseCurrentMods,
|
||||
DarkerColour = colours.Blue1,
|
||||
LighterColour = colours.Blue0,
|
||||
TextColour = colourProvider.Background6,
|
||||
Action = useCurrentMods,
|
||||
},
|
||||
saveButton = new ShearedButton
|
||||
saveButton = new ShearedButton(content_width)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = Resources.Localisation.Web.CommonStrings.ButtonsSave,
|
||||
DarkerColour = colours.Orange1,
|
||||
LighterColour = colours.Orange0,
|
||||
|
||||
@@ -243,11 +243,10 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Padding = new MarginPadding { Right = OsuGame.SCREEN_EDGE_MARGIN };
|
||||
|
||||
InternalChild = NextButton = new ShearedButton(0)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 12f },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Text = FirstRunSetupOverlayStrings.GetStarted,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@@ -81,5 +83,33 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// Create a fresh <see cref="Mod"/> instance based on this mod.
|
||||
/// </summary>
|
||||
Mod CreateInstance() => (Mod)Activator.CreateInstance(GetType())!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether any user adjustable setting attached to this mod has a non-default value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This returns the instantaneous state of this mod. It may change over time.
|
||||
/// For tracking changes on a dynamic display, make sure to setup a <see cref="ModSettingChangeTracker"/>.
|
||||
/// </remarks>
|
||||
bool HasNonDefaultSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
bool hasAdjustments = false;
|
||||
|
||||
foreach (var (_, property) in this.GetSettingsSourceProperties())
|
||||
{
|
||||
var bindable = (IBindable)property.GetValue(this)!;
|
||||
|
||||
if (!bindable.IsDefault)
|
||||
{
|
||||
hasAdjustments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hasAdjustments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@@ -67,6 +68,22 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
@@ -94,5 +111,26 @@ namespace osu.Game.Rulesets.Mods
|
||||
if (DrainRate.Value != null) difficulty.DrainRate = DrainRate.Value.Value;
|
||||
if (OverallDifficulty.Value != null) difficulty.OverallDifficulty = OverallDifficulty.Value.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of settings on this mod instance which have been adjusted by the user from their default values.
|
||||
/// </summary>
|
||||
protected int UserAdjustedSettingsCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
foreach (var (_, property) in this.GetSettingsSourceProperties())
|
||||
{
|
||||
var bindable = (IBindable)property.GetValue(this)!;
|
||||
|
||||
if (!bindable.IsDefault)
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
@@ -81,6 +82,11 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private Container extendedContent = null!;
|
||||
|
||||
private Drawable adjustmentMarker = null!;
|
||||
|
||||
private Circle cogBackground = null!;
|
||||
private SpriteIcon cog = null!;
|
||||
|
||||
private ModSettingChangeTracker? modSettingsChangeTracker;
|
||||
|
||||
/// <summary>
|
||||
@@ -139,7 +145,7 @@ namespace osu.Game.Rulesets.UI
|
||||
Origin = Anchor.CentreLeft,
|
||||
Name = "main content",
|
||||
Size = MOD_ICON_SIZE,
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
background = new Sprite
|
||||
{
|
||||
@@ -165,6 +171,29 @@ namespace osu.Game.Rulesets.UI
|
||||
Size = new Vector2(45),
|
||||
Icon = FontAwesome.Solid.Question
|
||||
},
|
||||
adjustmentMarker = new Container
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Origin = Anchor.Centre,
|
||||
Position = new Vector2(64, 14),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
cogBackground = new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
cog = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.Cog,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.6f),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -216,11 +245,18 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
extendedContent.Alpha = showExtended ? 1 : 0;
|
||||
extendedText.Text = mod.ExtendedIconInformation;
|
||||
|
||||
if (mod.HasNonDefaultSettings)
|
||||
adjustmentMarker.Show();
|
||||
else
|
||||
adjustmentMarker.Hide();
|
||||
}
|
||||
|
||||
private void updateColour()
|
||||
{
|
||||
modAcronym.Colour = modIcon.Colour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, backgroundColour, 0, 1);
|
||||
cogBackground.Colour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, backgroundColour, 0, 1);
|
||||
cog.Colour = backgroundColour;
|
||||
|
||||
extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour;
|
||||
extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f);
|
||||
|
||||
@@ -108,12 +108,14 @@ namespace osu.Game.Screens.Backgrounds
|
||||
if (Background != null)
|
||||
{
|
||||
newDepth = Background.Depth + 1;
|
||||
Background.FinishTransforms();
|
||||
Background.FadeOut(250);
|
||||
Background.Expire();
|
||||
}
|
||||
|
||||
b.Depth = newDepth;
|
||||
b.Anchor = b.Origin = Anchor.Centre;
|
||||
b.FadeInFromZero(500, Easing.OutQuint);
|
||||
b.ScaleTo(1.02f).ScaleTo(1, 3500, Easing.OutQuint);
|
||||
dimmable.Background = Background = b;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace osu.Game.Screens.Footer
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
Padding = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
@@ -89,7 +89,7 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Y = 10f,
|
||||
Y = ScreenFooterButton.Y_OFFSET,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(7, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Screens.Footer
|
||||
footerContentContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Y = -15f,
|
||||
Y = -OsuGame.SCREEN_EDGE_MARGIN,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -112,7 +112,7 @@ namespace osu.Game.Screens.Footer
|
||||
hiddenButtonsContainer = new Container<ScreenFooterButton>
|
||||
{
|
||||
Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
Y = 10f,
|
||||
Y = ScreenFooterButton.Y_OFFSET,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
|
||||
@@ -25,7 +25,8 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
protected const int CORNER_RADIUS = 10;
|
||||
public const int Y_OFFSET = 10;
|
||||
|
||||
protected const int BUTTON_HEIGHT = 75;
|
||||
protected const int BUTTON_WIDTH = 116;
|
||||
|
||||
@@ -87,7 +88,7 @@ namespace osu.Game.Screens.Footer
|
||||
},
|
||||
Shear = OsuGame.SHEAR,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
CornerRadius = 10,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@@ -134,7 +135,7 @@ namespace osu.Game.Screens.Footer
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = -CORNER_RADIUS,
|
||||
Y = -Y_OFFSET,
|
||||
Size = new Vector2(100, 5),
|
||||
Masking = true,
|
||||
CornerRadius = 3,
|
||||
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
@@ -20,8 +19,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class DrawableGameplayLeaderboard : CompositeDrawable
|
||||
{
|
||||
private readonly Cached sorting = new Cached();
|
||||
|
||||
public Bindable<bool> Expanded = new Bindable<bool>();
|
||||
|
||||
protected readonly FillFlowContainer<DrawableGameplayLeaderboardScore> Flow;
|
||||
@@ -87,7 +84,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}, true);
|
||||
}
|
||||
|
||||
Scheduler.AddDelayed(sort, 1000, true);
|
||||
configVisibility.BindValueChanged(_ => this.FadeTo(configVisibility.Value ? 1 : 0, 100, Easing.OutQuint), true);
|
||||
}
|
||||
|
||||
@@ -109,8 +105,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
drawable.Expanded.BindTo(Expanded);
|
||||
|
||||
Flow.Add(drawable);
|
||||
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
drawable.ScorePosition.BindValueChanged(_ => Scheduler.AddOnce(sort));
|
||||
drawable.DisplayOrder.BindValueChanged(_ => Scheduler.AddOnce(sort), true);
|
||||
|
||||
int displayCount = Math.Min(Flow.Count, max_panels);
|
||||
Height = displayCount * (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||
@@ -179,22 +175,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private void sort()
|
||||
{
|
||||
if (sorting.IsValid)
|
||||
return;
|
||||
|
||||
var orderedByScore = Flow
|
||||
.OrderByDescending(i => i.TotalScore.Value)
|
||||
.ThenBy(i => i.DisplayOrder.Value)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < Flow.Count; i++)
|
||||
{
|
||||
var score = orderedByScore[i];
|
||||
Flow.SetLayoutPosition(score, i);
|
||||
score.ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true && score.Tracked ? null : i + 1;
|
||||
}
|
||||
|
||||
sorting.Validate();
|
||||
foreach (var score in Flow.ToArray())
|
||||
Flow.SetLayoutPosition(score, score.DisplayOrder.Value);
|
||||
}
|
||||
|
||||
private partial class InputDisabledScrollContainer : OsuScrollContainer
|
||||
|
||||
@@ -56,6 +56,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<int?> ScorePosition { get; } = new Bindable<int?>();
|
||||
public Bindable<long> DisplayOrder { get; } = new Bindable<long>();
|
||||
|
||||
private Func<ScoringMode, long>? getDisplayScoreFunction;
|
||||
@@ -69,28 +70,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
public Color4? TextColour { get; set; }
|
||||
|
||||
private int? scorePosition;
|
||||
|
||||
private bool scorePositionIsSet;
|
||||
|
||||
public int? ScorePosition
|
||||
{
|
||||
get => scorePosition;
|
||||
set
|
||||
{
|
||||
// We always want to run once, as the incoming value may be null and require a visual update to "-".
|
||||
if (value == scorePosition && scorePositionIsSet)
|
||||
return;
|
||||
|
||||
scorePosition = value;
|
||||
|
||||
positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "-";
|
||||
scorePositionIsSet = true;
|
||||
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
public IUser? User { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -123,6 +102,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Accuracy.BindTo(score.Accuracy);
|
||||
Combo.BindTo(score.Combo);
|
||||
HasQuit.BindTo(score.HasQuit);
|
||||
ScorePosition.BindTo(score.Position);
|
||||
DisplayOrder.BindTo(score.DisplayOrder);
|
||||
GetDisplayScore = score.GetDisplayScore;
|
||||
|
||||
@@ -334,6 +314,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
updateState();
|
||||
Expanded.BindValueChanged(changeExpandedState, true);
|
||||
ScorePosition.BindValueChanged(_ => updateState(), true);
|
||||
|
||||
FinishTransforms(true);
|
||||
}
|
||||
@@ -392,7 +373,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
return;
|
||||
}
|
||||
|
||||
if (scorePosition == 1)
|
||||
positionText.Text = ScorePosition.Value.HasValue ? $"#{ScorePosition.Value.Value.FormatRank()}" : "-";
|
||||
|
||||
if (ScorePosition.Value == 1)
|
||||
{
|
||||
widthExtension = true;
|
||||
panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33");
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
|
||||
namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
public partial class SoloResultsScreen : ResultsScreen
|
||||
{
|
||||
private GetScoresRequest? getScoreRequest;
|
||||
private readonly IBindable<LeaderboardScores?> globalScores = new Bindable<LeaderboardScores?>();
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
public SoloResultsScreen(ScoreInfo score)
|
||||
: base(score)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
globalScores.BindTo(leaderboardManager.Scores);
|
||||
}
|
||||
|
||||
protected override async Task<ScoreInfo[]> FetchScores()
|
||||
{
|
||||
Debug.Assert(Score != null);
|
||||
@@ -39,52 +41,93 @@ namespace osu.Game.Screens.Ranking
|
||||
if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
|
||||
return [];
|
||||
|
||||
var requestTaskSource = new TaskCompletionSource<APIScoresCollection>();
|
||||
|
||||
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
|
||||
getScoreRequest.Success += requestTaskSource.SetResult;
|
||||
getScoreRequest.Failure += requestTaskSource.SetException;
|
||||
api.Queue(getScoreRequest);
|
||||
|
||||
try
|
||||
var criteria = new LeaderboardCriteria(
|
||||
Score.BeatmapInfo!,
|
||||
Score.Ruleset,
|
||||
leaderboardManager.CurrentCriteria?.Scope ?? BeatmapLeaderboardScope.Global,
|
||||
leaderboardManager.CurrentCriteria?.ExactMods
|
||||
);
|
||||
var requestTaskSource = new TaskCompletionSource<LeaderboardScores>();
|
||||
globalScores.BindValueChanged(_ =>
|
||||
{
|
||||
var scores = await requestTaskSource.Task.ConfigureAwait(false);
|
||||
var toDisplay = new List<ScoreInfo>();
|
||||
if (globalScores.Value != null && leaderboardManager.CurrentCriteria?.Equals(criteria) == true)
|
||||
requestTaskSource.TrySetResult(globalScores.Value);
|
||||
});
|
||||
leaderboardManager.FetchWithCriteria(criteria, forceRefresh: true);
|
||||
|
||||
for (int i = 0; i < scores.Scores.Count; ++i)
|
||||
{
|
||||
var score = scores.Scores[i];
|
||||
int position = i + 1;
|
||||
var result = await requestTaskSource.Task.ConfigureAwait(false);
|
||||
|
||||
if (score.MatchesOnlineID(Score))
|
||||
{
|
||||
// we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect,
|
||||
// so we have to fish out the actual drawable panel and set the position to it directly.
|
||||
var panel = ScorePanelList.GetPanelForScore(Score);
|
||||
Score.Position = panel.ScorePosition.Value = position;
|
||||
}
|
||||
else
|
||||
{
|
||||
var converted = score.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo);
|
||||
converted.Position = position;
|
||||
toDisplay.Add(converted);
|
||||
}
|
||||
}
|
||||
|
||||
return toDisplay.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
if (result.FailState != null)
|
||||
{
|
||||
Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {ex}");
|
||||
Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {result.FailState}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
var clonedScores = result.AllScores.Select(s => s.DeepClone()).ToArray();
|
||||
|
||||
getScoreRequest?.Cancel();
|
||||
List<ScoreInfo> sortedScores = [];
|
||||
|
||||
foreach (var clonedScore in clonedScores)
|
||||
{
|
||||
// ensure that we do not double up on the score being presented here.
|
||||
// additionally, ensure that the reference that ends up in `sortedScores` is the `Score` reference specifically.
|
||||
// this simplifies handling later.
|
||||
if (clonedScore.Equals(Score) || clonedScore.MatchesOnlineID(Score))
|
||||
{
|
||||
Score.Position = clonedScore.Position;
|
||||
sortedScores.Add(Score);
|
||||
}
|
||||
else
|
||||
sortedScores.Add(clonedScore);
|
||||
}
|
||||
|
||||
// if we haven't encountered a match for the presented score, we still need to attach it.
|
||||
// note that the above block ensuring that the `Score` reference makes it in here makes this valid to write in this way.
|
||||
if (!sortedScores.Contains(Score))
|
||||
sortedScores.Add(Score);
|
||||
|
||||
sortedScores = sortedScores.OrderByTotalScore().ToList();
|
||||
|
||||
int delta = 0;
|
||||
bool isPartialLeaderboard = leaderboardManager.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && result.TopScores.Count >= 50;
|
||||
|
||||
for (int i = 0; i < sortedScores.Count; i++)
|
||||
{
|
||||
var sortedScore = sortedScores[i];
|
||||
|
||||
// see `SoloGameplayLeaderboardProvider.sort()` for another place that does the same thing with slight deviations
|
||||
// if this code is changed, that code should probably be changed as well
|
||||
|
||||
if (!isPartialLeaderboard)
|
||||
sortedScore.Position = i + 1;
|
||||
else
|
||||
{
|
||||
if (ReferenceEquals(sortedScore, Score) && sortedScore.Position == null)
|
||||
{
|
||||
int? previousScorePosition = i > 0 ? sortedScores[i - 1].Position : 0;
|
||||
int? nextScorePosition = i < result.TopScores.Count - 1 ? sortedScores[i + 1].Position : null;
|
||||
|
||||
if (previousScorePosition != null && nextScorePosition != null && previousScorePosition + 1 == nextScorePosition)
|
||||
{
|
||||
sortedScore.Position = previousScorePosition + 1;
|
||||
delta += 1;
|
||||
}
|
||||
else
|
||||
sortedScore.Position = null;
|
||||
}
|
||||
else
|
||||
sortedScore.Position += delta;
|
||||
}
|
||||
}
|
||||
|
||||
// there's a non-zero chance that the `Score.Position` was mutated above,
|
||||
// but that is not actually coupled to `ScorePosition` of the relevant score panel in any way,
|
||||
// so ensure that the drawable panel also receives the updated position.
|
||||
// note that this is valid to do precisely because we ensured `Score` was in `sortedScores` earlier.
|
||||
ScorePanelList.GetPanelForScore(Score).ScorePosition.Value = Score.Position;
|
||||
|
||||
sortedScores.Remove(Score);
|
||||
return sortedScores.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
// For now, we forcefully refresh to keep things simple.
|
||||
// In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios
|
||||
// (like returning from gameplay after setting a new score, returning to song select after main menu).
|
||||
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null), forceRefresh: true);
|
||||
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.Where(m => m.UserPlayable).ToArray() : null), forceRefresh: true);
|
||||
|
||||
if (!initialFetchComplete)
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
/// An optional value to guarantee stable ordering.
|
||||
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
|
||||
/// </summary>
|
||||
public Bindable<long> DisplayOrder { get; } = new BindableLong();
|
||||
public long TotalScoreTiebreaker { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A custom function which handles converting a score to a display score using a provided <see cref="ScoringMode"/>.
|
||||
@@ -68,6 +68,25 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
/// </summary>
|
||||
public Colour4? TeamColour { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The initial position of the score on the leaderboard.
|
||||
/// Mostly used for cases like the local user's best score on the global leaderboard (which will not be contiguous with the other scores).
|
||||
/// </summary>
|
||||
public int? InitialPosition { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The displayed rank of the score on the leaderboard.
|
||||
/// </summary>
|
||||
public Bindable<int?> Position { get; } = new Bindable<int?>();
|
||||
|
||||
/// <summary>
|
||||
/// The index of the score on the leaderboard.
|
||||
/// This differs from <see cref="Position"/> in that it is required (must always be known)
|
||||
/// and that it doesn't represent the score's position on global leaderboards.
|
||||
/// It's a property completely local to and relative to all scores provided by the managing <see cref="IGameplayLeaderboardProvider"/>.
|
||||
/// </summary>
|
||||
public Bindable<long> DisplayOrder { get; } = new BindableLong();
|
||||
|
||||
public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked)
|
||||
{
|
||||
User = user;
|
||||
@@ -95,8 +114,9 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
TotalScore.Value = scoreInfo.TotalScore;
|
||||
Accuracy.Value = scoreInfo.Accuracy;
|
||||
Combo.Value = scoreInfo.Combo;
|
||||
DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
|
||||
TotalScoreTiebreaker = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
|
||||
GetDisplayScore = scoreInfo.GetDisplayScore;
|
||||
InitialPosition = scoreInfo.Position;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
|
||||
@@ -14,14 +14,5 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
/// List of all scores to display on the leaderboard.
|
||||
/// </summary>
|
||||
public IBindableList<GameplayLeaderboardScore> Scores { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores),
|
||||
/// or is a full leaderboard (contains all scores that there will ever be).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this is <see langword="true"/> and a tracked score is last on the leaderboard, it will show an "unknown" score position.
|
||||
/// </remarks>
|
||||
bool IsPartial { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -55,6 +56,8 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private readonly Cached sorting = new Cached();
|
||||
|
||||
public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users)
|
||||
{
|
||||
this.users = users;
|
||||
@@ -101,6 +104,8 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
HasQuit = { BindTarget = trackedUser.UserQuit },
|
||||
TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null,
|
||||
};
|
||||
leaderboardScore.TotalScore.BindValueChanged(_ => sorting.Invalidate());
|
||||
leaderboardScore.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
scores.Add(leaderboardScore);
|
||||
}
|
||||
});
|
||||
@@ -124,6 +129,8 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
// new players are not supported.
|
||||
playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
|
||||
playingUserIds.BindCollectionChanged(playingUsersChanged);
|
||||
|
||||
Scheduler.AddDelayed(sort, 1000, true);
|
||||
}
|
||||
|
||||
private void playingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
@@ -174,6 +181,26 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
}
|
||||
}
|
||||
|
||||
private void sort()
|
||||
{
|
||||
if (sorting.IsValid)
|
||||
return;
|
||||
|
||||
var orderedByScore = scores
|
||||
.OrderByDescending(i => i.TotalScore.Value)
|
||||
.ThenBy(i => i.TotalScoreTiebreaker)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < orderedByScore.Count; i++)
|
||||
{
|
||||
var score = orderedByScore[i];
|
||||
score.DisplayOrder.Value = i;
|
||||
score.Position.Value = i + 1;
|
||||
}
|
||||
|
||||
sorting.Validate();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Scoring;
|
||||
@@ -12,8 +14,6 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
public partial class SoloGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider
|
||||
{
|
||||
public bool IsPartial { get; private set; }
|
||||
|
||||
public IBindableList<GameplayLeaderboardScore> Scores => scores;
|
||||
private readonly BindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
|
||||
|
||||
@@ -23,13 +23,16 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
[Resolved]
|
||||
private GameplayState? gameplayState { get; set; }
|
||||
|
||||
private readonly Cached sorting = new Cached();
|
||||
private bool isPartial;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
var globalScores = leaderboardManager?.Scores.Value;
|
||||
|
||||
IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
|
||||
isPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
|
||||
|
||||
if (globalScores != null)
|
||||
{
|
||||
@@ -39,12 +42,73 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
if (gameplayState != null)
|
||||
{
|
||||
scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true)
|
||||
var localScore = new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true)
|
||||
{
|
||||
// Local score should always show lower than any existing scores in cases of ties.
|
||||
DisplayOrder = { Value = long.MaxValue }
|
||||
});
|
||||
TotalScoreTiebreaker = long.MaxValue
|
||||
};
|
||||
localScore.TotalScore.BindValueChanged(_ => sorting.Invalidate());
|
||||
scores.Add(localScore);
|
||||
}
|
||||
|
||||
Scheduler.AddDelayed(sort, 1000, true);
|
||||
}
|
||||
|
||||
private void sort()
|
||||
{
|
||||
if (sorting.IsValid)
|
||||
return;
|
||||
|
||||
var orderedByScore = scores
|
||||
.OrderByDescending(i => i.TotalScore.Value)
|
||||
.ThenBy(i => i.TotalScoreTiebreaker)
|
||||
.ToList();
|
||||
|
||||
int delta = 0;
|
||||
|
||||
for (int i = 0; i < orderedByScore.Count; i++)
|
||||
{
|
||||
var score = orderedByScore[i];
|
||||
|
||||
// see `SoloResultsScreen.FetchScores()` for another place that does the same thing with slight deviations
|
||||
// if this code is changed, that code should probably be changed as well
|
||||
|
||||
score.DisplayOrder.Value = i + 1;
|
||||
|
||||
// if we know we have all scores there can ever be, we can do the simple and obvious thing.
|
||||
if (!isPartial)
|
||||
score.Position.Value = i + 1;
|
||||
else
|
||||
{
|
||||
// we have a partial leaderboard, with potential gaps.
|
||||
// we have initial score positions which were valid at the point of starting play.
|
||||
// the assumption here is that non-tracked scores here cannot move around, only tracked ones can.
|
||||
if (score.Tracked)
|
||||
{
|
||||
int? previousScorePosition = i > 0 ? orderedByScore[i - 1].InitialPosition : 0;
|
||||
int? nextScorePosition = i < orderedByScore.Count - 1 ? orderedByScore[i + 1].InitialPosition : null;
|
||||
|
||||
// if the tracked score is perfectly between two scores which have known neighbouring initial positions,
|
||||
// we can assign it the position of the previous score plus one...
|
||||
if (previousScorePosition != null && nextScorePosition != null && previousScorePosition + 1 == nextScorePosition)
|
||||
{
|
||||
score.Position.Value = previousScorePosition + 1;
|
||||
// but we also need to ensure all subsequent scores get shifted down one position, too.
|
||||
delta++;
|
||||
}
|
||||
// conversely, if the tracked score is not between neighbouring two scores and the leaderboard is partial,
|
||||
// we can't really assign a valid position at all. it could be any number between the two neighbours.
|
||||
else
|
||||
score.Position.Value = null;
|
||||
}
|
||||
// for non-tracked scores, we just need to apply any delta that might have come from the tracked scores
|
||||
// which might have been encountered and assigned a position earlier.
|
||||
else
|
||||
score.Position.Value = score.InitialPosition + delta;
|
||||
}
|
||||
}
|
||||
|
||||
sorting.Validate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
/// <summary>
|
||||
/// The left portion of the song select screen which houses the metadata or leaderboards wedge, along with controls
|
||||
/// to switch between them and adjust specifics.
|
||||
/// </summary>
|
||||
public partial class BeatmapDetailsArea : VisibilityContainer
|
||||
{
|
||||
private Header header = null!;
|
||||
private Container contentContainer = null!;
|
||||
|
||||
public BeatmapDetailsArea()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const float header_height = 35f;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new ShearAligningWrapper(header = new Header
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = header_height,
|
||||
}),
|
||||
new ShearAligningWrapper(contentContainer = new Container
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Padding = new MarginPadding { Top = header_height },
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
})
|
||||
{
|
||||
Depth = 1f,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
header.Type.BindValueChanged(_ => updateDisplay(), true);
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.MoveToX(-150, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
private Drawable? currentContent;
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (currentContent != null)
|
||||
{
|
||||
currentContent.Hide();
|
||||
currentContent.Expire();
|
||||
}
|
||||
|
||||
switch (header.Type.Value)
|
||||
{
|
||||
default:
|
||||
case Header.Selection.Details:
|
||||
currentContent = new BeatmapMetadataWedge();
|
||||
break;
|
||||
|
||||
case Header.Selection.Ranking:
|
||||
currentContent = new BeatmapLeaderboardWedge
|
||||
{
|
||||
Scope = { BindTarget = header.Scope },
|
||||
FilterBySelectedMods = { BindTarget = header.FilterBySelectedMods },
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
contentContainer.Add(currentContent);
|
||||
currentContent.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapDetailsArea
|
||||
{
|
||||
public partial class Header : CompositeDrawable
|
||||
{
|
||||
private WedgeSelector<Selection> tabControl = null!;
|
||||
private FillFlowContainer leaderboardControls = null!;
|
||||
|
||||
private ShearedDropdown<BeatmapLeaderboardScope> scopeDropdown = null!;
|
||||
private ShearedToggleButton selectedModsToggle = null!;
|
||||
|
||||
public IBindable<Selection> Type => tabControl.Current;
|
||||
|
||||
public IBindable<BeatmapLeaderboardScope> Scope => scopeDropdown.Current;
|
||||
|
||||
public IBindable<bool> FilterBySelectedMods => selectedModsToggle.Active;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 20f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
tabControl = new WedgeSelector<Selection>(20f)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 200,
|
||||
Height = 22,
|
||||
Margin = new MarginPadding { Top = 2f },
|
||||
},
|
||||
leaderboardControls = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(5f, 0f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = new Vector2(128f, 30f),
|
||||
Child = selectedModsToggle = new ShearedToggleButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = @"Selected Mods",
|
||||
Height = 30,
|
||||
},
|
||||
},
|
||||
// new Container
|
||||
// {
|
||||
// Anchor = Anchor.CentreRight,
|
||||
// Origin = Anchor.CentreRight,
|
||||
// Size = new Vector2(150f, 33f),
|
||||
// Child = new ShearedDropdown<RankingsSort>(@"Sort")
|
||||
// {
|
||||
// Width = 150f,
|
||||
// Items = Enum.GetValues<RankingsSort>(),
|
||||
// },
|
||||
// },
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = new Vector2(160f, 32f),
|
||||
Child = scopeDropdown = new ScopeDropdown
|
||||
{
|
||||
Width = 160f,
|
||||
Current = { Value = BeatmapLeaderboardScope.Global },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
tabControl.Current.BindValueChanged(v =>
|
||||
{
|
||||
leaderboardControls.FadeTo(v.NewValue == Selection.Ranking ? 1 : 0, 300, Easing.OutQuint);
|
||||
}, true);
|
||||
}
|
||||
|
||||
public enum Selection
|
||||
{
|
||||
Details,
|
||||
Ranking,
|
||||
}
|
||||
|
||||
// public enum RankingsSort
|
||||
// {
|
||||
// Score,
|
||||
// Accuracy,
|
||||
// Combo,
|
||||
// Misses,
|
||||
// Date,
|
||||
// }
|
||||
|
||||
private partial class ScopeDropdown : ShearedDropdown<BeatmapLeaderboardScope>
|
||||
{
|
||||
public ScopeDropdown()
|
||||
: base("Scope")
|
||||
{
|
||||
Items = Enum.GetValues<BeatmapLeaderboardScope>();
|
||||
}
|
||||
|
||||
protected override LocalisableString GenerateItemText(BeatmapLeaderboardScope item) => item.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapDetailsArea
|
||||
{
|
||||
public partial class WedgeSelector<T> : TabControl<T>
|
||||
where T : struct, Enum
|
||||
{
|
||||
private Circle strip = null!;
|
||||
|
||||
protected override Dropdown<T>? CreateDropdown() => null;
|
||||
|
||||
protected override TabItem<T> CreateTabItem(T value) => new TabItem(value);
|
||||
|
||||
protected new TabItem SelectedTab => (TabItem)base.SelectedTab;
|
||||
|
||||
public WedgeSelector(float spacing)
|
||||
{
|
||||
TabContainer.Spacing = new Vector2(spacing, 0f);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
AddInternal(strip = new Circle
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Height = 2,
|
||||
Colour = colourProvider.Highlight1,
|
||||
});
|
||||
|
||||
foreach (var type in Enum.GetValues<T>())
|
||||
AddItem(type);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(_ => updateDisplay());
|
||||
|
||||
ScheduleAfterChildren(() =>
|
||||
{
|
||||
updateDisplay();
|
||||
FinishTransforms(true);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
strip.MoveToX(SelectedTab.Text.ToSpaceOfOtherDrawable(Vector2.Zero, this).X, 300, Easing.OutQuint);
|
||||
strip.ResizeWidthTo(SelectedTab.Text.Width, 0, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected partial class TabItem : TabItem<T>
|
||||
{
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public readonly OsuSpriteText Text;
|
||||
|
||||
public TabItem(T value)
|
||||
: base(value)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
Text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = value.ToString(),
|
||||
Font = OsuFont.Style.Body,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
protected override void OnActivated() => updateDisplay();
|
||||
|
||||
protected override void OnDeactivated() => updateDisplay();
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateDisplay();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e) => updateDisplay();
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (Active.Value || IsHovered)
|
||||
Text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint);
|
||||
else
|
||||
Text.FadeColour(colourProvider.Content2, 300, Easing.OutQuint);
|
||||
|
||||
Text.Font = Text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,371 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapLeaderboardScore
|
||||
{
|
||||
public partial class LeaderboardScoreTooltip : VisibilityContainer, ITooltip<ScoreInfo>
|
||||
{
|
||||
private const float spacing = 20f;
|
||||
|
||||
private DateAndStatisticsPanel dateAndStatistics = null!;
|
||||
private ModsPanel modsPanel = null!;
|
||||
private TotalScoreRankPanel totalScoreRankPanel = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider;
|
||||
|
||||
public LeaderboardScoreTooltip(OverlayColourProvider colourProvider)
|
||||
{
|
||||
this.colourProvider = colourProvider;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Width = 170;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChild = new ReverseChildIDFillFlowContainer<Drawable>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, -spacing),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
dateAndStatistics = new DateAndStatisticsPanel(),
|
||||
modsPanel = new ModsPanel(),
|
||||
totalScoreRankPanel = new TotalScoreRankPanel(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private ScoreInfo? lastContent;
|
||||
|
||||
public void SetContent(ScoreInfo content)
|
||||
{
|
||||
if (lastContent != null && lastContent.Equals(content))
|
||||
return;
|
||||
|
||||
dateAndStatistics.Score = content;
|
||||
modsPanel.Score = content;
|
||||
totalScoreRankPanel.Score = content;
|
||||
lastContent = content;
|
||||
}
|
||||
|
||||
protected override void PopIn() => this.FadeIn(300, Easing.OutQuint);
|
||||
protected override void PopOut() => this.FadeOut(300, Easing.OutQuint);
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
||||
private partial class DateAndStatisticsPanel : CompositeDrawable
|
||||
{
|
||||
private OsuSpriteText absoluteDate = null!;
|
||||
private DrawableDate relativeDate = null!;
|
||||
private FillFlowContainer statistics = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public ScoreInfo Score
|
||||
{
|
||||
set
|
||||
{
|
||||
absoluteDate.Text = value.Date.ToLocalisableString(@"dd MMMM yyyy h:mm tt");
|
||||
relativeDate.Date = value.Date;
|
||||
|
||||
var judgementsStatistics = value.GetStatisticsForDisplay().Select(s =>
|
||||
new StatisticRow(s.DisplayName.ToUpper(), colours.ForHitResult(s.Result), s.Count.ToLocalisableString("N0")));
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in value.Mods)
|
||||
multiplier *= mod.ScoreMultiplier;
|
||||
|
||||
var generalStatistics = new[]
|
||||
{
|
||||
new StatisticRow("Score Multiplier", colourProvider.Content2, ModUtils.FormatScoreMultiplier(multiplier)),
|
||||
new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, colourProvider.Content2, value.MaxCombo.ToLocalisableString(@"0\x")),
|
||||
new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, colourProvider.Content2, value.Accuracy.FormatAccuracy()),
|
||||
};
|
||||
|
||||
if (value.PP != null)
|
||||
{
|
||||
generalStatistics = new[]
|
||||
{
|
||||
new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), colourProvider.Content2, value.PP.ToLocalisableString("N0"))
|
||||
}.Concat(generalStatistics).ToArray();
|
||||
}
|
||||
|
||||
statistics.ChildrenEnumerable = judgementsStatistics
|
||||
.Append(Empty().With(d => d.Height = 20))
|
||||
.Concat(generalStatistics);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 4f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background4,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Margin = new MarginPadding { Top = 8f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
absoluteDate = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
relativeDate = new DrawableDate(default, OsuFont.Style.Caption1.Size)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Colour = colourProvider.Content2,
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
CornerRadius = corner_radius,
|
||||
Masking = true,
|
||||
Margin = new MarginPadding { Top = 4f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background3,
|
||||
},
|
||||
statistics = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Padding = new MarginPadding(8f),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class StatisticRow : CompositeDrawable
|
||||
{
|
||||
public StatisticRow(LocalisableString label, Color4 labelColour, LocalisableString value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = label,
|
||||
Colour = labelColour,
|
||||
Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = value,
|
||||
Colour = Color4.White,
|
||||
Font = OsuFont.Style.Caption2,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ModsPanel : CompositeDrawable
|
||||
{
|
||||
private FillFlowContainer modsFlow = null!;
|
||||
|
||||
public ScoreInfo Score
|
||||
{
|
||||
set
|
||||
{
|
||||
var mods = value.Mods;
|
||||
|
||||
if (!mods.Any())
|
||||
Hide();
|
||||
else
|
||||
{
|
||||
Show();
|
||||
|
||||
modsFlow.ChildrenEnumerable = mods.AsOrdered().Select(m => new ModIcon(m)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.3f),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 4f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background4,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Transparent,
|
||||
},
|
||||
modsFlow = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Bottom = 6f, Top = 6f + spacing },
|
||||
Padding = new MarginPadding { Horizontal = 16f },
|
||||
Spacing = new Vector2(2f, -4f),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public partial class TotalScoreRankPanel : CompositeDrawable
|
||||
{
|
||||
private Box rankBackground = null!;
|
||||
private Container<DrawableRank> rankContainer = null!;
|
||||
private OsuSpriteText totalScore = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
public ScoreInfo Score
|
||||
{
|
||||
set
|
||||
{
|
||||
rankBackground.Colour = ColourInfo.GradientVertical(
|
||||
OsuColour.ForRank(value.Rank).Opacity(0f),
|
||||
OsuColour.ForRank(value.Rank).Opacity(0.5f));
|
||||
rankContainer.Child = new DrawableRank(value.Rank);
|
||||
totalScore.Current = scoreManager.GetBindableTotalScoreString(value);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 4f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#353535"),
|
||||
},
|
||||
rankBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
rankContainer = new Container<DrawableRank>
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Size = new Vector2(25f, 14f),
|
||||
Margin = new MarginPadding { Bottom = 5f },
|
||||
},
|
||||
totalScore = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Margin = new MarginPadding { Bottom = 25f, Top = 10f + spacing },
|
||||
Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true),
|
||||
Spacing = new Vector2(-1.5f),
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Online.Placeholders;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapLeaderboardWedge : VisibilityContainer
|
||||
{
|
||||
public IBindable<BeatmapLeaderboardScope> Scope { get; } = new Bindable<BeatmapLeaderboardScope>();
|
||||
|
||||
public IBindable<bool> FilterBySelectedMods { get; } = new BindableBool();
|
||||
|
||||
[Resolved]
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
private Container<Placeholder> placeholderContainer = null!;
|
||||
private Placeholder? placeholder;
|
||||
|
||||
private Container scoresContainer = null!;
|
||||
|
||||
private OsuScrollContainer scoresScroll = null!;
|
||||
private Container personalBestDisplay = null!;
|
||||
|
||||
private Container<BeatmapLeaderboardScore> personalBestScoreContainer = null!;
|
||||
private LoadingLayer loading = null!;
|
||||
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
private readonly IBindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
|
||||
|
||||
private const float personal_best_height = 80;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
scoresScroll = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Child = scoresContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = 5,
|
||||
// Left padding offsets the shear to create a visually appealing list display.
|
||||
Left = 80f,
|
||||
// Bottom padding ensures the last entry's full width is displayed
|
||||
// (ie it is fully on screen after shear is considered).
|
||||
Bottom = BeatmapLeaderboardScore.HEIGHT * 3
|
||||
},
|
||||
},
|
||||
},
|
||||
personalBestDisplay = new Container
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = personal_best_height,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Margin = new MarginPadding { Left = -40f },
|
||||
CornerRadius = 10f,
|
||||
Masking = true,
|
||||
// push the personal best 1px down to hide masking issues
|
||||
Y = 1f,
|
||||
X = -100f,
|
||||
Alpha = 0f,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Padding = new MarginPadding { Top = 5f, Bottom = 5f, Left = 70f, Right = 10f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Text = "Personal Best",
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
personalBestScoreContainer = new Container<BeatmapLeaderboardScore>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Top = 20f },
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
placeholderContainer = new Container<Placeholder>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
loading = new LoadingLayer(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Scope.BindValueChanged(_ => refetchScores());
|
||||
FilterBySelectedMods.BindValueChanged(_ => refetchScores());
|
||||
beatmap.BindValueChanged(_ => refetchScores());
|
||||
ruleset.BindValueChanged(_ => refetchScores());
|
||||
mods.BindValueChanged(_ => refetchScoresFromMods());
|
||||
|
||||
refetchScores();
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void refetchScoresFromMods()
|
||||
{
|
||||
if (FilterBySelectedMods.Value)
|
||||
refetchScores();
|
||||
}
|
||||
|
||||
private bool initialFetchComplete;
|
||||
|
||||
private void refetchScores()
|
||||
{
|
||||
SetScores(Array.Empty<ScoreInfo>(), null);
|
||||
|
||||
if (beatmap.IsDefault)
|
||||
{
|
||||
SetState(LeaderboardState.NoneSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
SetState(LeaderboardState.Retrieving);
|
||||
|
||||
var fetchBeatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
|
||||
|
||||
// For now, we forcefully refresh to keep things simple.
|
||||
// In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios
|
||||
// (like returning from gameplay after setting a new score, returning to song select after main menu).
|
||||
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null), forceRefresh: true);
|
||||
|
||||
if (!initialFetchComplete)
|
||||
{
|
||||
// only bind this after the first fetch to avoid reading stale scores.
|
||||
fetchedScores.BindTo(leaderboardManager.Scores);
|
||||
fetchedScores.BindValueChanged(_ => updateScores(), true);
|
||||
initialFetchComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateScores()
|
||||
{
|
||||
var scores = fetchedScores.Value;
|
||||
|
||||
if (scores == null) return;
|
||||
|
||||
if (scores.FailState != null)
|
||||
SetState((LeaderboardState)scores.FailState);
|
||||
else
|
||||
SetScores(scores.TopScores, scores.UserScore);
|
||||
}
|
||||
|
||||
protected void SetScores(IEnumerable<ScoreInfo> scores, ScoreInfo? userScore)
|
||||
{
|
||||
cancellationTokenSource?.Cancel();
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
clearScores();
|
||||
SetState(LeaderboardState.Success);
|
||||
|
||||
if (!scores.Any())
|
||||
{
|
||||
SetState(LeaderboardState.NoScores);
|
||||
return;
|
||||
}
|
||||
|
||||
LoadComponentsAsync(scores.Select((s, i) => new BeatmapLeaderboardScore(s)
|
||||
{
|
||||
Rank = i + 1,
|
||||
IsPersonalBest = s.OnlineID == userScore?.OnlineID,
|
||||
SelectedMods = { BindTarget = mods },
|
||||
}), loadedScores =>
|
||||
{
|
||||
int delay = 200;
|
||||
int i = 0;
|
||||
|
||||
foreach (var d in loadedScores)
|
||||
{
|
||||
d.Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * i;
|
||||
|
||||
// This is a bit of a weird one. We're already in a sheared state and don't want top-level
|
||||
// shear applied, but still need the `BeatmapLeadeboardScore` to be in "sheared" mode (see ctor).
|
||||
d.Shear = Vector2.Zero;
|
||||
|
||||
scoresContainer.Add(d);
|
||||
|
||||
d.FadeOut()
|
||||
.MoveToX(-20f)
|
||||
.Delay(delay)
|
||||
.FadeIn(300, Easing.OutQuint)
|
||||
.MoveToX(0f, 300, Easing.OutQuint);
|
||||
|
||||
delay += 30;
|
||||
i++;
|
||||
}
|
||||
}, cancellation: cancellationTokenSource.Token);
|
||||
|
||||
if (userScore != null)
|
||||
{
|
||||
personalBestDisplay.MoveToX(0, 600, Easing.OutQuint);
|
||||
personalBestDisplay.FadeIn(600, Easing.OutQuint);
|
||||
personalBestScoreContainer.Child = new BeatmapLeaderboardScore(userScore)
|
||||
{
|
||||
IsPersonalBest = true,
|
||||
Rank = userScore.Position,
|
||||
SelectedMods = { BindTarget = mods },
|
||||
};
|
||||
|
||||
scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding { Bottom = personal_best_height }, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearScores()
|
||||
{
|
||||
float delay = 0;
|
||||
|
||||
foreach (var d in scoresContainer)
|
||||
{
|
||||
// Avoid applying animations a second time to drawables which are already fading out.
|
||||
if (d.LifetimeEnd != double.MaxValue)
|
||||
continue;
|
||||
|
||||
d.Delay(delay)
|
||||
.MoveToX(-10f, 120, Easing.Out)
|
||||
.FadeOut(120, Easing.Out)
|
||||
.Expire();
|
||||
|
||||
delay += 20;
|
||||
}
|
||||
|
||||
personalBestDisplay.MoveToX(-100, 300, Easing.OutQuint);
|
||||
personalBestDisplay.FadeOut(300, Easing.OutQuint);
|
||||
scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding(), 300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private LeaderboardState displayedState;
|
||||
|
||||
protected void SetState(LeaderboardState state)
|
||||
{
|
||||
if (state == displayedState)
|
||||
return;
|
||||
|
||||
if (state == LeaderboardState.Retrieving)
|
||||
loading.Show();
|
||||
else
|
||||
loading.Hide();
|
||||
|
||||
displayedState = state;
|
||||
|
||||
placeholder?.FadeOut(150, Easing.OutQuint).Expire();
|
||||
placeholder = getPlaceholderFor(state);
|
||||
|
||||
if (placeholder == null)
|
||||
return;
|
||||
|
||||
clearScores();
|
||||
|
||||
placeholderContainer.Child = placeholder;
|
||||
|
||||
placeholder.ScaleTo(0.8f).Then().ScaleTo(1, 900, Easing.OutQuint);
|
||||
placeholder.FadeInFromZero(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private Placeholder? getPlaceholderFor(LeaderboardState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case LeaderboardState.NetworkFailure:
|
||||
return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync)
|
||||
{
|
||||
Action = refetchScores
|
||||
};
|
||||
|
||||
case LeaderboardState.NoneSelected:
|
||||
return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap);
|
||||
|
||||
case LeaderboardState.RulesetUnavailable:
|
||||
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset);
|
||||
|
||||
case LeaderboardState.BeatmapUnavailable:
|
||||
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap);
|
||||
|
||||
case LeaderboardState.NoScores:
|
||||
return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet);
|
||||
|
||||
case LeaderboardState.NotLoggedIn:
|
||||
return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards);
|
||||
|
||||
case LeaderboardState.NotSupporter:
|
||||
return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard);
|
||||
|
||||
case LeaderboardState.NoTeam:
|
||||
return new MessagePlaceholder(LeaderboardStrings.NoTeam);
|
||||
|
||||
case LeaderboardState.Retrieving:
|
||||
return null;
|
||||
|
||||
case LeaderboardState.Success:
|
||||
return null;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -23,7 +24,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
private MetadataDisplay source = null!;
|
||||
private MetadataDisplay genre = null!;
|
||||
private MetadataDisplay language = null!;
|
||||
private MetadataDisplay tag = null!;
|
||||
private MetadataDisplay userTags = null!;
|
||||
private MetadataDisplay mapperTags = null!;
|
||||
private MetadataDisplay submitted = null!;
|
||||
private MetadataDisplay ranked = null!;
|
||||
|
||||
@@ -35,6 +37,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
private Drawable failRetryWedge = null!;
|
||||
private FailRetryDisplay failRetryDisplay = null!;
|
||||
|
||||
public bool RatingsVisible => ratingsWedge.Alpha > 0;
|
||||
public bool FailRetryVisible => failRetryWedge.Alpha > 0;
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
[Resolved]
|
||||
@@ -92,6 +97,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 10f),
|
||||
AutoSizeDuration = (float)transition_duration / 3,
|
||||
AutoSizeEasing = Easing.OutQuint,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
@@ -148,7 +155,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
},
|
||||
},
|
||||
},
|
||||
tag = new MetadataDisplay("Tags"),
|
||||
userTags = new MetadataDisplay("User Tags")
|
||||
{
|
||||
Alpha = 0,
|
||||
},
|
||||
mapperTags = new MetadataDisplay("Mapper Tags"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -250,7 +261,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
// We could consider hiding individual wedges based on zero data in the future.
|
||||
// Needs some experimentation on what looks good.
|
||||
|
||||
if (State.Value == Visibility.Visible && currentOnlineBeatmapSet != null)
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var currentOnlineBeatmap = currentOnlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID);
|
||||
|
||||
if (State.Value == Visibility.Visible && currentOnlineBeatmap != null)
|
||||
{
|
||||
ratingsWedge.FadeIn(transition_duration, Easing.OutQuint)
|
||||
.MoveToX(0, transition_duration, Easing.OutQuint);
|
||||
@@ -282,7 +296,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
else
|
||||
source.Data = ("-", null);
|
||||
|
||||
tag.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t));
|
||||
mapperTags.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t));
|
||||
submitted.Date = beatmapSetInfo.DateSubmitted;
|
||||
ranked.Date = beatmapSetInfo.DateRanked;
|
||||
|
||||
@@ -351,7 +365,34 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
}
|
||||
|
||||
updateUserTags();
|
||||
updateSubWedgeVisibility();
|
||||
}
|
||||
|
||||
private void updateUserTags()
|
||||
{
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var onlineBeatmapSet = currentOnlineBeatmapSet;
|
||||
var onlineBeatmap = onlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID);
|
||||
|
||||
if (onlineBeatmap?.TopTags == null || onlineBeatmap.TopTags.Length == 0 || onlineBeatmapSet?.RelatedTags == null)
|
||||
{
|
||||
userTags.FadeOut(transition_duration, Easing.OutQuint);
|
||||
return;
|
||||
}
|
||||
|
||||
var tagsById = onlineBeatmapSet.RelatedTags.ToDictionary(t => t.Id);
|
||||
string[] userTagsArray = onlineBeatmap.TopTags
|
||||
.Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId)))
|
||||
.Where(t => t.relatedTag != null)
|
||||
// see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria
|
||||
.OrderByDescending(t => t.topTag.VoteCount)
|
||||
.ThenBy(t => t.relatedTag!.Name)
|
||||
.Select(t => t.relatedTag!.Name)
|
||||
.ToArray();
|
||||
|
||||
userTags.FadeIn(transition_duration, Easing.OutQuint);
|
||||
userTags.Tags = (userTagsArray, t => songSelect?.Search(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Framework.Layout;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
@@ -163,7 +164,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
Text = "...",
|
||||
Colour = colourProvider.Background4,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
|
||||
}
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.Button),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
@@ -33,7 +35,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private const float corner_radius = 10;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
private IBindable<WorkingBeatmap> working { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
@@ -84,7 +86,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Shear = OsuGame.SHEAR;
|
||||
Masking = true;
|
||||
CornerRadius = corner_radius;
|
||||
|
||||
@@ -185,7 +186,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
beatmap.BindValueChanged(_ => updateDisplay());
|
||||
working.BindValueChanged(_ => updateDisplay());
|
||||
ruleset.BindValueChanged(_ => updateDisplay());
|
||||
|
||||
mods.BindValueChanged(m =>
|
||||
@@ -225,9 +226,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
var metadata = beatmap.Value.Metadata;
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
||||
var metadata = working.Value.Metadata;
|
||||
var beatmapInfo = working.Value.BeatmapInfo;
|
||||
var beatmapSetInfo = working.Value.BeatmapSetInfo;
|
||||
|
||||
statusPill.Status = beatmapInfo.Status;
|
||||
|
||||
@@ -247,30 +248,48 @@ namespace osu.Game.Screens.SelectV2
|
||||
updateOnlineDisplay();
|
||||
}
|
||||
|
||||
private CancellationTokenSource? lengthBpmCancellationSource;
|
||||
|
||||
private void updateLengthAndBpmStatistics()
|
||||
{
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
lengthBpmCancellationSource?.Cancel();
|
||||
lengthBpmCancellationSource = new CancellationTokenSource();
|
||||
|
||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||
var token = lengthBpmCancellationSource.Token;
|
||||
|
||||
int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate);
|
||||
int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate);
|
||||
int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate);
|
||||
Task.Run(() =>
|
||||
{
|
||||
var beatmapInfo = working.Value.BeatmapInfo;
|
||||
// This can take time as it is a synchronous task.
|
||||
var beatmap = working.Value.Beatmap;
|
||||
|
||||
double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate);
|
||||
double hitLength = Math.Round(beatmapInfo.Length / rate);
|
||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||
|
||||
lengthStatistic.Text = hitLength.ToFormattedDuration();
|
||||
lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration());
|
||||
int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate);
|
||||
int bpmMin = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMinimum, rate);
|
||||
int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.GetMostCommonBeatLength(), rate);
|
||||
|
||||
bpmStatistic.Text = bpmMin == bpmMax
|
||||
? $"{bpmMin}"
|
||||
: $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})";
|
||||
double drainLength = Math.Round(beatmap.CalculateDrainLength() / rate);
|
||||
double hitLength = Math.Round(beatmapInfo.Length / rate);
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
lengthStatistic.Text = hitLength.ToFormattedDuration();
|
||||
lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration());
|
||||
|
||||
bpmStatistic.Text = bpmMin == bpmMax
|
||||
? $"{bpmMin}"
|
||||
: $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})";
|
||||
});
|
||||
}, token);
|
||||
}
|
||||
|
||||
private void refetchBeatmapSet()
|
||||
{
|
||||
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
||||
var beatmapSetInfo = working.Value.BeatmapSetInfo;
|
||||
|
||||
currentRequest?.Cancel();
|
||||
currentRequest = null;
|
||||
@@ -306,20 +325,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
else
|
||||
{
|
||||
var onlineBeatmapSet = currentOnlineBeatmapSet;
|
||||
var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.Value.BeatmapInfo.OnlineID);
|
||||
var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == working.Value.BeatmapInfo.OnlineID);
|
||||
|
||||
if (onlineBeatmap != null)
|
||||
{
|
||||
playCount.FadeIn(300, Easing.OutQuint);
|
||||
playCount.Value = new StatisticPlayCount.Data(onlineBeatmap.PlayCount, onlineBeatmap.UserPlayCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
playCount.FadeOut(300, Easing.OutQuint);
|
||||
playCount.Value = null;
|
||||
}
|
||||
|
||||
favouritesStatistic.FadeIn(300, Easing.OutQuint);
|
||||
playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1);
|
||||
favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@@ -241,8 +242,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
cancellationSource?.Cancel();
|
||||
cancellationSource = new CancellationTokenSource();
|
||||
|
||||
computeStarDifficulty(cancellationSource.Token);
|
||||
|
||||
if (beatmap.IsDefault)
|
||||
{
|
||||
ratingAndNameContainer.FadeOut(300, Easing.OutQuint);
|
||||
@@ -254,17 +253,55 @@ namespace osu.Game.Screens.SelectV2
|
||||
difficultyText.Text = beatmap.Value.BeatmapInfo.DifficultyName;
|
||||
mapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, beatmap.Value.Metadata.Author));
|
||||
mapperText.Text = beatmap.Value.Metadata.Author.Username;
|
||||
|
||||
var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value);
|
||||
|
||||
countStatisticsDisplay.Statistics = playableBeatmap.GetStatistics()
|
||||
.Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
updateStarDifficulty(cancellationSource.Token);
|
||||
updateCountStatistics(cancellationSource.Token);
|
||||
updateDifficultyStatistics();
|
||||
}
|
||||
|
||||
private void updateStarDifficulty(CancellationToken cancellationToken)
|
||||
{
|
||||
difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
starRatingDisplay.Current.Value = task.GetResultSafely() ?? default;
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private void updateCountStatistics(CancellationToken cancellationToken)
|
||||
{
|
||||
if (beatmap.IsDefault)
|
||||
{
|
||||
countStatisticsDisplay.Statistics = Array.Empty<StatisticDifficulty.Data>();
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
// This can take time as it is a synchronous task.
|
||||
// TODO: We're calling `GetPlayableBeatmap` multiple times every map load at song select.
|
||||
var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value);
|
||||
var statistics = playableBeatmap.GetStatistics()
|
||||
.Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content))
|
||||
.ToList();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
countStatisticsDisplay.Statistics = statistics;
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private void updateDifficultyStatistics() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
if (beatmap.IsDefault)
|
||||
@@ -321,21 +358,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
};
|
||||
});
|
||||
|
||||
private void computeStarDifficulty(CancellationToken cancellationToken)
|
||||
{
|
||||
difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
starRatingDisplay.Current.Value = task.GetResultSafely() ?? default;
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class FilterControl : OverlayContainer
|
||||
{
|
||||
// taken from draw visualiser. used for carousel alignment purposes.
|
||||
public const float HEIGHT_FROM_SCREEN_TOP = 141 - corner_radius;
|
||||
|
||||
private const float corner_radius = 8;
|
||||
|
||||
private ShearedToggleButton showConvertedBeatmapsButton = null!;
|
||||
private DifficultyRangeSlider difficultyRangeSlider = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Shear = OsuGame.SHEAR;
|
||||
Margin = new MarginPadding { Top = -corner_radius, Right = -40 };
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CornerRadius = corner_radius,
|
||||
Masking = true,
|
||||
Child = new WedgeBackground
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Scale = new Vector2(-1, 1),
|
||||
}
|
||||
},
|
||||
new ReverseChildIDFillFlowContainer<Drawable>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Padding = new MarginPadding { Top = corner_radius + 5, Bottom = 2, Right = 40f, Left = 2f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Child = new SongSelectSearchTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
HoldFocus = true,
|
||||
// TODO: pending implementation
|
||||
FilterText = "12345 matches",
|
||||
},
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute), // can probably be removed?
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
difficultyRangeSlider = new DifficultyRangeSlider
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
MinRange = 0.1f,
|
||||
},
|
||||
Empty(),
|
||||
showConvertedBeatmapsButton = new ShearedToggleButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = UserInterfaceStrings.ShowConverts,
|
||||
Height = 30f,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 30,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(maxSize: 210),
|
||||
new Dimension(GridSizeMode.Absolute, 5),
|
||||
new Dimension(maxSize: 230),
|
||||
new Dimension(GridSizeMode.Absolute, 5),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new ShearedDropdown<SortMode>(SortStrings.Default)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = Enum.GetValues<SortMode>(),
|
||||
},
|
||||
Empty(),
|
||||
// todo: pending localisation
|
||||
new ShearedDropdown<GroupMode>("Group by")
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = Enum.GetValues<GroupMode>(),
|
||||
},
|
||||
Empty(),
|
||||
new CollectionDropdown
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
difficultyRangeSlider.LowerBound = config.GetBindable<double>(OsuSetting.DisplayStarsMinimum);
|
||||
difficultyRangeSlider.UpperBound = config.GetBindable<double>(OsuSetting.DisplayStarsMaximum);
|
||||
config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConvertedBeatmapsButton.Active);
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.MoveToX(150, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
private partial class SongSelectSearchTextBox : ShearedFilterTextBox
|
||||
{
|
||||
protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox();
|
||||
|
||||
private partial class InnerTextBox : InnerFilterTextBox
|
||||
{
|
||||
public override bool HandleLeftRightArrows => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Utils;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class FilterControl
|
||||
{
|
||||
public partial class DifficultyRangeSlider : ShearedRangeSlider
|
||||
{
|
||||
private Container borderContainer = null!;
|
||||
|
||||
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
private static readonly (float, Color4)[] spectrum = OsuColour.STAR_DIFFICULTY_SPECTRUM
|
||||
.Skip(1)
|
||||
.Prepend((0.0f, OsuColour.STAR_DIFFICULTY_SPECTRUM.ElementAt(1).Item2)).ToArray();
|
||||
|
||||
public DifficultyRangeSlider()
|
||||
: base("Star Rating")
|
||||
{
|
||||
NubWidth = ShearedNub.HEIGHT * 1.16f;
|
||||
TooltipSuffix = "stars";
|
||||
DefaultStringLowerBound = "0.0";
|
||||
DefaultStringUpperBound = "∞";
|
||||
DefaultTooltipUpperBound = UserInterfaceStrings.NoLimit;
|
||||
|
||||
AddLayout(drawSizeLayout);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider, OsuColour colours)
|
||||
{
|
||||
SliderContainer.AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Depth = 1,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Shear = OsuGame.SHEAR,
|
||||
CornerRadius = 5f,
|
||||
Masking = true,
|
||||
ChildrenEnumerable = spectrum.Zip(spectrum.Skip(1))
|
||||
.Select(p => new Box
|
||||
{
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = p.First.Item1 / 10f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = (p.Second.Item1 - p.First.Item1) / 10f,
|
||||
Colour = ColourInfo.GradientHorizontal(p.First.Item2, p.Second.Item2),
|
||||
}),
|
||||
},
|
||||
borderContainer = new Container
|
||||
{
|
||||
Depth = -1,
|
||||
RelativePositionAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
BorderColour = colourProvider.Highlight1,
|
||||
BorderThickness = 2,
|
||||
Masking = true,
|
||||
Shear = OsuGame.SHEAR,
|
||||
CornerRadius = 5f,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.Transparent,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
LowerBoundSlider.Current.ValueChanged += _ => updateBorderDisplay(false);
|
||||
UpperBoundSlider.Current.ValueChanged += _ => updateBorderDisplay(false);
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!drawSizeLayout.IsValid)
|
||||
{
|
||||
updateBorderDisplay(true);
|
||||
drawSizeLayout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBorderDisplay(bool instant)
|
||||
{
|
||||
float borderStart = LowerBoundSlider.NormalizedValue * LowerBoundSlider.UsableWidth / LowerBoundSlider.DrawWidth;
|
||||
float borderEnd = UpperBoundSlider.NormalizedValue * UpperBoundSlider.UsableWidth / UpperBoundSlider.DrawWidth;
|
||||
borderEnd += UpperBoundSlider.NubWidth / UpperBoundSlider.DrawWidth;
|
||||
|
||||
borderContainer.MoveToX(borderStart, instant ? 0 : 250, Easing.OutQuint);
|
||||
borderContainer.ResizeWidthTo(borderEnd - borderStart, instant ? 0 : 250, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override BoundSliderBar CreateBoundSlider(bool isUpper) => new DifficultyBoundSliderBar(this, isUpper);
|
||||
|
||||
private partial class DifficultyBoundSliderBar : BoundSliderBar
|
||||
{
|
||||
private readonly bool isUpper;
|
||||
|
||||
protected override bool FocusIndicator => false;
|
||||
|
||||
public DifficultyBoundSliderBar(ShearedRangeSlider slider, bool isUpper)
|
||||
: base(slider, isUpper)
|
||||
{
|
||||
this.isUpper = isUpper;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (isUpper)
|
||||
{
|
||||
LeftBox.Colour = OsuColour.Gray(0.4f).Opacity(0.2f);
|
||||
RightBox.Colour = OsuColour.Gray(0.05f).Opacity(0.7f);
|
||||
}
|
||||
else
|
||||
{
|
||||
LeftBox.Colour = OsuColour.Gray(0.05f).Opacity(0.7f);
|
||||
RightBox.Colour = OsuColour.Gray(0.4f).Opacity(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateDisplay(double value)
|
||||
{
|
||||
Colour4 nubColour = ColourUtils.SampleFromLinearGradient(spectrum, (float)Math.Round(value, 2, MidpointRounding.AwayFromZero));
|
||||
nubColour = nubColour.Lighten(0.4f);
|
||||
|
||||
if (value >= 8.0)
|
||||
nubColour = colours.Gray4;
|
||||
|
||||
Nub.AccentColour = nubColour;
|
||||
Nub.GlowingAccentColour = nubColour.Lighten(0.2f);
|
||||
Nub.ShadowColour = Color4.Black.Opacity(0.2f);
|
||||
NubText.Colour = OsuColour.ForegroundTextColourFor(nubColour);
|
||||
|
||||
base.UpdateDisplay(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Depth = float.MaxValue,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Shear = OsuGame.SHEAR,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
CornerRadius = Y_OFFSET,
|
||||
Size = new Vector2(BUTTON_WIDTH, bar_height),
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
@@ -115,7 +115,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
},
|
||||
new Container
|
||||
{
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
CornerRadius = Y_OFFSET,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = mod_display_portion,
|
||||
Masking = true,
|
||||
@@ -264,7 +264,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
CornerRadius = CORNER_RADIUS;
|
||||
CornerRadius = Y_OFFSET;
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
@@ -306,7 +306,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Depth = float.MaxValue;
|
||||
Origin = Anchor.BottomLeft;
|
||||
Shear = OsuGame.SHEAR;
|
||||
CornerRadius = CORNER_RADIUS;
|
||||
CornerRadius = Y_OFFSET;
|
||||
AutoSizeAxes = Axes.X;
|
||||
Height = bar_height;
|
||||
Masking = true;
|
||||
|
||||
@@ -3,14 +3,21 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
@@ -21,12 +28,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
public abstract partial class SongSelect : OsuScreen
|
||||
{
|
||||
private const float logo_scale = 0.4f;
|
||||
private const double fade_duration = 300;
|
||||
|
||||
public const float WEDGE_CONTENT_MARGIN = CORNER_RADIUS_HIDE_OFFSET + OsuGame.SCREEN_EDGE_MARGIN;
|
||||
public const float CORNER_RADIUS_HIDE_OFFSET = 20f;
|
||||
public const float ENTER_DURATION = 600;
|
||||
|
||||
private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay(OverlayColourScheme.Aquamarine)
|
||||
private readonly ModSelectOverlay modSelectOverlay = new UserModSelectOverlay(OverlayColourScheme.Aquamarine)
|
||||
{
|
||||
ShowPresets = true,
|
||||
};
|
||||
@@ -36,6 +44,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private BeatmapCarousel carousel = null!;
|
||||
|
||||
private FilterControl filterControl = null!;
|
||||
private BeatmapTitleWedge titleWedge = null!;
|
||||
private BeatmapDetailsArea detailsArea = null!;
|
||||
private FillFlowContainer wedgesContainer = null!;
|
||||
|
||||
public override bool ShowFooter => true;
|
||||
|
||||
[Resolved]
|
||||
@@ -46,33 +59,89 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
new GridContainer // used for max width implementation
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
Width = 0.5f,
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0f)),
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT },
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Empty(),
|
||||
new Container
|
||||
new GridContainer // used for max width implementation
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT },
|
||||
Child = carousel = new BeatmapCarousel
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
RequestPresentBeatmap = _ => OnStart(),
|
||||
RelativeSizeAxes = Axes.Both
|
||||
new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 850),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
wedgesContainer = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Top = -CORNER_RADIUS_HIDE_OFFSET,
|
||||
Left = -CORNER_RADIUS_HIDE_OFFSET
|
||||
},
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Direction = FillDirection.Vertical,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ShearAligningWrapper(titleWedge = new BeatmapTitleWedge()),
|
||||
new ShearAligningWrapper(detailsArea = new BeatmapDetailsArea()),
|
||||
},
|
||||
},
|
||||
Empty(),
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new CompositeDrawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = FilterControl.HEIGHT_FROM_SCREEN_TOP + 5,
|
||||
Bottom = 5,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
carousel = new BeatmapCarousel
|
||||
{
|
||||
BleedTop = FilterControl.HEIGHT_FROM_SCREEN_TOP + 5,
|
||||
BleedBottom = ScreenFooter.HEIGHT + 5,
|
||||
RequestPresentBeatmap = _ => OnStart(),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
},
|
||||
filterControl = new FilterControl
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
modSelectOverlay,
|
||||
});
|
||||
@@ -98,34 +167,44 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
|
||||
this.FadeIn();
|
||||
|
||||
titleWedge.Show();
|
||||
detailsArea.Show();
|
||||
filterControl.Show();
|
||||
|
||||
modSelectOverlay.SelectedMods.BindTo(Mods);
|
||||
|
||||
base.OnEntering(e);
|
||||
}
|
||||
|
||||
private const double fade_duration = 300;
|
||||
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnResuming(e);
|
||||
|
||||
this.FadeIn(fade_duration, Easing.OutQuint);
|
||||
|
||||
carousel.VisuallyFocusSelected = false;
|
||||
|
||||
titleWedge.Show();
|
||||
detailsArea.Show();
|
||||
filterControl.Show();
|
||||
|
||||
// required due to https://github.com/ppy/osu-framework/issues/3218
|
||||
modSelectOverlay.SelectedMods.Disabled = false;
|
||||
modSelectOverlay.SelectedMods.BindTo(Mods);
|
||||
|
||||
base.OnResuming(e);
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
this.Delay(100).FadeOut(fade_duration, Easing.OutQuint);
|
||||
this.FadeOut(fade_duration, Easing.OutQuint);
|
||||
|
||||
modSelectOverlay.SelectedMods.UnbindFrom(Mods);
|
||||
|
||||
titleWedge.Hide();
|
||||
detailsArea.Hide();
|
||||
filterControl.Hide();
|
||||
|
||||
carousel.VisuallyFocusSelected = true;
|
||||
|
||||
base.OnSuspending(e);
|
||||
@@ -134,6 +213,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
this.FadeOut(fade_duration, Easing.OutQuint);
|
||||
|
||||
titleWedge.Hide();
|
||||
detailsArea.Hide();
|
||||
filterControl.Hide();
|
||||
|
||||
return base.OnExiting(e);
|
||||
}
|
||||
|
||||
@@ -192,5 +276,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
SearchText = query,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
detailsArea.Height = wedgesContainer.DrawHeight - titleWedge.LayoutSize.Y - 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,7 @@ namespace osu.Game.Skinning
|
||||
public enum LegacyManiaSkinConfigurationLookups
|
||||
{
|
||||
ColumnWidth,
|
||||
ColumnSpacing,
|
||||
LightImage,
|
||||
LeftLineWidth,
|
||||
RightLineWidth,
|
||||
HitPosition,
|
||||
ComboPosition,
|
||||
ScorePosition,
|
||||
@@ -56,10 +53,8 @@ namespace osu.Game.Skinning
|
||||
HoldNoteTailImage,
|
||||
HoldNoteBodyImage,
|
||||
HoldNoteLightImage,
|
||||
HoldNoteLightScale,
|
||||
WidthForNoteHeightScale,
|
||||
ExplosionImage,
|
||||
ExplosionScale,
|
||||
ColumnLineColour,
|
||||
JudgementLineColour,
|
||||
ColumnBackgroundColour,
|
||||
@@ -83,6 +78,16 @@ namespace osu.Game.Skinning
|
||||
Hit0,
|
||||
KeysUnderNotes,
|
||||
NoteBodyStyle,
|
||||
LightFramePerSecond
|
||||
LightFramePerSecond,
|
||||
|
||||
// The following lookup entries are not directly tied to skin.ini settings
|
||||
// but are defined to simplify the process of determining such values.
|
||||
|
||||
LeftColumnSpacing,
|
||||
RightColumnSpacing,
|
||||
LeftLineWidth,
|
||||
RightLineWidth,
|
||||
ExplosionScale,
|
||||
HoldNoteLightScale,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,10 +148,6 @@ namespace osu.Game.Skinning
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.WidthForNoteHeightScale));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnSpacing:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value]));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.HitPosition:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.HitPosition));
|
||||
|
||||
@@ -170,17 +166,6 @@ namespace osu.Game.Skinning
|
||||
case LegacyManiaSkinConfigurationLookups.ExplosionImage:
|
||||
return SkinUtils.As<TValue>(getManiaImage(existing, "LightingN"));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ExplosionScale:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
|
||||
if (GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(1));
|
||||
|
||||
if (existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] != 0)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnLineColour:
|
||||
return SkinUtils.As<TValue>(getCustomColour(existing, "ColourColumnLine"));
|
||||
|
||||
@@ -236,17 +221,6 @@ namespace osu.Game.Skinning
|
||||
case LegacyManiaSkinConfigurationLookups.HoldNoteLightImage:
|
||||
return SkinUtils.As<TValue>(getManiaImage(existing, "LightingL"));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
|
||||
if (GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(1));
|
||||
|
||||
if (existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] != 0)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.KeyImage:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
return SkinUtils.As<TValue>(getManiaImage(existing, $"KeyImage{maniaLookup.ColumnIndex}"));
|
||||
@@ -270,14 +244,6 @@ namespace osu.Game.Skinning
|
||||
case LegacyManiaSkinConfigurationLookups.HitTargetImage:
|
||||
return SkinUtils.As<TValue>(getManiaImage(existing, "StageHint"));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.LeftLineWidth:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value]));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.RightLineWidth:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value + 1]));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.Hit0:
|
||||
case LegacyManiaSkinConfigurationLookups.Hit50:
|
||||
case LegacyManiaSkinConfigurationLookups.Hit100:
|
||||
@@ -291,6 +257,50 @@ namespace osu.Game.Skinning
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.LightFramePerSecond:
|
||||
return SkinUtils.As<TValue>(new Bindable<int>(existing.LightFramePerSecond));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.LeftColumnSpacing:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
if (maniaLookup.ColumnIndex == 0)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>());
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value - 1] / 2));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.RightColumnSpacing:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
if (maniaLookup.ColumnIndex == existing.ColumnSpacing.Length)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>());
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value] / 2));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.LeftLineWidth:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value]));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.RightLineWidth:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value + 1]));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ExplosionScale:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
|
||||
if (GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(1));
|
||||
|
||||
if (existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] != 0)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale:
|
||||
Debug.Assert(maniaLookup.ColumnIndex != null);
|
||||
|
||||
if (GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(1));
|
||||
|
||||
if (existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] != 0)
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -59,11 +59,8 @@ namespace osu.Game.Utils
|
||||
/// <summary>
|
||||
/// Applies rounding to the given BPM value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Double-rounding is applied intentionally (see https://github.com/ppy/osu/pull/18345#issue-1243311382 for rationale).
|
||||
/// </remarks>
|
||||
/// <param name="baseBpm">The base BPM to round.</param>
|
||||
/// <param name="rate">Rate adjustment, if applicable.</param>
|
||||
public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(Math.Round(baseBpm) * rate);
|
||||
public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(baseBpm * rate);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user