1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-20 22:21:10 +08:00

Merge branch 'master' into song-select-v2-wedges-leaderboard

This commit is contained in:
Dean Herbert
2025-04-23 18:30:14 +09:00
Unverified
43 changed files with 3667 additions and 86 deletions
+1 -1
View File
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!";
}
}
+1 -1
View File
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
}
}
+1 -1
View File
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!";
}
}
@@ -426,6 +426,31 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("countdown started", () => MultiplayerClient.ServerRoom!.ActiveCountdowns.Any());
}
[Test]
public void TestSettingsRemainsOpenOnRoomUpdate()
{
AddStep("set playlist", () =>
{
room.Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
];
});
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for room join", () => RoomJoined);
AddStep("open settings", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().Show());
AddAssert("settings opened", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("trigger room update", () => MultiplayerClient.AddPlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0].Clone()));
AddAssert("settings still open", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
{
[Resolved(canBeNull: true)]
@@ -6,8 +6,12 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
@@ -16,10 +20,12 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -153,10 +159,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
[Test]
public void TestFreeModSelectionDisable()
{
FooterButtonFreeMods freeMods = null!;
AddAssert("freestyle enabled", () => songSelect.Freestyle.Value, () => Is.True);
AddStep("click icon in free mods button", () =>
{
freeMods = this.ChildrenOfType<FooterButtonFreeMods>().Single();
InputManager.MoveMouseTo(freeMods.ChildrenOfType<SpriteIcon>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select not visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("toggle freestyle off", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreestyle>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("freestyle disabled", () => songSelect.Freestyle.Value, () => Is.False);
AddStep("click icon in free mods button", () =>
{
InputManager.MoveMouseTo(freeMods.ChildrenOfType<SpriteIcon>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
private partial class TestPlaylistsSongSelect : PlaylistsSongSelect
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
public new IBindable<bool> Freestyle => base.Freestyle;
public TestPlaylistsSongSelect(Room room)
: base(room)
{
@@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.Online
}
private void waitForLoad()
=> AddUntilStep("wait for panels to load", () => this.ChildrenOfType<LoadingSpinner>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
=> AddUntilStep("wait for panels to load", () => this.ChildrenOfType<LoadingSpinner>().First().State.Value, () => Is.EqualTo(Visibility.Hidden));
private void assertVisiblePanelCount<T>(int expectedVisible)
where T : UserPanel
@@ -0,0 +1,181 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapMetadataWedge : SongSelectComponentsTestScene
{
private APIBeatmapSet? currentOnlineSet;
private BeatmapMetadataWedge wedge = null!;
protected override void LoadComplete()
{
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;
}
};
Child = wedge = new BeatmapMetadataWedge
{
State = { Value = Visibility.Visible },
};
}
[Test]
public void TestShowHide()
{
AddStep("all metrics", () =>
{
var (working, onlineSet) = createTestBeatmap();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("hide wedge", () => wedge.Hide());
AddStep("show wedge", () => wedge.Show());
}
[Test]
public void TestVariousMetrics()
{
AddStep("all metrics", () =>
{
var (working, onlineSet) = createTestBeatmap();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("null beatmap", () => Beatmap.SetDefault());
AddStep("no source", () =>
{
var (working, onlineSet) = createTestBeatmap();
working.Metadata.Source = string.Empty;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("no success rate", () =>
{
var (working, onlineSet) = createTestBeatmap();
onlineSet.Beatmaps.Single().PlayCount = 0;
onlineSet.Beatmaps.Single().PassCount = 0;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("no user ratings", () =>
{
var (working, onlineSet) = createTestBeatmap();
onlineSet.Ratings = Array.Empty<int>();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("no fail times", () =>
{
var (working, onlineSet) = createTestBeatmap();
onlineSet.Beatmaps.Single().FailTimes = null;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("no metrics", () =>
{
var (working, onlineSet) = createTestBeatmap();
onlineSet.Ratings = Array.Empty<int>();
onlineSet.Beatmaps.Single().FailTimes = null;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("local beatmap", () =>
{
var (working, onlineSet) = createTestBeatmap();
working.BeatmapInfo.OnlineID = 0;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
}
[Test]
public void TestTruncation()
{
AddStep("long text", () =>
{
var (working, onlineSet) = createTestBeatmap();
working.BeatmapInfo.Metadata.Author = new RealmUser { Username = "Verrrrryyyy llooonngggggg author" };
working.BeatmapInfo.Metadata.Source = "Verrrrryyyy llooonngggggg source";
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" };
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
}
private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap()
{
var working = CreateWorkingBeatmap(Ruleset.Value);
var onlineSet = new APIBeatmapSet
{
OnlineID = working.BeatmapSetInfo.OnlineID,
Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Pop" },
Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "English" },
Ratings = Enumerable.Range(0, 11).ToArray(),
Beatmaps = new[]
{
new APIBeatmap
{
OnlineID = working.BeatmapInfo.OnlineID,
PlayCount = 10000,
PassCount = 4567,
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(),
},
},
}
};
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now;
return (working, onlineSet);
}
}
}
@@ -0,0 +1,161 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Visual.SongSelect;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapTitleWedge : SongSelectComponentsTestScene
{
private RulesetStore rulesets = null!;
private BeatmapTitleWedge titleWedge = null!;
private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType<BeatmapTitleWedge.DifficultyDisplay>().Single();
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
}
protected override void LoadComplete()
{
base.LoadComplete();
AddRange(new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
titleWedge = new BeatmapTitleWedge
{
State = { Value = Visibility.Visible },
},
},
}
});
AddSliderStep("change star difficulty", 0, 11.9, 4.18, v =>
{
difficultyDisplay.ChildrenOfType<StarRatingDisplay>().Single().Current.Value = new StarDifficulty(v, 0);
});
}
[Test]
public void TestRulesetChange()
{
selectBeatmap(Beatmap.Value.Beatmap);
AddWaitStep("wait for select", 3);
foreach (var rulesetInfo in rulesets.AvailableRulesets)
{
var testBeatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(rulesetInfo);
setRuleset(rulesetInfo);
selectBeatmap(testBeatmap);
}
}
[Test]
public void TestNullBeatmap()
{
selectBeatmap(null);
AddAssert("check default title", () => titleWedge.DisplayedTitle == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => titleWedge.DisplayedArtist == Beatmap.Default.BeatmapInfo.Metadata.Artist);
AddAssert("check empty version", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedVersion.ToString()));
AddAssert("check empty author", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedAuthor.ToString()));
AddAssert("check no statistics", () => difficultyDisplay.ChildrenOfType<BeatmapTitleWedge.DifficultyStatisticsDisplay>().All(d => !d.Statistics.Any()));
}
[Test]
public void TestBPMUpdates()
{
const double bpm = 120;
IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
OsuModDoubleTime doubleTime = null!;
selectBeatmap(beatmap);
checkDisplayedBPM($"{bpm}");
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
checkDisplayedBPM($"{bpm * 1.5f}");
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
checkDisplayedBPM($"{bpm * 2}");
AddStep("select HT", () => SelectedMods.Value = new[] { new OsuModHalfTime() });
checkDisplayedBPM($"{bpm * 0.75f}");
}
[Test]
public void TestWedgeVisibility()
{
AddStep("hide", () => { titleWedge.Hide(); });
AddWaitStep("wait for hide", 3);
AddAssert("check visibility", () => titleWedge.Alpha == 0);
AddStep("show", () => { titleWedge.Show(); });
AddWaitStep("wait for show", 1);
AddAssert("check visibility", () => titleWedge.Alpha > 0);
}
[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")]
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
{
IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm });
beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
if (mod != null)
AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) });
selectBeatmap(beatmap);
checkDisplayedBPM(expectedDisplay);
}
private void setRuleset(RulesetInfo rulesetInfo)
{
AddStep("set ruleset", () => Ruleset.Value = rulesetInfo);
}
private void selectBeatmap(IBeatmap? b)
{
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
});
}
private void checkDisplayedBPM(string target)
{
AddUntilStep($"displayed bpm is {target}", () =>
{
var label = titleWedge.ChildrenOfType<BeatmapTitleWedge.Statistic>().Single(l => l.TooltipText == BeatmapsetsStrings.ShowStatsBpm);
return label.Text == target;
});
}
}
}
@@ -0,0 +1,82 @@
// 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.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapTitleWedgeStatistic : ThemeComparisonTestScene
{
private BeatmapTitleWedge.StatisticPlayCount playCount = null!;
private BeatmapTitleWedge.Statistic statistic2 = null!;
private BeatmapTitleWedge.Statistic statistic3 = null!;
private BeatmapTitleWedge.Statistic statistic4 = null!;
public TestSceneBeatmapTitleWedgeStatistic()
: base(false)
{
}
[Test]
public void TestLoading()
{
AddStep("setup", () => CreateThemedContent(OverlayColourScheme.Aquamarine));
AddStep("set loading", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ForEach(s => s.Text = null));
AddWaitStep("wait", 3);
AddStep("set values", () =>
{
playCount.Value = new BeatmapTitleWedge.StatisticPlayCount.Data(1234, 12);
statistic2.Text = "3,234";
statistic3.Text = "12:34";
statistic4.Text = "123";
});
AddStep("set large values", () =>
{
playCount.Value = new BeatmapTitleWedge.StatisticPlayCount.Data(134587921, 502);
statistic2.Text = "1,048,576";
statistic3.Text = "2:50:23";
statistic4.Text = "1238014";
});
}
protected override Drawable CreateContent() => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Children = new[]
{
playCount = new BeatmapTitleWedge.StatisticPlayCount(true, minSize: 50)
{
Value = new BeatmapTitleWedge.StatisticPlayCount.Data(1234, 12),
},
statistic2 = new BeatmapTitleWedge.Statistic(OsuIcon.Clock, true, minSize: 30)
{
Text = "3,234",
TooltipText = "Statistic 2",
},
statistic3 = new BeatmapTitleWedge.Statistic(OsuIcon.Metronome)
{
Text = "12:34",
Margin = new MarginPadding { Right = 10f },
TooltipText = "Statistic 3",
},
statistic4 = new BeatmapTitleWedge.Statistic(OsuIcon.Graphics)
{
Text = "123",
TooltipText = "Statistic 4",
},
},
};
}
}
@@ -0,0 +1,166 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Screens.SelectV2;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneDifficultyStatisticsDisplay : OsuTestScene
{
private Container displayContainer = null!;
private BeatmapTitleWedge.DifficultyStatisticsDisplay display = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup", () =>
{
Child = displayContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 300,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
display = new BeatmapTitleWedge.DifficultyStatisticsDisplay
{
RelativeSizeAxes = Axes.X,
Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.5f, 0.5f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.7f, 0.7f, 1f),
}
}
}
};
});
AddSliderStep("display width", 0, 300, 300, v =>
{
if (displayContainer.IsNotNull())
displayContainer.Width = v;
});
}
[Test]
public void TestEmpty()
{
AddStep("set empty", () => display.Statistics = Array.Empty<BeatmapTitleWedge.StatisticDifficulty.Data>());
AddAssert("no statistics", () => !display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().Any());
AddAssert("no tiny statistics", () => !display.ChildrenOfType<GridContainer>().Single().Content.Any());
}
[Test]
public void TestDisplay()
{
AddStep("change data with same labels", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f),
});
AddStep("change data with different labels", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f),
});
AddAssert("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddStep("shrink width", () => displayContainer.Width = 100);
AddAssert("statistics hidden", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 0);
AddUntilStep("tiny statistics displayed", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 1);
}
[Test]
public void TestContraction()
{
AddAssert("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddStep("set too many statistics", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f),
});
AddAssert("statistics hidden", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 0);
AddUntilStep("tiny statistics displayed", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 1);
AddStep("set less statistics", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
});
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddUntilStep("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
}
[Test]
public void TestAutoSize()
{
AddStep("setup auto size", () => Child = display = new BeatmapTitleWedge.DifficultyStatisticsDisplay(true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.5f, 0.5f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.7f, 0.7f, 1f),
}
});
AddAssert("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddStep("set too many statistics", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f),
});
AddAssert("statistics still visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics still hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddStep("set less statistics", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
});
AddAssert("statistics still visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics still hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
}
}
}
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -14,6 +16,9 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneFPSCounter : OsuTestScene
{
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[SetUpSteps]
public void SetUpSteps()
{
@@ -41,6 +46,7 @@ namespace osu.Game.Tests.Visual.UserInterface
},
};
});
AddToggleStep("toggle show", b => config.SetValue(OsuSetting.ShowFpsDisplay, b));
}
[Test]
@@ -23,6 +23,8 @@ namespace osu.Game.Beatmaps.Drawables
/// </summary>
public partial class StarRatingDisplay : CompositeDrawable, IHasCurrentValue<StarDifficulty>
{
public const double TRANSFORM_DURATION = 750;
private readonly bool animated;
private readonly Box background;
private readonly SpriteIcon starIcon;
@@ -36,6 +38,12 @@ namespace osu.Game.Beatmaps.Drawables
set => current.Current = value;
}
/// <summary>
/// The difficulty colour currently displayed.
/// Can be used to have other components match the spectrum animation.
/// </summary>
public Color4 DisplayedDifficultyColour => background.Colour;
private readonly Bindable<double> displayedStars = new BindableDouble();
/// <summary>
@@ -139,7 +147,7 @@ namespace osu.Game.Beatmaps.Drawables
Current.BindValueChanged(c =>
{
if (animated)
this.TransformBindableTo(displayedStars, c.NewValue.Stars, 750, Easing.OutQuint);
this.TransformBindableTo(displayedStars, c.NewValue.Stars, TRANSFORM_DURATION, Easing.OutQuint);
else
displayedStars.Value = c.NewValue.Stars;
});
@@ -152,8 +160,8 @@ namespace osu.Game.Beatmaps.Drawables
background.Colour = colours.ForStarDifficulty(s.NewValue);
starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
starIcon.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
starsText.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
}, true);
}
}
+5
View File
@@ -20,6 +20,11 @@ namespace osu.Game.Graphics
public static Color4 Gray(float amt) => new Color4(amt, amt, amt, 1f);
public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255);
/// <summary>
/// The maximum star rating colour which can be distinguished against a black background.
/// </summary>
public const float STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF = 6.5f;
public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM =
{
(0.1f, Color4Extensions.FromHex("aaaaaa")),
@@ -44,7 +44,8 @@ namespace osu.Game.Graphics.UserInterface
AutoSizeAxes = Axes.Both,
TextAnchor = Anchor.TopRight,
Margin = new MarginPadding { Left = 5, Vertical = 10 },
Text = string.Join('\n', gameHost.Threads.Select(t => t.Name))
Text = string.Join('\n', gameHost.Threads.Select(t => t.Name)),
ParagraphSpacing = 0,
},
textFlow = new OsuTextFlowContainer(cp =>
{
@@ -56,6 +57,7 @@ namespace osu.Game.Graphics.UserInterface
Margin = new MarginPadding { Left = 35, Right = 10, Vertical = 10 },
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.TopRight,
ParagraphSpacing = 0,
},
};
}
+31
View File
@@ -58,6 +58,7 @@ namespace osu.Game.Online.API
public IBindable<APIUser> LocalUser => localUser;
public IBindableList<APIRelation> Friends => friends;
public IBindableList<APIRelation> Blocks => blocks;
public INotificationsClient NotificationsClient { get; }
@@ -66,6 +67,7 @@ namespace osu.Game.Online.API
private Bindable<APIUser> localUser { get; } = new Bindable<APIUser>(createGuestUser());
private BindableList<APIRelation> friends { get; } = new BindableList<APIRelation>();
private BindableList<APIRelation> blocks { get; } = new BindableList<APIRelation>();
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
@@ -638,6 +640,35 @@ namespace osu.Game.Online.API
Queue(friendsReq);
}
public void UpdateLocalBlocks()
{
if (!IsLoggedIn)
return;
var blocksReq = new GetBlocksRequest();
blocksReq.Failure += ex =>
{
if (ex is not WebRequestFlushedException)
state.Value = APIState.Failing;
};
blocksReq.Success += res =>
{
var existingBlocks = blocks.Select(f => f.TargetID).ToHashSet();
var updatedBlocks = res.Select(f => f.TargetID).ToHashSet();
// Add new blocked users to local list.
blocks.AddRange(res.Where(r => !existingBlocks.Contains(r.TargetID)));
// Remove non-blocked users from local list.
blocks.RemoveAll(b => !updatedBlocks.Contains(b.TargetID));
// Remove friends who got blocked since last check.
friends.RemoveAll(f => updatedBlocks.Contains(f.TargetID));
};
Queue(blocksReq);
}
private static APIUser createGuestUser() => new GuestUser();
protected override void Dispose(bool isDisposing)
+6
View File
@@ -26,6 +26,7 @@ namespace osu.Game.Online.API
});
public BindableList<APIRelation> Friends { get; } = new BindableList<APIRelation>();
public BindableList<APIRelation> Blocks { get; } = new BindableList<APIRelation>();
public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
@@ -180,6 +181,10 @@ namespace osu.Game.Online.API
{
}
public void UpdateLocalBlocks()
{
}
public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
public IChatClient GetChatClient() => new TestChatClientConnector(this);
@@ -194,6 +199,7 @@ namespace osu.Game.Online.API
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
IBindableList<APIRelation> IAPIProvider.Friends => Friends;
IBindableList<APIRelation> IAPIProvider.Blocks => Blocks;
/// <summary>
/// Skip 2FA requirement for next login.
+10
View File
@@ -23,6 +23,11 @@ namespace osu.Game.Online.API
/// </summary>
IBindableList<APIRelation> Friends { get; }
/// <summary>
/// The users blocked by the local user.
/// </summary>
IBindableList<APIRelation> Blocks { get; }
/// <summary>
/// The language supplied by this provider to API requests.
/// </summary>
@@ -118,6 +123,11 @@ namespace osu.Game.Online.API
/// </summary>
void UpdateLocalFriends();
/// <summary>
/// Update the list of users blocked by the current user.
/// </summary>
void UpdateLocalBlocks();
/// <summary>
/// Schedule a callback to run on the update thread.
/// </summary>
@@ -0,0 +1,30 @@
// 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.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class BlockUserRequest : APIRequest
{
public readonly int TargetId;
public BlockUserRequest(int targetId)
{
TargetId = targetId;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.AddParameter("target", TargetId.ToString(), RequestParameterType.Query);
return req;
}
protected override string Target => @"blocks";
}
}
@@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetBlocksRequest : APIRequest<List<APIRelation>>
{
protected override string Target => @"blocks";
}
}
@@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class UnblockUserRequest : APIRequest
{
public readonly int TargetId;
public UnblockUserRequest(int targetId)
{
TargetId = targetId;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Delete;
return req;
}
protected override string Target => @$"blocks/{TargetId}";
}
}
@@ -5,10 +5,10 @@ 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.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
@@ -30,8 +30,6 @@ namespace osu.Game.Online.Leaderboards
public LeaderboardCriteria? CurrentCriteria { get; private set; }
private IDisposable? localScoreSubscription;
private TaskCompletionSource<LeaderboardScores?>? localFetchCompletionSource;
private TaskCompletionSource<LeaderboardScores?>? lastFetchCompletionSource;
private GetScoresRequest? inFlightOnlineRequest;
[Resolved]
@@ -43,55 +41,70 @@ namespace osu.Game.Online.Leaderboards
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
public Task<LeaderboardScores?> FetchWithCriteriaAsync(LeaderboardCriteria newCriteria)
/// <summary>
/// Fetch leaderboard content with the new criteria specified in the background.
/// On completion, <see cref="Scores"/> will be updated with the results from this call (unless a more recent call with a different criteria has completed).
/// </summary>
public void FetchWithCriteria(LeaderboardCriteria newCriteria, bool forceRefresh = false)
{
if (CurrentCriteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false)
return lastFetchCompletionSource?.Task ?? Task.FromResult(Scores.Value);
if (!forceRefresh && CurrentCriteria?.Equals(newCriteria) == true && scores.Value?.FailState == null)
return;
CurrentCriteria = newCriteria;
localScoreSubscription?.Dispose();
inFlightOnlineRequest?.Cancel();
lastFetchCompletionSource?.TrySetCanceled();
scores.Value = null;
if (newCriteria.Beatmap == null || newCriteria.Ruleset == null)
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected));
{
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected);
return;
}
switch (newCriteria.Scope)
{
case BeatmapLeaderboardScope.Local:
{
// this task completion source will be marked completed in the `localScoresChanged()` below.
// yes it's twisty, but such are the costs of trying to reconcile data-push / subscription and data-pull / explicit fetch flows.
lastFetchCompletionSource = localFetchCompletionSource = new TaskCompletionSource<LeaderboardScores?>();
localScoreSubscription = realm.RegisterForNotifications(r =>
r.All<ScoreInfo>().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0"
+ $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}"
+ $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1"
+ $" AND {nameof(ScoreInfo.DeletePending)} == false"
, newCriteria.Beatmap.ID, newCriteria.Ruleset.ShortName), localScoresChanged);
return localFetchCompletionSource.Task;
return;
}
default:
{
if (!api.IsLoggedIn)
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn));
{
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn);
return;
}
if (!newCriteria.Ruleset.IsLegacyRuleset())
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable));
{
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable);
return;
}
if (newCriteria.Beatmap.OnlineID <= 0 || newCriteria.Beatmap.Status <= BeatmapOnlineStatus.Pending)
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable));
{
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable);
return;
}
if ((newCriteria.Scope.RequiresSupporter(newCriteria.ExactMods != null)) && !api.LocalUser.Value.IsSupporter)
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter));
{
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter);
return;
}
if (newCriteria.Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null)
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam));
var onlineFetchCompletionSource = new TaskCompletionSource<LeaderboardScores?>();
lastFetchCompletionSource = onlineFetchCompletionSource;
{
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam);
return;
}
IReadOnlyList<Mod>? requestMods = null;
@@ -116,12 +129,17 @@ namespace osu.Game.Online.Leaderboards
response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap)
);
inFlightOnlineRequest = null;
if (onlineFetchCompletionSource.TrySetResult(result))
scores.Value = result;
scores.Value = result;
};
newRequest.Failure += _ => onlineFetchCompletionSource.TrySetResult(LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure));
newRequest.Failure += ex =>
{
Logger.Log($@"Failed to fetch leaderboards when displaying results: {ex}", LoggingTarget.Network);
if (ex is not OperationCanceledException)
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure);
};
api.Queue(inFlightOnlineRequest = newRequest);
return onlineFetchCompletionSource.Task;
break;
}
}
}
@@ -157,12 +175,6 @@ namespace osu.Game.Online.Leaderboards
newScores = newScores.Detach().OrderByTotalScore();
scores.Value = LeaderboardScores.Success(newScores.ToArray(), null);
if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource)
{
localFetchCompletionSource.SetResult(scores.Value);
localFetchCompletionSource = lastFetchCompletionSource = null;
}
}
}
+1 -6
View File
@@ -801,12 +801,7 @@ namespace osu.Game
var newLeaderboard = currentLeaderboard != null
? currentLeaderboard with { Beatmap = databasedBeatmap, Ruleset = databasedScore.ScoreInfo.Ruleset }
: new LeaderboardCriteria(databasedBeatmap, databasedScore.ScoreInfo.Ruleset, BeatmapLeaderboardScope.Global, null);
LeaderboardManager.FetchWithCriteriaAsync(newLeaderboard)
.ContinueWith(t =>
{
if (t.Exception != null)
Logger.Log($@"Failed to fetch leaderboards when displaying results: {t.Exception}", LoggingTarget.Network);
});
LeaderboardManager.FetchWithCriteria(newLeaderboard);
}
switch (presentType)
@@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Mods
{
public partial class AdjustedAttributesTooltip : VisibilityContainer, ITooltip<AdjustedAttributesTooltip.Data?>
{
private readonly OverlayColourProvider? colourProvider;
private FillFlowContainer attributesFillFlow = null!;
private Container content = null!;
@@ -27,6 +28,11 @@ namespace osu.Game.Overlays.Mods
[Resolved]
private OsuColour colours { get; set; } = null!;
public AdjustedAttributesTooltip(OverlayColourProvider? colourProvider = null)
{
this.colourProvider = colourProvider;
}
[BackgroundDependencyLoader]
private void load()
{
@@ -45,7 +51,7 @@ namespace osu.Game.Overlays.Mods
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray3,
Colour = colourProvider?.Background4 ?? colours.Gray3,
},
new FillFlowContainer
{
@@ -44,6 +44,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
AutoSizeAxes = Axes.Both;
OsuTextFlowContainer label;
InternalChildren = new Drawable[]
{
content = new Container
@@ -69,12 +71,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
label = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
AutoSizeAxes = Axes.Both,
// can't use this because osu-web does weird stuff with \\n.
// Text = UsersStrings.ShowDailyChallengeTitle.,
Text = "Daily\nChallenge",
Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f },
},
new Container
@@ -129,6 +128,10 @@ namespace osu.Game.Overlays.Profile.Header.Components
}
},
};
// can't use this because osu-web does weird stuff with \\n.
// Text = UsersStrings.ShowDailyChallengeTitle.,
label.AddParagraph("Daily\nChallenge");
}
protected override void LoadComplete()
@@ -80,6 +80,7 @@ namespace osu.Game.Screens.OnlinePlay
Origin = Anchor.Centre,
Scale = new Vector2(0.8f),
Icon = FontAwesome.Solid.Bars,
Enabled = { BindTarget = Enabled },
Action = () => freeModSelectOverlay.ToggleVisibility()
}
});
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -431,14 +430,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
/// </summary>
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
bool newIsRoomJoined = client.Room != null;
bool wasRoomJoined = isRoomJoined;
isRoomJoined = client.Room != null;
if (newIsRoomJoined)
// Creating a room.
if (!wasRoomJoined && !isRoomJoined)
{
roomContent.Hide();
settingsOverlay.Show();
}
// Joining a room.
if (!wasRoomJoined && isRoomJoined)
{
roomContent.Show();
settingsOverlay.Hide();
}
else if (isRoomJoined)
// Leaving a room.
if (wasRoomJoined && !isRoomJoined)
{
Logger.Log($"{this} exiting due to loss of room or connection");
@@ -447,17 +457,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
else
ValidForResume = false;
}
else
{
Debug.Assert(!isRoomJoined && !newIsRoomJoined);
// A new room is being created.
// The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed.
roomContent.Hide();
settingsOverlay.Show();
}
isRoomJoined = newIsRoomJoined;
});
/// <summary>
@@ -62,7 +62,7 @@ namespace osu.Game.Screens.Select.Leaderboards
}
}
private readonly Bindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
private readonly IBindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -82,9 +82,10 @@ namespace osu.Game.Screens.Select.Leaderboards
if (filterMods)
RefetchScores();
};
((IBindable<LeaderboardScores?>)fetchedScores).BindTo(leaderboardManager.Scores);
}
private bool initialFetchComplete;
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
protected override APIRequest? FetchScores(CancellationToken cancellationToken)
@@ -92,30 +93,38 @@ namespace osu.Game.Screens.Select.Leaderboards
var fetchBeatmapInfo = BeatmapInfo;
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo?.Ruleset;
leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null))
.ContinueWith(t =>
{
if (t.Exception != null && !t.IsCanceled)
{
Schedule(() => SetErrorState(LeaderboardState.NetworkFailure));
return;
}
// Without this check, an initial fetch will be performed and clear global cache.
if (fetchBeatmapInfo == null)
return null;
fetchedScores.UnbindEvents();
fetchedScores.BindValueChanged(scores =>
{
if (scores.NewValue == null) return;
// 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);
if (scores.NewValue.FailState == null)
Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore));
else
Schedule(() => SetErrorState((LeaderboardState)scores.NewValue.FailState));
}, true);
}, cancellationToken);
if (!initialFetchComplete)
{
// only bind this after the first fetch to avoid reading stale scores.
fetchedScores.BindTo(leaderboardManager.Scores);
fetchedScores.BindValueChanged(_ => updateScores(), true);
initialFetchComplete = true;
}
return null;
}
private void updateScores()
{
var scores = fetchedScores.Value;
if (scores == null) return;
if (scores.FailState == null)
Schedule(() => SetScores(scores.TopScores, scores.UserScore));
else
Schedule(() => SetErrorState((LeaderboardState)scores.FailState));
}
protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope, Scope != BeatmapLeaderboardScope.Friend)
{
Action = () => ScoreSelected?.Invoke(model)
@@ -0,0 +1,357 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapMetadataWedge : VisibilityContainer
{
private MetadataDisplay creator = null!;
private MetadataDisplay source = null!;
private MetadataDisplay genre = null!;
private MetadataDisplay language = null!;
private MetadataDisplay tag = null!;
private MetadataDisplay submitted = null!;
private MetadataDisplay ranked = null!;
private Drawable ratingsWedge = null!;
private SuccessRateDisplay successRateDisplay = null!;
private UserRatingDisplay userRatingDisplay = null!;
private RatingSpreadDisplay ratingSpreadDisplay = null!;
private Drawable failRetryWedge = null!;
private FailRetryDisplay failRetryDisplay = null!;
protected override bool StartHidden => true;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private IBindable<APIState> apiState = null!;
[Resolved]
private ILinkHandler? linkHandler { get; set; }
[Resolved]
private SongSelect? songSelect { get; set; }
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Top = 4f };
Width = 0.9f;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 4f),
Shear = OsuGame.SHEAR,
Children = new[]
{
new ShearAligningWrapper(new Container
{
CornerRadius = 10,
Masking = true,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new WedgeBackground(),
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Shear = -OsuGame.SHEAR,
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 35, Vertical = 16 },
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 10f),
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(),
new Dimension(),
},
Content = new[]
{
new[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 10f),
Children = new[]
{
creator = new MetadataDisplay("Creator"),
genre = new MetadataDisplay("Genre"),
},
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 10f),
Children = new[]
{
source = new MetadataDisplay("Source"),
language = new MetadataDisplay("Language"),
},
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 10f),
Children = new[]
{
submitted = new MetadataDisplay("Submitted"),
ranked = new MetadataDisplay("Ranked"),
},
},
},
},
},
tag = new MetadataDisplay("Tags"),
},
},
},
},
},
}),
new ShearAligningWrapper(ratingsWedge = new Container
{
Alpha = 0f,
CornerRadius = 10,
Masking = true,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new WedgeBackground(),
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, 10),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(),
},
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 },
Content = new[]
{
new[]
{
successRateDisplay = new SuccessRateDisplay(),
Empty(),
userRatingDisplay = new UserRatingDisplay(),
Empty(),
ratingSpreadDisplay = new RatingSpreadDisplay(),
},
},
},
}
}),
new ShearAligningWrapper(failRetryWedge = new Container
{
Alpha = 0f,
CornerRadius = 10,
Masking = true,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new WedgeBackground(),
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Shear = -OsuGame.SHEAR,
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 },
Child = failRetryDisplay = new FailRetryDisplay(),
},
},
}),
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.BindValueChanged(_ => updateDisplay());
apiState = api.State.GetBoundCopy();
apiState.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay), true);
}
private const double transition_duration = 300;
protected override void PopIn()
{
this.FadeIn(transition_duration, Easing.OutQuint)
.MoveToX(0, transition_duration, Easing.OutQuint);
updateSubWedgeVisibility();
}
protected override void PopOut()
{
this.FadeOut(transition_duration, Easing.OutQuint)
.MoveToX(-100, transition_duration, Easing.OutQuint);
updateSubWedgeVisibility();
}
private void updateSubWedgeVisibility()
{
// 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)
{
ratingsWedge.FadeIn(transition_duration, Easing.OutQuint)
.MoveToX(0, transition_duration, Easing.OutQuint);
failRetryWedge.Delay(100)
.FadeIn(transition_duration, Easing.OutQuint)
.MoveToX(0, transition_duration, Easing.OutQuint);
}
else
{
ratingsWedge.FadeOut(transition_duration, Easing.OutQuint)
.MoveToX(-50, transition_duration, Easing.OutQuint);
failRetryWedge.Delay(100)
.FadeOut(transition_duration, Easing.OutQuint)
.MoveToX(-50, transition_duration, Easing.OutQuint);
}
}
private void updateDisplay()
{
var metadata = beatmap.Value.Metadata;
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
creator.Data = (metadata.Author.Username, () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, metadata.Author)));
if (!string.IsNullOrEmpty(metadata.Source))
source.Data = (metadata.Source, () => songSelect?.Search(metadata.Source));
else
source.Data = ("-", null);
tag.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t));
submitted.Date = beatmapSetInfo.DateSubmitted;
ranked.Date = beatmapSetInfo.DateRanked;
if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID)
refetchBeatmapSet();
updateOnlineDisplay();
}
private APIBeatmapSet? currentOnlineBeatmapSet;
private GetBeatmapSetRequest? currentRequest;
private void refetchBeatmapSet()
{
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
currentRequest?.Cancel();
currentRequest = null;
currentOnlineBeatmapSet = null;
if (beatmapSetInfo.OnlineID >= 1)
{
// todo: consider introducing a BeatmapSetLookupCache for caching benefits.
currentRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID);
currentRequest.Failure += _ => updateOnlineDisplay();
currentRequest.Success += s =>
{
currentOnlineBeatmapSet = s;
updateOnlineDisplay();
};
api.Queue(currentRequest);
}
}
private void updateOnlineDisplay()
{
if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting)
{
genre.Data = null;
language.Data = null;
return;
}
if (currentOnlineBeatmapSet == null)
{
genre.Data = ("-", null);
language.Data = ("-", null);
}
else
{
var beatmapInfo = beatmap.Value.BeatmapInfo;
var onlineBeatmapSet = currentOnlineBeatmapSet;
var onlineBeatmap = onlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID);
genre.Data = (onlineBeatmapSet.Genre.Name, () => songSelect?.Search(onlineBeatmapSet.Genre.Name));
language.Data = (onlineBeatmapSet.Language.Name, () => songSelect?.Search(onlineBeatmapSet.Language.Name));
if (onlineBeatmap != null)
{
userRatingDisplay.Data = onlineBeatmapSet.Ratings;
ratingSpreadDisplay.Data = onlineBeatmapSet.Ratings;
successRateDisplay.Data = (onlineBeatmap.PassCount, onlineBeatmap.PlayCount);
failRetryDisplay.Data = onlineBeatmap.FailTimes ?? new APIFailTimes();
}
}
updateSubWedgeVisibility();
}
}
}
@@ -0,0 +1,195 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapMetadataWedge
{
private partial class FailRetryDisplay : CompositeDrawable
{
private readonly GraphDrawable retriesGraph;
private readonly GraphDrawable failsGraph;
public APIFailTimes Data
{
set
{
int[] retries = value.Retries ?? Array.Empty<int>();
int[] fails = value.Fails ?? Array.Empty<int>();
int[] total = retries.Zip(fails, (r, f) => r + f).ToArray();
int maximum = total.DefaultIfEmpty(0).Max();
retriesGraph.Data = total.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray();
failsGraph.Data = fails.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray();
}
}
public FailRetryDisplay()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 4f),
Children = new Drawable[]
{
new OsuSpriteText
{
Text = BeatmapsetsStrings.ShowInfoPointsOfFailure,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
Margin = new MarginPadding { Bottom = 4f },
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = 65f,
Children = new[]
{
retriesGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both, Y = -1f },
failsGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both },
},
},
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
retriesGraph.Colour = colours.Orange1;
failsGraph.Colour = colours.DarkOrange2;
}
private partial class GraphDrawable : Drawable
{
private readonly float[] displayedData = new float[100];
private float[] data = new float[100];
public float[] Data
{
get => data;
set
{
data = value;
Invalidate(Invalidation.DrawNode);
}
}
protected override void Update()
{
base.Update();
bool changed = false;
for (int i = 0; i < displayedData.Length; i++)
{
float before = displayedData[i];
float value = data.ElementAtOrDefault(i);
displayedData[i] = (float)Interpolation.DampContinuously(displayedData[i], value, 40, Time.Elapsed);
changed |= displayedData[i] != before;
}
if (changed)
Invalidate(Invalidation.DrawNode);
}
protected override DrawNode CreateDrawNode() => new GraphDrawNode(this);
// todo: consider integrating this with BarGraph
// this is different from BarGraph since this displays each bar with corner radii applied.
private class GraphDrawNode : DrawNode
{
private readonly GraphDrawable source;
private Vector2 drawSize;
private float[] displayedData = null!;
public GraphDrawNode(GraphDrawable source)
: base(source)
{
this.source = source;
}
public override void ApplyState()
{
base.ApplyState();
drawSize = source.DrawSize;
displayedData = source.displayedData;
}
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);
const float spacing_constant = 1.5f;
float position = 0;
float barWidth = drawSize.X / displayedData.Length / spacing_constant;
float totalSpacing = drawSize.X - barWidth * displayedData.Length;
float spacing = totalSpacing / (displayedData.Length - 1);
for (int i = 0; i < displayedData.Length; i++)
{
float barHeight = MathF.Max(drawSize.Y * displayedData[i], barWidth);
drawBar(renderer, position, barWidth, barHeight);
position += barWidth + spacing;
}
}
private void drawBar(IRenderer renderer, float position, float width, float height)
{
float cornerRadius = width / 2f;
Vector3 scale = DrawInfo.MatrixInverse.ExtractScale();
float blendRange = (scale.X + scale.Y) / 2;
RectangleF drawRectangle = new RectangleF(new Vector2(position, drawSize.Y - height), new Vector2(width, height));
Quad screenSpaceDrawQuad = Quad.FromRectangle(drawRectangle) * DrawInfo.Matrix;
renderer.PushMaskingInfo(new MaskingInfo
{
ScreenSpaceAABB = screenSpaceDrawQuad.AABB,
MaskingRect = drawRectangle.Normalize(),
ConservativeScreenSpaceQuad = screenSpaceDrawQuad,
ToMaskingSpace = DrawInfo.MatrixInverse,
CornerRadius = cornerRadius,
CornerExponent = 2f,
// We are setting the linear blend range to the approximate size of a _pixel_ here.
// This results in the optimal trade-off between crispness and smoothness of the
// edges of the masked region according to sampling theory.
BlendRange = blendRange,
AlphaExponent = 1,
});
renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour);
renderer.PopMaskingInfo();
}
}
}
}
}
}
@@ -0,0 +1,174 @@
// 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.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapMetadataWedge
{
private partial class MetadataDisplay : FillFlowContainer
{
private readonly OsuSpriteText labelText;
private readonly OsuSpriteText contentText;
private readonly OsuSpriteText contentLinkText;
private readonly OsuHoverContainer contentLink;
private readonly DrawableDate contentDate;
private readonly TagsLine contentTags;
private readonly LoadingSpinner contentLoading;
private (LocalisableString value, Action? linkAction)? data;
public (LocalisableString value, Action? linkAction)? Data
{
get => data;
set
{
data = value;
if (value?.linkAction != null)
setLink(value.Value.value, value.Value.linkAction);
else if (value.HasValue)
setText(value.Value.value);
else
setLoading();
}
}
public DateTimeOffset? Date
{
set
{
if (value != null)
setDate(value.Value);
else
setText("-");
}
}
public (string[] tags, Action<string> linkAction) Tags
{
set => setTags(value.tags, value.linkAction);
}
public MetadataDisplay(LocalisableString label)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Right = 10 };
InternalChildren = new Drawable[]
{
labelText = new OsuSpriteText
{
Text = label,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = OsuFont.Style.Caption1.Size,
Children = new Drawable[]
{
contentText = new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Font = OsuFont.Style.Caption1,
},
contentLink = new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
Child = contentLinkText = new TruncatingSpriteText
{
Font = OsuFont.Style.Caption1,
},
},
contentDate = new DrawableDate(default, OsuFont.Style.Caption1.Size, false),
contentTags = new TagsLine(),
contentLoading = new LoadingSpinner
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Size = new Vector2(10),
Margin = new MarginPadding { Top = 3f },
State = { Value = Visibility.Visible },
}
},
},
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
labelText.Colour = colourProvider.Content1;
contentText.Colour = colourProvider.Content2;
contentLink.IdleColour = colourProvider.Light2;
}
protected override void Update()
{
base.Update();
contentLinkText.MaxWidth = ChildSize.X;
}
private void clear()
{
contentText.Text = string.Empty;
contentLinkText.Text = string.Empty;
contentDate.Hide();
contentTags.Tags = Array.Empty<string>();
contentLoading.Hide();
}
private void setText(LocalisableString text)
{
clear();
contentText.Text = text;
}
private void setLink(LocalisableString text, Action action) => Schedule(() =>
{
clear();
contentLinkText.Text = text;
contentLink.Action = action;
});
private void setDate(DateTimeOffset date)
{
clear();
contentDate.Show();
contentDate.Date = date;
}
private void setTags(string[] tags, Action<string> link)
{
clear();
contentTags.Tags = tags;
contentTags.Action = link;
}
private void setLoading()
{
clear();
contentLoading.Show();
}
}
}
}
@@ -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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapMetadataWedge
{
private partial class RatingSpreadDisplay : CompositeDrawable
{
private const float min_height = 4f;
private const float max_height = 32f;
private const int rating_range = 10;
private readonly GraphBar[] graph;
public int[] Data
{
set
{
if (!value.Any())
{
foreach (var bar in graph)
bar.ResizeHeightTo(min_height, 300, Easing.OutQuint);
}
else
{
var usableRange = value.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0.
int maxRating = usableRange.Max();
for (int i = 0; i < graph.Length; i++)
graph[i].ResizeHeightTo(min_height + (max_height - min_height) * (maxRating == 0 ? 0 : usableRange.ElementAt(i) / (float)maxRating), 300, Easing.OutQuint);
}
}
}
public RatingSpreadDisplay()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
graph = Enumerable.Range(0, rating_range).Select(_ => new GraphBar()).ToArray();
InternalChildren = new[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 1f),
Children = new Drawable[]
{
new OsuSpriteText
{
Text = BeatmapsetsStrings.ShowStatsRatingSpread,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
},
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, max_height) },
ColumnDimensions = graph.SkipLast(1).Select(_ => new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 1f),
}).SelectMany(d => d).Append(new Dimension()).ToArray(),
Content = new[]
{
graph.SkipLast(1).Select(g => new[]
{
g,
Empty()
}).SelectMany(g => g).Append(graph[^1]).ToArray()
},
}
},
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
for (int i = 0; i < 10; i++)
{
var left = Interpolation.ValueAt(i, colours.Blue4, colours.Blue0, 0, 10);
var right = Interpolation.ValueAt(i + 1, colours.Blue4, colours.Blue0, 0, 10);
graph[i].Colour = ColourInfo.GradientHorizontal(left, right);
}
}
private partial class GraphBar : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load()
{
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
RelativeSizeAxes = Axes.X;
CornerRadius = 2f;
Masking = true;
InternalChild = new Box { RelativeSizeAxes = Axes.Both };
}
}
}
}
}
@@ -0,0 +1,112 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapMetadataWedge
{
private partial class SuccessRateDisplay : CompositeDrawable, IHasTooltip
{
private readonly OsuSpriteText valueText;
private readonly Circle backgroundBar;
private readonly Circle valueBar;
private (int passes, int plays) data;
public (int passes, int plays) Data
{
get => data;
set
{
data = value;
float ratio = value.plays == 0 ? 0 : (float)value.passes / value.plays;
valueText.Text = ratio.ToLocalisableString(@"0.##%");
valueText.MoveToX(Math.Clamp(ratio, 0.05f, 0.95f), 300, Easing.OutQuint);
valueBar.ResizeWidthTo(ratio, 300, Easing.OutQuint);
}
}
public LocalisableString TooltipText => $"{data.passes:N0} / {data.plays:N0}";
public SuccessRateDisplay()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 2f),
Children = new Drawable[]
{
new OsuSpriteText
{
Text = BeatmapsetsStrings.ShowInfoSuccessRate,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10f },
Child = valueText = new OsuSpriteText
{
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Font = OsuFont.Style.Caption1,
}
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new[]
{
backgroundBar = new Circle
{
RelativeSizeAxes = Axes.X,
Height = 4f,
},
valueBar = new Circle
{
RelativeSizeAxes = Axes.X,
Width = 0f,
Height = 4f,
},
},
}
},
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, OverlayColourProvider colourProvider)
{
backgroundBar.Colour = colourProvider.Background6;
valueBar.Colour = colours.Lime1;
valueText.Colour = colourProvider.Content2;
}
}
}
}
@@ -0,0 +1,223 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
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.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapMetadataWedge
{
private partial class TagsLine : FillFlowContainer
{
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
private string[] tags = Array.Empty<string>();
private TagsOverflowButton? overflowButton;
public string[] Tags
{
get => tags;
set
{
tags = value;
updateTags();
}
}
public Action<string>? Action;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public TagsLine()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Horizontal;
Spacing = new Vector2(4, 0);
AddLayout(drawSizeLayout);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (!drawSizeLayout.IsValid)
{
updateLayout();
drawSizeLayout.Validate();
}
}
private void updateLayout()
{
if (tags.Length == 0)
return;
Debug.Assert(overflowButton != null);
float limit = DrawWidth - overflowButton.DrawWidth - 5;
bool showOverflow = false;
foreach (var text in Children)
{
if (text.X + text.DrawWidth < limit)
text.Show();
else
{
showOverflow = true;
text.AlwaysPresent = false;
text.Hide();
}
}
if (showOverflow)
overflowButton.Show();
else
overflowButton.Hide();
}
private void updateTags()
{
ChildrenEnumerable = tags.Select(t => new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
Action = () => Action?.Invoke(t),
IdleColour = colourProvider.Light2,
AlwaysPresent = true,
Alpha = 0f,
Child = new OsuSpriteText
{
Text = t,
Font = OsuFont.Style.Caption1,
},
});
Add(overflowButton = new TagsOverflowButton(tags)
{
Alpha = 0f,
});
drawSizeLayout.Invalidate();
}
private partial class TagsOverflowButton : CompositeDrawable, IHasPopover, IHasLineBaseHeight
{
private readonly string[] tags;
private Box box = null!;
private OsuSpriteText text = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private SongSelect? songSelect { get; set; }
public float LineBaseHeight => text.LineBaseHeight;
public TagsOverflowButton(string[] tags)
{
this.tags = tags;
}
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(OsuFont.Style.Caption1.Size);
CornerRadius = 1.5f;
Masking = true;
InternalChildren = new Drawable[]
{
box = new Box
{
Colour = colourProvider.Light1,
RelativeSizeAxes = Axes.Both,
},
text = new OsuSpriteText
{
Y = -2,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "...",
Colour = colourProvider.Background4,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
}
};
}
protected override bool OnHover(HoverEvent e)
{
box.FadeColour(colourProvider.Content2, 300, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
box.FadeColour(colourProvider.Light1, 300, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override bool OnClick(ClickEvent e)
{
box.FlashColour(colourProvider.Content1, 300, Easing.OutQuint);
this.ShowPopover();
return true;
}
public Popover GetPopover() => new TagsOverflowPopover(tags, songSelect);
}
public partial class TagsOverflowPopover : OsuPopover
{
private readonly string[] tags;
private readonly SongSelect? songSelect;
public TagsOverflowPopover(string[] tags, SongSelect? songSelect)
{
this.tags = tags;
this.songSelect = songSelect;
}
[BackgroundDependencyLoader]
private void load()
{
LinkFlowContainer textFlow;
Child = textFlow = new LinkFlowContainer(t => t.Font = OsuFont.Style.Caption1)
{
Width = 200,
AutoSizeAxes = Axes.Y,
};
foreach (string tag in tags)
{
textFlow.AddLink(tag, () => songSelect?.Search(tag));
textFlow.AddText(" ");
}
}
}
}
}
}
@@ -0,0 +1,130 @@
// 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.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapMetadataWedge
{
private partial class UserRatingDisplay : CompositeDrawable
{
private readonly OsuSpriteText negativeText;
private readonly OsuSpriteText positiveText;
private readonly Circle backgroundBar;
private readonly Circle positiveBar;
public int[] Data
{
set
{
const int rating_range = 10;
if (!value.Any())
{
negativeText.Text = 0.ToLocalisableString(@"N0");
positiveText.Text = 0.ToLocalisableString(@"N0");
positiveBar.ResizeWidthTo(0, 300, Easing.OutQuint);
}
else
{
var usableRange = value.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0.
int positiveCount = usableRange.Skip(rating_range / 2).Sum();
int totalCount = usableRange.Sum();
negativeText.Text = (totalCount - positiveCount).ToLocalisableString(@"N0");
positiveText.Text = positiveCount.ToLocalisableString(@"N0");
positiveBar.ResizeWidthTo(totalCount == 0 ? 0 : (float)positiveCount / totalCount, 300, Easing.OutQuint);
}
}
}
public UserRatingDisplay()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 2f),
Children = new Drawable[]
{
new OsuSpriteText
{
Text = BeatmapsetsStrings.ShowStatsUserRating,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10f },
Children = new[]
{
negativeText = new OsuSpriteText
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Font = OsuFont.Style.Caption1,
},
positiveText = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.Style.Caption1,
},
},
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new[]
{
backgroundBar = new Circle
{
RelativeSizeAxes = Axes.X,
Height = 4f,
},
positiveBar = new Circle
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
Width = 0f,
Height = 4f,
},
},
}
},
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, OverlayColourProvider colourProvider)
{
backgroundBar.Colour = colours.DarkOrange2;
positiveBar.Colour = colours.Lime1;
negativeText.Colour = colourProvider.Content2;
positiveText.Colour = colourProvider.Content2;
}
}
}
}
@@ -0,0 +1,327 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
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.Mods;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapTitleWedge : VisibilityContainer
{
private const float corner_radius = 10;
[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!;
protected override bool StartHidden => true;
private ModSettingChangeTracker? settingChangeTracker;
private BeatmapSetOnlineStatusPill statusPill = null!;
private Container titleContainer = null!;
private OsuHoverContainer titleLink = null!;
private OsuSpriteText titleLabel = null!;
private Container artistContainer = null!;
private OsuHoverContainer artistLink = null!;
private OsuSpriteText artistLabel = null!;
internal string DisplayedTitle => titleLabel.Text.ToString();
internal string DisplayedArtist => artistLabel.Text.ToString();
private StatisticPlayCount playCount = null!;
private Statistic favouritesStatistic = null!;
private Statistic lengthStatistic = null!;
private Statistic bpmStatistic = null!;
[Resolved]
private SongSelect? songSelect { get; set; }
[Resolved]
private LocalisationManager localisation { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private APIBeatmapSet? currentOnlineBeatmapSet;
private GetBeatmapSetRequest? currentRequest;
private FillFlowContainer statisticsFlow = null!;
public BeatmapTitleWedge()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load()
{
Shear = OsuGame.SHEAR;
Masking = true;
CornerRadius = corner_radius;
InternalChildren = new Drawable[]
{
new WedgeBackground(),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = new MarginPadding
{
Top = SongSelect.WEDGE_CONTENT_MARGIN,
Left = SongSelect.WEDGE_CONTENT_MARGIN
},
Spacing = new Vector2(0f, 4f),
Children = new Drawable[]
{
new ShearAligningWrapper(statusPill = new BeatmapSetOnlineStatusPill
{
Shear = -OsuGame.SHEAR,
ShowUnknownStatus = true,
TextSize = OsuFont.Style.Caption1.Size,
TextPadding = new MarginPadding { Horizontal = 6, Vertical = 1 },
}),
new ShearAligningWrapper(titleContainer = new Container
{
Shear = -OsuGame.SHEAR,
RelativeSizeAxes = Axes.X,
Height = OsuFont.Style.Title.Size,
Margin = new MarginPadding { Bottom = -4f },
Child = titleLink = new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
Child = titleLabel = new TruncatingSpriteText
{
Shadow = true,
Font = OsuFont.Style.Title,
},
}
}),
new ShearAligningWrapper(artistContainer = new Container
{
Shear = -OsuGame.SHEAR,
RelativeSizeAxes = Axes.X,
Height = OsuFont.Style.Heading2.Size,
Margin = new MarginPadding { Left = 1f },
Child = artistLink = new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
Child = artistLabel = new TruncatingSpriteText
{
Shadow = true,
Font = OsuFont.Style.Heading2,
},
}
}),
new ShearAligningWrapper(statisticsFlow = new FillFlowContainer
{
Shear = -OsuGame.SHEAR,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2f, 0f),
Children = new Drawable[]
{
playCount = new StatisticPlayCount(background: true, leftPadding: SongSelect.WEDGE_CONTENT_MARGIN, minSize: 50f)
{
Margin = new MarginPadding { Left = -SongSelect.WEDGE_CONTENT_MARGIN },
},
favouritesStatistic = new Statistic(OsuIcon.Heart, background: true, minSize: 25f)
{
TooltipText = BeatmapsStrings.StatusFavourites,
},
lengthStatistic = new Statistic(OsuIcon.Clock),
bpmStatistic = new Statistic(OsuIcon.Metronome)
{
TooltipText = BeatmapsetsStrings.ShowStatsBpm,
Margin = new MarginPadding { Left = 5f },
},
},
}),
new ShearAligningWrapper(new Container
{
Shear = -OsuGame.SHEAR,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Left = -SongSelect.WEDGE_CONTENT_MARGIN },
Padding = new MarginPadding { Right = -SongSelect.WEDGE_CONTENT_MARGIN },
Child = new DifficultyDisplay(),
}),
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.BindValueChanged(_ => updateDisplay());
ruleset.BindValueChanged(_ => updateDisplay());
mods.BindValueChanged(m =>
{
settingChangeTracker?.Dispose();
updateLengthAndBpmStatistics();
settingChangeTracker = new ModSettingChangeTracker(m.NewValue);
settingChangeTracker.SettingChanged += _ => updateLengthAndBpmStatistics();
});
updateDisplay();
statisticsFlow.AutoSizeDuration = 100;
statisticsFlow.AutoSizeEasing = Easing.OutQuint;
}
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);
}
protected override void Update()
{
base.Update();
titleLabel.MaxWidth = titleContainer.DrawWidth - 20;
artistLabel.MaxWidth = artistContainer.DrawWidth - 20;
}
private void updateDisplay()
{
var metadata = beatmap.Value.Metadata;
var beatmapInfo = beatmap.Value.BeatmapInfo;
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
statusPill.Status = beatmapInfo.Status;
var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title);
titleLabel.Text = titleText;
titleLink.Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript));
var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
artistLabel.Text = artistText;
artistLink.Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript));
updateLengthAndBpmStatistics();
if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID)
refetchBeatmapSet();
updateOnlineDisplay();
}
private void updateLengthAndBpmStatistics()
{
var beatmapInfo = beatmap.Value.BeatmapInfo;
double rate = ModUtils.CalculateRateWithMods(mods.Value);
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);
double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate);
double hitLength = Math.Round(beatmapInfo.Length / rate);
lengthStatistic.Text = hitLength.ToFormattedDuration();
lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration());
bpmStatistic.Text = bpmMin == bpmMax
? $"{bpmMin}"
: $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})";
}
private void refetchBeatmapSet()
{
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
currentRequest?.Cancel();
currentRequest = null;
currentOnlineBeatmapSet = null;
if (beatmapSetInfo.OnlineID >= 1)
{
// todo: consider introducing a BeatmapSetLookupCache for caching benefits.
currentRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID);
currentRequest.Failure += _ => updateOnlineDisplay();
currentRequest.Success += s =>
{
currentOnlineBeatmapSet = s;
updateOnlineDisplay();
};
api.Queue(currentRequest);
}
}
private void updateOnlineDisplay()
{
if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting)
{
playCount.Value = null;
favouritesStatistic.Text = null;
}
else if (currentOnlineBeatmapSet == null)
{
playCount.Value = new StatisticPlayCount.Data(-1, -1);
favouritesStatistic.Text = "-";
}
else
{
var onlineBeatmapSet = currentOnlineBeatmapSet;
var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.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);
favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0");
}
}
}
}
@@ -0,0 +1,380 @@
// 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.Extensions;
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.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapTitleWedge
{
public partial class DifficultyDisplay : CompositeDrawable
{
private const float border_weight = 2;
[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!;
private ModSettingChangeTracker? settingChangeTracker;
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
private StarRatingDisplay starRatingDisplay = null!;
private FillFlowContainer nameLine = null!;
private OsuSpriteText difficultyText = null!;
private OsuSpriteText mappedByText = null!;
private OsuHoverContainer mapperLink = null!;
private OsuSpriteText mapperText = null!;
internal LocalisableString DisplayedVersion => difficultyText.Text;
internal LocalisableString DisplayedAuthor => mapperText.Text;
private GridContainer ratingAndNameContainer = null!;
private DifficultyStatisticsDisplay countStatisticsDisplay = null!;
private AdjustableDifficultyStatisticsDisplay difficultyStatisticsDisplay = null!;
private CancellationTokenSource? cancellationSource;
public DifficultyDisplay()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Masking = true;
CornerRadius = 10;
Shear = OsuGame.SHEAR;
InternalChildren = new Drawable[]
{
new WedgeBackground(),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new ShearAligningWrapper(ratingAndNameContainer = new GridContainer
{
Shear = -OsuGame.SHEAR,
AlwaysPresent = true,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Vertical = 5f },
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 6),
new Dimension(),
},
Content = new[]
{
new[]
{
starRatingDisplay = new StarRatingDisplay(default, animated: true)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
Empty(),
nameLine = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Bottom = 2f },
Children = new Drawable[]
{
difficultyText = new TruncatingSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
},
mappedByText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Text = " mapped by ",
Font = OsuFont.Style.Body,
},
mapperLink = new MapperLinkContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Child = mapperText = new TruncatingSpriteText
{
Shadow = true,
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
},
},
},
},
}
},
}),
new ShearAligningWrapper(new Container
{
Shear = -OsuGame.SHEAR,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Bottom = border_weight, Right = border_weight },
Child = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Masking = true,
CornerRadius = 10 - border_weight,
Shear = OsuGame.SHEAR,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5.Opacity(0.8f),
},
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 20f, Vertical = 7.5f },
Shear = -OsuGame.SHEAR,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 30),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new[]
{
countStatisticsDisplay = new DifficultyStatisticsDisplay
{
RelativeSizeAxes = Axes.X,
},
Empty(),
difficultyStatisticsDisplay = new AdjustableDifficultyStatisticsDisplay(autoSize: true),
}
},
}
},
}
}),
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.BindValueChanged(_ => updateDisplay());
ruleset.BindValueChanged(_ => updateDisplay());
mods.BindValueChanged(m =>
{
settingChangeTracker?.Dispose();
updateDifficultyStatistics();
settingChangeTracker = new ModSettingChangeTracker(m.NewValue);
settingChangeTracker.SettingChanged += _ => updateDifficultyStatistics();
});
updateDisplay();
}
[Resolved]
private ILinkHandler? linkHandler { get; set; }
private void updateDisplay()
{
cancellationSource?.Cancel();
cancellationSource = new CancellationTokenSource();
computeStarDifficulty(cancellationSource.Token);
if (beatmap.IsDefault)
{
ratingAndNameContainer.FadeOut(300, Easing.OutQuint);
countStatisticsDisplay.Statistics = Array.Empty<StatisticDifficulty.Data>();
}
else
{
ratingAndNameContainer.FadeIn(300, Easing.OutQuint);
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();
}
updateDifficultyStatistics();
}
private void updateDifficultyStatistics() => Scheduler.AddOnce(() =>
{
if (beatmap.IsDefault)
{
difficultyStatisticsDisplay.TooltipContent = null;
difficultyStatisticsDisplay.Statistics = Array.Empty<StatisticDifficulty.Data>();
return;
}
BeatmapDifficulty baseDifficulty = beatmap.Value.BeatmapInfo.Difficulty;
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(baseDifficulty);
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(originalDifficulty);
var rateAdjustedDifficulty = originalDifficulty;
if (ruleset.Value != null)
{
double rate = ModUtils.CalculateRateWithMods(mods.Value);
rateAdjustedDifficulty = ruleset.Value.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
difficultyStatisticsDisplay.TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, rateAdjustedDifficulty);
}
StatisticDifficulty.Data firstStatistic;
switch (ruleset.Value?.OnlineID)
{
case 3:
// Account for mania differences locally for now.
// Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes.
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
// For the time being, the key count is static no matter what, because:
// a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering.
// b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion.
int keyCount = legacyRuleset.GetKeyCount(beatmap.Value.BeatmapInfo, mods.Value);
firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCsMania, keyCount, keyCount, 10);
break;
default:
firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCs, baseDifficulty.CircleSize, rateAdjustedDifficulty.CircleSize, 10);
break;
}
difficultyStatisticsDisplay.Statistics = new[]
{
firstStatistic,
new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAccuracy, baseDifficulty.OverallDifficulty, rateAdjustedDifficulty.OverallDifficulty, 10),
new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsDrain, baseDifficulty.DrainRate, rateAdjustedDifficulty.DrainRate, 10),
new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAr, baseDifficulty.ApproachRate, rateAdjustedDifficulty.ApproachRate, 10),
};
});
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();
difficultyText.MaxWidth = Math.Max(nameLine.DrawWidth - mappedByText.DrawWidth - mapperText.DrawWidth - 20, 0);
// Use difficulty colour until it gets too dark to be visible against dark backgrounds.
Color4 col = starRatingDisplay.DisplayedStars.Value >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : starRatingDisplay.DisplayedDifficultyColour;
difficultyText.Colour = col;
mappedByText.Colour = col;
countStatisticsDisplay.AccentColour = col;
difficultyStatisticsDisplay.AccentColour = col;
}
private partial class MapperLinkContainer : OsuHoverContainer
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours)
{
TooltipText = ContextMenuStrings.ViewProfile;
IdleColour = overlayColourProvider?.Light2 ?? colours.Blue;
}
}
private partial class AdjustableDifficultyStatisticsDisplay : DifficultyStatisticsDisplay, IHasCustomTooltip<AdjustedAttributesTooltip.Data>
{
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public ITooltip<AdjustedAttributesTooltip.Data> GetCustomTooltip() => new AdjustedAttributesTooltip(colourProvider);
public AdjustedAttributesTooltip.Data? TooltipContent { get; set; }
public AdjustableDifficultyStatisticsDisplay(bool autoSize)
: base(autoSize)
{
}
}
}
}
}
@@ -0,0 +1,209 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapTitleWedge
{
public partial class DifficultyStatisticsDisplay : CompositeDrawable
{
private readonly bool autoSize;
private readonly FillFlowContainer<StatisticDifficulty> statisticsFlow;
private readonly GridContainer tinyStatisticsGrid;
private IReadOnlyList<StatisticDifficulty.Data> statistics = Array.Empty<StatisticDifficulty.Data>();
public IReadOnlyList<StatisticDifficulty.Data> Statistics
{
get => statistics;
set
{
statistics = value;
if (IsLoaded)
{
updateStatistics();
updateTinyStatistics();
}
}
}
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
if (accentColour == value)
return;
accentColour = value;
foreach (var statistic in statisticsFlow)
statistic.AccentColour = value;
}
}
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public DifficultyStatisticsDisplay(bool autoSize = false)
{
this.autoSize = autoSize;
if (autoSize)
AutoSizeAxes = Axes.Both;
else
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
statisticsFlow = new FillFlowContainer<StatisticDifficulty>
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(8f, 0f),
Direction = FillDirection.Horizontal,
AlwaysPresent = true,
},
tinyStatisticsGrid = new GridContainer
{
Alpha = 0f,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 8),
new Dimension(GridSizeMode.AutoSize),
}
},
};
AddLayout(drawSizeLayout);
}
[Resolved]
private LocalisationManager localisations { get; set; } = null!;
private IBindable<LocalisationParameters>? localisationParameters;
protected override void LoadComplete()
{
base.LoadComplete();
localisationParameters = localisations.CurrentParameters.GetBoundCopy();
localisationParameters.BindValueChanged(_ => updateStatisticsSizing());
updateStatistics();
updateTinyStatistics();
}
protected override void Update()
{
base.Update();
if (!drawSizeLayout.IsValid)
{
updateLayout();
drawSizeLayout.Validate();
}
}
private bool displayedTinyStatistics;
private void updateLayout()
{
if (statisticsFlow.Count == 0)
return;
float flowWidth = statisticsFlow[0].Width * statisticsFlow.Count + statisticsFlow.Spacing.X * (statisticsFlow.Count - 1);
bool tiny = !autoSize && DrawWidth < flowWidth;
if (displayedTinyStatistics != tiny)
{
if (tiny)
{
statisticsFlow.Hide();
tinyStatisticsGrid.FadeIn(200, Easing.InQuint);
}
else
{
tinyStatisticsGrid.Hide();
statisticsFlow.FadeIn(200, Easing.InQuint);
}
displayedTinyStatistics = tiny;
}
}
private void updateStatisticsSizing() => SchedulerAfterChildren.AddOnce(() =>
{
if (statisticsFlow.Count == 0)
return;
float statisticWidth = Math.Max(65, statisticsFlow.Max(s => s.LabelWidth));
foreach (var statistic in statisticsFlow)
statistic.Width = statisticWidth;
drawSizeLayout.Invalidate();
});
private void updateStatistics()
{
if (statisticsFlow.Select(s => s.Value.Label)
.SequenceEqual(statistics.Select(s => s.Label)))
{
for (int i = 0; i < statistics.Count; i++)
statisticsFlow[i].Value = statistics[i];
}
else
{
statisticsFlow.ChildrenEnumerable = statistics.Select(d => new StatisticDifficulty { Value = d });
updateStatisticsSizing();
}
}
private void updateTinyStatistics()
{
tinyStatisticsGrid.RowDimensions = statistics.Select(_ => new Dimension(GridSizeMode.AutoSize)).ToArray();
tinyStatisticsGrid.Content = statistics.Select(s => new[]
{
new OsuSpriteText
{
Text = s.Label,
Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold),
Colour = colourProvider.Content2,
},
Empty(),
new OsuSpriteText
{
Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold),
Text = s.Content ?? s.Value.ToLocalisableString("0.##"),
Colour = colourProvider.Content1,
},
}).ToArray();
}
}
}
}
@@ -0,0 +1,158 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapTitleWedge
{
public partial class Statistic : CompositeDrawable, IHasTooltip
{
private readonly IconUsage icon;
private readonly bool background;
private readonly float leftPadding;
private readonly float? minSize;
private OsuSpriteText valueText = null!;
private LoadingSpinner loading = null!;
private LocalisableString? text;
public LocalisableString? Text
{
get => text;
set
{
text = value;
Scheduler.AddOnce(updateDisplay);
}
}
public LocalisableString TooltipText { get; set; }
public Statistic(IconUsage icon, bool background = false, float leftPadding = 10f, float? minSize = null)
{
this.icon = icon;
this.background = background;
this.leftPadding = leftPadding;
this.minSize = minSize;
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Masking = true;
CornerRadius = 5;
Shear = background ? OsuGame.SHEAR : Vector2.Zero;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = background ? 0.2f : 0f,
},
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Left = background ? leftPadding : 0, Right = background ? 10f : 0f, Vertical = 5f },
Spacing = new Vector2(4f, 0f),
Shear = background ? -OsuGame.SHEAR : Vector2.Zero,
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = icon,
Size = new Vector2(OsuFont.Style.Heading2.Size),
Colour = colourProvider.Content2,
},
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.X,
Height = 20,
Children = new Drawable[]
{
loading = new LoadingSpinner
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(14f),
State = { Value = Visibility.Visible },
},
new GridContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize, minSize: minSize ?? 0),
},
Content = new[]
{
new[]
{
valueText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Style.Heading2,
Colour = colourProvider.Content2,
Margin = new MarginPadding { Bottom = 2f },
AlwaysPresent = true,
},
}
}
},
},
},
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Scheduler.AddOnce(updateDisplay);
}
private void updateDisplay()
{
loading.State.Value = text != null ? Visibility.Hidden : Visibility.Visible;
if (text != null)
{
valueText.Text = text.Value;
valueText.FadeIn(120, Easing.OutQuint);
}
else
valueText.FadeOut(120, Easing.OutQuint);
}
}
}
}
@@ -0,0 +1,196 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapTitleWedge
{
public partial class StatisticDifficulty : CompositeDrawable, IHasAccentColour
{
private Data value = new Data(string.Empty, 0, 0, 0);
public Data Value
{
get => value;
set
{
this.value = value;
if (IsLoaded)
updateDisplay();
}
}
public float LabelWidth => labelText.DrawWidth;
private readonly Circle bar;
private readonly Circle adjustedBar;
private readonly OsuSpriteText labelText;
private readonly OsuSpriteText valueText;
private readonly SpriteIcon valueIcon;
private readonly Container bars;
public Color4 AccentColour
{
get => bar.Colour;
set => bar.Colour = value;
}
[Resolved]
private OsuColour colours { get; set; } = null!;
public StatisticDifficulty()
{
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
bars = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new[]
{
new Circle
{
RelativeSizeAxes = Axes.X,
Height = 2f,
Colour = Color4.Black,
Masking = true,
CornerRadius = 1f,
Depth = float.MaxValue,
},
bar = new Circle
{
RelativeSizeAxes = Axes.X,
Width = 0f,
Height = 2f,
Masking = true,
CornerRadius = 1f,
},
adjustedBar = new Circle
{
RelativeSizeAxes = Axes.X,
Width = 0f,
Height = 2f,
Masking = true,
CornerRadius = 1f,
},
},
},
labelText = new OsuSpriteText
{
Margin = new MarginPadding { Top = 2f },
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
valueText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.Style.Body,
},
valueIcon = new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding
{
Top = -4f,
Left = 2,
},
Size = new Vector2(8),
}
},
},
},
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
labelText.Colour = colourProvider.Content2;
valueText.Colour = colourProvider.Content1;
}
protected override void LoadComplete()
{
base.LoadComplete();
updateDisplay();
}
private void updateDisplay()
{
bar.ResizeWidthTo(value.Maximum == 0 ? 0 : Math.Clamp(value.Value / value.Maximum, 0, 1), 300, Easing.OutQuint);
adjustedBar.ResizeWidthTo(value.Maximum == 0 ? 0 : Math.Clamp(value.AdjustedValue / value.Maximum, 0, 1), 300, Easing.OutQuint);
labelText.Text = value.Label;
valueText.Text = value.Content ?? value.AdjustedValue.ToLocalisableString("0.##");
if (value.Value == value.AdjustedValue)
{
adjustedBar.FadeColour(Color4.Transparent, 300, Easing.OutQuint);
bar.FadeIn(300, Easing.OutQuint);
valueText.FadeColour(Color4.White, 300, Easing.OutQuint);
valueIcon.Hide();
}
else
{
bool difficultyIncrease = value.Value < value.AdjustedValue;
if (difficultyIncrease)
{
bars.ChangeChildDepth(adjustedBar, 1);
bar.FadeIn(300, Easing.OutQuint);
adjustedBar.FadeColour(ColourInfo.GradientHorizontal(Color4.Black, colours.Red1), 300, Easing.OutQuint);
valueText.FadeColour(colours.Red1, 300, Easing.OutQuint);
valueIcon.Show();
valueIcon.Colour = colours.Red1;
valueIcon.Icon = FontAwesome.Solid.SortUp;
}
else
{
bar.FadeTo(0.5f, 300, Easing.OutQuint);
bars.ChangeChildDepth(adjustedBar, -1);
adjustedBar.FadeColour(colours.Lime1, 300, Easing.OutQuint);
valueText.FadeColour(colours.Lime1, 300, Easing.OutQuint);
valueIcon.Show();
valueIcon.Colour = colours.Lime1;
valueIcon.Icon = FontAwesome.Solid.SortDown;
}
}
}
public record Data(LocalisableString Label, float Value, float AdjustedValue, float Maximum, string? Content = null);
}
}
}
@@ -0,0 +1,151 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
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.Sprites;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapTitleWedge
{
public partial class StatisticPlayCount : Statistic, IHasCustomTooltip<StatisticPlayCount.Data>
{
public Data? Value
{
set
{
base.Text = value?.Total < 0 ? "-" : value?.Total.ToLocalisableString("N0");
TooltipContent = value;
}
}
public new LocalisableString? Text
{
set => throw new InvalidOperationException($"Use {nameof(Value)} instead.");
}
public Data? TooltipContent { get; private set; }
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public StatisticPlayCount(bool background = false, float leftPadding = 10, float? minSize = null)
: base(OsuIcon.Play, background, leftPadding, minSize)
{
}
ITooltip<Data> IHasCustomTooltip<Data>.GetCustomTooltip() => new PlayCountTooltip(colourProvider);
public record Data(int Total, int User);
private partial class PlayCountTooltip : VisibilityContainer, ITooltip<Data>
{
private readonly OverlayColourProvider colourProvider;
private OsuSpriteText totalPlaysText = null!;
private OsuSpriteText personalPlaysText = null!;
public PlayCountTooltip(OverlayColourProvider colourProvider)
{
this.colourProvider = colourProvider;
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
CornerRadius = 10;
Masking = true;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.25f),
Radius = 10f,
};
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding(10),
Direction = FillDirection.Horizontal,
Spacing = new Vector2(16f, 0f),
Children = new[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new[]
{
new OsuSpriteText
{
Colour = colourProvider.Content2,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
Text = "Total Plays",
},
totalPlaysText = new OsuSpriteText
{
Colour = colourProvider.Content1,
Font = OsuFont.Style.Heading1.With(weight: FontWeight.Regular),
},
}
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new[]
{
new OsuSpriteText
{
Colour = colourProvider.Content2,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
Text = "Personal Plays",
},
personalPlaysText = new OsuSpriteText
{
Colour = colourProvider.Content1,
Font = OsuFont.Style.Heading1.With(weight: FontWeight.Regular),
},
}
},
}
},
};
}
public void SetContent(Data content)
{
totalPlaysText.Text = content.Total < 0 ? "-" : content.Total.ToLocalisableString("N0");
personalPlaysText.Text = content.User < 0 ? "-" : content.User.ToLocalisableString("N0");
}
public void Move(Vector2 pos) => Position = pos;
protected override void PopIn() => this.FadeIn(300, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(300, Easing.OutQuint);
}
}
}
}
+1 -1
View File
@@ -236,7 +236,7 @@ namespace osu.Game.Screens.SelectV2
starRatingDisplay.Current.Value = starDifficulty;
starCounter.Current = (float)starDifficulty.Stars;
difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars);
starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint);
@@ -270,7 +270,7 @@ namespace osu.Game.Screens.SelectV2
var starDifficulty = starDifficultyBindable?.Value ?? default;
AccentColour = colours.ForStarDifficulty(starDifficulty.Stars);
difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
difficultyStarRating.Current.Value = starDifficulty;
}
}
+32
View File
@@ -14,6 +14,7 @@ using osu.Game.Overlays;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Game.Graphics.Containers;
@@ -22,8 +23,10 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Users.Drawables;
@@ -80,6 +83,9 @@ namespace osu.Game.Users
[Resolved]
private MetadataClient? metadataClient { get; set; }
[Resolved]
private INotificationOverlay? notifications { get; set; }
[BackgroundDependencyLoader]
private void load()
{
@@ -157,6 +163,10 @@ namespace osu.Game.Users
chatOverlay?.Show();
}));
items.Add(isUserBlocked()
? new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => toggleBlock(false))
: new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => toggleBlock(true)));
if (isUserOnline())
{
items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () =>
@@ -179,9 +189,31 @@ namespace osu.Game.Users
bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null;
bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true;
bool isUserBlocked() => api.Blocks.Any(b => b.TargetID == User.OnlineID);
}
}
private void toggleBlock(bool block)
{
APIRequest req = block ? new BlockUserRequest(User.OnlineID) : new UnblockUserRequest(User.OnlineID);
req.Success += () =>
{
api.UpdateLocalBlocks();
};
req.Failure += e =>
{
notifications?.Post(new SimpleNotification
{
Text = e.Message,
Icon = FontAwesome.Solid.Times,
});
};
api.Queue(req);
}
public IEnumerable<LocalisableString> FilterTerms => [User.Username];
public bool MatchingFilter