mirror of
https://github.com/ppy/osu.git
synced 2026-06-07 04:13:38 +08:00
Merge branch 'master' into bss-audio-feedback
This commit is contained in:
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.418.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.419.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,10 +176,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
|
||||
AccentColour.Value = Color4.White;
|
||||
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
|
||||
Arrow.Alpha = 0;
|
||||
}
|
||||
|
||||
Arrow.Alpha = hit ? 0 : 1;
|
||||
|
||||
LifetimeEnd = HitStateUpdateTime + 700;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Extensions.PolygonExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -16,6 +16,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@@ -23,7 +24,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestFixture]
|
||||
public partial class TestSceneGameplayLeaderboard : OsuTestScene
|
||||
{
|
||||
private TestGameplayLeaderboard leaderboard;
|
||||
private TestDrawableGameplayLeaderboard leaderboard = null!;
|
||||
|
||||
[Cached(typeof(IGameplayLeaderboardProvider))]
|
||||
private TestGameplayLeaderboardProvider leaderboardProvider = new TestGameplayLeaderboardProvider();
|
||||
|
||||
private readonly BindableLong playerScore = new BindableLong();
|
||||
|
||||
@@ -31,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("toggle expanded", () =>
|
||||
{
|
||||
if (leaderboard != null)
|
||||
if (leaderboard.IsNotNull())
|
||||
leaderboard.Expanded.Value = !leaderboard.Expanded.Value;
|
||||
});
|
||||
|
||||
@@ -57,10 +61,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
// has caused layout to not work in the past.
|
||||
|
||||
AddUntilStep("wait for fill flow layout",
|
||||
() => leaderboard.ChildrenOfType<FillFlowContainer<GameplayLeaderboardScore>>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad));
|
||||
() => leaderboard.ChildrenOfType<FillFlowContainer<DrawableGameplayLeaderboardScore>>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad));
|
||||
|
||||
AddUntilStep("wait for some scores not masked away",
|
||||
() => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre)));
|
||||
() => leaderboard.ChildrenOfType<DrawableGameplayLeaderboardScore>().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
|
||||
|
||||
@@ -139,7 +143,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
checkHeight(8);
|
||||
|
||||
void checkHeight(int panelCount)
|
||||
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
|
||||
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -179,6 +183,30 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
() => Does.Contain("#FF549A"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTrackedScorePosition([Values] bool partial)
|
||||
{
|
||||
createLeaderboard(partial);
|
||||
|
||||
AddStep("add many scores in one go", () =>
|
||||
{
|
||||
for (int i = 0; i < 49; i++)
|
||||
createRandomScore(new APIUser { Username = $"Player {i + 1}" });
|
||||
|
||||
// Add player at end to force an animation down the whole list.
|
||||
playerScore.Value = 0;
|
||||
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
||||
});
|
||||
|
||||
if (partial)
|
||||
AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null);
|
||||
else
|
||||
AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50));
|
||||
|
||||
AddStep("move tracked player to top", () => leaderboard.TrackedScore!.TotalScore.Value = 8_000_000);
|
||||
AddUntilStep("all players have non-null position", () => leaderboard.AllScores.Select(s => s.ScorePosition), () => Does.Not.Contain(null));
|
||||
}
|
||||
|
||||
private void addLocalPlayer()
|
||||
{
|
||||
AddStep("add local player", () =>
|
||||
@@ -188,11 +216,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
private void createLeaderboard()
|
||||
private void createLeaderboard(bool partial = false)
|
||||
{
|
||||
AddStep("create leaderboard", () =>
|
||||
{
|
||||
Child = leaderboard = new TestGameplayLeaderboard
|
||||
leaderboardProvider.Scores.Clear();
|
||||
leaderboardProvider.IsPartial = partial;
|
||||
Child = leaderboard = new TestDrawableGameplayLeaderboard
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -205,11 +235,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false)
|
||||
{
|
||||
var leaderboardScore = leaderboard.Add(user, isTracked);
|
||||
leaderboardScore.TotalScore.BindTo(score);
|
||||
var leaderboardScore = new GameplayLeaderboardScore(user, isTracked, score);
|
||||
leaderboardProvider.Scores.Add(leaderboardScore);
|
||||
}
|
||||
|
||||
private partial class TestGameplayLeaderboard : GameplayLeaderboard
|
||||
private partial class TestDrawableGameplayLeaderboard : DrawableGameplayLeaderboard
|
||||
{
|
||||
public float Spacing => Flow.Spacing.Y;
|
||||
|
||||
@@ -220,8 +250,17 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
|
||||
}
|
||||
|
||||
public IEnumerable<GameplayLeaderboardScore> GetAllScoresForUsername(string username)
|
||||
public IEnumerable<DrawableGameplayLeaderboardScore> GetAllScoresForUsername(string username)
|
||||
=> Flow.Where(i => i.User?.Username == username);
|
||||
|
||||
public IEnumerable<DrawableGameplayLeaderboardScore> AllScores => Flow;
|
||||
}
|
||||
|
||||
private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider
|
||||
{
|
||||
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
|
||||
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
|
||||
public bool IsPartial { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene
|
||||
{
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private readonly ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
|
||||
|
||||
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();
|
||||
|
||||
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
|
||||
private readonly Bindable<PlayBeatmapDetailArea.TabType> beatmapTabType = new Bindable<PlayBeatmapDetailArea.TabType>();
|
||||
|
||||
private SoloGameplayLeaderboard leaderboard = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
|
||||
config.BindWith(OsuSetting.BeatmapDetailTab, beatmapTabType);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("clear scores", () => scores.Clear());
|
||||
|
||||
AddStep("create component", () =>
|
||||
{
|
||||
var trackingUser = new APIUser
|
||||
{
|
||||
Username = "local user",
|
||||
Id = 2,
|
||||
};
|
||||
|
||||
Child = leaderboard = new SoloGameplayLeaderboard(trackingUser)
|
||||
{
|
||||
Scores = { BindTarget = scores },
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AlwaysVisible = { Value = false },
|
||||
Expanded = { Value = true },
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("add scores", () => scores.AddRange(createSampleScores()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalUser()
|
||||
{
|
||||
AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v);
|
||||
AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v);
|
||||
AddSliderStep("combo", 0, 10000, 0, v => scoreProcessor.HighestCombo.Value = v);
|
||||
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
|
||||
}
|
||||
|
||||
[TestCase(PlayBeatmapDetailArea.TabType.Local, 51)]
|
||||
[TestCase(PlayBeatmapDetailArea.TabType.Global, null)]
|
||||
[TestCase(PlayBeatmapDetailArea.TabType.Country, null)]
|
||||
[TestCase(PlayBeatmapDetailArea.TabType.Friends, null)]
|
||||
public void TestTrackedScorePosition(PlayBeatmapDetailArea.TabType tabType, int? expectedOverflowIndex)
|
||||
{
|
||||
AddStep($"change TabType to {tabType}", () => beatmapTabType.Value = tabType);
|
||||
AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50));
|
||||
|
||||
AddStep("add one more score", () => scores.Add(new ScoreInfo { User = new APIUser { Username = "New player 1" }, TotalScore = RNG.Next(600000, 1000000) }));
|
||||
|
||||
AddUntilStep("wait for sort", () => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().First().ScorePosition != null);
|
||||
|
||||
if (expectedOverflowIndex == null)
|
||||
AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null);
|
||||
else
|
||||
AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVisibility()
|
||||
{
|
||||
AddStep("set config visible true", () => configVisibility.Value = true);
|
||||
AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
|
||||
|
||||
AddStep("set config visible false", () => configVisibility.Value = false);
|
||||
AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0);
|
||||
|
||||
AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true);
|
||||
AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
|
||||
|
||||
AddStep("set config visible true", () => configVisibility.Value = true);
|
||||
AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1);
|
||||
}
|
||||
|
||||
private static List<ScoreInfo> createSampleScores()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ScoreInfo { User = new APIUser { Username = @"peppy" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
new ScoreInfo { User = new APIUser { Username = @"smoogipoo" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) },
|
||||
}.Concat(Enumerable.Range(0, 44).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -48,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVideoSize()
|
||||
public void TestVideo()
|
||||
{
|
||||
AddStep("load storyboard with only video", () =>
|
||||
{
|
||||
@@ -56,6 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
loadStoryboard("storyboard_only_video.osu", s => s.Beatmap.WidescreenStoryboard = false);
|
||||
});
|
||||
|
||||
AddAssert("storyboard video present in hierarchy", () => this.ChildrenOfType<DrawableStoryboardVideo>().Any());
|
||||
AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
@@ -20,6 +21,7 @@ using osu.Game.Online.Spectator;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@@ -29,11 +31,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
protected readonly BindableList<MultiplayerRoomUser> MultiplayerUsers = new BindableList<MultiplayerRoomUser>();
|
||||
|
||||
protected MultiplayerGameplayLeaderboard? Leaderboard { get; private set; }
|
||||
protected MultiplayerLeaderboardProvider? LeaderboardProvider { get; private set; }
|
||||
|
||||
protected DrawableGameplayLeaderboard? Leaderboard { get; private set; }
|
||||
|
||||
protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId);
|
||||
|
||||
protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard();
|
||||
protected abstract MultiplayerLeaderboardProvider CreateLeaderboardProvider();
|
||||
|
||||
private readonly BindableList<int> multiplayerUserIds = new BindableList<int>();
|
||||
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
|
||||
@@ -124,19 +128,38 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create leaderboard", () =>
|
||||
{
|
||||
Leaderboard?.Expire();
|
||||
Clear(true);
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
||||
|
||||
LoadComponentAsync(Leaderboard = CreateLeaderboard(), Add);
|
||||
LoadComponentAsync(LeaderboardProvider = CreateLeaderboardProvider(), Add);
|
||||
Add(new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = [(typeof(IGameplayLeaderboardProvider), LeaderboardProvider)],
|
||||
Child = Leaderboard = new DrawableGameplayLeaderboard
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => Leaderboard!.IsLoaded);
|
||||
|
||||
AddStep("check watch requests were sent", () =>
|
||||
AddUntilStep("check watch requests were sent", () =>
|
||||
{
|
||||
foreach (var user in MultiplayerUsers)
|
||||
spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once);
|
||||
try
|
||||
{
|
||||
foreach (var user in MultiplayerUsers)
|
||||
spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (MockException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,10 +182,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("check stop watching requests were sent", () =>
|
||||
AddUntilStep("check stop watching requests were sent", () =>
|
||||
{
|
||||
foreach (var user in MultiplayerUsers)
|
||||
spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once);
|
||||
try
|
||||
{
|
||||
foreach (var user in MultiplayerUsers)
|
||||
spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once);
|
||||
return true;
|
||||
}
|
||||
catch (MockException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,12 +235,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
header.Combo++;
|
||||
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
|
||||
header.Statistics[HitResult.Meh]++;
|
||||
header.TotalScore += 50;
|
||||
break;
|
||||
|
||||
default:
|
||||
header.Combo++;
|
||||
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
|
||||
header.Statistics[HitResult.Great]++;
|
||||
header.TotalScore += 300;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -218,3 +251,4 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,16 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
|
||||
{
|
||||
private Dictionary<int, ManualClock> clocks = null!;
|
||||
private MultiSpectatorLeaderboard? leaderboard;
|
||||
private MultiSpectatorLeaderboardProvider? leaderboardProvider;
|
||||
private DrawableGameplayLeaderboard leaderboard = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
@@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("reset", () =>
|
||||
{
|
||||
leaderboard?.RemoveAndDisposeImmediately();
|
||||
Clear(true);
|
||||
|
||||
clocks = new Dictionary<int, ManualClock>
|
||||
{
|
||||
@@ -48,21 +49,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
||||
|
||||
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
|
||||
LoadComponentAsync(leaderboardProvider = new MultiSpectatorLeaderboardProvider(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()), Add);
|
||||
Add(new DependencyProvidingContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Expanded = { Value = true }
|
||||
}, Add);
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = [(typeof(IGameplayLeaderboardProvider), leaderboardProvider)],
|
||||
Child = leaderboard = new DrawableGameplayLeaderboard
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Expanded = { Value = true }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => leaderboard!.IsLoaded);
|
||||
AddUntilStep("wait for user population", () => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().Count() == 2);
|
||||
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
|
||||
AddUntilStep("wait for user population", () => leaderboard.ChildrenOfType<DrawableGameplayLeaderboardScore>().Count() == 2);
|
||||
|
||||
AddStep("add clock sources", () =>
|
||||
{
|
||||
foreach ((int userId, var clock) in clocks)
|
||||
leaderboard!.AddClock(userId, clock);
|
||||
leaderboardProvider!.AddClock(userId, clock);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,6 +130,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
=> AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
|
||||
|
||||
private void assertCombo(int userId, int expectedCombo)
|
||||
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo);
|
||||
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<DrawableGameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,7 +560,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
|
||||
|
||||
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
|
||||
private DrawableGameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<DrawableGameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
|
||||
|
||||
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@@ -25,27 +25,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
return user;
|
||||
}
|
||||
|
||||
protected override MultiplayerGameplayLeaderboard CreateLeaderboard()
|
||||
{
|
||||
return new TestLeaderboard(MultiplayerUsers.ToArray())
|
||||
protected override MultiplayerLeaderboardProvider CreateLeaderboardProvider() =>
|
||||
new TestLeaderboard(MultiplayerUsers.ToArray())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerUserMods()
|
||||
{
|
||||
AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)Leaderboard!).UserMods[0], Is.Empty));
|
||||
AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)LeaderboardProvider!).UserMods[0], Is.Empty));
|
||||
AddStep("last user has NF mod", () =>
|
||||
{
|
||||
Assert.That(((TestLeaderboard)Leaderboard!).UserMods[TOTAL_USERS - 1], Has.One.Items);
|
||||
Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf<OsuModNoFail>());
|
||||
Assert.That(((TestLeaderboard)LeaderboardProvider!).UserMods[TOTAL_USERS - 1], Has.One.Items);
|
||||
Assert.That(((TestLeaderboard)LeaderboardProvider).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf<OsuModNoFail>());
|
||||
});
|
||||
}
|
||||
|
||||
private partial class TestLeaderboard : MultiplayerGameplayLeaderboard
|
||||
private partial class TestLeaderboard : MultiplayerLeaderboardProvider
|
||||
{
|
||||
public Dictionary<int, IReadOnlyList<Mod>> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@@ -24,8 +25,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
return user;
|
||||
}
|
||||
|
||||
protected override MultiplayerGameplayLeaderboard CreateLeaderboard() =>
|
||||
new MultiplayerGameplayLeaderboard(MultiplayerUsers.ToArray())
|
||||
protected override MultiplayerLeaderboardProvider CreateLeaderboardProvider() =>
|
||||
new MultiplayerLeaderboardProvider(MultiplayerUsers.ToArray())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -39,17 +40,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
LoadComponentAsync(new MatchScoreDisplay
|
||||
{
|
||||
Team1Score = { BindTarget = Leaderboard!.TeamScores[0] },
|
||||
Team2Score = { BindTarget = Leaderboard.TeamScores[1] }
|
||||
Team1Score = { BindTarget = LeaderboardProvider!.TeamScores[0] },
|
||||
Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] }
|
||||
}, Add);
|
||||
|
||||
LoadComponentAsync(new GameplayMatchScoreDisplay
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Team1Score = { BindTarget = Leaderboard.TeamScores[0] },
|
||||
Team2Score = { BindTarget = Leaderboard.TeamScores[1] },
|
||||
Expanded = { BindTarget = Leaderboard.Expanded },
|
||||
Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] },
|
||||
Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] },
|
||||
Expanded = { BindTarget = Leaderboard!.Expanded },
|
||||
}, Add);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene
|
||||
{
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private CollectionDropdown dropdown = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
{
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
writeAndRefresh(r => r.RemoveAll<BeatmapCollection>());
|
||||
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Child = dropdown = new CollectionDropdown
|
||||
{
|
||||
Width = 300,
|
||||
Y = 100,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestEmptyCollectionFilterContainsAllBeatmaps()
|
||||
{
|
||||
assertCollectionDropdownContains("All beatmaps");
|
||||
assertCollectionHeaderDisplays("All beatmaps");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionAddedToDropdown()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
assertCollectionDropdownContains("2");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionsCleared()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
|
||||
|
||||
AddAssert("check count 5", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
|
||||
|
||||
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
|
||||
|
||||
AddAssert("check count 2", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionRemovedFromDropdown()
|
||||
{
|
||||
BeatmapCollection first = null!;
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first)));
|
||||
|
||||
assertCollectionDropdownContains("1", false);
|
||||
assertCollectionDropdownContains("2");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionRenamed()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
AddStep("select collection", () => dropdown.Current.Value = dropdown.ItemSource.ElementAt(1));
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First"));
|
||||
|
||||
assertCollectionDropdownContains("First");
|
||||
assertCollectionHeaderDisplays("First");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAllBeatmapFilterDoesNotHaveAddButton()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
|
||||
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionFilterHasAddButton()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
|
||||
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
|
||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
|
||||
|
||||
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
|
||||
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
|
||||
AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
|
||||
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
|
||||
|
||||
AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonAddsAndRemovesBeatmap()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManageCollectionsFilterIsNotSelected()
|
||||
{
|
||||
bool received = false;
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List<string> { "abc" }))));
|
||||
assertCollectionDropdownContains("1");
|
||||
|
||||
AddStep("select collection", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getCollectionDropdownItemAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("watch for filter requests", () =>
|
||||
{
|
||||
received = false;
|
||||
dropdown.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
|
||||
});
|
||||
|
||||
AddStep("click manage collections filter", () =>
|
||||
{
|
||||
int lastItemIndex = dropdown.ChildrenOfType<CollectionDropdown>().Single().Items.Count() - 1;
|
||||
InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("collection filter still selected", () => dropdown.Current.Value.CollectionName == "1");
|
||||
|
||||
AddAssert("filter request not fired", () => !received);
|
||||
}
|
||||
|
||||
private void writeAndRefresh(Action<Realm> action) => Realm.Write(r =>
|
||||
{
|
||||
action(r);
|
||||
r.Refresh();
|
||||
});
|
||||
|
||||
private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All<BeatmapCollection>().First());
|
||||
|
||||
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
|
||||
=> AddUntilStep($"collection dropdown header displays '{collectionName}'",
|
||||
() => shouldDisplay == dropdown.ChildrenOfType<CollectionDropdown.OsuDropdownHeader>().Any(h => h.ChildrenOfType<SpriteText>().Any(t => t.Text == collectionName)));
|
||||
|
||||
private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon));
|
||||
|
||||
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
|
||||
AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
|
||||
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
|
||||
() => shouldContain == dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName));
|
||||
|
||||
private IconButton getAddOrRemoveButton(int index)
|
||||
=> getCollectionDropdownItemAt(index).ChildrenOfType<IconButton>().Single();
|
||||
|
||||
private void addExpandHeaderStep() => AddStep("expand header", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(dropdown.ChildrenOfType<CollectionDropdown.OsuDropdownHeader>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index)
|
||||
{
|
||||
// todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079
|
||||
CollectionFilterMenuItem item = dropdown.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
|
||||
return dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public partial class TestSceneFilterControl : OsuManualInputManagerTestScene
|
||||
{
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private FilterControl control = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
{
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
Content
|
||||
});
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
writeAndRefresh(r => r.RemoveAll<BeatmapCollection>());
|
||||
|
||||
Child = control = new FilterControl
|
||||
Child = new FilterControl
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -59,216 +20,5 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Height = FilterControl.HEIGHT,
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestEmptyCollectionFilterContainsAllBeatmaps()
|
||||
{
|
||||
assertCollectionDropdownContains("All beatmaps");
|
||||
assertCollectionHeaderDisplays("All beatmaps");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionAddedToDropdown()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
assertCollectionDropdownContains("2");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionsCleared()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
|
||||
|
||||
AddAssert("check count 5", () => control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
|
||||
|
||||
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
|
||||
|
||||
AddAssert("check count 2", () => control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionRemovedFromDropdown()
|
||||
{
|
||||
BeatmapCollection first = null!;
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first)));
|
||||
|
||||
assertCollectionDropdownContains("1", false);
|
||||
assertCollectionDropdownContains("2");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionRenamed()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
AddStep("select collection", () =>
|
||||
{
|
||||
var dropdown = control.ChildrenOfType<CollectionDropdown>().Single();
|
||||
dropdown.Current.Value = dropdown.ItemSource.ElementAt(1);
|
||||
});
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First"));
|
||||
|
||||
assertCollectionDropdownContains("First");
|
||||
assertCollectionHeaderDisplays("First");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAllBeatmapFilterDoesNotHaveAddButton()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
|
||||
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionFilterHasAddButton()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
|
||||
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
|
||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
|
||||
|
||||
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
|
||||
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
|
||||
AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
|
||||
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
|
||||
|
||||
AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonAddsAndRemovesBeatmap()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManageCollectionsFilterIsNotSelected()
|
||||
{
|
||||
bool received = false;
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List<string> { "abc" }))));
|
||||
assertCollectionDropdownContains("1");
|
||||
|
||||
AddStep("select collection", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getCollectionDropdownItemAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("watch for filter requests", () =>
|
||||
{
|
||||
received = false;
|
||||
control.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
|
||||
});
|
||||
|
||||
AddStep("click manage collections filter", () =>
|
||||
{
|
||||
int lastItemIndex = control.ChildrenOfType<CollectionDropdown>().Single().Items.Count() - 1;
|
||||
InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes?.Any() == true);
|
||||
|
||||
AddAssert("filter request not fired", () => !received);
|
||||
}
|
||||
|
||||
private void writeAndRefresh(Action<Realm> action) => Realm.Write(r =>
|
||||
{
|
||||
action(r);
|
||||
r.Refresh();
|
||||
});
|
||||
|
||||
private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All<BeatmapCollection>().First());
|
||||
|
||||
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
|
||||
=> AddUntilStep($"collection dropdown header displays '{collectionName}'",
|
||||
() => shouldDisplay == (control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
|
||||
|
||||
private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon));
|
||||
|
||||
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
|
||||
AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
|
||||
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
|
||||
() => shouldContain == control.ChildrenOfType<Menu.DrawableMenuItem>().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName));
|
||||
|
||||
private IconButton getAddOrRemoveButton(int index)
|
||||
=> getCollectionDropdownItemAt(index).ChildrenOfType<IconButton>().Single();
|
||||
|
||||
private void addExpandHeaderStep() => AddStep("expand header", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index)
|
||||
{
|
||||
// todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079
|
||||
CollectionFilterMenuItem item = control.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
|
||||
return control.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Collections
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public partial class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene
|
||||
{
|
||||
@@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Overlays;
|
||||
@@ -24,21 +25,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
private Container? resizeContainer;
|
||||
private float relativeWidth;
|
||||
|
||||
protected virtual Anchor ComponentAnchor => Anchor.TopLeft;
|
||||
protected virtual float InitialRelativeWidth => 0.5f;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
base.Content.Child = resizeContainer = new Container
|
||||
base.Content.Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = relativeWidth,
|
||||
Children = new Drawable[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = resizeContainer = new Container
|
||||
{
|
||||
Content
|
||||
Anchor = ComponentAnchor,
|
||||
Origin = ComponentAnchor,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = relativeWidth,
|
||||
Child = Content
|
||||
}
|
||||
};
|
||||
|
||||
AddSliderStep("change relative width", 0, 1f, 1f, v =>
|
||||
AddSliderStep("change relative width", 0, 1f, InitialRelativeWidth, v =>
|
||||
{
|
||||
if (resizeContainer != null)
|
||||
resizeContainer.Width = v;
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
using Realms;
|
||||
using CollectionDropdown = osu.Game.Screens.SelectV2.CollectionDropdown;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene
|
||||
{
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private CollectionDropdown dropdown = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
{
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
writeAndRefresh(r => r.RemoveAll<BeatmapCollection>());
|
||||
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Child = dropdown = new CollectionDropdown
|
||||
{
|
||||
Width = 300,
|
||||
Y = 100,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestEmptyCollectionFilterContainsAllBeatmaps()
|
||||
{
|
||||
assertCollectionDropdownContains("All beatmaps");
|
||||
assertCollectionHeaderDisplays("All beatmaps");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionAddedToDropdown()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
assertCollectionDropdownContains("2");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionsCleared()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
|
||||
|
||||
AddAssert("check count 5", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
|
||||
|
||||
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
|
||||
|
||||
AddAssert("check count 2", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionRemovedFromDropdown()
|
||||
{
|
||||
BeatmapCollection first = null!;
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first)));
|
||||
|
||||
assertCollectionDropdownContains("1", false);
|
||||
assertCollectionDropdownContains("2");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionRenamed()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
AddStep("select collection", () => dropdown.Current.Value = dropdown.ItemSource.ElementAt(1));
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First"));
|
||||
|
||||
assertCollectionDropdownContains("First");
|
||||
assertCollectionHeaderDisplays("First");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAllBeatmapFilterDoesNotHaveAddButton()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
|
||||
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionFilterHasAddButton()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
|
||||
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
|
||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
|
||||
|
||||
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
|
||||
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
|
||||
AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
|
||||
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
|
||||
|
||||
AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonAddsAndRemovesBeatmap()
|
||||
{
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManageCollectionsFilterIsNotSelected()
|
||||
{
|
||||
bool received = false;
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List<string> { "abc" }))));
|
||||
assertCollectionDropdownContains("1");
|
||||
|
||||
AddStep("select collection", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getCollectionDropdownItemAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("watch for filter requests", () =>
|
||||
{
|
||||
received = false;
|
||||
dropdown.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
|
||||
});
|
||||
|
||||
AddStep("click manage collections filter", () =>
|
||||
{
|
||||
int lastItemIndex = dropdown.ChildrenOfType<CollectionDropdown>().Single().Items.Count() - 1;
|
||||
InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("collection filter still selected", () => dropdown.Current.Value.CollectionName == "1");
|
||||
|
||||
AddAssert("filter request not fired", () => !received);
|
||||
}
|
||||
|
||||
private void writeAndRefresh(Action<Realm> action) => Realm.Write(r =>
|
||||
{
|
||||
action(r);
|
||||
r.Refresh();
|
||||
});
|
||||
|
||||
private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All<BeatmapCollection>().First());
|
||||
|
||||
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
|
||||
=> AddUntilStep($"collection dropdown header displays '{collectionName}'",
|
||||
() => shouldDisplay == dropdown.ChildrenOfType<CollectionDropdown.ShearedDropdownHeader>().Any(h => h.ChildrenOfType<SpriteText>().Any(t => t.Text == collectionName)));
|
||||
|
||||
private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon));
|
||||
|
||||
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
|
||||
AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
|
||||
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
|
||||
() => shouldContain == dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName));
|
||||
|
||||
private IconButton getAddOrRemoveButton(int index)
|
||||
=> getCollectionDropdownItemAt(index).ChildrenOfType<IconButton>().Single();
|
||||
|
||||
private void addExpandHeaderStep() => AddStep("expand header", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(dropdown.ChildrenOfType<CollectionDropdown.ShearedDropdownHeader>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index)
|
||||
{
|
||||
// todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079
|
||||
CollectionFilterMenuItem item = dropdown.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
|
||||
return dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
@@ -66,6 +72,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalRank()
|
||||
{
|
||||
foreach (var rank in Enum.GetValues<ScoreRank>())
|
||||
{
|
||||
AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType<UpdateableRank>().ForEach(p =>
|
||||
{
|
||||
p.Show();
|
||||
p.Rank = rank;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent()
|
||||
{
|
||||
return new FillFlowContainer
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
@@ -66,6 +72,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalRank()
|
||||
{
|
||||
foreach (var rank in Enum.GetValues<ScoreRank>())
|
||||
{
|
||||
AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType<UpdateableRank>().ForEach(p =>
|
||||
{
|
||||
p.Show();
|
||||
p.Rank = rank;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent()
|
||||
{
|
||||
return new FillFlowContainer
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneShearAligningWrapper : OsuTestScene
|
||||
{
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
private ShearedBox first = null!;
|
||||
private ShearedBox second = null!;
|
||||
private ShearedBox third = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 200f,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = OsuGame.SHEAR,
|
||||
CornerRadius = 10f,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background6,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 10f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ShearAligningWrapper(first = new ShearedBox("Text 1", OsuColour.Gray(0.4f))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 30,
|
||||
}),
|
||||
new ShearAligningWrapper(second = new ShearedBox("Text 2", OsuColour.Gray(0.3f))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 30,
|
||||
}),
|
||||
new ShearAligningWrapper(third = new ShearedBox("Text 3", OsuColour.Gray(0.2f))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 30,
|
||||
}),
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddSliderStep("box 1 height", 0, 100, 30, h =>
|
||||
{
|
||||
if (first.IsNotNull())
|
||||
first.Height = h;
|
||||
});
|
||||
AddSliderStep("box 2 height", 0, 100, 30, h =>
|
||||
{
|
||||
if (second.IsNotNull())
|
||||
second.Height = h;
|
||||
});
|
||||
AddSliderStep("box 3 height", 0, 100, 30, h =>
|
||||
{
|
||||
if (third.IsNotNull())
|
||||
third.Height = h;
|
||||
});
|
||||
}
|
||||
|
||||
public partial class ShearedBox : Container
|
||||
{
|
||||
private readonly string text;
|
||||
private readonly Color4 boxColour;
|
||||
|
||||
public ShearedBox(string text, Color4 boxColour)
|
||||
{
|
||||
this.text = text;
|
||||
this.boxColour = boxColour;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
CornerRadius = 10;
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = boxColour,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = text,
|
||||
Colour = Color4.White,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Font = OsuFont.Torus.With(size: 24),
|
||||
Margin = new MarginPadding { Left = 50 },
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
|
||||
Add(countText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Default.With(size: 12),
|
||||
Font = OsuFont.Style.Caption1,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding { Bottom = 1 }
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// </summary>
|
||||
public sealed class CarouselItem : IComparable<CarouselItem>
|
||||
{
|
||||
public const float DEFAULT_HEIGHT = 50;
|
||||
public const float DEFAULT_HEIGHT = 45;
|
||||
|
||||
/// <summary>
|
||||
/// The model this item is representing.
|
||||
@@ -44,6 +44,11 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// </summary>
|
||||
public bool IsExpanded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of nested items underneath this header. Should only be used for headers of groups.
|
||||
/// </summary>
|
||||
public int NestedItemCount { get; set; }
|
||||
|
||||
public CarouselItem(object model)
|
||||
{
|
||||
Model = model;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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.Framework.Layout;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds left padding based on direct parent to make sheared pieces in a vertical flow aligned appropriately.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See associated test scene for further demonstration.
|
||||
/// </remarks>
|
||||
public partial class ShearAligningWrapper : CompositeDrawable
|
||||
{
|
||||
private readonly LayoutValue layout = new LayoutValue(Invalidation.MiscGeometry);
|
||||
|
||||
public ShearAligningWrapper(Drawable drawable)
|
||||
{
|
||||
RelativeSizeAxes = drawable.RelativeSizeAxes;
|
||||
AutoSizeAxes = Axes.Both & ~drawable.RelativeSizeAxes;
|
||||
|
||||
InternalChild = drawable;
|
||||
|
||||
AddLayout(layout);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!layout.IsValid)
|
||||
{
|
||||
updateLayout();
|
||||
layout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLayout()
|
||||
{
|
||||
float shearWidth = OsuGame.SHEAR.X * Parent!.DrawHeight;
|
||||
float relativeY = Parent!.DrawHeight == 0 ? 0 : InternalChild.ToSpaceOfOtherDrawable(Vector2.Zero, Parent).Y / Parent!.DrawHeight;
|
||||
Padding = new MarginPadding { Left = shearWidth * relativeY };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
channel.Play();
|
||||
}
|
||||
|
||||
public LocalisableString GetDisplayableValue(T value) => CurrentNumber.Value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage);
|
||||
public LocalisableString GetDisplayableValue(T value) => value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,16 +36,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
var header = (ShearedDropdownHeader)Header;
|
||||
var menu = (ShearedDropdownMenu)Menu;
|
||||
|
||||
menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 10f, Right = 6f };
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat) return false;
|
||||
@@ -62,16 +52,15 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
protected partial class ShearedDropdownMenu : OsuDropdown<T>.OsuDropdownMenu
|
||||
{
|
||||
public new MarginPadding Padding
|
||||
{
|
||||
get => base.Padding;
|
||||
set => base.Padding = value;
|
||||
}
|
||||
|
||||
public ShearedDropdownMenu()
|
||||
{
|
||||
Shear = OsuGame.SHEAR;
|
||||
Margin = new MarginPadding { Top = 5f };
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Left = -6f,
|
||||
Right = 6f
|
||||
};
|
||||
}
|
||||
|
||||
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new ShearedMenuItem(item)
|
||||
@@ -92,8 +81,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
public partial class ShearedDropdownHeader : DropdownHeader
|
||||
{
|
||||
private const float corner_radius = 5f;
|
||||
|
||||
private LocalisableString label;
|
||||
|
||||
protected override LocalisableString Label
|
||||
@@ -127,7 +114,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
public ShearedDropdownHeader()
|
||||
{
|
||||
Shear = OsuGame.SHEAR;
|
||||
CornerRadius = corner_radius;
|
||||
CornerRadius = ShearedButton.CORNER_RADIUS;
|
||||
Masking = true;
|
||||
|
||||
Foreground.Children = new Drawable[]
|
||||
@@ -148,7 +135,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
LabelContainer = new Container
|
||||
{
|
||||
CornerRadius = corner_radius,
|
||||
Depth = float.MaxValue,
|
||||
CornerRadius = ShearedButton.CORNER_RADIUS,
|
||||
Masking = true,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
@@ -159,8 +147,13 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
},
|
||||
labelText = new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f },
|
||||
Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold),
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Horizontal = 10f,
|
||||
// Chosen specifically so the height of these dropdowns matches ShearedToggleButton (30).
|
||||
Vertical = 7f
|
||||
},
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Shear = -OsuGame.SHEAR,
|
||||
},
|
||||
},
|
||||
@@ -180,7 +173,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Padding = new MarginPadding { Right = 15f },
|
||||
Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Body,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
chevron = new SpriteIcon
|
||||
@@ -197,8 +190,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
AddInternal(LabelContainer.CreateProxy());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -223,7 +214,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
searchBar.Padding = new MarginPadding { Left = LabelContainer.DrawWidth };
|
||||
|
||||
// By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it.
|
||||
Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - corner_radius };
|
||||
Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - ShearedButton.CORNER_RADIUS };
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
|
||||
@@ -54,6 +54,9 @@ namespace osu.Game.Online.Leaderboards
|
||||
lastFetchCompletionSource?.TrySetCanceled();
|
||||
scores.Value = null;
|
||||
|
||||
if (newCriteria.Beatmap == null || newCriteria.Ruleset == null)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected));
|
||||
|
||||
switch (newCriteria.Scope)
|
||||
{
|
||||
case BeatmapLeaderboardScope.Local:
|
||||
@@ -72,6 +75,21 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
default:
|
||||
{
|
||||
if (!api.IsLoggedIn)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn));
|
||||
|
||||
if (!newCriteria.Ruleset.IsLegacyRuleset())
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable));
|
||||
|
||||
if (newCriteria.Beatmap.OnlineID <= 0 || newCriteria.Beatmap.Status <= BeatmapOnlineStatus.Pending)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable));
|
||||
|
||||
if ((newCriteria.Scope.RequiresSupporter(newCriteria.ExactMods != null)) && !api.LocalUser.Value.IsSupporter)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter));
|
||||
|
||||
if (newCriteria.Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam));
|
||||
|
||||
var onlineFetchCompletionSource = new TaskCompletionSource<LeaderboardScores?>();
|
||||
lastFetchCompletionSource = onlineFetchCompletionSource;
|
||||
|
||||
@@ -92,16 +110,16 @@ namespace osu.Game.Online.Leaderboards
|
||||
if (inFlightOnlineRequest != null && !newRequest.Equals(inFlightOnlineRequest))
|
||||
return;
|
||||
|
||||
var result = new LeaderboardScores
|
||||
var result = LeaderboardScores.Success
|
||||
(
|
||||
response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(),
|
||||
response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore().ToArray(),
|
||||
response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap)
|
||||
);
|
||||
inFlightOnlineRequest = null;
|
||||
if (onlineFetchCompletionSource.TrySetResult(result))
|
||||
scores.Value = result;
|
||||
};
|
||||
newRequest.Failure += ex => onlineFetchCompletionSource.TrySetException(ex);
|
||||
newRequest.Failure += _ => onlineFetchCompletionSource.TrySetResult(LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure));
|
||||
api.Queue(inFlightOnlineRequest = newRequest);
|
||||
return onlineFetchCompletionSource.Task;
|
||||
}
|
||||
@@ -138,7 +156,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
newScores = newScores.Detach().OrderByTotalScore();
|
||||
|
||||
scores.Value = new LeaderboardScores(newScores, null);
|
||||
scores.Value = LeaderboardScores.Success(newScores.ToArray(), null);
|
||||
|
||||
if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource)
|
||||
{
|
||||
@@ -149,14 +167,18 @@ namespace osu.Game.Online.Leaderboards
|
||||
}
|
||||
|
||||
public record LeaderboardCriteria(
|
||||
BeatmapInfo Beatmap,
|
||||
RulesetInfo Ruleset,
|
||||
BeatmapInfo? Beatmap,
|
||||
RulesetInfo? Ruleset,
|
||||
BeatmapLeaderboardScope Scope,
|
||||
Mod[]? ExactMods
|
||||
);
|
||||
|
||||
public record LeaderboardScores(IEnumerable<ScoreInfo> TopScores, ScoreInfo? UserScore)
|
||||
public record LeaderboardScores
|
||||
{
|
||||
public ICollection<ScoreInfo> TopScores { get; }
|
||||
public ScoreInfo? UserScore { get; }
|
||||
public LeaderboardFailState? FailState { get; }
|
||||
|
||||
public IEnumerable<ScoreInfo> AllScores
|
||||
{
|
||||
get
|
||||
@@ -168,5 +190,26 @@ namespace osu.Game.Online.Leaderboards
|
||||
yield return UserScore;
|
||||
}
|
||||
}
|
||||
|
||||
private LeaderboardScores(ICollection<ScoreInfo> topScores, ScoreInfo? userScore, LeaderboardFailState? failState)
|
||||
{
|
||||
TopScores = topScores;
|
||||
UserScore = userScore;
|
||||
FailState = failState;
|
||||
}
|
||||
|
||||
public static LeaderboardScores Success(ICollection<ScoreInfo> topScores, ScoreInfo? userScore) => new LeaderboardScores(topScores, userScore, null);
|
||||
public static LeaderboardScores Failure(LeaderboardFailState failState) => new LeaderboardScores([], null, failState);
|
||||
}
|
||||
|
||||
public enum LeaderboardFailState
|
||||
{
|
||||
NetworkFailure = -1,
|
||||
BeatmapUnavailable = -2,
|
||||
RulesetUnavailable = -3,
|
||||
NoneSelected = -4,
|
||||
NotLoggedIn = -5,
|
||||
NotSupporter = -6,
|
||||
NoTeam = -7
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
Success,
|
||||
Retrieving,
|
||||
NetworkFailure,
|
||||
BeatmapUnavailable,
|
||||
RulesetUnavailable,
|
||||
NoneSelected,
|
||||
NoScores,
|
||||
NotLoggedIn,
|
||||
NotSupporter,
|
||||
NoTeam
|
||||
|
||||
NetworkFailure = LeaderboardFailState.NetworkFailure,
|
||||
BeatmapUnavailable = LeaderboardFailState.BeatmapUnavailable,
|
||||
RulesetUnavailable = LeaderboardFailState.RulesetUnavailable,
|
||||
NoneSelected = LeaderboardFailState.NoneSelected,
|
||||
NotLoggedIn = LeaderboardFailState.NotLoggedIn,
|
||||
NotSupporter = LeaderboardFailState.NotSupporter,
|
||||
NoTeam = LeaderboardFailState.NoTeam,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Music
|
||||
/// <summary>
|
||||
/// A <see cref="CollectionDropdown"/> for use in the <see cref="NowPlayingOverlay"/>.
|
||||
/// </summary>
|
||||
public partial class NowPlayingCollectionDropdown : CollectionDropdown
|
||||
public partial class NowPlayingCollectionDropdown : CollectionDropdown // TODO: class is now unused. if we decide this isn't coming back it can be nuked.
|
||||
{
|
||||
protected override bool ShowManageCollectionsItem => false;
|
||||
|
||||
|
||||
@@ -34,5 +34,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
yield return ("Speed change", $"{SpeedChange.Value:N2}x");
|
||||
}
|
||||
}
|
||||
|
||||
public override string ExtendedIconInformation => SpeedChange.IsDefault ? string.Empty : FormattableString.Invariant($"{SpeedChange.Value:N2}x");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1266,7 +1266,7 @@ namespace osu.Game.Screens.Edit
|
||||
yield return createDifficultyCreationMenu();
|
||||
yield return createDifficultySwitchMenu();
|
||||
yield return new OsuMenuItemSpacer();
|
||||
yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } };
|
||||
yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Destructive, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } };
|
||||
yield return new OsuMenuItemSpacer();
|
||||
|
||||
var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save)) { Hotkey = new Hotkey(PlatformAction.Save) };
|
||||
|
||||
@@ -31,14 +31,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private readonly Bindable<bool> expandedFromTextBoxFocus = new Bindable<bool>();
|
||||
|
||||
private const float height = 100;
|
||||
private const float width = 260;
|
||||
|
||||
public override bool PropagateNonPositionalInputSubTree => true;
|
||||
|
||||
public GameplayChatDisplay(Room room)
|
||||
: base(room, leaveChannelOnDispose: false)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Background.Alpha = 0.2f;
|
||||
Width = width;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -15,8 +15,8 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
@@ -25,6 +25,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
protected override bool ShowLeaderboard => true;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.InMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
|
||||
|
||||
[Resolved]
|
||||
@@ -33,10 +35,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private IBindable<bool> isConnected = null!;
|
||||
|
||||
private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>();
|
||||
private readonly MultiplayerRoomUser[] users;
|
||||
|
||||
private LoadingLayer loadingDisplay = null!;
|
||||
private MultiplayerGameplayLeaderboard multiplayerLeaderboard = null!;
|
||||
|
||||
[Cached(typeof(IGameplayLeaderboardProvider))]
|
||||
private readonly MultiplayerLeaderboardProvider leaderboardProvider;
|
||||
|
||||
private GameplayMatchScoreDisplay teamScoreDisplay = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a multiplayer player.
|
||||
@@ -55,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
AlwaysShowLeaderboard = true,
|
||||
})
|
||||
{
|
||||
this.users = users;
|
||||
leaderboardProvider = new MultiplayerLeaderboardProvider(users);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -71,26 +76,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Expanded = { BindTarget = LeaderboardExpandedState },
|
||||
}, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat));
|
||||
|
||||
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() => multiplayerLeaderboard = new MultiplayerGameplayLeaderboard(users);
|
||||
|
||||
protected override void AddLeaderboardToHUD(GameplayLeaderboard leaderboard)
|
||||
{
|
||||
Debug.Assert(leaderboard == multiplayerLeaderboard);
|
||||
|
||||
HUDOverlay.LeaderboardFlow.Insert(0, leaderboard);
|
||||
|
||||
if (multiplayerLeaderboard.TeamScores.Count >= 2)
|
||||
LoadComponentAsync(teamScoreDisplay = new GameplayMatchScoreDisplay
|
||||
{
|
||||
LoadComponentAsync(new GameplayMatchScoreDisplay
|
||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||
Alpha = 0,
|
||||
}, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
|
||||
LoadComponentAsync(leaderboardProvider, loaded =>
|
||||
{
|
||||
AddInternal(loaded);
|
||||
|
||||
if (loaded.HasTeams)
|
||||
{
|
||||
Team1Score = { BindTarget = multiplayerLeaderboard.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = multiplayerLeaderboard.TeamScores.Last().Value },
|
||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||
}, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
|
||||
}
|
||||
teamScoreDisplay.Alpha = 1;
|
||||
teamScoreDisplay.Team1Score.BindTarget = leaderboardProvider.TeamScores.First().Value;
|
||||
teamScoreDisplay.Team2Score.BindTarget = leaderboardProvider.TeamScores.Last().Value;
|
||||
}
|
||||
});
|
||||
|
||||
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
@@ -195,8 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
Debug.Assert(Room.RoomID != null);
|
||||
|
||||
return multiplayerLeaderboard.TeamScores.Count == 2
|
||||
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, multiplayerLeaderboard.TeamScores)
|
||||
return leaderboardProvider.TeamScores.Count == 2
|
||||
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, leaderboardProvider.TeamScores)
|
||||
{
|
||||
IsLocalPlay = true,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Screens.Spectate;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
@@ -47,17 +48,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
[Cached(typeof(IGameplayLeaderboardProvider))]
|
||||
private MultiSpectatorLeaderboardProvider leaderboardProvider { get; set; }
|
||||
|
||||
private IAggregateAudioAdjustment? boundAdjustments;
|
||||
|
||||
private readonly PlayerArea[] instances;
|
||||
private MasterGameplayClockContainer masterClockContainer = null!;
|
||||
private SpectatorSyncManager syncManager = null!;
|
||||
private PlayerGrid grid = null!;
|
||||
private MultiSpectatorLeaderboard leaderboard = null!;
|
||||
private PlayerArea? currentAudioSource;
|
||||
|
||||
private readonly Room room;
|
||||
private readonly MultiplayerRoomUser[] users;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MultiSpectatorScreen"/>.
|
||||
@@ -68,9 +70,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
: base(users.Select(u => u.UserID).ToArray())
|
||||
{
|
||||
this.room = room;
|
||||
this.users = users;
|
||||
|
||||
instances = new PlayerArea[Users.Count];
|
||||
leaderboardProvider = new MultiSpectatorLeaderboardProvider(users);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -133,25 +135,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
for (int i = 0; i < Users.Count; i++)
|
||||
grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.CreateManagedClock()));
|
||||
|
||||
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users)
|
||||
{
|
||||
Expanded = { Value = true },
|
||||
}, _ =>
|
||||
LoadComponentAsync(leaderboardProvider, _ =>
|
||||
{
|
||||
AddInternal(leaderboardProvider);
|
||||
foreach (var instance in instances)
|
||||
leaderboard.AddClock(instance.UserId, instance.SpectatorPlayerClock);
|
||||
leaderboardProvider.AddClock(instance.UserId, instance.SpectatorPlayerClock);
|
||||
|
||||
leaderboardFlow.Insert(0, leaderboard);
|
||||
|
||||
if (leaderboard.TeamScores.Count == 2)
|
||||
if (leaderboardProvider.TeamScores.Count == 2)
|
||||
{
|
||||
LoadComponentAsync(new MatchScoreDisplay
|
||||
{
|
||||
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
|
||||
Team1Score = { BindTarget = leaderboardProvider.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = leaderboardProvider.TeamScores.Last().Value },
|
||||
}, scoreDisplayContainer.Add);
|
||||
}
|
||||
});
|
||||
leaderboardFlow.Insert(0, new DrawableGameplayLeaderboard
|
||||
{
|
||||
Expanded = { Value = true }
|
||||
});
|
||||
|
||||
LoadComponentAsync(new GameplayChatDisplay(room)
|
||||
{
|
||||
|
||||
+44
-26
@@ -3,40 +3,48 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public abstract partial class GameplayLeaderboard : CompositeDrawable
|
||||
public partial class DrawableGameplayLeaderboard : CompositeDrawable
|
||||
{
|
||||
private readonly Cached sorting = new Cached();
|
||||
|
||||
public Bindable<bool> Expanded = new Bindable<bool>();
|
||||
|
||||
protected readonly FillFlowContainer<GameplayLeaderboardScore> Flow;
|
||||
protected readonly FillFlowContainer<DrawableGameplayLeaderboardScore> Flow;
|
||||
|
||||
private bool requiresScroll;
|
||||
private readonly OsuScrollContainer scroll;
|
||||
|
||||
public GameplayLeaderboardScore? TrackedScore { get; private set; }
|
||||
public DrawableGameplayLeaderboardScore? TrackedScore { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private IGameplayLeaderboardProvider? leaderboardProvider { get; set; }
|
||||
|
||||
private readonly IBindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
|
||||
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
|
||||
|
||||
private const int max_panels = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new leaderboard.
|
||||
/// </summary>
|
||||
protected GameplayLeaderboard()
|
||||
public DrawableGameplayLeaderboard()
|
||||
{
|
||||
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
|
||||
Width = DrawableGameplayLeaderboardScore.EXTENDED_WIDTH + DrawableGameplayLeaderboardScore.SHEAR_WIDTH;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@@ -44,10 +52,10 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
ClampExtension = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = Flow = new FillFlowContainer<GameplayLeaderboardScore>
|
||||
Child = Flow = new FillFlowContainer<DrawableGameplayLeaderboardScore>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
X = GameplayLeaderboardScore.SHEAR_WIDTH,
|
||||
X = DrawableGameplayLeaderboardScore.SHEAR_WIDTH,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(2.5f),
|
||||
@@ -58,26 +66,39 @@ namespace osu.Game.Screens.Play.HUD
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (leaderboardProvider != null)
|
||||
{
|
||||
scores.BindTo(leaderboardProvider.Scores);
|
||||
scores.BindCollectionChanged((_, _) =>
|
||||
{
|
||||
Clear();
|
||||
foreach (var score in scores)
|
||||
Add(score);
|
||||
}, true);
|
||||
}
|
||||
|
||||
Scheduler.AddDelayed(sort, 1000, true);
|
||||
configVisibility.BindValueChanged(_ => this.FadeTo(configVisibility.Value ? 1 : 0, 100, Easing.OutQuint), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a player to the leaderboard.
|
||||
/// </summary>
|
||||
/// <param name="user">The player.</param>
|
||||
/// <param name="isTracked">
|
||||
/// Whether the player should be tracked on the leaderboard.
|
||||
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
|
||||
/// </param>
|
||||
public ILeaderboardScore Add(IUser? user, bool isTracked)
|
||||
public void Add(GameplayLeaderboardScore score)
|
||||
{
|
||||
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
|
||||
var drawable = CreateLeaderboardScoreDrawable(score);
|
||||
|
||||
if (isTracked)
|
||||
if (score.Tracked)
|
||||
{
|
||||
if (TrackedScore != null)
|
||||
throw new InvalidOperationException("Cannot track more than one score.");
|
||||
@@ -92,10 +113,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
|
||||
int displayCount = Math.Min(Flow.Count, max_panels);
|
||||
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||
Height = displayCount * (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||
requiresScroll = displayCount != Flow.Count;
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
@@ -105,8 +124,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
scroll.ScrollToStart(false);
|
||||
}
|
||||
|
||||
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) =>
|
||||
new GameplayLeaderboardScore(user, isTracked);
|
||||
protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(GameplayLeaderboardScore score) =>
|
||||
new DrawableGameplayLeaderboardScore(score);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@@ -119,7 +138,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
scroll.ScrollTo(scrollTarget);
|
||||
}
|
||||
|
||||
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
|
||||
const float panel_height = DrawableGameplayLeaderboardScore.PANEL_HEIGHT;
|
||||
|
||||
float fadeBottom = (float)(scroll.Current + scroll.DrawHeight);
|
||||
float fadeTop = (float)(scroll.Current + panel_height);
|
||||
@@ -170,15 +189,14 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
for (int i = 0; i < Flow.Count; i++)
|
||||
{
|
||||
Flow.SetLayoutPosition(orderedByScore[i], i);
|
||||
orderedByScore[i].ScorePosition = CheckValidScorePosition(orderedByScore[i], i + 1) ? i + 1 : null;
|
||||
var score = orderedByScore[i];
|
||||
Flow.SetLayoutPosition(score, i);
|
||||
score.ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true && score.Tracked ? null : i + 1;
|
||||
}
|
||||
|
||||
sorting.Validate();
|
||||
}
|
||||
|
||||
protected virtual bool CheckValidScorePosition(GameplayLeaderboardScore score, int position) => true;
|
||||
|
||||
private partial class InputDisabledScrollContainer : OsuScrollContainer
|
||||
{
|
||||
public InputDisabledScrollContainer()
|
||||
+18
-9
@@ -14,6 +14,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osu.Game.Utils;
|
||||
@@ -22,7 +23,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore
|
||||
public partial class DrawableGameplayLeaderboardScore : CompositeDrawable
|
||||
{
|
||||
public const float EXTENDED_WIDTH = regular_width + top_player_left_width_extension;
|
||||
|
||||
@@ -112,19 +113,27 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private bool isFriend;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
|
||||
/// Creates a new <see cref="DrawableGameplayLeaderboardScore"/>.
|
||||
/// </summary>
|
||||
/// <param name="user">The score's player.</param>
|
||||
/// <param name="tracked">Whether the player is the local user or a replay player.</param>
|
||||
public GameplayLeaderboardScore(IUser? user, bool tracked)
|
||||
public DrawableGameplayLeaderboardScore(GameplayLeaderboardScore score)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
User = score.User;
|
||||
Tracked = score.Tracked;
|
||||
TotalScore.BindTo(score.TotalScore);
|
||||
Accuracy.BindTo(score.Accuracy);
|
||||
Combo.BindTo(score.Combo);
|
||||
HasQuit.BindTo(score.HasQuit);
|
||||
DisplayOrder.BindTo(score.DisplayOrder);
|
||||
GetDisplayScore = score.GetDisplayScore;
|
||||
|
||||
if (score.TeamColour != null)
|
||||
{
|
||||
BackgroundColour = score.TeamColour.Value;
|
||||
TextColour = Color4.White;
|
||||
}
|
||||
|
||||
AutoSizeAxes = Axes.X;
|
||||
Height = PANEL_HEIGHT;
|
||||
|
||||
GetDisplayScore = _ => TotalScore.Value;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -1,31 +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 osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public interface ILeaderboardScore
|
||||
{
|
||||
BindableLong TotalScore { get; }
|
||||
BindableDouble Accuracy { get; }
|
||||
BindableInt Combo { get; }
|
||||
|
||||
BindableBool HasQuit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional value to guarantee stable ordering.
|
||||
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
|
||||
/// </summary>
|
||||
Bindable<long> DisplayOrder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A custom function which handles converting a score to a display score using a provide <see cref="ScoringMode"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no function is provided, <see cref="TotalScore"/> will be used verbatim.</remarks>
|
||||
Func<ScoringMode, long> GetDisplayScore { set; }
|
||||
}
|
||||
}
|
||||
@@ -1,108 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class SoloGameplayLeaderboard : GameplayLeaderboard
|
||||
{
|
||||
private const int duration = 100;
|
||||
|
||||
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
|
||||
|
||||
private readonly Bindable<PlayBeatmapDetailArea.TabType> scoreSource = new Bindable<PlayBeatmapDetailArea.TabType>();
|
||||
|
||||
private readonly IUser trackingUser;
|
||||
|
||||
public readonly IBindableList<ScoreInfo> Scores = new BindableList<ScoreInfo>();
|
||||
|
||||
[Resolved]
|
||||
private ScoreProcessor scoreProcessor { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the leaderboard should be visible regardless of the configuration value.
|
||||
/// This is true by default, but can be changed.
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> AlwaysVisible = new Bindable<bool>(true);
|
||||
|
||||
public SoloGameplayLeaderboard(IUser trackingUser)
|
||||
{
|
||||
this.trackingUser = trackingUser;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
|
||||
config.BindWith(OsuSetting.BeatmapDetailTab, scoreSource);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true);
|
||||
|
||||
// Alpha will be updated via `updateVisibility` below.
|
||||
Alpha = 0;
|
||||
|
||||
AlwaysVisible.BindValueChanged(_ => updateVisibility());
|
||||
configVisibility.BindValueChanged(_ => updateVisibility(), true);
|
||||
}
|
||||
|
||||
private void showScores()
|
||||
{
|
||||
Clear();
|
||||
|
||||
if (!Scores.Any())
|
||||
return;
|
||||
|
||||
foreach (var s in Scores)
|
||||
{
|
||||
var score = Add(s.User, false);
|
||||
|
||||
score.GetDisplayScore = s.GetDisplayScore;
|
||||
score.TotalScore.Value = s.TotalScore;
|
||||
score.Accuracy.Value = s.Accuracy;
|
||||
score.Combo.Value = s.MaxCombo;
|
||||
score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
ILeaderboardScore local = Add(trackingUser, true);
|
||||
|
||||
local.GetDisplayScore = scoreProcessor.GetDisplayScore;
|
||||
local.TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
local.Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||
local.Combo.BindTarget = scoreProcessor.HighestCombo;
|
||||
|
||||
// Local score should always show lower than any existing scores in cases of ties.
|
||||
local.DisplayOrder.Value = long.MaxValue;
|
||||
}
|
||||
|
||||
protected override bool CheckValidScorePosition(GameplayLeaderboardScore score, int position)
|
||||
{
|
||||
// change displayed position to '-' when there are 50 already submitted scores and tracked score is last
|
||||
if (score.Tracked && scoreSource.Value != PlayBeatmapDetailArea.TabType.Local)
|
||||
{
|
||||
if (position == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST)
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.CheckValidScorePosition(score, position);
|
||||
}
|
||||
|
||||
private void updateVisibility() =>
|
||||
this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
|
||||
}
|
||||
}
|
||||
@@ -935,34 +935,30 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
#region Gameplay leaderboard
|
||||
|
||||
protected virtual bool ShowLeaderboard => false;
|
||||
|
||||
protected readonly Bindable<bool> LeaderboardExpandedState = new BindableBool();
|
||||
|
||||
private void loadLeaderboard()
|
||||
{
|
||||
if (!ShowLeaderboard)
|
||||
return;
|
||||
|
||||
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
|
||||
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
|
||||
|
||||
var gameplayLeaderboard = CreateGameplayLeaderboard();
|
||||
|
||||
if (gameplayLeaderboard != null)
|
||||
var gameplayLeaderboard = new DrawableGameplayLeaderboard();
|
||||
LoadComponentAsync(gameplayLeaderboard, leaderboard =>
|
||||
{
|
||||
LoadComponentAsync(gameplayLeaderboard, leaderboard =>
|
||||
{
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
leaderboard.Expanded.BindTo(LeaderboardExpandedState);
|
||||
leaderboard.Expanded.BindTo(LeaderboardExpandedState);
|
||||
|
||||
AddLeaderboardToHUD(leaderboard);
|
||||
});
|
||||
}
|
||||
HUDOverlay.LeaderboardFlow.Add(leaderboard);
|
||||
});
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
protected virtual GameplayLeaderboard CreateGameplayLeaderboard() => null;
|
||||
|
||||
protected virtual void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) => HUDOverlay.LeaderboardFlow.Add(leaderboard);
|
||||
|
||||
private void updateLeaderboardExpandedState() =>
|
||||
LeaderboardExpandedState.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
|
||||
|
||||
|
||||
@@ -8,18 +8,16 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Bindings;
|
||||
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;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
@@ -35,6 +33,9 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private PlaybackSettings playbackSettings;
|
||||
|
||||
[Cached(typeof(IGameplayLeaderboardProvider))]
|
||||
private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider();
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo);
|
||||
|
||||
private bool isAutoplayPlayback => GameplayState.Mods.OfType<ModAutoplay>().Any();
|
||||
@@ -48,6 +49,8 @@ namespace osu.Game.Screens.Play
|
||||
return base.CheckModsAllowFailure();
|
||||
}
|
||||
|
||||
protected override bool ShowLeaderboard => true;
|
||||
|
||||
public ReplayPlayer(Score score, PlayerConfiguration configuration = null)
|
||||
: this((_, _) => score, configuration)
|
||||
{
|
||||
@@ -60,12 +63,6 @@ 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>
|
||||
@@ -82,6 +79,8 @@ namespace osu.Game.Screens.Play
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
AddInternal(leaderboardProvider);
|
||||
|
||||
playbackSettings = new PlaybackSettings
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
@@ -94,20 +93,6 @@ 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);
|
||||
@@ -118,13 +103,6 @@ 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;
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||
{
|
||||
AlwaysVisible = { Value = true },
|
||||
Scores = { BindTarget = localScores }
|
||||
};
|
||||
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score)
|
||||
{
|
||||
// Only show the relevant button otherwise things look silly.
|
||||
|
||||
@@ -5,50 +5,34 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
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;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
public partial class SoloPlayer : SubmittingPlayer
|
||||
{
|
||||
public SoloPlayer()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
protected override bool ShowLeaderboard => true;
|
||||
|
||||
protected SoloPlayer(PlayerConfiguration configuration = null)
|
||||
[Cached(typeof(IGameplayLeaderboardProvider))]
|
||||
private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider();
|
||||
|
||||
public SoloPlayer([CanBeNull] PlayerConfiguration configuration = null)
|
||||
: base(configuration)
|
||||
{
|
||||
}
|
||||
|
||||
[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()
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
globalScores.BindTo(leaderboardManager.Scores);
|
||||
globalScores.BindValueChanged(_ =>
|
||||
{
|
||||
localScores.Clear();
|
||||
|
||||
if (globalScores.Value is LeaderboardScores g)
|
||||
localScores.AddRange(g.AllScores.OrderByTotalScore());
|
||||
}, true);
|
||||
AddInternal(leaderboardProvider);
|
||||
}
|
||||
|
||||
protected override APIRequest<APIScoreToken> CreateTokenRequest()
|
||||
@@ -65,30 +49,13 @@ namespace osu.Game.Screens.Play
|
||||
return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||
{
|
||||
AlwaysVisible = { Value = false },
|
||||
Scores = { BindTarget = localScores }
|
||||
};
|
||||
|
||||
protected override bool ShouldExitOnTokenRetrievalFailure(Exception exception) => false;
|
||||
|
||||
protected override Task ImportScore(Score score)
|
||||
{
|
||||
// Before importing a score, stop binding the leaderboard with its score source.
|
||||
// This avoids a case where the imported score may cause a leaderboard refresh
|
||||
// (if the leaderboard's source is local).
|
||||
globalScores.UnbindBindings();
|
||||
|
||||
return base.ImportScore(score);
|
||||
}
|
||||
|
||||
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
|
||||
{
|
||||
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo;
|
||||
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo!;
|
||||
|
||||
Debug.Assert(beatmap!.OnlineID > 0);
|
||||
Debug.Assert(beatmap.OnlineID > 0);
|
||||
|
||||
return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Threading;
|
||||
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.Rulesets;
|
||||
@@ -71,9 +70,6 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
@@ -94,44 +90,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
protected override APIRequest? FetchScores(CancellationToken cancellationToken)
|
||||
{
|
||||
var fetchBeatmapInfo = BeatmapInfo;
|
||||
|
||||
if (fetchBeatmapInfo == null)
|
||||
{
|
||||
SetErrorState(LeaderboardState.NoneSelected);
|
||||
return null;
|
||||
}
|
||||
|
||||
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
|
||||
|
||||
if (!api.IsLoggedIn && IsOnlineScope)
|
||||
{
|
||||
SetErrorState(LeaderboardState.NotLoggedIn);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!fetchRuleset.IsLegacyRuleset())
|
||||
{
|
||||
SetErrorState(LeaderboardState.RulesetUnavailable);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && IsOnlineScope)
|
||||
{
|
||||
SetErrorState(LeaderboardState.BeatmapUnavailable);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Scope.RequiresSupporter(filterMods) && !api.LocalUser.Value.IsSupporter)
|
||||
{
|
||||
SetErrorState(LeaderboardState.NotSupporter);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null)
|
||||
{
|
||||
SetErrorState(LeaderboardState.NoTeam);
|
||||
return null;
|
||||
}
|
||||
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo?.Ruleset;
|
||||
|
||||
leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null))
|
||||
.ContinueWith(t =>
|
||||
@@ -145,8 +104,12 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
fetchedScores.UnbindEvents();
|
||||
fetchedScores.BindValueChanged(scores =>
|
||||
{
|
||||
if (scores.NewValue != null)
|
||||
if (scores.NewValue == null) return;
|
||||
|
||||
if (scores.NewValue.FailState == null)
|
||||
Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore));
|
||||
else
|
||||
Schedule(() => SetErrorState((LeaderboardState)scores.NewValue.FailState));
|
||||
}, true);
|
||||
}, cancellationToken);
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a score shown on a gameplay leaderboard.
|
||||
/// The score is expected to update itself as gameplay progresses.
|
||||
/// </summary>
|
||||
public class GameplayLeaderboardScore
|
||||
{
|
||||
/// <summary>
|
||||
/// The user playing.
|
||||
/// </summary>
|
||||
public IUser User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the score is being tracked.
|
||||
/// Generally understood as true when this score is the score of the local user currently playing.
|
||||
/// </summary>
|
||||
public bool Tracked { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current total of the score.
|
||||
/// </summary>
|
||||
public BindableLong TotalScore { get; } = new BindableLong();
|
||||
|
||||
/// <summary>
|
||||
/// The current accuracy of the score.
|
||||
/// </summary>
|
||||
public BindableDouble Accuracy { get; } = new BindableDouble();
|
||||
|
||||
/// <summary>
|
||||
/// The current combo of the score.
|
||||
/// </summary>
|
||||
public BindableInt Combo { get; } = new BindableInt();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user playing has quit.
|
||||
/// </summary>
|
||||
public BindableBool HasQuit { get; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// An optional value to guarantee stable ordering.
|
||||
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
|
||||
/// </summary>
|
||||
public Bindable<long> DisplayOrder { get; } = new BindableLong();
|
||||
|
||||
/// <summary>
|
||||
/// A custom function which handles converting a score to a display score using a provided <see cref="ScoringMode"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no function is provided, <see cref="TotalScore"/> will be used verbatim.
|
||||
/// </remarks>
|
||||
public Func<ScoringMode, long> GetDisplayScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The colour of the team that the user playing is on, if any.
|
||||
/// </summary>
|
||||
public Colour4? TeamColour { get; init; }
|
||||
|
||||
public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||
Combo.BindTarget = scoreProcessor.Combo;
|
||||
GetDisplayScore = scoreProcessor.GetDisplayScore;
|
||||
}
|
||||
|
||||
public GameplayLeaderboardScore(IUser user, SpectatorScoreProcessor scoreProcessor, bool tracked)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||
Combo.BindTarget = scoreProcessor.Combo;
|
||||
GetDisplayScore = scoreProcessor.GetDisplayScore;
|
||||
}
|
||||
|
||||
public GameplayLeaderboardScore(ScoreInfo scoreInfo, bool tracked)
|
||||
{
|
||||
User = scoreInfo.User;
|
||||
Tracked = tracked;
|
||||
TotalScore.Value = scoreInfo.TotalScore;
|
||||
Accuracy.Value = scoreInfo.Accuracy;
|
||||
Combo.Value = scoreInfo.Combo;
|
||||
DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
|
||||
GetDisplayScore = scoreInfo.GetDisplayScore;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Used for testing.
|
||||
/// </remarks>
|
||||
internal GameplayLeaderboardScore(IUser user, bool tracked, Bindable<long> displayScore)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
TotalScore.BindTarget = displayScore;
|
||||
GetDisplayScore = _ => displayScore.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a leaderboard to show during gameplay.
|
||||
/// </summary>
|
||||
public interface IGameplayLeaderboardProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// List of all scores to display on the leaderboard.
|
||||
/// </summary>
|
||||
public IBindableList<GameplayLeaderboardScore> Scores { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores),
|
||||
/// or is a full leaderboard (contains all scores that there will ever be).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this is <see langword="true"/> and a tracked score is last on the leaderboard, it will show an "unknown" score position.
|
||||
/// </remarks>
|
||||
bool IsPartial { get; }
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -4,13 +4,12 @@
|
||||
using System;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
public partial class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
|
||||
public partial class MultiSpectatorLeaderboardProvider : MultiplayerLeaderboardProvider
|
||||
{
|
||||
public MultiSpectatorLeaderboard(MultiplayerRoomUser[] users)
|
||||
public MultiSpectatorLeaderboardProvider(MultiplayerRoomUser[] users)
|
||||
: base(users)
|
||||
{
|
||||
}
|
||||
+48
-65
@@ -11,6 +11,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
@@ -20,20 +21,30 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
[LongRunningLoad]
|
||||
public partial class MultiplayerGameplayLeaderboard : GameplayLeaderboard
|
||||
public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGameplayLeaderboardProvider
|
||||
{
|
||||
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
|
||||
public IBindableList<GameplayLeaderboardScore> Scores => scores;
|
||||
private readonly BindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
|
||||
|
||||
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
|
||||
public readonly SortedDictionary<int, BindableLong> TeamScores = new SortedDictionary<int, BindableLong>();
|
||||
|
||||
public bool HasTeams => TeamScores.Count > 0;
|
||||
|
||||
public bool IsPartial => false;
|
||||
|
||||
private readonly MultiplayerRoomUser[] users;
|
||||
|
||||
private readonly Bindable<ScoringMode> scoringMode = new Bindable<ScoringMode>();
|
||||
private readonly IBindableList<int> playingUserIds = new BindableList<int>();
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SpectatorClient spectatorClient { get; set; } = null!;
|
||||
@@ -42,31 +53,19 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private Bindable<ScoringMode> scoringMode = null!;
|
||||
|
||||
private readonly MultiplayerRoomUser[] playingUsers;
|
||||
|
||||
private readonly IBindableList<int> playingUserIds = new BindableList<int>();
|
||||
|
||||
private bool hasTeams => TeamScores.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new leaderboard.
|
||||
/// </summary>
|
||||
/// <param name="users">IDs of all users in this match.</param>
|
||||
public MultiplayerGameplayLeaderboard(MultiplayerRoomUser[] users)
|
||||
public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users)
|
||||
{
|
||||
playingUsers = users;
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config, IAPIProvider api, CancellationToken cancellationToken)
|
||||
{
|
||||
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
|
||||
config.BindWith(OsuSetting.ScoreDisplayMode, scoringMode);
|
||||
|
||||
foreach (var user in playingUsers)
|
||||
foreach (var user in users)
|
||||
{
|
||||
var scoreProcessor = new SpectatorScoreProcessor(user.UserID);
|
||||
scoreProcessor.Mode.BindTo(scoringMode);
|
||||
@@ -80,29 +79,29 @@ namespace osu.Game.Screens.Play.HUD
|
||||
TeamScores.Add(team, new BindableLong());
|
||||
}
|
||||
|
||||
userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray(), cancellationToken)
|
||||
userLookupCache.GetUsersAsync(users.Select(u => u.UserID).ToArray(), cancellationToken)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
var users = task.GetResultSafely();
|
||||
var lookedUpUsers = task.GetResultSafely();
|
||||
|
||||
for (int i = 0; i < users.Length; i++)
|
||||
for (int i = 0; i < lookedUpUsers.Length; i++)
|
||||
{
|
||||
var user = users[i] ?? new APIUser
|
||||
var user = lookedUpUsers[i] ?? new APIUser
|
||||
{
|
||||
Id = playingUsers[i].UserID,
|
||||
Id = users[i].UserID,
|
||||
Username = "Unknown user",
|
||||
};
|
||||
|
||||
var trackedUser = UserScores[user.Id];
|
||||
|
||||
var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
|
||||
leaderboardScore.GetDisplayScore = trackedUser.ScoreProcessor.GetDisplayScore;
|
||||
leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy);
|
||||
leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore);
|
||||
leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo);
|
||||
leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit);
|
||||
var leaderboardScore = new GameplayLeaderboardScore(user, trackedUser.ScoreProcessor, user.Id == api.LocalUser.Value.Id)
|
||||
{
|
||||
HasQuit = { BindTarget = trackedUser.UserQuit },
|
||||
TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null,
|
||||
};
|
||||
scores.Add(leaderboardScore);
|
||||
}
|
||||
});
|
||||
}, cancellationToken);
|
||||
@@ -113,7 +112,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
base.LoadComplete();
|
||||
|
||||
// BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually..
|
||||
foreach (var user in playingUsers)
|
||||
foreach (var user in users)
|
||||
{
|
||||
spectatorClient.WatchUser(user.UserID);
|
||||
|
||||
@@ -127,34 +126,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
playingUserIds.BindCollectionChanged(playingUsersChanged);
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked)
|
||||
{
|
||||
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
if (UserScores[user.OnlineID].Team is int team)
|
||||
{
|
||||
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
|
||||
leaderboardScore.TextColour = Color4.White;
|
||||
}
|
||||
}
|
||||
|
||||
return leaderboardScore;
|
||||
}
|
||||
|
||||
private Color4 getTeamColour(int team)
|
||||
{
|
||||
switch (team)
|
||||
{
|
||||
case 0:
|
||||
return colours.TeamColourRed;
|
||||
|
||||
default:
|
||||
return colours.TeamColourBlue;
|
||||
}
|
||||
}
|
||||
|
||||
private void playingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
@@ -176,10 +147,10 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private void updateTotals()
|
||||
{
|
||||
if (!hasTeams)
|
||||
if (!HasTeams)
|
||||
return;
|
||||
|
||||
foreach (var scores in TeamScores.Values) scores.Value = 0;
|
||||
foreach (var teamTotal in TeamScores.Values) teamTotal.Value = 0;
|
||||
|
||||
foreach (var u in UserScores.Values)
|
||||
{
|
||||
@@ -191,13 +162,25 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
}
|
||||
|
||||
private Color4 getTeamColour(int team)
|
||||
{
|
||||
switch (team)
|
||||
{
|
||||
case 0:
|
||||
return colours.TeamColourRed.Lighten(1.2f);
|
||||
|
||||
default:
|
||||
return colours.TeamColourBlue.Lighten(1.2f);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (spectatorClient.IsNotNull())
|
||||
{
|
||||
foreach (var user in playingUsers)
|
||||
foreach (var user in users)
|
||||
spectatorClient.StopWatchingUser(user.UserID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
public partial class SoloGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider
|
||||
{
|
||||
public bool IsPartial { get; private set; }
|
||||
|
||||
public IBindableList<GameplayLeaderboardScore> Scores => scores;
|
||||
private readonly BindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
|
||||
|
||||
[Resolved]
|
||||
private LeaderboardManager? leaderboardManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private GameplayState? gameplayState { get; set; }
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
var globalScores = leaderboardManager?.Scores.Value;
|
||||
|
||||
IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
|
||||
|
||||
if (globalScores != null)
|
||||
{
|
||||
foreach (var topScore in globalScores.AllScores.OrderByTotalScore())
|
||||
scores.Add(new GameplayLeaderboardScore(topScore, false));
|
||||
}
|
||||
|
||||
if (gameplayState != null)
|
||||
{
|
||||
scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true)
|
||||
{
|
||||
// Local score should always show lower than any existing scores in cases of ties.
|
||||
DisplayOrder = { Value = long.MaxValue }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
var newItems = new List<CarouselItem>();
|
||||
|
||||
BeatmapInfo? lastBeatmap = null;
|
||||
|
||||
GroupDefinition? lastGroup = null;
|
||||
CarouselItem? lastGroupItem = null;
|
||||
|
||||
HashSet<CarouselItem>? currentGroupItems = null;
|
||||
HashSet<CarouselItem>? currentSetItems = null;
|
||||
@@ -69,7 +71,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
groupItems[newGroup] = currentGroupItems = new HashSet<CarouselItem>();
|
||||
lastGroup = newGroup;
|
||||
|
||||
addItem(new CarouselItem(newGroup)
|
||||
addItem(lastGroupItem = new CarouselItem(newGroup)
|
||||
{
|
||||
DrawHeight = PanelGroup.HEIGHT,
|
||||
DepthLayer = -2,
|
||||
@@ -84,6 +86,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet<CarouselItem>();
|
||||
|
||||
if (lastGroupItem != null)
|
||||
lastGroupItem.NestedItemCount++;
|
||||
|
||||
addItem(new CarouselItem(beatmap.BeatmapSet!)
|
||||
{
|
||||
DrawHeight = PanelBeatmapSet.HEIGHT,
|
||||
@@ -91,6 +96,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lastGroupItem != null)
|
||||
lastGroupItem.NestedItemCount++;
|
||||
}
|
||||
|
||||
addItem(item);
|
||||
lastBeatmap = beatmap;
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
/// <summary>
|
||||
/// A dropdown to select the collection to be used to filter results.
|
||||
/// </summary>
|
||||
public partial class CollectionDropdown : ShearedDropdown<CollectionFilterMenuItem> // TODO: partial class under FilterControl?
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to show the "manage collections..." menu item in the dropdown.
|
||||
/// </summary>
|
||||
protected virtual bool ShowManageCollectionsItem => true;
|
||||
|
||||
public Action? RequestFilter { private get; set; }
|
||||
|
||||
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
|
||||
|
||||
[Resolved]
|
||||
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
private IDisposable? realmSubscription;
|
||||
|
||||
private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem();
|
||||
|
||||
public CollectionDropdown()
|
||||
: base("Collection")
|
||||
{
|
||||
ItemSource = filters;
|
||||
|
||||
Current.Value = allBeatmapsItem;
|
||||
AlwaysShowSearchBar = true;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
|
||||
|
||||
Current.BindValueChanged(selectionChanged);
|
||||
}
|
||||
|
||||
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
|
||||
{
|
||||
if (changes == null)
|
||||
{
|
||||
filters.Clear();
|
||||
filters.Add(allBeatmapsItem);
|
||||
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm))));
|
||||
if (ShowManageCollectionsItem)
|
||||
filters.Add(new ManageCollectionsFilterMenuItem());
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (int i in changes.DeletedIndices.OrderDescending())
|
||||
filters.RemoveAt(i + 1);
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
filters.Insert(i + 1, new CollectionFilterMenuItem(collections[i].ToLive(realm)));
|
||||
|
||||
var selectedItem = SelectedItem?.Value;
|
||||
|
||||
foreach (int i in changes.NewModifiedIndices)
|
||||
{
|
||||
var updatedItem = collections[i];
|
||||
|
||||
// This is responsible for updating the state of the +/- button and the collection's name.
|
||||
// TODO: we can probably make the menu items update with changes to avoid this.
|
||||
filters.RemoveAt(i + 1);
|
||||
filters.Insert(i + 1, new CollectionFilterMenuItem(updatedItem.ToLive(realm)));
|
||||
|
||||
if (updatedItem.ID == selectedItem?.Collection?.ID)
|
||||
{
|
||||
// This current update and schedule is required to work around dropdown headers not updating text even when the selected item
|
||||
// changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue
|
||||
// a warning that it's going to be a frustrating journey.
|
||||
Current.Value = allBeatmapsItem;
|
||||
Schedule(() =>
|
||||
{
|
||||
// current may have changed before the scheduled call is run.
|
||||
if (Current.Value != allBeatmapsItem)
|
||||
return;
|
||||
|
||||
Current.Value = filters.SingleOrDefault(f => f.Collection?.ID == selectedItem.Collection?.ID) ?? filters[0];
|
||||
});
|
||||
|
||||
// Trigger an external re-filter if the current item was in the change set.
|
||||
RequestFilter?.Invoke();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Live<BeatmapCollection>? lastFiltered;
|
||||
|
||||
private void selectionChanged(ValueChangedEvent<CollectionFilterMenuItem> filter)
|
||||
{
|
||||
// May be null during .Clear().
|
||||
if (filter.NewValue.IsNull())
|
||||
return;
|
||||
|
||||
// Never select the manage collection filter - rollback to the previous filter.
|
||||
// This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value.
|
||||
if (filter.NewValue is ManageCollectionsFilterMenuItem)
|
||||
{
|
||||
Current.Value = filter.OldValue;
|
||||
manageCollectionsDialog?.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
var newCollection = filter.NewValue.Collection;
|
||||
|
||||
// This dropdown be weird.
|
||||
// We only care about filtering if the actual collection has changed.
|
||||
if (newCollection != lastFiltered)
|
||||
{
|
||||
RequestFilter?.Invoke();
|
||||
lastFiltered = newCollection;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
realmSubscription?.Dispose();
|
||||
}
|
||||
|
||||
protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName;
|
||||
|
||||
protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu();
|
||||
|
||||
protected virtual ShearedCollectionDropdownMenu CreateCollectionMenu() => new ShearedCollectionDropdownMenu();
|
||||
|
||||
protected partial class ShearedCollectionDropdownMenu : ShearedDropdownMenu
|
||||
{
|
||||
public ShearedCollectionDropdownMenu()
|
||||
{
|
||||
MaxHeight = 200;
|
||||
}
|
||||
|
||||
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableCollectionMenuItem(item)
|
||||
{
|
||||
BackgroundColourHover = HoverColour,
|
||||
BackgroundColourSelected = SelectionColour
|
||||
};
|
||||
}
|
||||
|
||||
protected partial class DrawableCollectionMenuItem : ShearedDropdownMenu.ShearedMenuItem
|
||||
{
|
||||
private IconButton addOrRemoveButton = null!;
|
||||
|
||||
private bool beatmapInCollection;
|
||||
|
||||
private readonly Live<BeatmapCollection>? collection;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
public DrawableCollectionMenuItem(MenuItem item)
|
||||
: base(item)
|
||||
{
|
||||
collection = ((DropdownMenuItem<CollectionFilterMenuItem>)item).Value.Collection;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(addOrRemoveButton = new NoFocusChangeIconButton
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
X = -OsuScrollContainer.SCROLL_BAR_WIDTH,
|
||||
Scale = new Vector2(0.65f),
|
||||
Action = addOrRemove,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (collection != null)
|
||||
{
|
||||
beatmap.BindValueChanged(_ =>
|
||||
{
|
||||
beatmapInCollection = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
|
||||
addOrRemoveButton.Enabled.Value = !beatmap.IsDefault;
|
||||
addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare;
|
||||
addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap";
|
||||
|
||||
updateButtonVisibility();
|
||||
}, true);
|
||||
}
|
||||
|
||||
updateButtonVisibility();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateButtonVisibility();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateButtonVisibility();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override void OnSelectChange()
|
||||
{
|
||||
base.OnSelectChange();
|
||||
updateButtonVisibility();
|
||||
}
|
||||
|
||||
private void updateButtonVisibility()
|
||||
{
|
||||
if (collection == null)
|
||||
addOrRemoveButton.Alpha = 0;
|
||||
else
|
||||
addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0;
|
||||
}
|
||||
|
||||
private void addOrRemove()
|
||||
{
|
||||
Debug.Assert(collection != null);
|
||||
|
||||
collection.PerformWrite(c =>
|
||||
{
|
||||
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
|
||||
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
|
||||
});
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => (Content)base.CreateContent();
|
||||
|
||||
private partial class NoFocusChangeIconButton : IconButton
|
||||
{
|
||||
public override bool ChangeFocusOnClick => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Size = new Vector2(16f),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
Colour = colourProvider.Background5,
|
||||
};
|
||||
@@ -100,12 +100,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.875f),
|
||||
},
|
||||
localRank = new PanelLocalRankDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.75f)
|
||||
Scale = new Vector2(0.65f)
|
||||
},
|
||||
starCounter = new StarCounter
|
||||
{
|
||||
@@ -123,22 +124,22 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
keyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
},
|
||||
difficultyText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 8f },
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
authorText = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class PanelBeatmapSet : Panel
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.7f;
|
||||
|
||||
private PanelSetBackground background = null!;
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.ChevronRight,
|
||||
Size = new Vector2(12),
|
||||
Size = new Vector2(8),
|
||||
X = 1f,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
@@ -77,17 +77,17 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
||||
Font = OsuFont.Style.Heading1.With(typeface: Typeface.TorusAlternate),
|
||||
},
|
||||
artistText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5f },
|
||||
Margin = new MarginPadding { Top = 4f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
updateButton = new PanelUpdateBeatmapButton
|
||||
@@ -100,8 +100,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
TextSize = 11,
|
||||
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||
TextSize = OsuFont.Style.Caption2.Size,
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultiesDisplay = new DifficultySpectrumDisplay
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class PanelBeatmapStandalone : Panel
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.7f;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
@@ -76,7 +76,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Size = new Vector2(16),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
Colour = colourProvider.Background5,
|
||||
};
|
||||
@@ -95,19 +95,16 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
||||
Shadow = true,
|
||||
Font = OsuFont.Style.Heading1.With(typeface: Typeface.TorusAlternate),
|
||||
},
|
||||
artistText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
|
||||
Shadow = true,
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
updateButton = new PanelUpdateBeatmapButton
|
||||
@@ -120,8 +117,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
TextSize = 11,
|
||||
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||
TextSize = OsuFont.Style.Caption2.Size,
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultyLine = new FillFlowContainer
|
||||
@@ -134,19 +130,19 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Scale = new Vector2(8f / 9f),
|
||||
Scale = new Vector2(0.875f),
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultyRank = new PanelLocalRankDisplay
|
||||
{
|
||||
Scale = new Vector2(8f / 11),
|
||||
Scale = new Vector2(0.65f),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultyKeyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Heading2,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
@@ -154,7 +150,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
},
|
||||
difficultyName = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Heading2,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 5f, Bottom = 2f },
|
||||
@@ -162,7 +158,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
difficultyAuthor = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 5f, Bottom = 2f },
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private Drawable iconContainer = null!;
|
||||
private OsuSpriteText titleText = null!;
|
||||
private TrianglesV2 triangles = null!;
|
||||
private OsuSpriteText countText = null!;
|
||||
private Box glow = null!;
|
||||
|
||||
[Resolved]
|
||||
@@ -99,13 +100,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.7f),
|
||||
},
|
||||
new OsuSpriteText
|
||||
countText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
|
||||
// TODO: requires Carousel/CarouselItem-side implementation
|
||||
Text = "43",
|
||||
UseFullGlyphHeight = false,
|
||||
}
|
||||
},
|
||||
@@ -144,6 +143,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
GroupDefinition group = (GroupDefinition)Item.Model;
|
||||
|
||||
titleText.Text = group.Title;
|
||||
countText.Text = Item.NestedItemCount.ToString("N0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public PanelUpdateBeatmapButton()
|
||||
{
|
||||
Size = new Vector2(75f, 22f);
|
||||
Size = new Vector2(72, 22f);
|
||||
}
|
||||
|
||||
private Bindable<bool> preferNoVideo = null!;
|
||||
@@ -63,7 +63,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
const float icon_size = 14;
|
||||
const float icon_size = 12;
|
||||
|
||||
preferNoVideo = config.GetBindable<bool>(OsuSetting.PreferNoVideo);
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.Default.With(weight: FontWeight.Bold),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Text = "Update",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
private const float logo_scale = 0.4f;
|
||||
|
||||
public const float WEDGE_CONTENT_MARGIN = CORNER_RADIUS_HIDE_OFFSET + OsuGame.SCREEN_EDGE_MARGIN;
|
||||
public const float CORNER_RADIUS_HIDE_OFFSET = 20f;
|
||||
public const float ENTER_DURATION = 600;
|
||||
|
||||
private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay(OverlayColourScheme.Aquamarine)
|
||||
{
|
||||
ShowPresets = true,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
internal partial class WedgeBackground : CompositeDrawable
|
||||
{
|
||||
public float StartAlpha { get; init; } = 0.9f;
|
||||
|
||||
public float FinalAlpha { get; init; } = 0.6f;
|
||||
|
||||
public float WidthForGradient { get; init; } = 0.3f;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.6f,
|
||||
Alpha = 0.5f,
|
||||
Colour = ColourInfo.GradientHorizontal(colourProvider.Background2, colourProvider.Background2.Opacity(0)),
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 1 - WidthForGradient,
|
||||
Colour = colourProvider.Background5.Opacity(StartAlpha),
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = WidthForGradient,
|
||||
Colour = ColourInfo.GradientHorizontal(colourProvider.Background5.Opacity(StartAlpha), colourProvider.Background5.Opacity(FinalAlpha)),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Storyboards
|
||||
private readonly List<StoryboardTriggerGroup> triggerGroups = new List<StoryboardTriggerGroup>();
|
||||
|
||||
public string Path { get; }
|
||||
public bool IsDrawable => HasCommands;
|
||||
public virtual bool IsDrawable => HasCommands;
|
||||
|
||||
public Anchor Origin;
|
||||
public Vector2 InitialPosition;
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace osu.Game.Storyboards
|
||||
|
||||
public override double StartTime { get; }
|
||||
|
||||
public override bool IsDrawable => true;
|
||||
|
||||
public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="20.1.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.418.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.419.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.422.0" />
|
||||
<PackageReference Include="Sentry" Version="5.1.1" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
||||
+1
-1
@@ -17,6 +17,6 @@
|
||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.418.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.419.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user