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

Merge branch 'master' into stable-hitwindow-tests

This commit is contained in:
Bartłomiej Dach
2025-04-17 11:40:50 +02:00
committed by GitHub
Unverified
112 changed files with 1441 additions and 1478 deletions
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -16,26 +17,30 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
int fruits = HitObjects.Count(s => s is Fruit);
int juiceStreams = HitObjects.Count(s => s is JuiceStream);
int bananaShowers = HitObjects.Count(s => s is BananaShower);
int sum = Math.Max(1, fruits + juiceStreams);
return new[]
{
new BeatmapStatistic
{
Name = @"Fruit Count",
Name = @"Fruits",
Content = fruits.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
BarDisplayLength = fruits / (float)sum,
},
new BeatmapStatistic
{
Name = @"Juice Stream Count",
Name = @"Juice Streams",
Content = juiceStreams.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
BarDisplayLength = juiceStreams / (float)sum,
},
new BeatmapStatistic
{
Name = @"Banana Shower Count",
Name = @"Banana Showers",
Content = bananaShowers.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
BarDisplayLength = Math.Min(bananaShowers / 10f, 1),
}
};
}
@@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 5f, -19d, HitResult.Perfect },
new object[] { 5f, -19.2d, HitResult.Perfect },
new object[] { 5f, -19.38d, HitResult.Perfect },
// new object[] { 5f, -19.4d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
new object[] { 5f, -19.44d, HitResult.Great },
new object[] { 5f, -19.7d, HitResult.Great },
new object[] { 5f, -20d, HitResult.Great },
@@ -73,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 9.3f, 14d, HitResult.Perfect },
new object[] { 9.3f, 14.2d, HitResult.Perfect },
new object[] { 9.3f, 14.6d, HitResult.Perfect },
// new object[] { 9.3f, 14.67d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
new object[] { 9.3f, 14.7d, HitResult.Great },
new object[] { 9.3f, 15d, HitResult.Great },
new object[] { 9.3f, 35d, HitResult.Great },
@@ -36,20 +36,23 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
int notes = HitObjects.Count(s => s is Note);
int holdNotes = HitObjects.Count(s => s is HoldNote);
int sum = Math.Max(1, notes + holdNotes);
return new[]
{
new BeatmapStatistic
{
Name = @"Note Count",
Name = @"Notes",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = notes.ToString(),
BarDisplayLength = notes / (float)sum,
},
new BeatmapStatistic
{
Name = @"Hold Note Count",
Name = @"Hold Notes",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = holdNotes.ToString(),
BarDisplayLength = holdNotes / (float)sum,
},
};
}
+8 -4
View File
@@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
@@ -16,26 +16,30 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
int circles = HitObjects.Count(c => c is HitCircle);
int sliders = HitObjects.Count(s => s is Slider);
int spinners = HitObjects.Count(s => s is Spinner);
int sum = Math.Max(1, circles + sliders);
return new[]
{
new BeatmapStatistic
{
Name = BeatmapsetsStrings.ShowStatsCountCircles,
Name = "Circles",
Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
BarDisplayLength = circles / (float)sum,
},
new BeatmapStatistic
{
Name = BeatmapsetsStrings.ShowStatsCountSliders,
Name = "Sliders",
Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
BarDisplayLength = sliders / (float)sum,
},
new BeatmapStatistic
{
Name = @"Spinner Count",
Name = @"Spinners",
Content = spinners.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
BarDisplayLength = Math.Min(spinners / 10f, 1),
}
};
}
+1 -1
View File
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
var spinner = (DrawableSpinner)drawable;
spinner.RotationTracker.Tracking = true;
spinner.RotationTracker.Tracking = spinner.RotationTracker.IsSpinnableTime;
// early-return if we were paused to avoid division-by-zero in the subsequent calculations.
if (Precision.AlmostEquals(spinner.Clock.Rate, 0))
@@ -277,13 +277,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.Update();
if (HandleUserInput)
{
bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime;
RotationTracker.Tracking = !Result.HasResult
&& correctButtonPressed()
&& isValidSpinningTime;
}
RotationTracker.Tracking = RotationTracker.IsSpinnableTime && !Result.HasResult && correctButtonPressed();
if (spinningSample != null && spinnerFrequencyModulate)
spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// <summary>
/// Whether currently in the correct time range to allow spinning.
/// </summary>
private bool isSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current;
public bool IsSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current;
protected override bool OnMouseMove(MouseMoveEvent e)
{
@@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
lastAngle = thisAngle;
}
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
IsSpinning.Value = IsSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed));
}
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// <param name="delta">The delta angle.</param>
public void AddRotation(float delta)
{
if (!isSpinnableTime)
if (!IsSpinnableTime)
return;
if (!rotationTransferred)
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -15,26 +16,30 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
int hits = HitObjects.Count(s => s is Hit);
int drumRolls = HitObjects.Count(s => s is DrumRoll);
int swells = HitObjects.Count(s => s is Swell);
int sum = Math.Max(1, hits + drumRolls);
return new[]
{
new BeatmapStatistic
{
Name = @"Hit Count",
Name = @"Hits",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = hits.ToString(),
BarDisplayLength = hits / (float)sum,
},
new BeatmapStatistic
{
Name = @"Drumroll Count",
Name = @"Drumrolls",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = drumRolls.ToString(),
BarDisplayLength = drumRolls / (float)sum,
},
new BeatmapStatistic
{
Name = @"Swell Count",
Name = @"Swells",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
Content = swells.ToString(),
BarDisplayLength = Math.Min(swells / 10f, 1),
}
};
}
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Shapes;
@@ -112,5 +113,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableHitObject.IsNotNull())
drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@@ -202,5 +203,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
.Then()
.FadeEdgeEffectTo(edge_alpha_kiai, duration, Easing.OutQuint);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableHitObject.IsNotNull())
drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -17,12 +17,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
internal partial class LegacyKiaiGlow : BeatSyncedContainer
{
private bool isKiaiActive;
[Resolved]
private HealthProcessor? healthProcessor { get; set; }
private bool isKiaiActive;
private Sprite sprite = null!;
[BackgroundDependencyLoader(true)]
private void load(ISkinSource skin, HealthProcessor? healthProcessor)
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
Child = sprite = new Sprite
{
@@ -33,6 +35,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
Scale = new Vector2(TaikoLegacyHitTarget.SCALE),
Colour = new Colour4(255, 228, 0, 255),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
if (healthProcessor != null)
healthProcessor.NewJudgement += onNewJudgement;
@@ -61,5 +68,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then()
.ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (healthProcessor != null)
healthProcessor.NewJudgement -= onNewJudgement;
}
}
}
@@ -19,6 +19,8 @@ namespace osu.Game.Tests.Visual.Beatmaps
{
public partial class TestSceneBeatmapSetOnlineStatusPill : ThemeComparisonTestScene
{
private bool showUnknownStatus;
protected override Drawable CreateContent() => new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@@ -26,12 +28,20 @@ namespace osu.Game.Tests.Visual.Beatmaps
Origin = Anchor.Centre,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast<BeatmapOnlineStatus>().Select(status => new BeatmapSetOnlineStatusPill
ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast<BeatmapOnlineStatus>().Select(status => new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Status = status
RelativeSizeAxes = Axes.X,
Height = 20,
Children = new Drawable[]
{
new BeatmapSetOnlineStatusPill
{
ShowUnknownStatus = showUnknownStatus,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Status = status
}
}
})
};
@@ -48,6 +58,12 @@ namespace osu.Game.Tests.Visual.Beatmaps
pill.Width = 90;
}));
AddStep("toggle show unknown", () =>
{
showUnknownStatus = !showUnknownStatus;
CreateThemedContent(OverlayColourScheme.Red);
});
AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
}
@@ -65,11 +81,6 @@ namespace osu.Game.Tests.Visual.Beatmaps
pill.Status = BeatmapOnlineStatus.LocallyModified;
break;
// skip none
case BeatmapOnlineStatus.LocallyModified:
pill.Status = BeatmapOnlineStatus.Graveyard;
break;
default:
pill.Status = (pill.Status + 1);
break;
@@ -15,7 +15,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.SelectV2.Leaderboards;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Tests.Visual.OnlinePlay;
@@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddStep("force transforms to finish", () => FinishTransforms(true));
AddStep("right click second score", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<LeaderboardScoreV2>().ElementAt(1));
InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapLeaderboardScore>().ElementAt(1));
InputManager.Click(MouseButton.Right);
});
AddAssert("use these mods not present",
@@ -105,6 +105,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
[Test]
public void TestMarkCompleted()
{
createPlaylist();
AddStep("mark some items as complete", () =>
{
playlist.Items[0].MarkCompleted();
playlist.Items[2].MarkCompleted();
playlist.Items[3].MarkCompleted();
playlist.Items[5].MarkCompleted();
});
}
[Test]
public void TestSelectable()
{
@@ -215,6 +215,32 @@ namespace osu.Game.Tests.Visual.Online
});
}
[Test]
public void TestChannelCloseViaMiddleClick()
{
var testPMChannel = new Channel(testUser);
AddStep("Show overlay", () => chatOverlay.Show());
joinTestChannel(0);
joinChannel(testPMChannel);
AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
AddStep("Middle click", () =>
{
var item = getChannelListItem(testPMChannel);
InputManager.MoveMouseTo(item);
InputManager.Click(MouseButton.Middle);
});
AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel));
AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Click close button", () =>
{
var item = getChannelListItem(testChannel1);
InputManager.MoveMouseTo(item);
InputManager.Click(MouseButton.Middle);
});
AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1));
}
[Test]
public void TestChannelCloseButton()
{
@@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.SongSelect
foreach (var rulesetInfo in rulesets.AvailableRulesets)
{
var instance = rulesetInfo.CreateInstance();
var testBeatmap = createTestBeatmap(rulesetInfo);
var testBeatmap = CreateTestBeatmap(rulesetInfo);
beatmaps.Add(testBeatmap);
@@ -124,6 +124,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("reset mods", () => SelectedMods.SetDefault());
}
[Test]
public void TestTruncation()
{
selectBeatmap(CreateLongMetadata());
}
[Test]
public void TestNullBeatmap()
{
@@ -135,17 +141,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
}
[Test]
public void TestTruncation()
{
selectBeatmap(createLongMetadata());
}
[Test]
public void TestBPMUpdates()
{
const double bpm = 120;
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
OsuModDoubleTime doubleTime = null!;
@@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestCase(120, 120.4, "DT", "180")]
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
{
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
IBeatmap beatmap = 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 });
@@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestCase]
public void TestLengthUpdates()
{
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
double drain = beatmap.CalculateDrainLength();
beatmap.BeatmapInfo.Length = drain;
@@ -248,7 +248,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
public static IBeatmap CreateTestBeatmap(RulesetInfo ruleset)
{
List<HitObject> objects = new List<HitObject>();
for (double i = 0; i < 50000; i += 1000)
@@ -274,7 +274,7 @@ namespace osu.Game.Tests.Visual.SongSelect
};
}
private IBeatmap createLongMetadata()
public static IBeatmap CreateLongMetadata()
{
return new Beatmap
{
@@ -43,6 +43,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private BeatmapManager beatmapManager = null!;
private PlaySongSelect songSelect = null!;
private LeaderboardManager leaderboardManager = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@@ -51,6 +53,8 @@ namespace osu.Game.Tests.Visual.SongSelect
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
dependencies.CacheAs<Screens.Select.SongSelect>(songSelect = new PlaySongSelect());
dependencies.Cache(leaderboardManager = new LeaderboardManager());
Dependencies.Cache(Realm);
return dependencies;
@@ -60,6 +64,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void load()
{
LoadComponent(songSelect);
LoadComponent(leaderboardManager);
}
public TestSceneBeatmapLeaderboard()
@@ -112,6 +117,27 @@ namespace osu.Game.Tests.Visual.SongSelect
checkDisplayedCount(0);
}
[Test]
public void TestLocalScoresDisplayWorksWhenStartingOffline()
{
BeatmapInfo beatmapInfo = null!;
AddStep("Log out", () => API.Logout());
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
AddStep(@"Set beatmap", () =>
{
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
leaderboard.BeatmapInfo = beatmapInfo;
});
clearScores();
importMoreScores(() => beatmapInfo);
checkDisplayedCount(10);
}
[Test]
public void TestLocalScoresDisplayOnBeatmapEdit()
{
@@ -180,8 +206,8 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestGlobalScoresDisplay()
{
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global);
AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo())));
AddStep(@"New Scores with teams", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()).Select(s =>
AddStep(@"New Scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo())));
AddStep(@"New Scores with teams", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()).Select(s =>
{
s.User.Team = new APITeam();
return s;
@@ -286,7 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
AddStep(@"Import new scores", () =>
{
foreach (var score in generateSampleScores(beatmapInfo()))
foreach (var score in GenerateSampleScores(beatmapInfo()))
scoreManager.Import(score);
});
}
@@ -302,7 +328,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void checkStoredCount(int expected) =>
AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All<ScoreInfo>().Count(s => !s.DeletePending)), () => Is.EqualTo(expected));
private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo)
public static ScoreInfo[] GenerateSampleScores(BeatmapInfo beatmapInfo)
{
return new[]
{
@@ -16,6 +16,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Screens.Select;
@@ -27,9 +28,9 @@ using osuTK.Graphics;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene
public abstract partial class BeatmapCarouselTestScene : OsuManualInputManagerTestScene
{
protected readonly BindableList<BeatmapSetInfo> BeatmapSets = new BindableList<BeatmapSetInfo>();
@@ -47,7 +48,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private int beatmapCount;
protected BeatmapCarouselV2TestScene()
protected BeatmapCarouselTestScene()
{
store = new TestBeatmapStore
{
@@ -96,6 +97,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Carousel = new BeatmapCarousel
{
BleedTop = 50,
BleedBottom = 50,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 800,
@@ -189,7 +192,7 @@ namespace osu.Game.Tests.Visual.SongSelect
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
.OrderBy(p => p.Y)
.ElementAt(index)
.ChildrenOfType<PanelBase>().Single()
.ChildrenOfType<Panel>().Single()
.TriggerClick();
});
}
@@ -4,7 +4,6 @@
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.Graphics.Cursor;
using osu.Game.Overlays;
@@ -20,7 +19,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(10),
};
private Container? resizeContainer;
@@ -33,15 +31,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(10),
Width = relativeWidth,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background5,
},
Content
}
};
@@ -55,6 +47,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
});
}
protected override void LoadComplete()
{
base.LoadComplete();
ChangeBackgroundColour(ColourProvider.Background6);
}
[SetUpSteps]
public virtual void SetUpSteps()
{
@@ -10,13 +10,13 @@ using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
/// <summary>
/// Covers common steps which can be used for manual testing.
/// </summary>
[TestFixture]
public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarousel : BeatmapCarouselTestScene
{
[Test]
[Explicit]
@@ -9,10 +9,10 @@ using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselArtistGrouping : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
@@ -5,15 +5,16 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselDifficultyGrouping : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
@@ -5,16 +5,17 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselNoGrouping : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
@@ -8,10 +8,10 @@ using osu.Framework.Testing;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselScrolling : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
@@ -1,83 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapCarouselV2GroupPanel : ThemeComparisonTestScene
{
public TestSceneBeatmapCarouselV2GroupPanel()
: base(false)
{
}
protected override Drawable CreateContent()
{
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A"))
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
KeyboardSelected = { Value = true }
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
Expanded = { Value = true }
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
KeyboardSelected = { Value = true },
Expanded = { Value = true }
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(1, "1"))
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(3, "3")),
Expanded = { Value = true }
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(5, "5")),
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(7, "7")),
Expanded = { Value = true }
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(8, "8")),
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(9, "9")),
Expanded = { Value = true }
},
}
};
}
}
}
@@ -1,213 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapInfoWedge : SongSelectComponentsTestScene
{
private RulesetStore rulesets = null!;
private TestBeatmapInfoWedgeV2 infoWedge = null!;
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("reset mods", () => SelectedMods.SetDefault());
}
protected override void LoadComplete()
{
base.LoadComplete();
AddRange(new Drawable[]
{
// This exists only to make the wedge more visible in the test scene
new Box
{
Y = -20,
Colour = Colour4.Cornsilk.Darken(0.2f),
Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40,
Width = 0.65f,
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Top = 20, Left = -10 }
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 20 },
Child = infoWedge = new TestBeatmapInfoWedgeV2
{
Width = 0.6f,
RelativeSizeAxes = Axes.X,
},
}
});
AddSliderStep("change star difficulty", 0, 11.9, 5.55, v =>
{
foreach (var hasCurrentValue in infoWedge.ChildrenOfType<IHasCurrentValue<StarDifficulty>>())
hasCurrentValue.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 instance = rulesetInfo.CreateInstance();
var testBeatmap = createTestBeatmap(rulesetInfo);
beatmaps.Add(testBeatmap);
setRuleset(rulesetInfo);
selectBeatmap(testBeatmap);
testBeatmapLabels(instance);
}
}
[Test]
public void TestWedgeVisibility()
{
AddStep("hide", () => { infoWedge.Hide(); });
AddWaitStep("wait for hide", 3);
AddAssert("check visibility", () => infoWedge.Alpha == 0);
AddStep("show", () => { infoWedge.Show(); });
AddWaitStep("wait for show", 1);
AddAssert("check visibility", () => infoWedge.Alpha > 0);
}
private void testBeatmapLabels(Ruleset ruleset)
{
AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title");
AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
}
[Test]
public void TestTruncation()
{
selectBeatmap(createLongMetadata());
}
[Test]
public void TestNullBeatmapWithBackground()
{
selectBeatmap(null);
AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
}
private void setRuleset(RulesetInfo rulesetInfo)
{
Container? containerBefore = null;
AddStep("set ruleset", () =>
{
// wedge content is only refreshed if the ruleset changes, so only wait for load in that case.
if (!rulesetInfo.Equals(Ruleset.Value))
containerBefore = infoWedge.DisplayedContent;
Ruleset.Value = rulesetInfo;
});
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private void selectBeatmap(IBeatmap? b)
{
Container? containerBefore = null;
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
containerBefore = infoWedge.DisplayedContent;
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
infoWedge.Show();
});
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
{
List<HitObject> objects = new List<HitObject>();
for (double i = 0; i < 50000; i += 1000)
objects.Add(new TestHitObject { StartTime = i });
return new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Author = { Username = $"{ruleset.ShortName}Author" },
Artist = $"{ruleset.ShortName}Artist",
Source = $"{ruleset.ShortName}Source",
Title = $"{ruleset.ShortName}Title"
},
Ruleset = ruleset,
StarRating = 6,
DifficultyName = $"{ruleset.ShortName}Version",
Difficulty = new BeatmapDifficulty()
},
HitObjects = objects
};
}
private IBeatmap createLongMetadata()
{
return new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Author = { Username = "WWWWWWWWWWWWWWW" },
Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
Source = "Verrrrry long Source",
Title = "Verrrrry long Title"
},
DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
Status = BeatmapOnlineStatus.Graveyard,
},
};
}
private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2
{
public new Container? DisplayedContent => base.DisplayedContent;
public new WedgeInfoText? Info => base.Info;
}
private class TestHitObject : ConvertHitObject;
}
}
@@ -1,44 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.SelectV2.Wedge;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneDifficultyNameContent : SongSelectComponentsTestScene
{
private DifficultyNameContent? difficultyNameContent;
[Test]
public void TestLocalBeatmap()
{
AddStep("set component", () => Child = difficultyNameContent = new LocalDifficultyNameContent());
AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType<TruncatingSpriteText>().Single().Text));
AddAssert("author is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType<OsuHoverContainer>().Single().ChildrenOfType<OsuSpriteText>().Single().Text));
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
DifficultyName = "really long difficulty name that gets truncated",
Metadata = new BeatmapMetadata
{
Author = { Username = "really long username that is autosized" },
},
OnlineID = 1,
}
}));
AddAssert("difficulty name is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType<TruncatingSpriteText>().Single().Text));
AddAssert("author is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType<OsuHoverContainer>().Single().ChildrenOfType<OsuSpriteText>().Single().Text));
}
}
}
@@ -13,19 +13,19 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.SelectV2.Footer;
using osu.Game.Screens.SelectV2;
using osu.Game.Utils;
namespace osu.Game.Tests.Visual.UserInterface
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneScreenFooterButtonMods : OsuTestScene
public partial class TestSceneFooterButtonMods : OsuTestScene
{
private readonly TestScreenFooterButtonMods footerButtonMods;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
public TestSceneScreenFooterButtonMods()
public TestSceneFooterButtonMods()
{
Add(footerButtonMods = new TestScreenFooterButtonMods(new TestModSelectOverlay())
{
@@ -98,9 +98,9 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestUnrankedBadge()
{
AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() }));
AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType<ScreenFooterButtonMods.UnrankedBadge>().Single().Alpha == 1);
AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType<FooterButtonMods.UnrankedBadge>().Single().Alpha == 1);
AddStep(@"Clear selected mod", () => changeMods(Array.Empty<Mod>()));
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<ScreenFooterButtonMods.UnrankedBadge>().Single().Alpha == 0);
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<FooterButtonMods.UnrankedBadge>().Single().Alpha == 0);
}
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Current.Value = mods;
@@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
}
private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods
private partial class TestScreenFooterButtonMods : FooterButtonMods
{
public new OsuSpriteText MultiplierText => base.MultiplierText;
@@ -20,7 +20,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.SelectV2.Leaderboards;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
@@ -53,14 +53,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 2f),
Shear = new Vector2(OsuGame.SHEAR, 0)
Shear = OsuGame.SHEAR,
},
drawWidthText = new OsuSpriteText(),
};
foreach (var scoreInfo in getTestScores())
{
fillFlow.Add(new LeaderboardScoreV2(scoreInfo)
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo)
{
Rank = scoreInfo.Position,
IsPersonalBest = scoreInfo.User.Id == 2,
@@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
foreach (var scoreInfo in getTestScores())
{
fillFlow.Add(new LeaderboardScoreV2(scoreInfo)
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo)
{
Rank = scoreInfo.Position,
IsPersonalBest = scoreInfo.User.Id == 2,
@@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Test]
public void TestUseTheseModsDoesNotCopySystemMods()
{
LeaderboardScoreV2 score = null!;
BeatmapLeaderboardScore score = null!;
AddStep("create content", () =>
{
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 2f),
Shear = new Vector2(OsuGame.SHEAR, 0)
Shear = OsuGame.SHEAR,
},
drawWidthText = new OsuSpriteText(),
};
@@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
Date = DateTimeOffset.Now.AddYears(-2),
};
fillFlow.Add(score = new LeaderboardScoreV2(scoreInfo)
fillFlow.Add(score = new BeatmapLeaderboardScore(scoreInfo)
{
Rank = scoreInfo.Position,
Shear = Vector2.Zero,
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
@@ -18,14 +19,14 @@ using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapCarouselV2DifficultyPanel : ThemeComparisonTestScene
public partial class TestScenePanelBeatmap : ThemeComparisonTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapInfo beatmap = null!;
public TestSceneBeatmapCarouselV2DifficultyPanel()
public TestScenePanelBeatmap()
: base(false)
{
}
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
@@ -18,14 +19,14 @@ using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapCarouselV2StandalonePanel : ThemeComparisonTestScene
public partial class TestScenePanelBeatmapStandalone : ThemeComparisonTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapInfo beatmap = null!;
public TestSceneBeatmapCarouselV2StandalonePanel()
public TestScenePanelBeatmapStandalone()
: base(false)
{
}
@@ -0,0 +1,120 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestScenePanelGroup : ThemeComparisonTestScene
{
public TestScenePanelGroup()
: base(false)
{
}
[Test]
public void TestGeneral()
{
AddStep("general", () => CreateThemedContent(OverlayColourScheme.Aquamarine));
}
[Test]
public void TestStars()
{
for (int i = 0; i <= 10; i++)
{
int star = i;
AddStep($"display {i} star(s)", () =>
{
ContentContainer.Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
},
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new[]
{
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(star, star.ToString()))
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(star, star.ToString())),
KeyboardSelected = { Value = true },
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(star, star.ToString())),
Expanded = { Value = true },
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(star, star.ToString())),
Expanded = { Value = true },
KeyboardSelected = { Value = true },
},
},
}
};
});
}
}
protected override Drawable CreateContent()
{
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A"))
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
KeyboardSelected = { Value = true }
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
Expanded = { Value = true }
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
KeyboardSelected = { Value = true },
Expanded = { Value = true }
},
}
};
}
}
}
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Overlays;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
@@ -16,14 +17,14 @@ using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapCarouselV2SetPanel : ThemeComparisonTestScene
public partial class TestScenePanelSet : ThemeComparisonTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapSetInfo beatmapSet = null!;
public TestSceneBeatmapCarouselV2SetPanel()
public TestScenePanelSet()
: base(false)
{
}
@@ -9,14 +9,14 @@ using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene
public partial class TestScenePanelUpdateBeatmapButton : OsuTestScene
{
private UpdateBeatmapSetButton button = null!;
private PanelUpdateBeatmapButton button = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = button = new UpdateBeatmapSetButton
Child = button = new PanelUpdateBeatmapButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -15,9 +15,9 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.SelectV2.Footer;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.UserInterface
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene
{
@@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.UserInterface
screenFooter.SetButtons(new ScreenFooterButton[]
{
new ScreenFooterButtonMods(modOverlay) { Current = SelectedMods },
new ScreenFooterButtonRandom(),
new ScreenFooterButtonOptions(),
new FooterButtonMods(modOverlay) { Current = SelectedMods },
new FooterButtonRandom(),
new FooterButtonOptions(),
});
});
@@ -22,7 +22,7 @@ using osu.Game.Rulesets.Taiko;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
using osu.Game.Screens.SelectV2.Footer;
using osu.Game.Screens.SelectV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelectV2
@@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
base.SetUpSteps();
AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SoloSongSelect()));
AddStep("load screen", () => Stack.Push(new SoloSongSelect()));
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded);
}
@@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
AddStep("Press F1", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ScreenFooterButtonMods>().Single());
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonMods>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("Overlay visible", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
@@ -53,6 +53,14 @@ namespace osu.Game.Tournament
return new ProductionEndpointConfiguration();
}
public override void SetHost(GameHost host)
{
base.SetHost(host);
if (host.Window != null)
host.Window.Title = $"{Name} [tournament client]";
}
private TournamentSpriteText initialisationText = null!;
[BackgroundDependencyLoader]
+1 -1
View File
@@ -14,10 +14,10 @@ namespace osu.Game.Beatmaps
/// This is a special status given when local changes are made via the editor.
/// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted.
/// </summary>
[Description("Local")]
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))]
LocallyModified = -4,
[Description("Unknown")]
None = -3,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))]
+13 -1
View File
@@ -16,7 +16,19 @@ namespace osu.Game.Beatmaps
/// </summary>
public Func<Drawable> CreateIcon;
public string Content;
/// <summary>
/// The name of this statistic.
/// </summary>
public LocalisableString Name;
/// <summary>
/// The text representing the value of this statistic.
/// </summary>
public string Content;
/// <summary>
/// The length of a bar which visually represents this statistic's relevance in the beatmap.
/// </summary>
public float? BarDisplayLength;
}
}
@@ -19,9 +19,10 @@ namespace osu.Game.Beatmaps.Drawables
{
public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
{
private const double animation_duration = 400;
private BeatmapOnlineStatus status;
/// <summary>
/// Whether to show <see cref="BeatmapOnlineStatus.None"/> as "unknown" instead of fading out.
/// </summary>
public bool ShowUnknownStatus { get; init; }
public BeatmapOnlineStatus Status
{
@@ -34,30 +35,27 @@ namespace osu.Game.Beatmaps.Drawables
status = value;
if (IsLoaded)
{
AutoSizeDuration = (float)animation_duration;
AutoSizeEasing = Easing.OutQuint;
updateState();
}
}
}
private BeatmapOnlineStatus status;
public float TextSize
{
get => statusText.Font.Size;
set => statusText.Font = statusText.Font.With(size: value);
init => statusText.Font = statusText.Font.With(size: value);
}
public MarginPadding TextPadding
{
get => statusText.Padding;
set => statusText.Padding = value;
init => statusText.Padding = value;
}
private readonly OsuSpriteText statusText;
private readonly Box background;
private const double animation_duration = 400;
[Resolved]
private OsuColour colours { get; set; } = null!;
@@ -66,6 +64,7 @@ namespace osu.Game.Beatmaps.Drawables
public BeatmapSetOnlineStatusPill()
{
AutoSizeAxes = Axes.Both;
Masking = true;
Alpha = 0;
@@ -99,14 +98,27 @@ namespace osu.Game.Beatmaps.Drawables
private void updateState()
{
if (Status == BeatmapOnlineStatus.None)
if (Status == BeatmapOnlineStatus.None && !ShowUnknownStatus)
{
Hide();
this.FadeOut(animation_duration, Easing.OutQuint);
return;
}
// The autosize animation on this component is intended to animate horizontal sizing only.
// To avoid vertical autosize animating from zero to non-zero, only apply the duration
// after we have a valid size.
if (Height > 0)
{
AutoSizeDuration = (float)animation_duration;
AutoSizeEasing = Easing.OutQuint;
}
this.FadeIn(animation_duration, Easing.OutQuint);
// Handle the case where transition from hidden to non-hidden may cause
// a fade from a colour that doesn't make sense (due to not being able to see the previous colour).
double duration = Alpha > 0 ? animation_duration : 0;
Color4 statusTextColour;
if (colourProvider != null)
@@ -114,8 +126,8 @@ namespace osu.Game.Beatmaps.Drawables
else
statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black;
statusText.FadeColour(statusTextColour, animation_duration, Easing.OutQuint);
background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, animation_duration, Easing.OutQuint);
statusText.FadeColour(statusTextColour, duration, Easing.OutQuint);
background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, duration, Easing.OutQuint);
statusText.Text = Status.GetLocalisableDescription().ToUpper();
}
@@ -30,7 +30,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new BeatmapSetOnlineStatusPill
{
AutoSizeAxes = Axes.Both,
Status = beatmapSet.Status,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -39,8 +38,6 @@ namespace osu.Game.Beatmaps.Drawables
private readonly Bindable<double> displayedStars = new BindableDouble();
private readonly Container textContainer;
/// <summary>
/// The currently displayed stars of this display wrapped in a bindable.
/// This bindable gets transformed on change rather than instantaneous, if animation is enabled.
@@ -119,19 +116,14 @@ namespace osu.Game.Beatmaps.Drawables
Size = new Vector2(8f),
},
Empty(),
textContainer = new Container
starsText = new OsuSpriteText
{
AutoSizeAxes = Axes.Y,
Child = starsText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 1.5f },
// todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f
// see https://github.com/ppy/osu-framework/issues/3271.
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold),
Shadow = false,
},
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 1.5f },
Spacing = new Vector2(-1.4f),
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold, fixedWidth: true),
Shadow = false,
},
}
}
@@ -162,11 +154,6 @@ namespace osu.Game.Beatmaps.Drawables
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);
// In order to avoid autosize throwing the width of these displays all over the place,
// let's lock in some sane defaults for the text width based on how many digits we're
// displaying.
textContainer.Width = 24 + Math.Max(starsText.Text.ToString().Length - 4, 0) * 6;
}, true);
}
}
+25 -15
View File
@@ -30,7 +30,8 @@ namespace osu.Game.Database
public override IBindableList<BeatmapSetInfo> GetBeatmapSets(CancellationToken? cancellationToken)
{
loaded.Wait(cancellationToken ?? CancellationToken.None);
return detachedBeatmapSets.GetBoundCopy();
lock (detachedBeatmapSets)
return detachedBeatmapSets.GetBoundCopy();
}
[BackgroundDependencyLoader]
@@ -65,8 +66,11 @@ namespace osu.Game.Database
{
var detached = frozenSets.Detach();
detachedBeatmapSets.Clear();
detachedBeatmapSets.AddRange(detached);
lock (detachedBeatmapSets)
{
detachedBeatmapSets.Clear();
detachedBeatmapSets.AddRange(detached);
}
});
}
finally
@@ -116,22 +120,28 @@ namespace osu.Game.Database
if (!loaded.IsSet)
return;
// If this ever leads to performance issues, we could dequeue a limited number of operations per update frame.
while (pendingOperations.TryDequeue(out var op))
if (pendingOperations.Count == 0)
return;
lock (detachedBeatmapSets)
{
switch (op.Type)
// If this ever leads to performance issues, we could dequeue a limited number of operations per update frame.
while (pendingOperations.TryDequeue(out var op))
{
case OperationType.Insert:
detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!);
break;
switch (op.Type)
{
case OperationType.Insert:
detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!);
break;
case OperationType.Update:
detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! });
break;
case OperationType.Update:
detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! });
break;
case OperationType.Remove:
detachedBeatmapSets.RemoveAt(op.Index);
break;
case OperationType.Remove:
detachedBeatmapSets.RemoveAt(op.Index);
break;
}
}
}
}
@@ -24,7 +24,7 @@ using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.SelectV2
namespace osu.Game.Graphics.Carousel
{
/// <summary>
/// A highly efficient vertical list display that is used primarily for the song select screen,
@@ -38,12 +38,12 @@ namespace osu.Game.Screens.SelectV2
/// <summary>
/// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
/// </summary>
public float BleedTop { get; set; } = 0;
public float BleedTop { get; set; }
/// <summary>
/// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it.
/// </summary>
public float BleedBottom { get; set; } = 0;
public float BleedBottom { get; set; }
/// <summary>
/// The number of pixels outside the carousel's vertical bounds to manifest drawables.
@@ -228,6 +228,7 @@ namespace osu.Game.Screens.SelectV2
{
InternalChild = Scroll = new CarouselScrollContainer
{
Masking = false,
RelativeSizeAxes = Axes.Both,
};
@@ -505,7 +506,7 @@ namespace osu.Game.Screens.SelectV2
private void scrollToSelection()
{
if (currentKeyboardSelection.CarouselItem != null)
Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight);
Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight + BleedTop);
}
#endregion
@@ -519,17 +520,17 @@ namespace osu.Game.Screens.SelectV2
/// <summary>
/// The position of the lower visible bound with respect to the current scroll position.
/// </summary>
private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom);
private float visibleBottomBound;
/// <summary>
/// The position of the upper visible bound with respect to the current scroll position.
/// </summary>
private float visibleUpperBound => (float)(Scroll.Current - BleedTop);
private float visibleUpperBound;
/// <summary>
/// Half the height of the visible content.
/// </summary>
private float visibleHalfHeight => (DrawHeight + BleedBottom + BleedTop) / 2;
private float visibleHalfHeight;
protected override void Update()
{
@@ -538,6 +539,10 @@ namespace osu.Game.Screens.SelectV2
if (carouselItems == null)
return;
visibleBottomBound = (float)(Scroll.Current + DrawHeight + BleedBottom);
visibleUpperBound = (float)(Scroll.Current - BleedTop);
visibleHalfHeight = (DrawHeight + BleedBottom + BleedTop) / 2;
if (!selectionValid.IsValid)
{
refreshAfterSelection();
@@ -582,7 +587,7 @@ namespace osu.Game.Screens.SelectV2
protected virtual float GetPanelXOffset(Drawable panel)
{
Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre);
float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight);
float dist = Math.Abs(1f - (posInScroll.Y + BleedTop) / visibleHalfHeight);
return offsetX(dist, visibleHalfHeight);
}
@@ -3,7 +3,7 @@
using System;
namespace osu.Game.Screens.SelectV2
namespace osu.Game.Graphics.Carousel
{
/// <summary>
/// Represents a single display item for display in a <see cref="Carousel{T}"/>.
@@ -5,7 +5,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace osu.Game.Screens.SelectV2
namespace osu.Game.Graphics.Carousel
{
/// <summary>
/// An interface representing a filter operation which can be run on a <see cref="Carousel{T}"/>.
@@ -5,7 +5,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
namespace osu.Game.Screens.SelectV2
namespace osu.Game.Graphics.Carousel
{
/// <summary>
/// An interface to be attached to any <see cref="Drawable"/>s which are used for display inside a <see cref="Carousel{T}"/>.
+17 -5
View File
@@ -20,10 +20,7 @@ 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>
/// Retrieves the colour for a given point in the star range.
/// </summary>
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM =
{
(0.1f, Color4Extensions.FromHex("aaaaaa")),
(0.1f, Color4Extensions.FromHex("4290fb")),
@@ -37,7 +34,13 @@ namespace osu.Game.Graphics
(6.7f, Color4Extensions.FromHex("6563de")),
(7.7f, Color4Extensions.FromHex("18158e")),
(9.0f, Color4.Black),
}, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
(10.0f, Color4.Black),
};
/// <summary>
/// Retrieves the colour for a given point in the star range.
/// </summary>
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
/// <summary>
/// Retrieves the colour for a <see cref="ScoreRank"/>.
@@ -120,6 +123,9 @@ namespace osu.Game.Graphics
{
switch (status)
{
case BeatmapOnlineStatus.None:
return Color4.RosyBrown;
case BeatmapOnlineStatus.LocallyModified:
return Color4.OrangeRed;
@@ -403,6 +409,12 @@ namespace osu.Game.Graphics
public readonly Color4 Orange3 = Color4Extensions.FromHex(@"cca633");
public readonly Color4 Orange4 = Color4Extensions.FromHex(@"6b5c2e");
public readonly Color4 DarkOrange0 = Color4Extensions.FromHex(@"ffbb99");
public readonly Color4 DarkOrange1 = Color4Extensions.FromHex(@"ff9966");
public readonly Color4 DarkOrange2 = Color4Extensions.FromHex(@"eb7e47");
public readonly Color4 DarkOrange3 = Color4Extensions.FromHex(@"cc6633");
public readonly Color4 DarkOrange4 = Color4Extensions.FromHex(@"6b422e");
public readonly Color4 Red0 = Color4Extensions.FromHex(@"ff9b9b");
public readonly Color4 Red1 = Color4Extensions.FromHex(@"ff6666");
public readonly Color4 Red2 = Color4Extensions.FromHex(@"eb4747");
+51 -1
View File
@@ -15,15 +15,65 @@ namespace osu.Game.Graphics
/// </summary>
public const float DEFAULT_FONT_SIZE = 16;
/// <summary>
/// Template font styles which should be preferred whenever possible for UI elements.
/// </summary>
public static class Style
{
/// <summary>
/// Equivalent to Torus with 32px size and semi-bold weight.
/// </summary>
public static FontUsage Title => GetFont(Typeface.TorusAlternate, size: 32, weight: FontWeight.Regular);
/// <summary>
/// Torus with 28px size and semi-bold weight.
/// </summary>
public static FontUsage Subtitle => GetFont(size: 28, weight: FontWeight.Regular);
/// <summary>
/// Torus with 22px size and bold weight.
/// </summary>
public static FontUsage Heading1 => GetFont(size: 22, weight: FontWeight.Bold);
/// <summary>
/// Torus with 18px size and semi-bold weight.
/// </summary>
public static FontUsage Heading2 => GetFont(size: 18, weight: FontWeight.SemiBold);
/// <summary>
/// Torus with 16px size and regular weight.
/// </summary>
public static FontUsage Body => GetFont(size: DEFAULT_FONT_SIZE, weight: FontWeight.Regular);
/// <summary>
/// Torus with 14px size and regular weight.
/// </summary>
public static FontUsage Caption1 => GetFont(size: 14, weight: FontWeight.Regular);
/// <summary>
/// Torus with 12px size and regular weight.
/// </summary>
public static FontUsage Caption2 => GetFont(size: 12, weight: FontWeight.Regular);
}
/// <summary>
/// The default font.
/// </summary>
public static FontUsage Default => GetFont();
public static FontUsage Default => GetFont(weight: FontWeight.Medium);
/// <summary>
/// Font face for numeric display.
/// </summary>
public static FontUsage Numeric => GetFont(Typeface.Venera, weight: FontWeight.Bold);
/// <summary>
/// Default font face for UI and game elements.
/// </summary>
public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular);
/// <summary>
/// Default font face with alternate character set for headings and flair text.
/// </summary>
public static FontUsage TorusAlternate => GetFont(Typeface.TorusAlternate, weight: FontWeight.Regular);
public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular);
+8
View File
@@ -115,6 +115,7 @@ namespace osu.Game.Graphics
public static IconUsage ChangelogB => get(OsuIconMapping.ChangelogB);
public static IconUsage Chat => get(OsuIconMapping.Chat);
public static IconUsage CheckCircle => get(OsuIconMapping.CheckCircle);
public static IconUsage Clock => get(OsuIconMapping.Clock);
public static IconUsage CollapseA => get(OsuIconMapping.CollapseA);
public static IconUsage Collections => get(OsuIconMapping.Collections);
public static IconUsage Cross => get(OsuIconMapping.Cross);
@@ -141,6 +142,7 @@ namespace osu.Game.Graphics
public static IconUsage Input => get(OsuIconMapping.Input);
public static IconUsage Maintenance => get(OsuIconMapping.Maintenance);
public static IconUsage Megaphone => get(OsuIconMapping.Megaphone);
public static IconUsage Metronome => get(OsuIconMapping.Metronome);
public static IconUsage Music => get(OsuIconMapping.Music);
public static IconUsage News => get(OsuIconMapping.News);
public static IconUsage Next => get(OsuIconMapping.Next);
@@ -204,6 +206,9 @@ namespace osu.Game.Graphics
[Description(@"check-circle")]
CheckCircle,
[Description(@"clock")]
Clock,
[Description(@"collapse-a")]
CollapseA,
@@ -282,6 +287,9 @@ namespace osu.Game.Graphics
[Description(@"megaphone")]
Megaphone,
[Description(@"metronome")]
Metronome,
[Description(@"music")]
Music,
@@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface
Radius = 5,
},
Colour = ButtonColour,
Shear = new Vector2(0.2f, 0),
Shear = OsuGame.SHEAR,
Children = new Drawable[]
{
new Box
@@ -149,7 +149,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.Both,
TriangleScale = 4,
ColourDark = OsuColour.Gray(0.88f),
Shear = new Vector2(-0.2f, 0),
Shear = -OsuGame.SHEAR,
ClampAxes = Axes.Y
},
},
@@ -11,7 +11,6 @@ using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
@@ -66,8 +65,6 @@ namespace osu.Game.Graphics.UserInterface
private readonly Box background;
private readonly OsuSpriteText text;
private const float shear = OsuGame.SHEAR;
private Colour4? darkerColour;
private Colour4? lighterColour;
private Colour4? textColour;
@@ -91,10 +88,10 @@ namespace osu.Game.Graphics.UserInterface
public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT)
{
Height = height;
Padding = new MarginPadding { Horizontal = shear * height };
Padding = new MarginPadding { Horizontal = OsuGame.SHEAR.X * height };
Content.CornerRadius = CORNER_RADIUS;
Content.Shear = new Vector2(shear, 0);
Content.Shear = OsuGame.SHEAR;
Content.Masking = true;
Content.Anchor = Content.Origin = Anchor.Centre;
@@ -117,7 +114,7 @@ namespace osu.Game.Graphics.UserInterface
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Shear = new Vector2(-shear, 0),
Shear = -OsuGame.SHEAR,
Child = text = new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 17),
@@ -26,8 +26,6 @@ namespace osu.Game.Graphics.UserInterface
public const int HEIGHT = 30;
public const float EXPANDED_SIZE = 50;
public static readonly Vector2 SHEAR = new Vector2(0.15f, 0);
private readonly Box fill;
private readonly Container main;
@@ -40,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface
Size = new Vector2(EXPANDED_SIZE, HEIGHT);
InternalChild = main = new Container
{
Shear = SHEAR,
Shear = OsuGame.SHEAR,
BorderColour = Colour4.White,
BorderThickness = BORDER_WIDTH,
Masking = true,
@@ -52,7 +52,7 @@ namespace osu.Game.Graphics.UserInterface
public ShearedSearchTextBox()
{
Height = 42;
Shear = new Vector2(OsuGame.SHEAR, 0);
Shear = OsuGame.SHEAR;
Masking = true;
CornerRadius = corner_radius;
@@ -115,7 +115,7 @@ namespace osu.Game.Graphics.UserInterface
PlaceholderText = CommonStrings.InputSearch;
CornerRadius = corner_radius;
TextContainer.Shear = new Vector2(-OsuGame.SHEAR, 0);
TextContainer.Shear = -OsuGame.SHEAR;
}
protected override SpriteText CreatePlaceholder() => new SearchPlaceholder();
@@ -58,7 +58,7 @@ namespace osu.Game.Graphics.UserInterface
public ShearedSliderBar()
{
Shear = SHEAR;
Shear = OsuGame.SHEAR;
Height = HEIGHT;
RangePadding = EXPANDED_SIZE / 2;
Children = new Drawable[]
@@ -98,11 +98,11 @@ namespace osu.Game.Graphics.UserInterface
},
nubContainer = new Container
{
Shear = -SHEAR,
Shear = -OsuGame.SHEAR,
RelativeSizeAxes = Axes.Both,
Child = Nub = new ShearedNub
{
X = -SHEAR.X * HEIGHT / 2f,
X = -OsuGame.SHEAR.X * HEIGHT / 2f,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true },
@@ -62,8 +62,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected partial class ShearedDropdownMenu : OsuDropdown<T>.OsuDropdownMenu
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
public new MarginPadding Padding
{
get => base.Padding;
@@ -72,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public ShearedDropdownMenu()
{
Shear = shear;
Shear = OsuGame.SHEAR;
Margin = new MarginPadding { Top = 5f };
}
@@ -84,12 +82,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
public partial class ShearedMenuItem : DrawableOsuDropdownMenuItem
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
public ShearedMenuItem(MenuItem item)
: base(item)
{
Foreground.Shear = -shear;
Foreground.Shear = -OsuGame.SHEAR;
}
}
}
@@ -125,14 +121,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
public ShearedDropdown<T> Dropdown = null!;
private ShearedDropdownSearchBar searchBar = null!;
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public ShearedDropdownHeader()
{
Shear = shear;
Shear = OsuGame.SHEAR;
CornerRadius = corner_radius;
Masking = true;
@@ -167,7 +161,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f },
Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold),
Shear = -shear,
Shear = -OsuGame.SHEAR,
},
},
},
@@ -178,7 +172,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10f },
Shear = -shear,
Shear = -OsuGame.SHEAR,
Children = new Drawable[]
{
valueText = new TruncatingSpriteText
@@ -286,12 +280,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
private partial class DropdownSearchTextBox : OsuTextBox
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
[BackgroundDependencyLoader]
private void load(OverlayColourProvider? colourProvider)
{
TextContainer.Shear = -shear;
TextContainer.Shear = -OsuGame.SHEAR;
BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
}
@@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class DrawableRoomPlaylistItemStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.DrawableRoomPlaylistItem";
/// <summary>
/// "You have completed this beatmap"
/// </summary>
public static LocalisableString CompletedTooltip => new TranslatableString(getKey(@"completed_tooltip"), @"You have completed this beatmap");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -36,7 +36,7 @@ namespace osu.Game.Online.API.Requests
this.mods = mods ?? Array.Empty<IMod>();
}
protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/solo-scores{createQueryParameters()}";
protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/scores{createQueryParameters()}";
private string createQueryParameters()
{
@@ -32,6 +32,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"playcount")]
public int PlayCount { get; set; }
[JsonProperty(@"current_user_playcount")]
public int UserPlayCount { get; set; }
[JsonProperty(@"passcount")]
public int PassCount { get; set; }
@@ -0,0 +1,172 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using Realms;
namespace osu.Game.Online.Leaderboards
{
public partial class LeaderboardManager : Component
{
public IBindable<LeaderboardScores?> Scores => scores;
private readonly Bindable<LeaderboardScores?> scores = new Bindable<LeaderboardScores?>();
public LeaderboardCriteria? CurrentCriteria { get; private set; }
private IDisposable? localScoreSubscription;
private TaskCompletionSource<LeaderboardScores?>? localFetchCompletionSource;
private TaskCompletionSource<LeaderboardScores?>? lastFetchCompletionSource;
private GetScoresRequest? inFlightOnlineRequest;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
public Task<LeaderboardScores?> FetchWithCriteriaAsync(LeaderboardCriteria newCriteria)
{
if (CurrentCriteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false)
return lastFetchCompletionSource?.Task ?? Task.FromResult(Scores.Value);
CurrentCriteria = newCriteria;
localScoreSubscription?.Dispose();
inFlightOnlineRequest?.Cancel();
lastFetchCompletionSource?.TrySetCanceled();
scores.Value = null;
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;
}
default:
{
var onlineFetchCompletionSource = new TaskCompletionSource<LeaderboardScores?>();
lastFetchCompletionSource = onlineFetchCompletionSource;
IReadOnlyList<Mod>? requestMods = null;
if (newCriteria.ExactMods != null)
{
if (!newCriteria.ExactMods.Any())
// add nomod for the request
requestMods = new Mod[] { new ModNoMod() };
else
requestMods = newCriteria.ExactMods;
}
var newRequest = new GetScoresRequest(newCriteria.Beatmap, newCriteria.Ruleset, newCriteria.Scope, requestMods);
newRequest.Success += response =>
{
if (inFlightOnlineRequest != null && !newRequest.Equals(inFlightOnlineRequest))
return;
var result = new LeaderboardScores
(
response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(),
response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap)
);
inFlightOnlineRequest = null;
if (onlineFetchCompletionSource.TrySetResult(result))
scores.Value = result;
};
newRequest.Failure += ex => onlineFetchCompletionSource.TrySetException(ex);
api.Queue(inFlightOnlineRequest = newRequest);
return onlineFetchCompletionSource.Task;
}
}
}
private void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes)
{
Debug.Assert(CurrentCriteria != null);
// This subscription may fire from changes to linked beatmaps, which we don't care about.
// It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.
if (changes?.HasCollectionChanges() == false)
return;
var newScores = sender.AsEnumerable();
if (CurrentCriteria.ExactMods != null)
{
if (!CurrentCriteria.ExactMods.Any())
{
// we need to filter out all scores that have any mods to get all local nomod scores
newScores = newScores.Where(s => !s.Mods.Any());
}
else
{
// otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters)
// we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself
var selectedMods = CurrentCriteria.ExactMods.Select(m => m.Acronym).ToHashSet();
newScores = newScores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym)));
}
}
newScores = newScores.Detach().OrderByTotalScore();
scores.Value = new LeaderboardScores(newScores, null);
if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource)
{
localFetchCompletionSource.SetResult(scores.Value);
localFetchCompletionSource = lastFetchCompletionSource = null;
}
}
}
public record LeaderboardCriteria(
BeatmapInfo Beatmap,
RulesetInfo Ruleset,
BeatmapLeaderboardScope Scope,
Mod[]? ExactMods
);
public record LeaderboardScores(IEnumerable<ScoreInfo> TopScores, ScoreInfo? UserScore)
{
public IEnumerable<ScoreInfo> AllScores
{
get
{
foreach (var score in TopScores)
yield return score;
if (UserScore != null && TopScores.All(topScore => !topScore.Equals(UserScore) && !topScore.MatchesOnlineID(UserScore)))
yield return UserScore;
}
}
}
}
@@ -10,10 +10,22 @@ namespace osu.Game.Online.Rooms
/// </summary>
public class ItemAttemptsCount
{
/// <summary>
/// The playlist item this object describes.
/// </summary>
[JsonProperty("id")]
public int PlaylistItemID { get; set; }
/// <summary>
/// The number of times the user attempted the playlist item.
/// </summary>
[JsonProperty("attempts")]
public int Attempts { get; set; }
/// <summary>
/// Whether the user has a passing score on the playlist item.
/// </summary>
[JsonProperty("passed")]
public bool Passed { get; set; }
}
}
+7
View File
@@ -85,6 +85,11 @@ namespace osu.Game.Online.Rooms
private readonly Bindable<bool> valid = new BindableBool(true);
[JsonIgnore]
public IBindable<bool> Completed => completed;
private readonly Bindable<bool> completed = new BindableBool(false);
[JsonConstructor]
private PlaylistItem()
: this(new APIBeatmap())
@@ -118,6 +123,8 @@ namespace osu.Game.Online.Rooms
public void MarkInvalid() => valid.Value = false;
public void MarkCompleted() => completed.Value = true;
#region Newtonsoft.Json implicit ShouldSerialize() methods
// The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases.
+26 -1
View File
@@ -47,6 +47,7 @@ using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.Chat;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
@@ -67,6 +68,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Seasonal;
using osu.Game.Skinning;
using osu.Game.Updater;
@@ -100,7 +102,12 @@ namespace osu.Game
/// <summary>
/// A common shear factor applied to most components of the game.
/// </summary>
public const float SHEAR = 0.2f;
public static readonly Vector2 SHEAR = new Vector2(0.2f, 0);
/// <summary>
/// For elements placed close to the screen edge, this is the margin to leave to the edge.
/// </summary>
public const float SCREEN_EDGE_MARGIN = 12f;
public Toolbar Toolbar { get; private set; }
@@ -784,6 +791,24 @@ namespace osu.Game
if (!Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap))
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
var currentLeaderboard = LeaderboardManager.CurrentCriteria;
bool leaderboardBeatmapMatches = currentLeaderboard != null && databasedBeatmap.Equals(currentLeaderboard.Beatmap);
bool leaderboardRulesetMatches = currentLeaderboard != null && databasedScore.ScoreInfo.Ruleset.Equals(currentLeaderboard.Ruleset);
if (!leaderboardBeatmapMatches || !leaderboardRulesetMatches)
{
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);
});
}
switch (presentType)
{
case ScorePresentType.Gameplay:
+5
View File
@@ -49,6 +49,7 @@ using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
@@ -203,6 +204,7 @@ namespace osu.Game
private UserLookupCache userCache;
private BeatmapLookupCache beatmapCache;
protected LeaderboardManager LeaderboardManager { get; private set; }
private RulesetConfigCache rulesetConfigCache;
@@ -365,6 +367,9 @@ namespace osu.Game
dependencies.CacheAs<IBindable<WorkingBeatmap>>(Beatmap);
dependencies.CacheAs(Beatmap);
dependencies.Cache(LeaderboardManager = new LeaderboardManager());
base.Content.Add(LeaderboardManager);
// add api components to hierarchy.
if (API is APIAccess apiAccess)
base.Content.Add(apiAccess);
@@ -177,7 +177,6 @@ namespace osu.Game.Overlays.BeatmapSet
{
onlineStatusPill = new BeatmapSetOnlineStatusPill
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
TextSize = 14,
@@ -18,6 +18,7 @@ using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat.Listing;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Input;
namespace osu.Game.Overlays.Chat.ChannelList
{
@@ -160,6 +161,17 @@ namespace osu.Game.Overlays.Chat.ChannelList
};
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Middle)
{
close?.TriggerClick();
return true;
}
return base.OnMouseDown(e);
}
private ChannelListItemCloseButton? createCloseButton()
{
if (isSelector || !CanLeave)
@@ -20,7 +20,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Overlays.Mods
{
@@ -66,21 +65,19 @@ namespace osu.Game.Overlays.Mods
[BackgroundDependencyLoader]
private void load()
{
const float shear = OsuGame.SHEAR;
LeftContent.AddRange(new Drawable[]
{
starRatingDisplay = new StarRatingDisplay(default, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Shear = new Vector2(-shear, 0),
Shear = -OsuGame.SHEAR,
},
bpmDisplay = new BPMDisplay
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Shear = new Vector2(-shear, 0),
Shear = -OsuGame.SHEAR,
AutoSizeAxes = Axes.Y,
Width = 75,
}
@@ -89,10 +86,10 @@ namespace osu.Game.Overlays.Mods
RightContent.Alpha = 0;
RightContent.AddRange(new Drawable[]
{
circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = new Vector2(-shear, 0), },
drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = new Vector2(-shear, 0), },
overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), },
approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), },
circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = -OsuGame.SHEAR, },
drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = -OsuGame.SHEAR, },
overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = -OsuGame.SHEAR, },
approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = -OsuGame.SHEAR, },
});
}
+1 -1
View File
@@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.8f),
RelativeSizeAxes = Axes.X,
Shear = new Vector2(-OsuGame.SHEAR, 0)
Shear = -OsuGame.SHEAR
});
ItemsFlow.Padding = new MarginPadding
{
@@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomRight,
AutoSizeAxes = Axes.X,
Height = ShearedButton.DEFAULT_HEIGHT,
Shear = new Vector2(OsuGame.SHEAR, 0),
Shear = OsuGame.SHEAR,
CornerRadius = ShearedButton.CORNER_RADIUS,
BorderThickness = ShearedButton.BORDER_THICKNESS,
Masking = true,
+1 -1
View File
@@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Active = { BindTarget = Active },
Shear = new Vector2(-OsuGame.SHEAR, 0),
Shear = -OsuGame.SHEAR,
Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE)
};
}
+3 -3
View File
@@ -70,7 +70,7 @@ namespace osu.Game.Overlays.Mods
{
Width = WIDTH;
RelativeSizeAxes = Axes.Y;
Shear = new Vector2(OsuGame.SHEAR, 0);
Shear = OsuGame.SHEAR;
InternalChildren = new Drawable[]
{
@@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods
{
RelativeSizeAxes = Axes.X,
Height = header_height,
Shear = new Vector2(-OsuGame.SHEAR, 0),
Shear = -OsuGame.SHEAR,
Velocity = 0.7f,
ClampAxes = Axes.Y
},
@@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Mods
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Shear = new Vector2(-OsuGame.SHEAR, 0),
Shear = -OsuGame.SHEAR,
Padding = new MarginPadding
{
Horizontal = 17,
+2 -2
View File
@@ -168,7 +168,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Horizontal,
Shear = new Vector2(OsuGame.SHEAR, 0),
Shear = OsuGame.SHEAR,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Horizontal = 70 },
@@ -726,7 +726,7 @@ namespace osu.Game.Overlays.Mods
// DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear,
// so we have to manually compensate.
var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent);
var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR, 0), ScrollContent);
var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR.X, 0), ScrollContent);
bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound)
&& Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X);
+4 -5
View File
@@ -20,7 +20,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@@ -87,7 +86,7 @@ namespace osu.Game.Overlays.Mods
Content.CornerRadius = CORNER_RADIUS;
Content.BorderThickness = 2;
Shear = new Vector2(OsuGame.SHEAR, 0);
Shear = OsuGame.SHEAR;
Children = new Drawable[]
{
@@ -128,10 +127,10 @@ namespace osu.Game.Overlays.Mods
{
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Shear = new Vector2(-OsuGame.SHEAR, 0),
Shear = -OsuGame.SHEAR,
Margin = new MarginPadding
{
Left = -18 * OsuGame.SHEAR
Left = -18 * OsuGame.SHEAR.X
},
ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
},
@@ -139,7 +138,7 @@ namespace osu.Game.Overlays.Mods
{
Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X,
Shear = new Vector2(-OsuGame.SHEAR, 0),
Shear = -OsuGame.SHEAR,
ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
}
}
@@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Overlays.Mods
{
@@ -52,7 +51,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
RelativeSizeAxes = Axes.Both,
Shear = new Vector2(OsuGame.SHEAR, 0),
Shear = OsuGame.SHEAR,
CornerRadius = ShearedButton.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
@@ -79,7 +78,7 @@ namespace osu.Game.Overlays.Mods
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = new Vector2(-OsuGame.SHEAR, 0),
Shear = -OsuGame.SHEAR,
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
}
}
@@ -94,7 +93,7 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.Centre,
Child = counter = new EffectCounter
{
Shear = new Vector2(-OsuGame.SHEAR, 0),
Shear = -OsuGame.SHEAR,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = { BindTarget = ModMultiplier }
@@ -23,6 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
LabelText = UserInterfaceStrings.ShowConvertedBeatmaps,
Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
Keywords = new[] { "converts", "converted" }
},
new SettingsEnumDropdown<RandomSelectAlgorithm>
{
@@ -54,6 +54,23 @@ namespace osu.Game.Rulesets.Scoring
return result;
}
/// <summary>
/// Calculates the average hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
/// </summary>
/// <returns>
/// A non-null <see langword="double"/> value if unstable rate could be calculated,
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
/// </returns>
public static double? CalculateAverageHitError(this IEnumerable<HitEvent> hitEvents)
{
double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
if (timeOffsets.Length == 0)
return null;
return timeOffsets.Average();
}
/// <summary>
/// Calculates the median hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
/// </summary>
+2 -2
View File
@@ -104,14 +104,14 @@ namespace osu.Game.Screens.Footer
},
BackButton = new ScreenBackButton
{
Margin = new MarginPadding { Bottom = 15f, Left = 12f },
Margin = new MarginPadding { Bottom = OsuGame.SCREEN_EDGE_MARGIN, Left = OsuGame.SCREEN_EDGE_MARGIN },
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = onBackPressed,
},
hiddenButtonsContainer = new Container<ScreenFooterButton>
{
Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding },
Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
Y = 10f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
@@ -25,16 +25,12 @@ namespace osu.Game.Screens.Footer
{
public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
{
private const float shear = OsuGame.SHEAR;
protected const int CORNER_RADIUS = 10;
protected const int BUTTON_HEIGHT = 75;
protected const int BUTTON_WIDTH = 116;
public Bindable<Visibility> OverlayState = new Bindable<Visibility>();
protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear, 0);
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -89,7 +85,7 @@ namespace osu.Game.Screens.Footer
Colour = Colour4.Black.Opacity(0.25f),
Offset = new Vector2(0, 2),
},
Shear = BUTTON_SHEAR,
Shear = OsuGame.SHEAR,
Masking = true,
CornerRadius = CORNER_RADIUS,
RelativeSizeAxes = Axes.Both,
@@ -108,7 +104,7 @@ namespace osu.Game.Screens.Footer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Shear = -BUTTON_SHEAR,
Shear = -OsuGame.SHEAR,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
@@ -135,7 +131,7 @@ namespace osu.Game.Screens.Footer
},
new Container
{
Shear = -BUTTON_SHEAR,
Shear = -OsuGame.SHEAR,
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre,
Y = -CORNER_RADIUS,
@@ -116,7 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = new Vector2(OsuGame.SHEAR, 0f),
Shear = OsuGame.SHEAR,
Children = new Drawable[]
{
titleContainer = new Container
@@ -147,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Origin = Anchor.Centre,
Text = "Today's Challenge",
Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f },
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Shear = -OsuGame.SHEAR,
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
},
}
@@ -173,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Origin = Anchor.Centre,
Text = room.Name.Split(':', StringSplitOptions.TrimEntries).Last(),
Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f },
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Shear = -OsuGame.SHEAR,
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
},
}
@@ -246,7 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Shear = -OsuGame.SHEAR,
MaxWidth = horizontal_info_size,
Text = beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false),
Padding = new MarginPadding { Horizontal = 5f },
@@ -257,7 +257,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Text = $"Difficulty: {beatmap.DifficultyName}",
Font = OsuFont.GetFont(size: 20, italics: true),
MaxWidth = horizontal_info_size,
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Shear = -OsuGame.SHEAR,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
@@ -266,13 +266,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Text = $"by {beatmap.Metadata.Author.Username}",
Font = OsuFont.GetFont(size: 16, italics: true),
MaxWidth = horizontal_info_size,
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Shear = -OsuGame.SHEAR,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
starRatingDisplay = new StarRatingDisplay(default)
{
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Shear = -OsuGame.SHEAR,
Margin = new MarginPadding(5),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@@ -301,7 +301,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Shear = -OsuGame.SHEAR,
Current =
{
Value = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray()
@@ -329,7 +329,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Origin = Anchor.Centre,
FillMode = FillMode.Fit,
Scale = new Vector2(1.2f),
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Shear = -OsuGame.SHEAR,
}, c =>
{
beatmapBackground.Add(c);
@@ -17,7 +17,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.SelectV2.Leaderboards;
using osu.Game.Screens.SelectV2;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
@@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private readonly Room room;
private readonly PlaylistItem playlistItem;
private FillFlowContainer<LeaderboardScoreV2> scoreFlow = null!;
private FillFlowContainer<BeatmapLeaderboardScore> scoreFlow = null!;
private Container userBestContainer = null!;
private SectionHeader userBestHeader = null!;
private LoadingLayer loadingLayer = null!;
@@ -91,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = scoreFlow = new FillFlowContainer<LeaderboardScoreV2>
Child = scoreFlow = new FillFlowContainer<BeatmapLeaderboardScore>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
}
else
{
LoadComponentsAsync(best.Select((s, index) => new LeaderboardScoreV2(s, sheared: false)
LoadComponentsAsync(best.Select((s, index) => new BeatmapLeaderboardScore(s, sheared: false)
{
Rank = index + 1,
IsPersonalBest = s.UserID == api.LocalUser.Value.Id,
@@ -178,7 +178,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
if (userBest != null)
{
userBestContainer.Add(new LeaderboardScoreV2(userBest, sheared: false)
userBestContainer.Add(new BeatmapLeaderboardScore(userBest, sheared: false)
{
Rank = userBest.Position,
IsPersonalBest = true,
@@ -31,13 +31,13 @@ using osu.Game.Online.Chat;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay
{
@@ -76,6 +76,7 @@ namespace osu.Game.Screens.OnlinePlay
private readonly DelayedLoadWrapper onScreenLoader;
private readonly IBindable<bool> valid = new Bindable<bool>();
private readonly IBindable<bool> completed = new Bindable<bool>();
private IBeatmapInfo? beatmap;
private IRulesetInfo? ruleset;
@@ -128,6 +129,7 @@ namespace osu.Game.Screens.OnlinePlay
Item = item;
valid.BindTo(item.Valid);
completed.BindTo(item.Completed);
}
[BackgroundDependencyLoader]
@@ -525,9 +527,27 @@ namespace osu.Game.Screens.OnlinePlay
private IEnumerable<Drawable> createButtons() => new[]
{
beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap),
new CompletionIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Visible = { BindTarget = completed }
},
beatmap == null
? Empty().With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
})
: new PlaylistDownloadButton(beatmap)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(30, 30),
Action = () => RequestResults?.Invoke(Item),
Alpha = AllowShowingResults ? 1 : 0,
@@ -535,13 +555,17 @@ namespace osu.Game.Screens.OnlinePlay
},
editButton = new PlaylistEditButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(30, 30),
Alpha = AllowEditing ? 1 : 0,
Action = () => RequestEdit?.Invoke(Item),
TooltipText = CommonStrings.ButtonsEdit
TooltipText = Resources.Localisation.Web.CommonStrings.ButtonsEdit
},
removeButton = new PlaylistRemoveButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(30, 30),
Alpha = AllowDeletion ? 1 : 0,
Action = () => RequestDeletion?.Invoke(Item),
@@ -768,5 +792,64 @@ namespace osu.Game.Screens.OnlinePlay
this.allowInteraction = allowInteraction;
}
}
private partial class CompletionIcon : CompositeDrawable, IHasTooltip
{
public readonly BindableBool Visible = new BindableBool();
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(16),
Masking = true,
Colour = colours.Lime0,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(0.5f),
Colour = OsuColour.Gray(0.5f),
Icon = FontAwesome.Solid.Check
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Visible.BindValueChanged(onVisibleChanged, true);
}
private void onVisibleChanged(ValueChangedEvent<bool> visible)
{
if (visible.NewValue)
{
Size = new Vector2(16);
Alpha = 1;
}
else
{
Size = Vector2.Zero;
Alpha = 0;
}
}
public LocalisableString TooltipText => DrawableRoomPlaylistItemStrings.CompletedTooltip;
}
}
}
@@ -465,6 +465,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
});
updateSetupState();
updateUserScore();
updateGameplayState();
}
@@ -480,6 +481,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
case nameof(Room.RoomID):
updateSetupState();
break;
case nameof(Room.UserScore):
updateUserScore();
break;
}
}
@@ -507,12 +512,32 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
progressSection.Alpha = room.MaxAttempts != null ? 1 : 0;
drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist);
updateUserScore();
// Select an initial item for the user to help them get into a playable state quicker.
SelectedItem.Value = room.Playlist.FirstOrDefault();
});
}
}
/// <summary>
/// Responds to changes in <see cref="Room.UserScore"/> to mark playlist items as completed.
/// </summary>
private void updateUserScore()
{
if (room.UserScore == null)
return;
if (drawablePlaylist.Items.Count == 0)
return;
foreach (var item in room.UserScore.PlaylistItemAttempts)
{
if (item.Passed)
drawablePlaylist.Items.Single(i => i.ID == item.PlaylistItemID).MarkCompleted();
}
}
/// <summary>
/// Adjusts the rate at which the <see cref="Room"/> is updated.
/// </summary>
+22 -3
View File
@@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
@@ -59,6 +60,12 @@ namespace osu.Game.Screens.Play
this.createScore = createScore;
}
[Resolved]
private LeaderboardManager leaderboardManager { get; set; } = null!;
private readonly IBindable<LeaderboardScores> globalScores = new Bindable<LeaderboardScores>();
private readonly BindableList<ScoreInfo> localScores = new BindableList<ScoreInfo>();
/// <summary>
/// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings.
/// </summary>
@@ -87,6 +94,20 @@ namespace osu.Game.Screens.Play
HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings);
}
protected override void LoadComplete()
{
base.LoadComplete();
globalScores.BindTo(leaderboardManager.Scores);
globalScores.BindValueChanged(_ =>
{
localScores.Clear();
if (globalScores.Value is LeaderboardScores g)
localScores.AddRange(g.AllScores.OrderByTotalScore());
}, true);
}
protected override void PrepareReplay()
{
DrawableRuleset?.SetReplayScore(Score);
@@ -97,13 +118,11 @@ namespace osu.Game.Screens.Play
// Don't re-import replay scores as they're already present in the database.
protected override Task ImportScore(Score score) => Task.CompletedTask;
public readonly BindableList<ScoreInfo> LeaderboardScores = new BindableList<ScoreInfo>();
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
{
AlwaysVisible = { Value = true },
Scores = { BindTarget = LeaderboardScores }
Scores = { BindTarget = localScores }
};
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score)
+24 -4
View File
@@ -6,10 +6,12 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Scoring;
@@ -29,6 +31,26 @@ namespace osu.Game.Screens.Play
{
}
[Resolved]
private LeaderboardManager leaderboardManager { get; set; } = null!;
private readonly IBindable<LeaderboardScores> globalScores = new Bindable<LeaderboardScores>();
private readonly BindableList<ScoreInfo> localScores = new BindableList<ScoreInfo>();
protected override void LoadComplete()
{
base.LoadComplete();
globalScores.BindTo(leaderboardManager.Scores);
globalScores.BindValueChanged(_ =>
{
localScores.Clear();
if (globalScores.Value is LeaderboardScores g)
localScores.AddRange(g.AllScores.OrderByTotalScore());
}, true);
}
protected override APIRequest<APIScoreToken> CreateTokenRequest()
{
int beatmapId = Beatmap.Value.BeatmapInfo.OnlineID;
@@ -43,13 +65,11 @@ namespace osu.Game.Screens.Play
return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
}
public readonly BindableList<ScoreInfo> LeaderboardScores = new BindableList<ScoreInfo>();
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
{
AlwaysVisible = { Value = false },
Scores = { BindTarget = LeaderboardScores }
Scores = { BindTarget = localScores }
};
protected override bool ShouldExitOnTokenRetrievalFailure(Exception exception) => false;
@@ -59,7 +79,7 @@ namespace osu.Game.Screens.Play
// Before importing a score, stop binding the leaderboard with its score source.
// This avoids a case where the imported score may cause a leaderboard refresh
// (if the leaderboard's source is local).
LeaderboardScores.UnbindBindings();
globalScores.UnbindBindings();
return base.ImportScore(score);
}
@@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Ranking.Statistics
{
/// <summary>
/// Displays the average hit error statistic for a given play.
/// Displays the unstable rate statistic for a given play.
/// </summary>
public partial class AverageHitError : SimpleStatisticItem<double?>
{
/// <summary>
/// Creates and computes an <see cref="AverageHitError"/> statistic.
/// </summary>
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the average hit error based on.</param>
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
public AverageHitError(IEnumerable<HitEvent> hitEvents)
: base("Average Hit Error")
{
Value = hitEvents.CalculateMedianHitError();
Value = hitEvents.CalculateAverageHitError();
}
protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}";
@@ -263,7 +263,6 @@ namespace osu.Game.Screens.Select
},
new BeatmapSetOnlineStatusPill
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Shear = -wedged_container_shear,
@@ -77,7 +77,6 @@ namespace osu.Game.Screens.Select.Carousel
},
new BeatmapSetOnlineStatusPill
{
AutoSizeAxes = Axes.Both,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
TextSize = 11,
@@ -3,21 +3,17 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using Realms;
namespace osu.Game.Screens.Select.Leaderboards
{
@@ -67,6 +63,8 @@ namespace osu.Game.Screens.Select.Leaderboards
}
}
private readonly Bindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -77,14 +75,7 @@ namespace osu.Game.Screens.Select.Leaderboards
private IAPIProvider api { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private IDisposable? scoreSubscription;
private GetScoresRequest? scoreRetrievalRequest;
private LeaderboardManager leaderboardManager { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
@@ -95,15 +86,13 @@ namespace osu.Game.Screens.Select.Leaderboards
if (filterMods)
RefetchScores();
};
((IBindable<LeaderboardScores?>)fetchedScores).BindTo(leaderboardManager.Scores);
}
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
protected override APIRequest? FetchScores(CancellationToken cancellationToken)
{
scoreRetrievalRequest?.Cancel();
scoreRetrievalRequest = null;
var fetchBeatmapInfo = BeatmapInfo;
if (fetchBeatmapInfo == null)
@@ -114,13 +103,7 @@ namespace osu.Game.Screens.Select.Leaderboards
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
if (Scope == BeatmapLeaderboardScope.Local)
{
subscribeToLocalScores(fetchBeatmapInfo, cancellationToken);
return null;
}
if (!api.IsLoggedIn)
if (!api.IsLoggedIn && IsOnlineScope)
{
SetErrorState(LeaderboardState.NotLoggedIn);
return null;
@@ -132,7 +115,7 @@ namespace osu.Game.Screens.Select.Leaderboards
return null;
}
if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && IsOnlineScope)
{
SetErrorState(LeaderboardState.BeatmapUnavailable);
return null;
@@ -150,29 +133,24 @@ namespace osu.Game.Screens.Select.Leaderboards
return null;
}
IReadOnlyList<Mod>? requestMods = null;
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;
}
if (filterMods && !mods.Value.Any())
// add nomod for the request
requestMods = new Mod[] { new ModNoMod() };
else if (filterMods)
requestMods = mods.Value;
fetchedScores.UnbindEvents();
fetchedScores.BindValueChanged(scores =>
{
if (scores.NewValue != null)
Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore));
}, true);
}, cancellationToken);
var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
newRequest.Success += response => Schedule(() =>
{
// Request may have changed since fetch request.
// Can't rely on request cancellation due to Schedule inside SetScores so let's play it safe.
if (!newRequest.Equals(scoreRetrievalRequest))
return;
SetScores(
response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo)).OrderByTotalScore(),
response.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)
);
});
return scoreRetrievalRequest = newRequest;
return null;
}
protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope, Scope != BeatmapLeaderboardScope.Friend)
@@ -184,59 +162,5 @@ namespace osu.Game.Screens.Select.Leaderboards
{
Action = () => ScoreSelected?.Invoke(model)
};
private void subscribeToLocalScores(BeatmapInfo beatmapInfo, CancellationToken cancellationToken)
{
Debug.Assert(beatmapInfo != null);
scoreSubscription?.Dispose();
scoreSubscription = null;
scoreSubscription = 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"
, beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged);
void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes)
{
if (cancellationToken.IsCancellationRequested)
return;
// This subscription may fire from changes to linked beatmaps, which we don't care about.
// It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.
if (changes?.HasCollectionChanges() == false)
return;
var scores = sender.AsEnumerable();
if (filterMods && !mods.Value.Any())
{
// we need to filter out all scores that have any mods to get all local nomod scores
scores = scores.Where(s => !s.Mods.Any());
}
else if (filterMods)
{
// otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters)
// we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself
var selectedMods = mods.Value.Select(m => m.Acronym).ToHashSet();
scores = scores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym)));
}
scores = scores.Detach().OrderByTotalScore();
SetScores(scores);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
scoreSubscription?.Dispose();
scoreRetrievalRequest?.Cancel();
}
}
}
@@ -89,7 +89,7 @@ namespace osu.Game.Screens.Select.Options
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Shear = new Vector2(0.2f, 0f),
Shear = OsuGame.SHEAR,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
+2 -8
View File
@@ -129,17 +129,11 @@ namespace osu.Game.Screens.Select
if (replayGeneratingMod != null)
{
player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))
{
LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores }
};
player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods));
}
else
{
player = new SoloPlayer
{
LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores }
};
player = new SoloPlayer();
}
return player;
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Pooling;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Select;
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
@@ -7,6 +7,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Utils;
@@ -1,330 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Screens.Select;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapInfoWedgeV2 : VisibilityContainer
{
public const float WEDGE_HEIGHT = 120;
private const float shear_width = 21;
private const float transition_duration = 250;
private const float corner_radius = 10;
private const float colour_bar_width = 30;
/// Todo: move this const out to song select when more new design elements are implemented for the beatmap details area, since it applies to text alignment of various elements
private const float text_margin = 62;
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / WEDGE_HEIGHT, 0);
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
protected Container? DisplayedContent { get; private set; }
protected WedgeInfoText? Info { get; private set; }
private Container difficultyColourBar = null!;
private StarCounter starCounter = null!;
private StarRatingDisplay starRatingDisplay = null!;
private BeatmapSetOnlineStatusPill statusPill = null!;
private Container content = null!;
private IBindable<StarDifficulty?>? starDifficulty;
private CancellationTokenSource? cancellationSource;
public BeatmapInfoWedgeV2()
{
Height = WEDGE_HEIGHT;
Shear = wedged_container_shear;
Masking = true;
Margin = new MarginPadding { Left = -corner_radius };
EdgeEffect = new EdgeEffectParameters
{
Colour = Colour4.Black.Opacity(0.2f),
Type = EdgeEffectType.Shadow,
Radius = 3,
};
CornerRadius = corner_radius;
}
[BackgroundDependencyLoader]
private void load()
{
Child = content = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
// These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area
difficultyColourBar = new Container
{
Colour = Colour4.Transparent,
Depth = float.MaxValue,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
// By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it.
Width = colour_bar_width + corner_radius,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
new Container
{
// Applying the shear to this container and nesting the starCounter inside avoids
// the deformation that occurs if the shear is applied to the starCounter whilst rotated
Shear = -wedged_container_shear,
X = -colour_bar_width / 2,
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = colour_bar_width,
Child = starCounter = new StarCounter
{
Rotation = (float)(Math.Atan(shear_width / WEDGE_HEIGHT) * (180 / Math.PI)),
Colour = Colour4.Transparent,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(0.35f),
Direction = FillDirection.Vertical
}
},
new FillFlowContainer
{
Name = "Topright-aligned metadata",
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Top = 3, Right = colour_bar_width + 8 },
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(0, 5),
Depth = float.MinValue,
Children = new Drawable[]
{
starRatingDisplay = new StarRatingDisplay(default, animated: true)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Shear = -wedged_container_shear,
Alpha = 0,
},
statusPill = new BeatmapSetOnlineStatusPill
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Shear = -wedged_container_shear,
TextSize = 11,
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
Alpha = 0,
}
}
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ruleset.BindValueChanged(_ => updateDisplay());
starRatingDisplay.Current.BindValueChanged(s =>
{
// use actual stars as star counter has its own animation
starCounter.Current = (float)s.NewValue.Stars;
}, true);
starRatingDisplay.DisplayedStars.BindValueChanged(s =>
{
// sync color with star rating display
starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f);
difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue));
}, true);
}
private const double animation_duration = 600;
protected override void PopIn()
{
this.MoveToX(0, animation_duration, Easing.OutQuint);
this.FadeIn(200, Easing.In);
}
protected override void PopOut()
{
this.MoveToX(-150, animation_duration, Easing.OutQuint);
this.FadeOut(200, Easing.OutQuint);
}
private WorkingBeatmap beatmap = null!;
public WorkingBeatmap Beatmap
{
get => beatmap;
set
{
if (beatmap == value) return;
beatmap = value;
updateDisplay();
}
}
private Container? loadingInfo;
private void updateDisplay()
{
statusPill.Status = beatmap.BeatmapInfo.Status;
starDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token);
starDifficulty.BindValueChanged(s =>
{
starRatingDisplay.Current.Value = s.NewValue ?? default;
starRatingDisplay.FadeIn(transition_duration);
});
Scheduler.AddOnce(() =>
{
LoadComponentAsync(loadingInfo = new Container
{
Padding = new MarginPadding { Right = colour_bar_width },
RelativeSizeAxes = Axes.Both,
Depth = DisplayedContent?.Depth + 1 ?? 0,
Child = new Container
{
Masking = true,
CornerRadius = corner_radius,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
// TODO: New wedge design uses a coloured horizontal gradient for its background, however this lacks implementation information in the figma draft.
// pending https://www.figma.com/file/DXKwqZhD5yyb1igc3mKo1P?node-id=2980:3361#340801912 being answered.
new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear },
Info = new WedgeInfoText(beatmap) { Shear = -Shear }
}
}
}, d =>
{
// Ensure we are the most recent loaded wedge.
if (d != loadingInfo) return;
removeOldInfo();
content.Add(DisplayedContent = d);
});
});
void removeOldInfo()
{
DisplayedContent?.FadeOut(transition_duration);
DisplayedContent?.Expire();
DisplayedContent = null;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
cancellationSource?.Cancel();
}
public partial class WedgeInfoText : Container
{
public OsuSpriteText TitleLabel { get; private set; } = null!;
public OsuSpriteText ArtistLabel { get; private set; } = null!;
private readonly WorkingBeatmap working;
public WedgeInfoText(WorkingBeatmap working)
{
this.working = working;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(SongSelect? songSelect, LocalisationManager localisation)
{
var metadata = working.Metadata;
var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title);
var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
Child = new FillFlowContainer
{
Name = "Top-left aligned metadata",
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Left = text_margin, Top = 12 },
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)),
Child = TitleLabel = new TruncatingSpriteText
{
Shadow = true,
Text = titleText,
Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold),
},
},
new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)),
Child = ArtistLabel = new TruncatingSpriteText
{
// TODO : figma design has a diffused shadow, instead of the solid one present here, not possible currently as far as i'm aware.
Shadow = true,
Text = artistText,
// Not sure if this should be semi bold or medium
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
},
},
}
};
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
// best effort to confine the auto-sized text to wedge bounds
// the artist label doesn't have an extra text_margin as it doesn't touch the right metadata
TitleLabel.MaxWidth = DrawWidth - text_margin * 2 - shear_width;
ArtistLabel.MaxWidth = DrawWidth - text_margin - shear_width;
}
}
}
}
@@ -41,9 +41,9 @@ using osuTK;
using osuTK.Graphics;
using CommonStrings = osu.Game.Localisation.CommonStrings;
namespace osu.Game.Screens.SelectV2.Leaderboards
namespace osu.Game.Screens.SelectV2
{
public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip<ScoreInfo>
public partial class BeatmapLeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip<ScoreInfo>
{
public Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>();
@@ -117,12 +117,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
public ITooltip<ScoreInfo> GetCustomTooltip() => new LeaderboardScoreTooltip();
public virtual ScoreInfo TooltipContent => score;
public LeaderboardScoreV2(ScoreInfo score, bool sheared = true)
public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true)
{
this.score = score;
this.sheared = sheared;
Shear = new Vector2(sheared ? OsuGame.SHEAR : 0, 0);
Shear = sheared ? OsuGame.SHEAR : Vector2.Zero;
RelativeSizeAxes = Axes.X;
Height = height;
}
@@ -255,7 +255,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
{
RelativeSizeAxes = Axes.Both,
User = score.User,
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)),
@@ -286,7 +286,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1.1f),
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
RelativeSizeAxes = Axes.Both,
})
{
@@ -326,7 +326,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
{
flagBadgeAndDateContainer = new FillFlowContainer
{
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
AutoSizeAxes = Axes.Both,
@@ -356,7 +356,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
nameLabel = new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
Text = user.Username,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold)
}
@@ -372,7 +372,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
Name = @"Statistics container",
Padding = new MarginPadding { Right = 40 },
Spacing = new Vector2(25, 0),
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
@@ -430,7 +430,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
},
RankContainer = new Container
{
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
@@ -488,7 +488,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
UseFullGlyphHeight = false,
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
Current = scoreManager.GetBindableTotalScoreString(score),
Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light),
},
@@ -496,7 +496,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2f, 0f),
@@ -704,7 +704,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
Child = new OsuSpriteText
{
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true),
@@ -1,195 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
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.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Screens.SelectV2.Footer
{
public partial class BeatmapOptionsPopover : OsuPopover
{
private FillFlowContainer buttonFlow = null!;
private readonly ScreenFooterButtonOptions footerButton;
[Cached]
private readonly OverlayColourProvider colourProvider;
private WorkingBeatmap beatmapWhenOpening = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
public BeatmapOptionsPopover(ScreenFooterButtonOptions footerButton, OverlayColourProvider colourProvider)
{
this.footerButton = footerButton;
this.colourProvider = colourProvider;
}
[BackgroundDependencyLoader]
private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager)
{
Content.Padding = new MarginPadding(5);
Child = buttonFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(3),
};
beatmapWhenOpening = beatmap.Value;
addHeader(CommonStrings.General);
addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show());
addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString());
addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo);
addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName);
// TODO: make work, and make show "unplayed" or "played" based on status.
addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null);
addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo);
// if (songSelect != null && songSelect.AllowEditing)
addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo);
addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo));
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this));
beatmap.BindValueChanged(_ => Hide());
}
private void addHeader(LocalisableString text, string? context = null)
{
var textFlow = new OsuTextFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding(10),
};
textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold));
if (context != null)
{
textFlow.NewLine();
textFlow.AddText(context, t =>
{
t.Colour = colourProvider.Content2;
t.Font = t.Font.With(size: 13);
});
}
buttonFlow.Add(textFlow);
}
private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null)
{
var button = new OptionButton
{
Text = text,
Icon = icon,
TextColour = colour,
Action = () =>
{
Scheduler.AddDelayed(Hide, 50);
action?.Invoke();
},
};
buttonFlow.Add(button);
}
private partial class OptionButton : OsuButton
{
public IconUsage Icon { get; init; }
public Color4? TextColour { get; init; }
public OptionButton()
{
Size = new Vector2(265, 50);
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
BackgroundColour = colourProvider.Background3;
SpriteText.Colour = TextColour ?? Color4.White;
Content.CornerRadius = 10;
Add(new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(17),
X = 15,
Icon = Icon,
Colour = TextColour ?? Color4.White,
});
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 40
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// don't absorb control as ToolbarRulesetSelector uses control + number to navigate
if (e.ControlPressed) return false;
if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9)
{
int requested = e.Key - Key.Number1;
OptionButton? found = buttonFlow.Children.OfType<OptionButton>().ElementAtOrDefault(requested);
if (found != null)
{
found.TriggerClick();
return true;
}
}
return base.OnKeyDown(e);
}
protected override void UpdateState(ValueChangedEvent<Visibility> state)
{
base.UpdateState(state);
footerButton.OverlayState.Value = state.NewValue;
}
}
}
@@ -28,9 +28,9 @@ using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2.Footer
namespace osu.Game.Screens.SelectV2
{
public partial class ScreenFooterButtonMods : ScreenFooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
public partial class FooterButtonMods : ScreenFooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
{
private const float bar_height = 30f;
private const float mod_display_portion = 0.65f;
@@ -58,7 +58,7 @@ namespace osu.Game.Screens.SelectV2.Footer
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public ScreenFooterButtonMods(ModSelectOverlay overlay)
public FooterButtonMods(ModSelectOverlay overlay)
: base(overlay)
{
}
@@ -78,7 +78,7 @@ namespace osu.Game.Screens.SelectV2.Footer
Y = -5f,
Depth = float.MaxValue,
Origin = Anchor.BottomLeft,
Shear = BUTTON_SHEAR,
Shear = OsuGame.SHEAR,
CornerRadius = CORNER_RADIUS,
Size = new Vector2(BUTTON_WIDTH, bar_height),
Masking = true,
@@ -108,7 +108,7 @@ namespace osu.Game.Screens.SelectV2.Footer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -BUTTON_SHEAR,
Shear = -OsuGame.SHEAR,
UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold)
}
@@ -130,7 +130,7 @@ namespace osu.Game.Screens.SelectV2.Footer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -BUTTON_SHEAR,
Shear = -OsuGame.SHEAR,
Scale = new Vector2(0.5f),
Current = { BindTarget = Current },
ExpansionMode = ExpansionMode.AlwaysContracted,
@@ -139,7 +139,7 @@ namespace osu.Game.Screens.SelectV2.Footer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -BUTTON_SHEAR,
Shear = -OsuGame.SHEAR,
Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold),
Mods = { BindTarget = Current },
}
@@ -305,7 +305,7 @@ namespace osu.Game.Screens.SelectV2.Footer
Y = -5f;
Depth = float.MaxValue;
Origin = Anchor.BottomLeft;
Shear = BUTTON_SHEAR;
Shear = OsuGame.SHEAR;
CornerRadius = CORNER_RADIUS;
AutoSizeAxes = Axes.X;
Height = bar_height;
@@ -329,7 +329,7 @@ namespace osu.Game.Screens.SelectV2.Footer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -BUTTON_SHEAR,
Shear = -OsuGame.SHEAR,
Text = ModSelectOverlayStrings.Unranked.ToUpper(),
Margin = new MarginPadding { Horizontal = 15 },
UseFullGlyphHeight = false,
@@ -5,15 +5,14 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Screens.Footer;
namespace osu.Game.Screens.SelectV2.Footer
namespace osu.Game.Screens.SelectV2
{
public partial class ScreenFooterButtonOptions : ScreenFooterButton, IHasPopover
public partial class FooterButtonOptions : ScreenFooterButton, IHasPopover
{
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -29,6 +28,6 @@ namespace osu.Game.Screens.SelectV2.Footer
Action = this.ShowPopover;
}
public Popover GetPopover() => new BeatmapOptionsPopover(this, colourProvider);
public Framework.Graphics.UserInterface.Popover GetPopover() => new Popover(this, colourProvider);
}
}
@@ -0,0 +1,198 @@
// 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.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Screens.SelectV2
{
public partial class FooterButtonOptions
{
public partial class Popover : OsuPopover
{
private FillFlowContainer buttonFlow = null!;
private readonly FooterButtonOptions footerButton;
[Cached]
private readonly OverlayColourProvider colourProvider;
private WorkingBeatmap beatmapWhenOpening = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
public Popover(FooterButtonOptions footerButton, OverlayColourProvider colourProvider)
{
this.footerButton = footerButton;
this.colourProvider = colourProvider;
}
[BackgroundDependencyLoader]
private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager)
{
Content.Padding = new MarginPadding(5);
Child = buttonFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(3),
};
beatmapWhenOpening = beatmap.Value;
addHeader(CommonStrings.General);
addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show());
addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString());
addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo);
addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName);
// TODO: make work, and make show "unplayed" or "played" based on status.
addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null);
addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo);
// if (songSelect != null && songSelect.AllowEditing)
addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo);
addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo));
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this));
beatmap.BindValueChanged(_ => Hide());
}
private void addHeader(LocalisableString text, string? context = null)
{
var textFlow = new OsuTextFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding(10),
};
textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold));
if (context != null)
{
textFlow.NewLine();
textFlow.AddText(context, t =>
{
t.Colour = colourProvider.Content2;
t.Font = t.Font.With(size: 13);
});
}
buttonFlow.Add(textFlow);
}
private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null)
{
var button = new OptionButton
{
Text = text,
Icon = icon,
TextColour = colour,
Action = () =>
{
Scheduler.AddDelayed(Hide, 50);
action?.Invoke();
},
};
buttonFlow.Add(button);
}
private partial class OptionButton : OsuButton
{
public IconUsage Icon { get; init; }
public Color4? TextColour { get; init; }
public OptionButton()
{
Size = new Vector2(265, 50);
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
BackgroundColour = colourProvider.Background3;
SpriteText.Colour = TextColour ?? Color4.White;
Content.CornerRadius = 10;
Add(new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(17),
X = 15,
Icon = Icon,
Colour = TextColour ?? Color4.White,
});
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 40
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// don't absorb control as ToolbarRulesetSelector uses control + number to navigate
if (e.ControlPressed) return false;
if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9)
{
int requested = e.Key - Key.Number1;
OptionButton? found = buttonFlow.Children.OfType<OptionButton>().ElementAtOrDefault(requested);
if (found != null)
{
found.TriggerClick();
return true;
}
}
return base.OnKeyDown(e);
}
protected override void UpdateState(ValueChangedEvent<Visibility> state)
{
base.UpdateState(state);
footerButton.OverlayState.Value = state.NewValue;
}
}
}
}
@@ -14,9 +14,9 @@ using osu.Game.Screens.Footer;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.SelectV2.Footer
namespace osu.Game.Screens.SelectV2
{
public partial class ScreenFooterButtonRandom : ScreenFooterButton
public partial class FooterButtonRandom : ScreenFooterButton
{
public Action? NextRandom { get; set; }
public Action? PreviousRandom { get; set; }

Some files were not shown because too many files have changed in this diff Show More