1
0
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:
Bartłomiej Dach
2025-05-07 09:31:58 +02:00
Unverified
77 changed files with 4833 additions and 1054 deletions
@@ -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));
+16 -2
View File
@@ -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
+7 -6
View File
@@ -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,
},
};
});
}
}
@@ -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;
}
}
}
+12 -1
View File
@@ -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 };
}
}
}
}
+74 -42
View File
@@ -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>
+7 -1
View File
@@ -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;
+1 -1
View File
@@ -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 = "+";
+18 -4
View File
@@ -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
{
+13 -9
View File
@@ -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,
+2 -3
View File
@@ -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,
+30
View File
@@ -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;
}
}
}
}
+37 -1
View File
@@ -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;
}
+4 -4
View File
@@ -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");
+91 -48
View File
@@ -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),
};
}
+40 -32
View File
@@ -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();
+182
View File
@@ -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;
+115 -25
View File
@@ -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,
}
}
+44 -34
View File
@@ -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;
+1 -4
View File
@@ -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);
}
}