diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 65ac05261a..cbd0231fdb 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2022.417.0", + "version": "2022.607.0", "commands": [ "localisation" ] diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index d87b25a4c7..bc5763a3b4 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -30,6 +30,9 @@ namespace osu.Desktop private IBindable user; + [Resolved] + private IAPIProvider api { get; set; } + private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); @@ -41,7 +44,7 @@ namespace osu.Desktop }; [BackgroundDependencyLoader] - private void load(IAPIProvider provider, OsuConfigManager config) + private void load(OsuConfigManager config) { client = new DiscordRpcClient(client_id) { @@ -57,7 +60,8 @@ namespace osu.Desktop config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); - (user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u => + user = api.LocalUser.GetBoundCopy(); + user.BindValueChanged(u => { status.UnbindBindings(); status.BindTo(u.NewValue.Status); @@ -106,7 +110,12 @@ namespace osu.Desktop if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; else - presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); + { + if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics statistics)) + presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); + else + presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); + } // update ruleset presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a904658a4c..fa095edafa 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -79,7 +79,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Result = { BindTarget = SpinsPerMinute }, }, - ticks = new Container(), + ticks = new Container + { + RelativeSizeAxes = Axes.Both, + }, new AspectContainer { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 726fbd3ea6..39239c8233 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; + namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject @@ -10,13 +12,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject; public DrawableSpinnerTick() - : base(null) + : this(null) { } public DrawableSpinnerTick(SpinnerTick spinnerTick) : base(spinnerTick) { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; } protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration; diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index ddee4d3ebd..1a130e96b3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -69,8 +69,8 @@ namespace osu.Game.Rulesets.Osu.Objects double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; AddNested(i < SpinsRequired - ? new SpinnerTick { StartTime = startTime, Position = Position } - : new SpinnerBonusTick { StartTime = startTime, Position = Position }); + ? new SpinnerTick { StartTime = startTime } + : new SpinnerBonusTick { StartTime = startTime }); } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 504b10e9bc..2dd332fc13 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Primitives; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; @@ -321,12 +322,14 @@ namespace osu.Game.Rulesets.Taiko.UI private class ProxyContainer : LifetimeManagementContainer { - public new MarginPadding Padding - { - set => base.Padding = value; - } - public void Add(Drawable proxy) => AddInternal(proxy); + + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) + { + // DrawableHitObject disables masking. + // Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this. + return false; + } } } } diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index eaacc623c9..2bb6459f20 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Chat [SetUp] public void Setup() => Schedule(() => { - var container = new ChannelManagerContainer(); + var container = new ChannelManagerContainer(API); Child = container; channelManager = container.ChannelManager; }); @@ -145,11 +145,11 @@ namespace osu.Game.Tests.Chat private class ChannelManagerContainer : CompositeDrawable { [Cached] - public ChannelManager ChannelManager { get; } = new ChannelManager(); + public ChannelManager ChannelManager { get; } - public ChannelManagerContainer() + public ChannelManagerContainer(IAPIProvider apiProvider) { - InternalChild = ChannelManager; + InternalChild = ChannelManager = new ChannelManager(apiProvider); } } } diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index af4b002bc9..97be1dcfaa 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -59,12 +59,14 @@ namespace osu.Game.Tests.Gameplay scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great }); Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000)); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1)); // No header shouldn't cause any change scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame()); Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000)); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1)); // Reset with a miss instead. scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame @@ -74,6 +76,7 @@ namespace osu.Game.Tests.Gameplay Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); // Reset with no judged hit. scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame @@ -83,6 +86,7 @@ namespace osu.Game.Tests.Gameplay Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); Assert.That(scoreProcessor.JudgedHits, Is.Zero); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); } private class TestJudgement : Judgement diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs index d3cacaa88c..d68398236a 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -59,11 +59,13 @@ namespace osu.Game.Tests.Skins AddAssert("Check float parse lookup", () => requester.GetConfig("FloatTest")?.Value == 1.1f); } - [Test] - public void TestBoolLookup() + [TestCase("0", false)] + [TestCase("1", true)] + [TestCase("2", true)] // https://github.com/ppy/osu/issues/18579 + public void TestBoolLookup(string originalValue, bool expectedParsedValue) { - AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = "1"); - AddAssert("Check bool parse lookup", () => requester.GetConfig("BoolTest")?.Value == true); + AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = originalValue); + AddAssert("Check bool parse lookup", () => requester.GetConfig("BoolTest")?.Value == expectedParsedValue); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 4e6342868a..a738debecc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -18,7 +18,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; @@ -27,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public abstract class MultiplayerGameplayLeaderboardTestScene : OsuTestScene { - private const int total_users = 16; + protected const int TOTAL_USERS = 16; protected readonly BindableList MultiplayerUsers = new BindableList(); @@ -35,9 +34,10 @@ namespace osu.Game.Tests.Visual.Multiplayer protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId); - protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor); + protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(); private readonly BindableList multiplayerUserIds = new BindableList(); + private readonly BindableDictionary watchedUserStates = new BindableDictionary(); private OsuConfigManager config; @@ -81,6 +81,9 @@ namespace osu.Game.Tests.Visual.Multiplayer multiplayerClient.SetupGet(c => c.CurrentMatchPlayingUserIds) .Returns(() => multiplayerUserIds); + + spectatorClient.SetupGet(c => c.WatchedUserStates) + .Returns(() => watchedUserStates); } [SetUpSteps] @@ -100,8 +103,26 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("populate users", () => { MultiplayerUsers.Clear(); - for (int i = 0; i < total_users; i++) - MultiplayerUsers.Add(CreateUser(i)); + + for (int i = 0; i < TOTAL_USERS; i++) + { + var user = CreateUser(i); + + MultiplayerUsers.Add(user); + + watchedUserStates[i] = new SpectatorState + { + BeatmapID = 0, + RulesetID = 0, + Mods = user.Mods, + MaximumScoringValues = new ScoringValues + { + BaseScore = 10000, + MaxCombo = 1000, + CountBasicHitObjects = 1000 + } + }; + } }); AddStep("create leaderboard", () => @@ -109,13 +130,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Leaderboard?.Expire(); Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); - OsuScoreProcessor scoreProcessor = new OsuScoreProcessor(); - scoreProcessor.ApplyBeatmap(playableBeatmap); - Child = scoreProcessor; - - LoadComponentAsync(Leaderboard = CreateLeaderboard(scoreProcessor), Add); + LoadComponentAsync(Leaderboard = CreateLeaderboard(), Add); }); AddUntilStep("wait for load", () => Leaderboard.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index f57a54d84c..60215dc8b3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -4,11 +4,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play.HUD; @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("reset", () => { - Clear(); + leaderboard?.RemoveAndDisposeImmediately(); clocks = new Dictionary { @@ -32,21 +32,18 @@ namespace osu.Game.Tests.Visual.Multiplayer { PLAYER_2_ID, new ManualClock() } }; - foreach ((int userId, var _) in clocks) + foreach ((int userId, _) in clocks) { SpectatorClient.SendStartPlay(userId, 0); - OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId }); + OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId }, true); } }); AddStep("create leaderboard", () => { Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); - var scoreProcessor = new OsuScoreProcessor(); - scoreProcessor.ApplyBeatmap(playable); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { Expanded = { Value = true } }, Add); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 6e4aa48b0e..cbbd535cee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -1,22 +1,58 @@ // Copyright (c) ppy Pty Ltd . 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.Graphics; -using osu.Game.Rulesets.Osu.Scoring; +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; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerGameplayLeaderboardTestScene { - protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor) + protected override MultiplayerRoomUser CreateUser(int userId) { - return new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray()) + var user = base.CreateUser(userId); + + if (userId == TOTAL_USERS - 1) + user.Mods = new[] { new APIMod(new OsuModNoFail()) }; + + return user; + } + + protected override MultiplayerGameplayLeaderboard CreateLeaderboard() + { + return 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("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()); + }); + } + + private class TestLeaderboard : MultiplayerGameplayLeaderboard + { + public Dictionary> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods); + + public TestLeaderboard(MultiplayerRoomUser[] users) + : base(users) + { + } + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 5caab9487e..c25884039f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -5,7 +5,6 @@ using System.Linq; using osu.Framework.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play.HUD; @@ -25,8 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer return user; } - protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor) => - new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray()) + protected override MultiplayerGameplayLeaderboard CreateLeaderboard() => + new MultiplayerGameplayLeaderboard(MultiplayerUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index c1f5f110d1..51bb27f93e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -609,8 +609,6 @@ namespace osu.Game.Tests.Visual.Navigation public ModSelectOverlay ModSelectOverlay => ModSelect; public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions; - - protected override bool DisplayStableImportPrompt => false; } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index a28de3be1e..4d227af2cb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Online { linkColour = colours.Blue; - var chatManager = new ChannelManager(); + var chatManager = new ChannelManager(API); BindableList availableChannels = (BindableList)chatManager.AvailableChannels; availableChannels.Add(new Channel { Name = "#english" }); availableChannels.Add(new Channel { Name = "#japanese" }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 2cf1114f30..e3792c0780 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online RelativeSizeAxes = Axes.Both, CachedDependencies = new (Type, object)[] { - (typeof(ChannelManager), channelManager = new ChannelManager()), + (typeof(ChannelManager), channelManager = new ChannelManager(API)), }, Children = new Drawable[] { @@ -572,15 +572,15 @@ namespace osu.Game.Tests.Visual.Online public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType().Single(c => c.Channel == channel); - protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) + protected override DrawableChannel CreateDrawableChannel(Channel newChannel) { return SlowLoading ? new SlowLoadingDrawableChannel(newChannel) - : new ChatOverlayDrawableChannel(newChannel); + : new DrawableChannel(newChannel); } } - private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel + private class SlowLoadingDrawableChannel : DrawableChannel { public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 79f62a16e3..c7ca3b4457 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online Schedule(() => { - Child = testContainer = new TestContainer(new[] { publicChannel, privateMessageChannel }) + Child = testContainer = new TestContainer(API, new[] { publicChannel, privateMessageChannel }) { RelativeSizeAxes = Axes.Both, }; @@ -178,6 +179,36 @@ namespace osu.Game.Tests.Visual.Online AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); } + /// + /// Ensures that handles channels which have not been or could not be resolved (i.e. = 0). + /// + [Test] + public void TestSendInUnresolvedChannel() + { + int i = 1; + Channel unresolved = null; + + AddRepeatStep("join unresolved channels", () => testContainer.ChannelManager.JoinChannel(unresolved = new Channel(new APIUser + { + Id = 100 + i, + Username = $"Foreign #{i++}", + })), 5); + + AddStep("send message in unresolved channel", () => + { + Debug.Assert(unresolved.Id == 0); + + unresolved.AddLocalEcho(new LocalEchoMessage + { + Sender = API.LocalUser.Value, + ChannelId = unresolved.Id, + Content = "Some message", + }); + }); + + AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); + } + private void receiveMessage(APIUser sender, Channel channel, string content) => channel.AddNewMessages(createMessage(sender, channel, content)); private Message createMessage(APIUser sender, Channel channel, string content) => new Message(messageIdCounter++) @@ -198,7 +229,7 @@ namespace osu.Game.Tests.Visual.Online private class TestContainer : Container { [Cached] - public ChannelManager ChannelManager { get; } = new ChannelManager(); + public ChannelManager ChannelManager { get; } [Cached(typeof(INotificationOverlay))] public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay @@ -214,9 +245,10 @@ namespace osu.Game.Tests.Visual.Online private readonly Channel[] channels; - public TestContainer(Channel[] channels) + public TestContainer(IAPIProvider api, Channel[] channels) { this.channels = channels; + ChannelManager = new ChannelManager(api); } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index 860ef5d565..8d5eebd31f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Chat; using osuTK.Input; @@ -44,17 +45,22 @@ namespace osu.Game.Tests.Visual.Online Id = 5, }; - [Cached] - private ChannelManager channelManager = new ChannelManager(); + private ChannelManager channelManager; private TestStandAloneChatDisplay chatDisplay; private int messageIdSequence; private Channel testChannel; - public TestSceneStandAloneChatDisplay() + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - Add(channelManager); + Add(channelManager = new ChannelManager(parent.Get())); + + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.Cache(channelManager); + + return dependencies; } [SetUp] @@ -128,11 +134,11 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Ensure no adjacent day separators", () => { - var indices = chatDisplay.FillFlow.OfType().Select(ds => chatDisplay.FillFlow.IndexOf(ds)); + var indices = chatDisplay.FillFlow.OfType().Select(ds => chatDisplay.FillFlow.IndexOf(ds)); foreach (int i in indices) { - if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator) + if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DaySeparator) return false; } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 8b646df362..2a31728f87 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -96,6 +96,7 @@ namespace osu.Game.Tests.Visual.Ranking beatmap.Metadata.Author = author; beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title"; beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist"; + beatmap.DifficultyName = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong difficulty name"; return beatmap; } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 940d001c5b..a78a8aa028 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -5,12 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Extensions; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; @@ -23,38 +22,28 @@ namespace osu.Game.Tests.Visual.SongSelect { public class TestSceneBeatmapRecommendations : OsuGameTestScene { + [Resolved] + private IRulesetStore rulesetStore { get; set; } + [SetUpSteps] public override void SetUpSteps() { - AddStep("register request handling", () => - { - ((DummyAPIAccess)API).HandleRequest = req => - { - switch (req) - { - case GetUserRequest userRequest: - userRequest.TriggerSuccess(getUser(userRequest.Ruleset.OnlineID)); - return true; - } - - return false; - }; - }); - base.SetUpSteps(); - APIUser getUser(int? rulesetID) + AddStep("populate ruleset statistics", () => { - return new APIUser + Dictionary rulesetStatistics = new Dictionary(); + + rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo => { - Username = @"Dummy", - Id = 1001, - Statistics = new UserStatistics + rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics { - PP = getNecessaryPP(rulesetID) - } - }; - } + PP = getNecessaryPP(rulesetInfo.OnlineID) + }; + }); + + API.LocalUser.Value.RulesetsStatistics = rulesetStatistics; + }); decimal getNecessaryPP(int? rulesetID) { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index aad7f6b301..77f5bd83d6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -18,6 +18,7 @@ using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; @@ -80,6 +81,37 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("delete all beatmaps", () => manager?.Delete()); } + [Test] + public void TestPlaceholderBeatmapPresence() + { + createSongSelect(); + + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); + + addRulesetImportStep(0); + AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden); + + AddStep("delete all beatmaps", () => manager?.Delete()); + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); + } + + [Test] + public void TestPlaceholderConvertSetting() + { + changeRuleset(2); + addRulesetImportStep(0); + AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); + + createSongSelect(); + + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); + + AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType().First().TriggerClick()); + + AddUntilStep("convert setting changed", () => config.Get(OsuSetting.ShowConvertedBeatmaps)); + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden); + } + [Test] public void TestSingleFilterOnEnter() { @@ -941,6 +973,8 @@ namespace osu.Game.Tests.Visual.SongSelect private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.IndexOf(info); + private NoResultsPlaceholder getPlaceholder() => songSelect.ChildrenOfType().FirstOrDefault(); + private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmapInfo); private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index a5ead6c2f0..c30250c86a 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -5,6 +5,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Overlays.Chat; using osu.Game.Tournament.IPC; @@ -29,7 +30,7 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader(true)] - private void load(MatchIPCInfo ipc) + private void load(MatchIPCInfo ipc, IAPIProvider api) { if (ipc != null) { @@ -45,7 +46,7 @@ namespace osu.Game.Tournament.Components if (manager == null) { - AddInternal(manager = new ChannelManager { HighPollRate = { Value = true } }); + AddInternal(manager = new ChannelManager(api) { HighPollRate = { Value = true } }); Channel.BindTo(manager.CurrentChannel); } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 5f7de0d762..dba457c81c 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -319,6 +319,15 @@ namespace osu.Game.Beatmaps }); } + public void DeleteAllVideos() + { + realm.Write(r => + { + var items = r.All().Where(s => !s.DeletePending && !s.Protected); + beatmapModelManager.DeleteVideos(items.ToList()); + }); + } + public void UndeleteAll() { realm.Run(r => beatmapModelManager.Undelete(r.All().Where(s => s.DeletePending).ToList())); diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 4c680bbcc9..277047348e 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -16,6 +16,7 @@ using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Skinning; using osu.Game.Stores; +using osu.Game.Overlays.Notifications; #nullable enable @@ -33,6 +34,8 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; + public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" }; + public BeatmapModelManager(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) : base(realm, storage, onlineLookupQueue) { @@ -114,5 +117,50 @@ namespace osu.Game.Beatmaps item.CopyChangesToRealm(existing); }); } + + /// + /// Delete videos from a list of beatmaps. + /// This will post notifications tracking progress. + /// + public void DeleteVideos(List items, bool silent = false) + { + if (items.Count == 0) return; + + var notification = new ProgressNotification + { + Progress = 0, + Text = $"Preparing to delete all {HumanisedModelName} videos...", + CompletionText = "No videos found to delete!", + State = ProgressNotificationState.Active, + }; + + if (!silent) + PostNotification?.Invoke(notification); + + int i = 0; + int deleted = 0; + + foreach (var b in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + var video = b.Files.FirstOrDefault(f => VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal))); + + if (video != null) + { + DeleteFile(b, video); + deleted++; + notification.CompletionText = $"Deleted {deleted} {HumanisedModelName} video(s)!"; + } + + notification.Text = $"Deleting videos from {HumanisedModelName}s ({deleted} deleted)"; + + notification.Progress = (float)++i / items.Count; + } + + notification.State = ProgressNotificationState.Completed; + } } } diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index 93c2fccbc7..4629b20569 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -7,11 +7,8 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; -using osu.Game.Extensions; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Rulesets; namespace osu.Game.Beatmaps @@ -25,26 +22,15 @@ namespace osu.Game.Beatmaps [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private IRulesetStore rulesets { get; set; } - [Resolved] private Bindable ruleset { get; set; } - /// - /// The user for which the last requests were run. - /// - private int? requestedUserId; - - private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); - - private readonly IBindable apiState = new Bindable(); + private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); [BackgroundDependencyLoader] private void load() { - apiState.BindTo(api.State); - apiState.BindValueChanged(onlineStateChanged, true); + api.LocalUser.BindValueChanged(_ => populateValues(), true); } /// @@ -58,12 +44,12 @@ namespace osu.Game.Beatmaps [CanBeNull] public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) { - foreach (var r in orderedRulesets) + foreach (string r in orderedRulesets) { if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation)) continue; - BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => + BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r)).OrderBy(b => { double difference = b.StarRating - recommendation; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder @@ -76,55 +62,35 @@ namespace osu.Game.Beatmaps return null; } - private void fetchRecommendedValues() + private void populateValues() { - if (recommendedDifficultyMapping.Count > 0 && api.LocalUser.Value.Id == requestedUserId) + if (api.LocalUser.Value.RulesetsStatistics == null) return; - requestedUserId = api.LocalUser.Value.Id; - - // only query API for built-in rulesets - rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo => + foreach (var kvp in api.LocalUser.Value.RulesetsStatistics) { - var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); - - req.Success += result => - { - // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 - recommendedDifficultyMapping[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; - }; - - api.Queue(req); - }); + // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 + recommendedDifficultyMapping[kvp.Key] = Math.Pow((double)(kvp.Value.PP ?? 0), 0.4) * 0.195; + } } /// /// Rulesets ordered descending by their respective recommended difficulties. /// The currently selected ruleset will always be first. /// - private IEnumerable orderedRulesets + private IEnumerable orderedRulesets { get { if (LoadState < LoadState.Ready || ruleset.Value == null) - return Enumerable.Empty(); + return Enumerable.Empty(); return recommendedDifficultyMapping .OrderByDescending(pair => pair.Value) .Select(pair => pair.Key) - .Where(r => !r.Equals(ruleset.Value)) - .Prepend(ruleset.Value); + .Where(r => !r.Equals(ruleset.Value.ShortName)) + .Prepend(ruleset.Value.ShortName); } } - - private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => - { - switch (state.NewValue) - { - case APIState.Online: - fetchRecommendedValues(); - break; - } - }); } } diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 11bfd80ec1..5c6e315225 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers /// public class ScalingContainer : Container { - private const float duration = 500; + internal const float TRANSITION_DURATION = 500; private Bindable sizeX; private Bindable sizeY; @@ -99,7 +99,7 @@ namespace osu.Game.Graphics.Containers if (applyUIScale) { uiScale = osuConfig.GetBindable(OsuSetting.UIScale); - uiScale.BindValueChanged(args => this.TransformTo(nameof(CurrentScale), args.NewValue, duration, Easing.OutQuart), true); + uiScale.BindValueChanged(args => this.TransformTo(nameof(CurrentScale), args.NewValue, TRANSITION_DURATION, Easing.OutQuart), true); } } @@ -163,10 +163,10 @@ namespace osu.Game.Graphics.Containers backgroundStack.Push(new ScalingBackgroundScreen()); } - backgroundStack.FadeIn(duration); + backgroundStack.FadeIn(TRANSITION_DURATION); } else - backgroundStack?.FadeOut(duration); + backgroundStack?.FadeOut(TRANSITION_DURATION); } RectangleF targetRect = new RectangleF(Vector2.Zero, Vector2.One); @@ -195,13 +195,13 @@ namespace osu.Game.Graphics.Containers if (requiresMasking) sizableContainer.Masking = true; - sizableContainer.MoveTo(targetRect.Location, duration, Easing.OutQuart); - sizableContainer.ResizeTo(targetRect.Size, duration, Easing.OutQuart); + sizableContainer.MoveTo(targetRect.Location, TRANSITION_DURATION, Easing.OutQuart); + sizableContainer.ResizeTo(targetRect.Size, TRANSITION_DURATION, Easing.OutQuart); // Of note, this will not work great in the case of nested ScalingContainers where multiple are applying corner radius. // Masking and corner radius should likely only be applied at one point in the full game stack to fix this. // An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything". - sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, duration, requiresMasking ? Easing.OutQuart : Easing.None) + sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, TRANSITION_DURATION, requiresMasking ? Easing.OutQuart : Easing.None) .OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index 3d09d09833..d9dbf4974b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -65,6 +67,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 { get { + if (BeatmapModelManager.VIDEO_EXTENSIONS.Contains(File.Extension)) + return FontAwesome.Regular.FileVideo; + switch (File.Extension) { case @".ogg": @@ -77,12 +82,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 case @".png": return FontAwesome.Regular.FileImage; - case @".mp4": - case @".avi": - case @".mov": - case @".flv": - return FontAwesome.Regular.FileVideo; - default: return FontAwesome.Regular.File; } diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs index f298717c99..0f0f560df9 100644 --- a/osu.Game/Localisation/AudioSettingsStrings.cs +++ b/osu.Game/Localisation/AudioSettingsStrings.cs @@ -30,12 +30,12 @@ namespace osu.Game.Localisation public static LocalisableString OutputDevice => new TranslatableString(getKey(@"output_device"), @"Output device"); /// - /// "Master" + /// "Hitsound stereo separation" /// public static LocalisableString PositionalLevel => new TranslatableString(getKey(@"positional_hitsound_audio_level"), @"Hitsound stereo separation"); /// - /// "Level" + /// "Master" /// public static LocalisableString MasterVolume => new TranslatableString(getKey(@"master_volume"), @"Master"); @@ -69,6 +69,6 @@ namespace osu.Game.Localisation /// public static LocalisableString OffsetWizard => new TranslatableString(getKey(@"offset_wizard"), @"Offset wizard"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 586e29a432..82d03dbb5b 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -210,7 +210,7 @@ namespace osu.Game.Localisation public static LocalisableString ToggleInGameInterface => new TranslatableString(getKey(@"toggle_in_game_interface"), @"Toggle in-game interface"); /// - /// "Toggle Mod Select" + /// "Toggle mod select" /// public static LocalisableString ToggleModSelection => new TranslatableString(getKey(@"toggle_mod_selection"), @"Toggle mod select"); @@ -299,6 +299,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/JoystickSettingsStrings.cs b/osu.Game/Localisation/JoystickSettingsStrings.cs index 410cd0a6f5..976ec1adde 100644 --- a/osu.Game/Localisation/JoystickSettingsStrings.cs +++ b/osu.Game/Localisation/JoystickSettingsStrings.cs @@ -15,10 +15,10 @@ namespace osu.Game.Localisation public static LocalisableString JoystickGamepad => new TranslatableString(getKey(@"joystick_gamepad"), @"Joystick / Gamepad"); /// - /// "Deadzone Threshold" + /// "Deadzone" /// public static LocalisableString DeadzoneThreshold => new TranslatableString(getKey(@"deadzone_threshold"), @"Deadzone"); private static string getKey(string key) => $@"{prefix}:{key}"; } -} \ No newline at end of file +} diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs index a0e1a9ddab..7a04bcd1ca 100644 --- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs +++ b/osu.Game/Localisation/MaintenanceSettingsStrings.cs @@ -29,6 +29,11 @@ namespace osu.Game.Localisation /// public static LocalisableString DeleteAllBeatmaps => new TranslatableString(getKey(@"delete_all_beatmaps"), @"Delete ALL beatmaps"); + /// + /// "Delete ALL beatmap videos" + /// + public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos"); + /// /// "Import scores from stable" /// diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs index 8b74b94d59..81035c5a5e 100644 --- a/osu.Game/Localisation/SkinSettingsStrings.cs +++ b/osu.Game/Localisation/SkinSettingsStrings.cs @@ -54,6 +54,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin"); + /// + /// "Delete selected skin" + /// + public static LocalisableString DeleteSkinButton => new TranslatableString(getKey(@"delete_skin_button"), @"Delete selected skin"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 31f67bcecc..c96e7c4cd3 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -61,8 +61,7 @@ namespace osu.Game.Online.Chat /// public IBindableList AvailableChannels => availableChannels; - [Resolved] - private IAPIProvider api { get; set; } + private readonly IAPIProvider api; [Resolved] private UserLookupCache users { get; set; } @@ -71,8 +70,9 @@ namespace osu.Game.Online.Chat private readonly IBindable isIdle = new BindableBool(); - public ChannelManager() + public ChannelManager(IAPIProvider api) { + this.api = api; CurrentChannel.ValueChanged += currentChannelChanged; } diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index ca6082e19b..fbc5ef79ef 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -77,7 +77,7 @@ namespace osu.Game.Online.Chat if (!messages.Any()) return; - var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == messages.First().ChannelId); + var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id > 0 && c.Id == messages.First().ChannelId); if (channel == null) return; diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index b7e1bc999b..bbfffea6fd 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -155,39 +155,42 @@ namespace osu.Game.Online.Chat { public Func CreateChatLineAction; - [Resolved] - private OsuColour colours { get; set; } - public StandAloneDrawableChannel(Channel channel) : base(channel) { } - [BackgroundDependencyLoader] - private void load() - { - ChatLineFlow.Padding = new MarginPadding { Horizontal = 0 }; - } - protected override ChatLine CreateChatLine(Message m) => CreateChatLineAction(m); - protected override Drawable CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time) + protected override DaySeparator CreateDaySeparator(DateTimeOffset time) => new StandAloneDaySeparator(time); + } + + protected class StandAloneDaySeparator : DaySeparator + { + protected override float TextSize => 14; + protected override float LineHeight => 1; + protected override float Spacing => 5; + protected override float DateAlign => 125; + + public StandAloneDaySeparator(DateTimeOffset time) + : base(time) { - TextSize = 14, - Colour = colours.Yellow, - LineHeight = 1, - Padding = new MarginPadding { Horizontal = 10 }, - Margin = new MarginPadding { Vertical = 5 }, - }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Height = 25; + Colour = colours.Yellow; + } } protected class StandAloneMessage : ChatLine { protected override float TextSize => 15; - - protected override float HorizontalPadding => 10; - protected override float MessagePadding => 120; - protected override float TimestampPadding => 50; + protected override float Spacing => 5; + protected override float TimestampWidth => 45; + protected override float UsernameWidth => 75; public StandAloneMessage(Message message) : base(message) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 78beda6298..9bf49364f3 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -39,7 +39,7 @@ namespace osu.Game.Online.Spectator /// /// The states of all users currently being watched. /// - public IBindableDictionary WatchedUserStates => watchedUserStates; + public virtual IBindableDictionary WatchedUserStates => watchedUserStates; /// /// A global list of all players currently playing. @@ -172,6 +172,7 @@ namespace osu.Game.Online.Spectator currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); currentState.State = SpectatedUserState.Playing; + currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues; currentBeatmap = state.Beatmap; currentScore = score; diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs new file mode 100644 index 0000000000..a1e8715c8f --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs @@ -0,0 +1,192 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using System.Collections.Generic; +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.Timing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Online.Spectator +{ + /// + /// A wrapper over a for spectated users. + /// This should be used when a local "playable" beatmap is unavailable or expensive to generate for the spectated user. + /// + public class SpectatorScoreProcessor : Component + { + /// + /// The current total score. + /// + public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 }; + + /// + /// The current accuracy. + /// + public readonly BindableDouble Accuracy = new BindableDouble(1) { MinValue = 0, MaxValue = 1 }; + + /// + /// The current combo. + /// + public readonly BindableInt Combo = new BindableInt(); + + /// + /// The used to calculate scores. + /// + public readonly Bindable Mode = new Bindable(); + + /// + /// The applied s. + /// + public IReadOnlyList Mods => scoreProcessor?.Mods.Value ?? Array.Empty(); + + private IClock? referenceClock; + + /// + /// The clock used to determine the current score. + /// + public IClock ReferenceClock + { + get => referenceClock ?? Clock; + set => referenceClock = value; + } + + [Resolved] + private SpectatorClient spectatorClient { get; set; } = null!; + + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + + private readonly IBindableDictionary spectatorStates = new BindableDictionary(); + private readonly List replayFrames = new List(); + private readonly int userId; + + private SpectatorState? spectatorState; + private ScoreProcessor? scoreProcessor; + private ScoreInfo? scoreInfo; + + public SpectatorScoreProcessor(int userId) + { + this.userId = userId; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Mode.BindValueChanged(_ => UpdateScore()); + + spectatorStates.BindTo(spectatorClient.WatchedUserStates); + spectatorStates.BindCollectionChanged(onSpectatorStatesChanged, true); + + spectatorClient.OnNewFrames += onNewFrames; + } + + private void onSpectatorStatesChanged(object? sender, NotifyDictionaryChangedEventArgs e) + { + if (!spectatorStates.TryGetValue(userId, out var userState) || userState.BeatmapID == null || userState.RulesetID == null) + { + scoreProcessor?.RemoveAndDisposeImmediately(); + scoreProcessor = null; + scoreInfo = null; + spectatorState = null; + replayFrames.Clear(); + return; + } + + if (scoreProcessor != null) + return; + + Debug.Assert(scoreInfo == null); + + RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value); + if (rulesetInfo == null) + return; + + Ruleset ruleset = rulesetInfo.CreateInstance(); + + spectatorState = userState; + scoreInfo = new ScoreInfo { Ruleset = rulesetInfo }; + scoreProcessor = ruleset.CreateScoreProcessor(); + scoreProcessor.Mods.Value = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray(); + } + + private void onNewFrames(int incomingUserId, FrameDataBundle bundle) + { + if (incomingUserId != userId) + return; + + Schedule(() => + { + if (scoreProcessor == null) + return; + + replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); + UpdateScore(); + }); + } + + public void UpdateScore() + { + if (scoreInfo == null || replayFrames.Count == 0) + return; + + Debug.Assert(spectatorState != null); + Debug.Assert(scoreProcessor != null); + + int frameIndex = replayFrames.BinarySearch(new TimedFrame(ReferenceClock.CurrentTime)); + if (frameIndex < 0) + frameIndex = ~frameIndex; + frameIndex = Math.Clamp(frameIndex - 1, 0, replayFrames.Count - 1); + + TimedFrame frame = replayFrames[frameIndex]; + Debug.Assert(frame.Header != null); + + scoreInfo.MaxCombo = frame.Header.MaxCombo; + scoreInfo.Statistics = frame.Header.Statistics; + + Accuracy.Value = frame.Header.Accuracy; + Combo.Value = frame.Header.Combo; + + scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _); + TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorClient.IsNotNull()) + spectatorClient.OnNewFrames -= onNewFrames; + } + + private class TimedFrame : IComparable + { + public readonly double Time; + public readonly FrameHeader? Header; + + public TimedFrame(double time) + { + Time = time; + } + + public TimedFrame(double time, FrameHeader header) + { + Time = time; + Header = header; + } + + public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time); + } + } +} diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 77686d12da..64e5f8b3a1 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using MessagePack; using osu.Game.Online.API; +using osu.Game.Scoring; namespace osu.Game.Online.Spectator { @@ -27,6 +28,9 @@ namespace osu.Game.Online.Spectator [Key(3)] public SpectatedUserState State { get; set; } + [Key(4)] + public ScoringValues MaximumScoringValues { get; set; } + public bool Equals(SpectatorState other) { if (ReferenceEquals(null, other)) return false; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b91d523151..1f9a1ce938 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -851,7 +851,7 @@ namespace osu.Game loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); - loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); + loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true); diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index a1d8cd5d38..56e39f212d 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Linq; using System.Collections.Generic; @@ -26,42 +28,6 @@ namespace osu.Game.Overlays.Chat { public class ChatLine : CompositeDrawable { - public const float LEFT_PADDING = default_message_padding + default_horizontal_padding * 2; - - private const float default_message_padding = 200; - - protected virtual float MessagePadding => default_message_padding; - - private const float default_timestamp_padding = 65; - - protected virtual float TimestampPadding => default_timestamp_padding; - - private const float default_horizontal_padding = 15; - - protected virtual float HorizontalPadding => default_horizontal_padding; - - protected virtual float TextSize => 20; - - private Color4 usernameColour; - - private OsuSpriteText timestamp; - - public ChatLine(Message message) - { - Message = message; - Padding = new MarginPadding { Left = HorizontalPadding, Right = HorizontalPadding }; - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - } - - [Resolved(CanBeNull = true)] - private ChannelManager chatManager { get; set; } - - private Message message; - private OsuSpriteText username; - - public LinkFlowContainer ContentFlow { get; private set; } - public Message Message { get => message; @@ -78,119 +44,101 @@ namespace osu.Game.Overlays.Chat } } + public LinkFlowContainer ContentFlow { get; private set; } = null!; + + protected virtual float TextSize => 20; + + protected virtual float Spacing => 15; + + protected virtual float TimestampWidth => 60; + + protected virtual float UsernameWidth => 130; + + private Color4 usernameColour; + + private OsuSpriteText timestamp = null!; + + private Message message = null!; + + private OsuSpriteText username = null!; + + private Container? highlight; + private bool senderHasColour => !string.IsNullOrEmpty(message.Sender.Colour); + private bool messageHasColour => Message.IsAction && senderHasColour; + [Resolved] - private OsuColour colours { get; set; } + private ChannelManager? chatManager { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ChatLine(Message message) + { + Message = message; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider? colourProvider) { usernameColour = senderHasColour ? Color4Extensions.FromHex(message.Sender.Colour) : username_colours[message.Sender.Id % username_colours.Length]; - Drawable effectedUsername = username = new OsuSpriteText + InternalChild = new GridContainer { - Shadow = false, - Colour = senderHasColour ? colours.ChatBlue : usernameColour, - Truncate = true, - EllipsisString = "… :", - Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true), - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - MaxWidth = MessagePadding - TimestampPadding - }; - - if (senderHasColour) - { - // Background effect - effectedUsername = new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] { - AutoSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 4, - EdgeEffect = new EdgeEffectParameters - { - Roundness = 1, - Radius = 1, - Colour = Color4.Black.Opacity(0.3f), - Offset = new Vector2(0, 1), - Type = EdgeEffectType.Shadow, - }, - Child = new Container - { - AutoSizeAxes = Axes.Both, - Y = 0, - Masking = true, - CornerRadius = 4, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = usernameColour, - }, - new Container - { - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 }, - Child = username - } - } - } - }; - } - - InternalChildren = new Drawable[] - { - new Container - { - Size = new Vector2(MessagePadding, TextSize), - Children = new Drawable[] - { - timestamp = new OsuSpriteText - { - Shadow = false, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true) - }, - new MessageSender(message.Sender) - { - AutoSizeAxes = Axes.Both, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Child = effectedUsername, - }, - } + new Dimension(GridSizeMode.Absolute, TimestampWidth + Spacing + UsernameWidth + Spacing), + new Dimension(), }, - new Container + Content = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = MessagePadding + HorizontalPadding }, - Children = new Drawable[] + new Drawable[] { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + timestamp = new OsuSpriteText + { + Shadow = false, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), + MaxWidth = TimestampWidth, + Colour = colourProvider?.Background1 ?? Colour4.White, + }, + new MessageSender(message.Sender) + { + Width = UsernameWidth, + AutoSizeAxes = Axes.Y, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Child = createUsername(), + Margin = new MarginPadding { Horizontal = Spacing }, + }, + }, + }, ContentFlow = new LinkFlowContainer(t => { t.Shadow = false; - - if (Message.IsAction) - { - t.Font = OsuFont.GetFont(italics: true); - - if (senderHasColour) - t.Colour = Color4Extensions.FromHex(message.Sender.Colour); - } - - t.Font = t.Font.With(size: TextSize); + t.Font = t.Font.With(size: TextSize, italics: Message.IsAction); + t.Colour = messageHasColour ? Color4Extensions.FromHex(message.Sender.Colour) : colourProvider?.Content1 ?? Colour4.White; }) { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, } - } + }, } }; } @@ -203,8 +151,6 @@ namespace osu.Game.Overlays.Chat FinishTransforms(true); } - private Container highlight; - /// /// Performs a highlight animation on this . /// @@ -233,7 +179,7 @@ namespace osu.Game.Overlays.Chat timestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint); timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"; - username.Text = $@"{message.Sender.Username}" + (senderHasColour || message.IsAction ? "" : ":"); + username.Text = $@"{message.Sender.Username}"; // remove non-existent channels from the link list message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument.ToString()) != true); @@ -242,22 +188,78 @@ namespace osu.Game.Overlays.Chat ContentFlow.AddLinks(message.DisplayContent, message.Links); } + private Drawable createUsername() + { + username = new OsuSpriteText + { + Shadow = false, + Colour = senderHasColour ? colours.ChatBlue : usernameColour, + Truncate = true, + EllipsisString = "…", + Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + MaxWidth = UsernameWidth, + }; + + if (!senderHasColour) + return username; + + // Background effect + return new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 4, + EdgeEffect = new EdgeEffectParameters + { + Roundness = 1, + Radius = 1, + Colour = Color4.Black.Opacity(0.3f), + Offset = new Vector2(0, 1), + Type = EdgeEffectType.Shadow, + }, + Child = new Container + { + AutoSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 4, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = usernameColour, + }, + new Container + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 }, + Child = username + } + } + } + }; + } + private class MessageSender : OsuClickableContainer, IHasContextMenu { private readonly APIUser sender; - private Action startChatAction; + private Action startChatAction = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; public MessageSender(APIUser sender) { this.sender = sender; } - [BackgroundDependencyLoader(true)] - private void load(UserProfileOverlay profile, ChannelManager chatManager) + [BackgroundDependencyLoader] + private void load(UserProfileOverlay? profile, ChannelManager? chatManager) { Action = () => profile?.ShowUser(sender); startChatAction = () => chatManager?.OpenPrivateChannel(sender); diff --git a/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs b/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs deleted file mode 100644 index 3b47adc4b7..0000000000 --- a/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable enable - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Chat; - -namespace osu.Game.Overlays.Chat -{ - public class ChatOverlayDrawableChannel : DrawableChannel - { - public ChatOverlayDrawableChannel(Channel channel) - : base(channel) - { - } - - [BackgroundDependencyLoader] - private void load() - { - ChatLineFlow.Padding = new MarginPadding(0); - } - - protected override Drawable CreateDaySeparator(DateTimeOffset time) => new ChatOverlayDaySeparator(time); - - private class ChatOverlayDaySeparator : Container - { - private readonly DateTimeOffset time; - - public ChatOverlayDaySeparator(DateTimeOffset time) - { - this.time = time; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Padding = new MarginPadding { Horizontal = 15, Vertical = 20 }; - Child = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 200), - new Dimension(GridSizeMode.Absolute, 15), - new Dimension(), - }, - Content = new[] - { - new[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 15), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new[] - { - new Circle - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Colour = colourProvider.Background5, - RelativeSizeAxes = Axes.X, - Height = 2, - }, - Drawable.Empty(), - new OsuSpriteText - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Text = time.ToLocalTime().ToString("dd MMMM yyyy").ToUpper(), - Font = OsuFont.Torus.With(size: 15, weight: FontWeight.SemiBold), - Colour = colourProvider.Content1, - }, - }, - }, - }, - Drawable.Empty(), - new Circle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = colourProvider.Background5, - RelativeSizeAxes = Axes.X, - Height = 2, - }, - }, - }, - }; - } - } - } -} diff --git a/osu.Game/Overlays/Chat/DaySeparator.cs b/osu.Game/Overlays/Chat/DaySeparator.cs new file mode 100644 index 0000000000..9ae35b0c38 --- /dev/null +++ b/osu.Game/Overlays/Chat/DaySeparator.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Overlays.Chat +{ + public class DaySeparator : Container + { + protected virtual float TextSize => 15; + + protected virtual float LineHeight => 2; + + protected virtual float DateAlign => 205; + + protected virtual float Spacing => 15; + + private readonly DateTimeOffset time; + + [Resolved(CanBeNull = true)] + private OverlayColourProvider? colourProvider { get; set; } + + public DaySeparator(DateTimeOffset time) + { + this.time = time; + Height = 40; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RowDimensions = new[] { new Dimension() }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, DateAlign), + new Dimension(GridSizeMode.Absolute, Spacing), + new Dimension(), + }, + Content = new[] + { + new[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { new Dimension() }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, Spacing), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = LineHeight, + Colour = colourProvider?.Background5 ?? Colour4.White, + }, + Drawable.Empty(), + new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Text = time.ToLocalTime().ToString("dd MMMM yyyy").ToUpper(), + Font = OsuFont.Torus.With(size: TextSize, weight: FontWeight.SemiBold), + Colour = colourProvider?.Content1 ?? Colour4.White, + }, + } + }, + }, + Drawable.Empty(), + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = LineHeight, + Colour = colourProvider?.Background5 ?? Colour4.White, + }, + } + } + }; + } + } +} diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index f2d4a3e301..c3a341bca4 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -7,14 +7,9 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; using osu.Game.Graphics.Cursor; -using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osuTK.Graphics; @@ -40,9 +35,6 @@ namespace osu.Game.Overlays.Chat } } - [Resolved] - private OsuColour colours { get; set; } - public DrawableChannel(Channel channel) { Channel = channel; @@ -67,7 +59,7 @@ namespace osu.Game.Overlays.Chat Padding = new MarginPadding { Bottom = 5 }, Child = ChatLineFlow = new FillFlowContainer { - Padding = new MarginPadding { Left = 20, Right = 20 }, + Padding = new MarginPadding { Horizontal = 10 }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, @@ -121,11 +113,7 @@ namespace osu.Game.Overlays.Chat protected virtual ChatLine CreateChatLine(Message m) => new ChatLine(m); - protected virtual Drawable CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time) - { - Colour = colours.ChatBlue.Lighten(0.7f), - Margin = new MarginPadding { Vertical = 10 }, - }; + protected virtual DaySeparator CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time); private void newMessagesArrived(IEnumerable newMessages) => Schedule(() => { @@ -203,69 +191,5 @@ namespace osu.Game.Overlays.Chat }); private IEnumerable chatLines => ChatLineFlow.Children.OfType(); - - public class DaySeparator : Container - { - public float TextSize - { - get => text.Font.Size; - set => text.Font = text.Font.With(size: value); - } - - private float lineHeight = 2; - - public float LineHeight - { - get => lineHeight; - set => lineHeight = leftBox.Height = rightBox.Height = value; - } - - private readonly SpriteText text; - private readonly Box leftBox; - private readonly Box rightBox; - - public DaySeparator(DateTimeOffset time) - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Child = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - }, - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), }, - Content = new[] - { - new Drawable[] - { - leftBox = new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = lineHeight, - }, - text = new OsuSpriteText - { - Margin = new MarginPadding { Horizontal = 10 }, - Text = time.ToLocalTime().ToString("dd MMM yyyy"), - }, - rightBox = new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = lineHeight, - }, - } - } - }; - } - } } } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 02769b5d68..f04bf76c18 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -38,9 +38,9 @@ namespace osu.Game.Overlays private LoadingLayer loading = null!; private ChannelListing channelListing = null!; private ChatTextBar textBar = null!; - private Container currentChannelContainer = null!; + private Container currentChannelContainer = null!; - private readonly Dictionary loadedChannels = new Dictionary(); + private readonly Dictionary loadedChannels = new Dictionary(); protected IEnumerable DrawableChannels => loadedChannels.Values; @@ -126,7 +126,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background4, }, - currentChannelContainer = new Container + currentChannelContainer = new Container { RelativeSizeAxes = Axes.Both, }, @@ -313,7 +313,7 @@ namespace osu.Game.Overlays loading.Show(); // Ensure the drawable channel is stored before async load to prevent double loading - ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel); + DrawableChannel drawableChannel = CreateDrawableChannel(newChannel); loadedChannels.Add(newChannel, drawableChannel); LoadComponentAsync(drawableChannel, loadedDrawable => @@ -338,7 +338,7 @@ namespace osu.Game.Overlays channelManager.MarkChannelAsRead(newChannel); } - protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel); + protected virtual DrawableChannel CreateDrawableChannel(Channel newChannel) => new DrawableChannel(newChannel); private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { @@ -361,7 +361,7 @@ namespace osu.Game.Overlays if (loadedChannels.ContainsKey(channel)) { - ChatOverlayDrawableChannel loaded = loadedChannels[channel]; + DrawableChannel loaded = loadedChannels[channel]; loadedChannels.Remove(channel); // DrawableChannel removed from cache must be manually disposed loaded.Dispose(); diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index 8452691bb5..f09a26a527 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -126,6 +126,7 @@ namespace osu.Game.Overlays.FirstRunSetup private class SampleScreenContainer : CompositeDrawable { private readonly OsuScreen screen; + // Minimal isolation from main game. [Cached] @@ -151,6 +152,9 @@ namespace osu.Game.Overlays.FirstRunSetup RelativeSizeAxes = Axes.Both; } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => + new DependencyContainer(new DependencyIsolationContainer(base.CreateChildDependencies(parent))); + [BackgroundDependencyLoader] private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets) { @@ -197,5 +201,41 @@ namespace osu.Game.Overlays.FirstRunSetup stack.PushSynchronously(screen); } } + + private class DependencyIsolationContainer : IReadOnlyDependencyContainer + { + private readonly IReadOnlyDependencyContainer parentDependencies; + + private readonly Type[] isolatedTypes = + { + typeof(OsuGame) + }; + + public DependencyIsolationContainer(IReadOnlyDependencyContainer parentDependencies) + { + this.parentDependencies = parentDependencies; + } + + public object Get(Type type) + { + if (isolatedTypes.Contains(type)) + return null; + + return parentDependencies.Get(type); + } + + public object Get(Type type, CacheInfo info) + { + if (isolatedTypes.Contains(type)) + return null; + + return parentDependencies.Get(type, info); + } + + public void Inject(T instance) where T : class + { + parentDependencies.Inject(instance); + } + } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index be4b0decd9..054de8dbd7 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private SettingsButton deleteSkinsButton; private SettingsButton restoreButton; private SettingsButton undeleteButton; + private SettingsButton deleteBeatmapVideosButton; [BackgroundDependencyLoader(permitNulls: true)] private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] LegacyImportManager legacyImportManager, IDialogOverlay dialogOverlay) @@ -58,6 +59,19 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance } }); + Add(deleteBeatmapVideosButton = new DangerousSettingsButton + { + Text = MaintenanceSettingsStrings.DeleteAllBeatmapVideos, + Action = () => + { + dialogOverlay?.Push(new MassVideoDeleteConfirmationDialog(() => + { + deleteBeatmapVideosButton.Enabled.Value = false; + Task.Run(beatmaps.DeleteAllVideos).ContinueWith(t => Schedule(() => deleteBeatmapVideosButton.Enabled.Value = true)); + })); + } + }); + if (legacyImportManager?.SupportsImportFromStable == true) { Add(importScoresButton = new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs new file mode 100644 index 0000000000..fc8c9d497b --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class MassVideoDeleteConfirmationDialog : MassDeleteConfirmationDialog + { + public MassVideoDeleteConfirmationDialog(Action deleteAction) + : base(deleteAction) + { + BodyText = "All beatmap videos? This cannot be undone!"; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index a87e65b735..b83600a16d 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -16,6 +16,7 @@ using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Screens.Select; using osu.Game.Skinning; using osu.Game.Skinning.Editor; using Realms; @@ -67,6 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections Action = () => skinEditor?.ToggleVisibility(), }, new ExportSkinButton(), + new DeleteSkinButton(), }; config.BindWith(OsuSetting.Skin, configBindable); @@ -202,5 +204,36 @@ namespace osu.Game.Overlays.Settings.Sections } } } + + public class DeleteSkinButton : DangerousSettingsButton + { + [Resolved] + private SkinManager skins { get; set; } + + [Resolved(CanBeNull = true)] + private IDialogOverlay dialogOverlay { get; set; } + + private Bindable currentSkin; + + [BackgroundDependencyLoader] + private void load() + { + Text = SkinSettingsStrings.DeleteSkinButton; + Action = delete; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + currentSkin = skins.CurrentSkin.GetBoundCopy(); + currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true); + } + + private void delete() + { + dialogOverlay?.Push(new SkinDeleteDialog(currentSkin.Value)); + } + } } } diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 929c362bd8..24d9f785f2 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; @@ -28,7 +27,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Volume { - public class VolumeMeter : Container, IKeyBindingHandler, IStateful + public class VolumeMeter : Container, IStateful { private CircularProgress volumeCircle; private CircularProgress volumeCircleGlow; @@ -80,7 +79,7 @@ namespace osu.Game.Overlays.Volume [BackgroundDependencyLoader] private void load(OsuColour colours, AudioManager audio) { - hoverSample = audio.Samples.Get($"UI/{HoverSampleSet.Button.GetDescription()}-hover"); + hoverSample = audio.Samples.Get($@"UI/{HoverSampleSet.Button.GetDescription()}-hover"); notchSample = audio.Samples.Get(@"UI/notch-tick"); sampleLastPlaybackTime = Time.Current; @@ -132,7 +131,7 @@ namespace osu.Game.Overlays.Volume { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Name = "Progress under covers for smoothing", + Name = @"Progress under covers for smoothing", RelativeSizeAxes = Axes.Both, Rotation = 180, Child = volumeCircle = new CircularProgress @@ -144,7 +143,7 @@ namespace osu.Game.Overlays.Volume }, new Circle { - Name = "Inner Cover", + Name = @"Inner Cover", Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, @@ -153,7 +152,7 @@ namespace osu.Game.Overlays.Volume }, new Container { - Name = "Progress overlay for glow", + Name = @"Progress overlay for glow", Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, @@ -365,27 +364,6 @@ namespace osu.Game.Overlays.Volume { } - public bool OnPressed(KeyBindingPressEvent e) - { - if (!IsHovered) - return false; - - switch (e.Action) - { - case GlobalAction.SelectPreviousGroup: - State = SelectionState.Selected; - adjust(1, false); - return true; - - case GlobalAction.SelectNextGroup: - State = SelectionState.Selected; - adjust(-1, false); - return true; - } - - return false; - } - public void OnReleased(KeyBindingReleaseEvent e) { } diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index a96949e96f..9d2ed3f837 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -17,6 +17,7 @@ using osu.Game.Input.Bindings; using osu.Game.Overlays.Volume; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Overlays { @@ -171,6 +172,30 @@ namespace osu.Game.Overlays return base.OnMouseMove(e); } + protected override bool OnKeyDown(KeyDownEvent e) + { + switch (e.Key) + { + case Key.Left: + Adjust(GlobalAction.PreviousVolumeMeter); + return true; + + case Key.Right: + Adjust(GlobalAction.NextVolumeMeter); + return true; + + case Key.Down: + Adjust(GlobalAction.DecreaseVolume); + return true; + + case Key.Up: + Adjust(GlobalAction.IncreaseVolume); + return true; + } + + return base.OnKeyDown(e); + } + protected override bool OnHover(HoverEvent e) { schedulePopOut(); diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 4a5897c621..df094ddb7c 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -460,6 +460,7 @@ namespace osu.Game.Rulesets.Scoring currentMaximumScoringValues.BaseScore = maximum.BaseScore; currentMaximumScoringValues.MaxCombo = maximum.MaxCombo; + Combo.Value = frame.Header.Combo; HighestCombo.Value = frame.Header.MaxCombo; scoreResultCounts.Clear(); diff --git a/osu.Game/Screens/Edit/Components/EditorSidebar.cs b/osu.Game/Screens/Edit/Components/EditorSidebar.cs index 4edcef41b1..4e9b1d5222 100644 --- a/osu.Game/Screens/Edit/Components/EditorSidebar.cs +++ b/osu.Game/Screens/Edit/Components/EditorSidebar.cs @@ -16,13 +16,15 @@ namespace osu.Game.Screens.Edit.Components /// internal class EditorSidebar : Container { + public const float WIDTH = 250; + private readonly Box background; protected override Container Content { get; } public EditorSidebar() { - Width = 250; + Width = WIDTH; RelativeSizeAxes = Axes.Y; InternalChildren = new Drawable[] diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 3bf8a09cb9..43b128b971 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -82,7 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true); // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(GameplayState.Ruleset.RulesetInfo, ScoreProcessor, users), l => + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(users), l => { if (!LoadedBeatmapSuccessfully) return; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs index 4545913db8..4e9ab07e4c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs @@ -2,19 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; -using JetBrains.Annotations; using osu.Framework.Timing; using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard { - public MultiSpectatorLeaderboard(RulesetInfo ruleset, [NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) - : base(ruleset, scoreProcessor, users) + public MultiSpectatorLeaderboard(MultiplayerRoomUser[] users) + : base(users) { } @@ -23,52 +20,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (!UserScores.TryGetValue(userId, out var data)) throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId)); - ((SpectatingTrackedUserData)data).Clock = clock; + data.ScoreProcessor.ReferenceClock = clock; } - public void RemoveClock(int userId) - { - if (!UserScores.TryGetValue(userId, out var data)) - throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId)); - - ((SpectatingTrackedUserData)data).Clock = null; - } - - protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, ruleset, scoreProcessor); - protected override void Update() { base.Update(); foreach (var (_, data) in UserScores) - data.UpdateScore(); - } - - private class SpectatingTrackedUserData : TrackedUserData - { - [CanBeNull] - public IClock Clock; - - public SpectatingTrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) - : base(user, ruleset, scoreProcessor) - { - } - - public override void UpdateScore() - { - if (Frames.Count == 0) - return; - - if (Clock == null) - return; - - int frameIndex = Frames.BinarySearch(new TimedFrame(Clock.CurrentTime)); - if (frameIndex < 0) - frameIndex = ~frameIndex; - frameIndex = Math.Clamp(frameIndex - 1, 0, Frames.Count - 1); - - SetFrame(Frames[frameIndex]); - } + data.ScoreProcessor.UpdateScore(); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 2d03276fe5..d9c19cdfdd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -128,12 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate syncManager.AddPlayerClock(instances[i].GameplayClock); } - // Todo: This is not quite correct - it should be per-user to adjust for other mod combinations. - var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); - var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); - scoreProcessor.ApplyBeatmap(playableBeatmap); - - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, users) + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users) { Expanded = { Value = true }, }, l => @@ -240,7 +235,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate instance.FadeColour(colours.Gray4, 400, Easing.OutQuint); syncManager.RemovePlayerClock(instance.GameplayClock); - leaderboard.RemoveClock(userId); } public override bool OnBackButton() diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index c6a072da74..586cdcda51 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using osu.Framework.Bindables; @@ -8,10 +10,9 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -#nullable enable - namespace osu.Game.Screens.Play { /// @@ -39,6 +40,8 @@ namespace osu.Game.Screens.Play /// public readonly Score Score; + public readonly ScoreProcessor ScoreProcessor; + /// /// Whether gameplay completed without the user failing. /// @@ -61,7 +64,7 @@ namespace osu.Game.Screens.Play private readonly Bindable lastJudgementResult = new Bindable(); - public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList? mods = null, Score? score = null) + public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList? mods = null, Score? score = null, ScoreProcessor? scoreProcessor = null) { Beatmap = beatmap; Ruleset = ruleset; @@ -72,7 +75,8 @@ namespace osu.Game.Screens.Play Ruleset = ruleset.RulesetInfo } }; - Mods = mods ?? ArraySegment.Empty; + Mods = mods ?? Array.Empty(); + ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor(); } /// diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 41b40e9a91..5ee6000cf0 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -17,9 +18,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Spectator; -using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD @@ -43,8 +42,6 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private UserLookupCache userLookupCache { get; set; } - private readonly RulesetInfo ruleset; - private readonly ScoreProcessor scoreProcessor; private readonly MultiplayerRoomUser[] playingUsers; private Bindable scoringMode; @@ -55,57 +52,56 @@ namespace osu.Game.Screens.Play.HUD /// /// Construct a new leaderboard. /// - /// The ruleset. - /// A score processor instance to handle score calculation for scores of users in the match. /// IDs of all users in this match. - public MultiplayerGameplayLeaderboard(RulesetInfo ruleset, ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) + public MultiplayerGameplayLeaderboard(MultiplayerRoomUser[] users) { - // todo: this will eventually need to be created per user to support different mod combinations. - this.ruleset = ruleset; - this.scoreProcessor = scoreProcessor; - playingUsers = users; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, IAPIProvider api) + private void load(OsuConfigManager config, IAPIProvider api, CancellationToken cancellationToken) { scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); foreach (var user in playingUsers) { - var trackedUser = CreateUserData(user, ruleset, scoreProcessor); - - trackedUser.ScoringMode.BindTo(scoringMode); - trackedUser.Score.BindValueChanged(_ => Scheduler.AddOnce(updateTotals)); + var scoreProcessor = new SpectatorScoreProcessor(user.UserID); + scoreProcessor.Mode.BindTo(scoringMode); + scoreProcessor.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(updateTotals)); + AddInternal(scoreProcessor); + var trackedUser = new TrackedUserData(user, scoreProcessor); UserScores[user.UserID] = trackedUser; if (trackedUser.Team is int team && !TeamScores.ContainsKey(team)) TeamScores.Add(team, new BindableLong()); } - userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray()).ContinueWith(task => Schedule(() => - { - var users = task.GetResultSafely(); + userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray(), cancellationToken) + .ContinueWith(task => + { + Schedule(() => + { + var users = task.GetResultSafely(); - for (int i = 0; i < users.Length; i++) - { - var user = users[i] ?? new APIUser - { - Id = playingUsers[i].UserID, - Username = "Unknown user", - }; + for (int i = 0; i < users.Length; i++) + { + var user = users[i] ?? new APIUser + { + Id = playingUsers[i].UserID, + Username = "Unknown user", + }; - var trackedUser = UserScores[user.Id]; + var trackedUser = UserScores[user.Id]; - var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id); - leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy); - leaderboardScore.TotalScore.BindTo(trackedUser.Score); - leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo); - leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); - } - })); + var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id); + leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy); + leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore); + leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo); + leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); + } + }); + }, cancellationToken); } protected override void LoadComplete() @@ -118,20 +114,15 @@ namespace osu.Game.Screens.Play.HUD spectatorClient.WatchUser(user.UserID); if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(user.UserID)) - usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID })); + playingUsersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID })); } // bind here is to support players leaving the match. // new players are not supported. playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); - playingUserIds.BindCollectionChanged(usersChanged); - - // this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer). - spectatorClient.OnNewFrames += handleIncomingFrames; + playingUserIds.BindCollectionChanged(playingUsersChanged); } - protected virtual TrackedUserData CreateUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) => new TrackedUserData(user, ruleset, scoreProcessor); - protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked) { var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked); @@ -157,7 +148,7 @@ namespace osu.Game.Screens.Play.HUD } } - private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) + private void playingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { @@ -174,15 +165,6 @@ namespace osu.Game.Screens.Play.HUD } } - private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() => - { - if (!UserScores.TryGetValue(userId, out var trackedData)) - return; - - trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); - trackedData.UpdateScore(); - }); - private void updateTotals() { if (!hasTeams) @@ -196,7 +178,7 @@ namespace osu.Game.Screens.Play.HUD continue; if (TeamScores.TryGetValue(u.Team.Value, out var team)) - team.Value += (int)Math.Round(u.Score.Value); + team.Value += (int)Math.Round(u.ScoreProcessor.TotalScore.Value); } } @@ -207,83 +189,26 @@ namespace osu.Game.Screens.Play.HUD if (spectatorClient != null) { foreach (var user in playingUsers) - { spectatorClient.StopWatchingUser(user.UserID); - } - - spectatorClient.OnNewFrames -= handleIncomingFrames; } } protected class TrackedUserData { public readonly MultiplayerRoomUser User; - public readonly ScoreProcessor ScoreProcessor; + public readonly SpectatorScoreProcessor ScoreProcessor; - public readonly BindableDouble Score = new BindableDouble(); - public readonly BindableDouble Accuracy = new BindableDouble(1); - public readonly BindableInt CurrentCombo = new BindableInt(); public readonly BindableBool UserQuit = new BindableBool(); - public readonly IBindable ScoringMode = new Bindable(); - - public readonly List Frames = new List(); - public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID; - private readonly ScoreInfo scoreInfo; - - public TrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) + public TrackedUserData(MultiplayerRoomUser user, SpectatorScoreProcessor scoreProcessor) { User = user; ScoreProcessor = scoreProcessor; - - scoreInfo = new ScoreInfo { Ruleset = ruleset }; - - ScoringMode.BindValueChanged(_ => UpdateScore()); } public void MarkUserQuit() => UserQuit.Value = true; - - public virtual void UpdateScore() - { - if (Frames.Count == 0) - return; - - SetFrame(Frames.Last()); - } - - protected void SetFrame(TimedFrame frame) - { - var header = frame.Header; - - scoreInfo.MaxCombo = header.MaxCombo; - scoreInfo.Statistics = header.Statistics; - - Score.Value = ScoreProcessor.ComputePartialScore(ScoringMode.Value, scoreInfo); - - Accuracy.Value = header.Accuracy; - CurrentCombo.Value = header.Combo; - } - } - - protected class TimedFrame : IComparable - { - public readonly double Time; - public readonly FrameHeader Header; - - public TimedFrame(double time) - { - Time = time; - } - - public TimedFrame(double time, FrameHeader header) - { - Time = time; - Header = header; - } - - public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 51c1e6b43b..dfc0fa1d1d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -213,8 +213,8 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(DrawableRuleset); ScoreProcessor = ruleset.CreateScoreProcessor(); - ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.Mods.Value = gameplayMods; + ScoreProcessor.ApplyBeatmap(playableBeatmap); dependencies.CacheAs(ScoreProcessor); @@ -237,7 +237,7 @@ namespace osu.Game.Screens.Play Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Mods = gameplayMods; - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor)); var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 5b3129dad6..b924fbd5df 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -159,6 +159,8 @@ namespace osu.Game.Screens.Ranking.Expanded Origin = Anchor.TopCentre, Text = beatmap.DifficultyName, Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, + Truncate = true, }, new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) { diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a59f14647d..e62b285966 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -10,6 +10,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Caching; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -97,6 +98,8 @@ namespace osu.Game.Screens.Select protected readonly CarouselScrollContainer Scroll; + private readonly NoResultsPlaceholder noResultsPlaceholder; + private IEnumerable beatmapSets => root.Children.OfType(); // todo: only used for testing, maybe remove. @@ -170,7 +173,8 @@ namespace osu.Game.Screens.Select Scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, - } + }, + noResultsPlaceholder = new NoResultsPlaceholder() } }; } @@ -633,7 +637,7 @@ namespace osu.Game.Screens.Select protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { // handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed). - if ((invalidation & Invalidation.Layout) > 0) + if (invalidation.HasFlagFast(Invalidation.DrawSize)) itemsCache.Invalidate(); return base.OnInvalidate(invalidation, source); @@ -648,8 +652,18 @@ namespace osu.Game.Screens.Select // First we iterate over all non-filtered carousel items and populate their // vertical position data. if (revalidateItems) + { updateYPositions(); + if (visibleItems.Count == 0) + { + noResultsPlaceholder.Filter = activeCriteria; + noResultsPlaceholder.Show(); + } + else + noResultsPlaceholder.Hide(); + } + // if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels. // this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad. if (pendingScrollOperation != PendingScrollOperation.None) diff --git a/osu.Game/Screens/Select/ImportFromStablePopup.cs b/osu.Game/Screens/Select/ImportFromStablePopup.cs deleted file mode 100644 index d8137432bd..0000000000 --- a/osu.Game/Screens/Select/ImportFromStablePopup.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Graphics.Sprites; -using osu.Game.Overlays.Dialog; - -namespace osu.Game.Screens.Select -{ - public class ImportFromStablePopup : PopupDialog - { - public ImportFromStablePopup(Action importFromStable) - { - HeaderText = @"You have no beatmaps!"; - BodyText = "Would you like to import your beatmaps, skins, collections and scores from an existing osu!stable installation?\nThis will create a second copy of all files on disk."; - - Icon = FontAwesome.Solid.Plane; - - Buttons = new PopupDialogButton[] - { - new PopupDialogOkButton - { - Text = @"Yes please!", - Action = importFromStable - }, - new PopupDialogCancelButton - { - Text = @"No, I'd like to start from scratch", - }, - }; - } - } -} diff --git a/osu.Game/Screens/Select/NoResultsPlaceholder.cs b/osu.Game/Screens/Select/NoResultsPlaceholder.cs new file mode 100644 index 0000000000..28a0541a22 --- /dev/null +++ b/osu.Game/Screens/Select/NoResultsPlaceholder.cs @@ -0,0 +1,145 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Localisation; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Select +{ + public class NoResultsPlaceholder : VisibilityContainer + { + private FilterCriteria? filter; + + private LinkFlowContainer textFlow = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Resolved] + private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; } + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + public FilterCriteria Filter + { + set + { + if (filter == value) + return; + + filter = value; + Scheduler.AddOnce(updateText); + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Masking = true; + CornerRadius = 10; + + Width = 300; + AutoSizeAxes = Axes.Y; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colours.Gray2, + RelativeSizeAxes = Axes.Both, + }, + new SpriteIcon + { + Icon = FontAwesome.Regular.SadTear, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding(10), + Size = new Vector2(50), + }, + textFlow = new LinkFlowContainer + { + Y = 60, + Padding = new MarginPadding(10), + TextAnchor = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + }; + } + + protected override void PopIn() + { + this.FadeIn(600, Easing.OutQuint); + + Scheduler.AddOnce(updateText); + } + + protected override void PopOut() + { + this.FadeOut(200, Easing.OutQuint); + } + + private void updateText() + { + // TODO: Refresh this text when new beatmaps are imported. Right now it won't get up-to-date suggestions. + + // Bounce should play every time the filter criteria is updated. + this.ScaleTo(0.9f) + .ScaleTo(1f, 1000, Easing.OutElastic); + + textFlow.Clear(); + + if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null) + { + textFlow.AddParagraph("No beatmaps found!"); + textFlow.AddParagraph(string.Empty); + + textFlow.AddParagraph("Consider using the \""); + textFlow.AddLink(FirstRunSetupOverlayStrings.FirstRunSetupTitle, () => firstRunSetupOverlay?.Show()); + textFlow.AddText("\" to download or import some beatmaps!"); + } + else + { + textFlow.AddParagraph("No beatmaps match your filter criteria!"); + textFlow.AddParagraph(string.Empty); + + if (string.IsNullOrEmpty(filter?.SearchText)) + { + // TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch). + // TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting. + if (filter?.Ruleset.OnlineID > 0 && !filter.AllowConvertedBeatmaps) + { + textFlow.AddParagraph("Beatmaps may be available by "); + textFlow.AddLink("enabling automatic conversion", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); + textFlow.AddText("!"); + } + } + else + { + textFlow.AddParagraph("You can try "); + textFlow.AddLink("searching online", LinkAction.SearchBeatmapSet, filter.SearchText); + textFlow.AddText(" for this query."); + } + } + + // TODO: add clickable link to reset criteria. + } + } +} diff --git a/osu.Game/Screens/Select/SkinDeleteDialog.cs b/osu.Game/Screens/Select/SkinDeleteDialog.cs new file mode 100644 index 0000000000..4262118658 --- /dev/null +++ b/osu.Game/Screens/Select/SkinDeleteDialog.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Select +{ + public class SkinDeleteDialog : PopupDialog + { + [Resolved] + private SkinManager manager { get; set; } + + public SkinDeleteDialog(Skin skin) + { + BodyText = skin.SkinInfo.Value.Name; + Icon = FontAwesome.Regular.TrashAlt; + HeaderText = @"Confirm deletion of"; + Buttons = new PopupDialogButton[] + { + new PopupDialogDangerousButton + { + Text = @"Yes. Totally. Delete it.", + Action = () => + { + if (manager == null) + return; + + manager.Delete(skin.SkinInfo.Value); + manager.CurrentSkinInfo.SetDefault(); + }, + }, + new PopupDialogCancelButton + { + Text = @"Firetruck, I didn't mean to!", + }, + }; + } + } +} diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 8870239485..3bfdc845ab 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -28,7 +28,6 @@ using osuTK.Input; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -37,7 +36,6 @@ using osu.Game.Graphics.UserInterface; using System.Diagnostics; using JetBrains.Annotations; using osu.Game.Screens.Play; -using osu.Game.Database; using osu.Game.Skinning; namespace osu.Game.Screens.Select @@ -59,8 +57,6 @@ namespace osu.Game.Screens.Select protected virtual bool ShowFooter => true; - protected virtual bool DisplayStableImportPrompt => legacyImportManager?.SupportsImportFromStable == true; - public override bool? AllowTrackAdjustments => true; /// @@ -94,14 +90,13 @@ namespace osu.Game.Screens.Select protected Container LeftArea { get; private set; } private BeatmapInfoWedge beatmapInfoWedge; - private IDialogOverlay dialogOverlay; + + [Resolved(canBeNull: true)] + private IDialogOverlay dialogOverlay { get; set; } [Resolved] private BeatmapManager beatmaps { get; set; } - [Resolved(CanBeNull = true)] - private LegacyImportManager legacyImportManager { get; set; } - protected ModSelectOverlay ModSelect { get; private set; } protected Sample SampleConfirm { get; private set; } @@ -127,7 +122,7 @@ namespace osu.Game.Screens.Select internal IOverlayManager OverlayManager { get; private set; } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, IDialogOverlay dialog, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender) + private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender) { // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). transferRulesetValue(); @@ -289,26 +284,9 @@ namespace osu.Game.Screens.Select BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); } - dialogOverlay = dialog; - sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); sampleChangeBeatmap = audio.Samples.Get(@"SongSelect/select-expand"); SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); - - if (dialogOverlay != null) - { - Schedule(() => - { - // if we have no beatmaps, let's prompt the user to import from over a stable install if he has one. - if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null && DisplayStableImportPrompt) - { - dialogOverlay.Push(new ImportFromStablePopup(() => - { - Task.Run(() => legacyImportManager.ImportFromStableAsync(StableContent.All)); - })); - } - }); - } } protected override void LoadComplete() diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index e36d5ca3c6..095763de18 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -29,6 +29,8 @@ namespace osu.Game.Skinning.Editor { public const double TRANSITION_DURATION = 500; + public const float MENU_HEIGHT = 40; + public readonly BindableList SelectedComponents = new BindableList(); protected override bool StartHidden => true; @@ -78,8 +80,6 @@ namespace osu.Game.Skinning.Editor { RelativeSizeAxes = Axes.Both; - const float menu_height = 40; - InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -102,7 +102,7 @@ namespace osu.Game.Skinning.Editor Name = "Menu container", RelativeSizeAxes = Axes.X, Depth = float.MinValue, - Height = menu_height, + Height = MENU_HEIGHT, Children = new Drawable[] { new EditorMenuBar @@ -322,7 +322,10 @@ namespace osu.Game.Skinning.Editor protected override void PopIn() { - this.FadeIn(TRANSITION_DURATION, Easing.OutQuint); + this + // align animation to happen after the majority of the ScalingContainer animation completes. + .Delay(ScalingContainer.TRANSITION_DURATION * 0.3f) + .FadeIn(TRANSITION_DURATION, Easing.OutQuint); } protected override void PopOut() diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index 497283a820..a3110ced24 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -12,6 +12,8 @@ using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Screens; +using osu.Game.Screens.Edit.Components; +using osuTK; namespace osu.Game.Skinning.Editor { @@ -33,6 +35,8 @@ namespace osu.Game.Skinning.Editor private OsuScreen lastTargetScreen; + private Vector2 lastDrawSize; + public SkinEditorOverlay(ScalingContainer scalingContainer) { this.scalingContainer = scalingContainer; @@ -81,15 +85,42 @@ namespace osu.Game.Skinning.Editor protected override void PopOut() => skinEditor?.Hide(); + protected override void Update() + { + base.Update(); + + if (game.DrawSize != lastDrawSize) + { + lastDrawSize = game.DrawSize; + updateScreenSizing(); + } + } + + private void updateScreenSizing() + { + if (skinEditor?.State.Value != Visibility.Visible) return; + + const float padding = 10; + + float relativeSidebarWidth = (EditorSidebar.WIDTH + padding) / DrawWidth; + float relativeToolbarHeight = (SkinEditorSceneLibrary.HEIGHT + SkinEditor.MENU_HEIGHT + padding) / DrawHeight; + + var rect = new RectangleF( + relativeSidebarWidth, + relativeToolbarHeight, + 1 - relativeSidebarWidth * 2, + 1f - relativeToolbarHeight - padding / DrawHeight); + + scalingContainer.SetCustomRect(rect, true); + } + private void updateComponentVisibility() { Debug.Assert(skinEditor != null); - const float toolbar_padding_requirement = 0.18f; - if (skinEditor.State.Value == Visibility.Visible) { - scalingContainer.SetCustomRect(new RectangleF(toolbar_padding_requirement, 0.2f, 0.8f - toolbar_padding_requirement, 0.7f), true); + Scheduler.AddOnce(updateScreenSizing); game?.Toolbar.Hide(); game?.CloseAllOverlays(); @@ -127,6 +158,9 @@ namespace osu.Game.Skinning.Editor private void setTarget(OsuScreen target) { + if (target == null) + return; + Debug.Assert(skinEditor != null); if (!target.IsLoaded) diff --git a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs index 2124ba9b6d..dc5a8aefc0 100644 --- a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs @@ -27,6 +27,8 @@ namespace osu.Game.Skinning.Editor { public class SkinEditorSceneLibrary : CompositeDrawable { + public const float HEIGHT = BUTTON_HEIGHT + padding * 2; + public const float BUTTON_HEIGHT = 40; private const float padding = 10; @@ -42,7 +44,7 @@ namespace osu.Game.Skinning.Editor public SkinEditorSceneLibrary() { - Height = BUTTON_HEIGHT + padding * 2; + Height = HEIGHT; } [BackgroundDependencyLoader] diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index b65ba8b04c..9524d3f615 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -303,8 +303,13 @@ namespace osu.Game.Skinning if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out string val)) { // special case for handling skins which use 1 or 0 to signify a boolean state. + // ..or in some cases 2 (https://github.com/ppy/osu/issues/18579). if (typeof(TValue) == typeof(bool)) - val = val == "1" ? "true" : "false"; + { + val = bool.TryParse(val, out bool boolVal) + ? Convert.ChangeType(boolVal, typeof(bool)).ToString() + : Convert.ChangeType(Convert.ToInt32(val), typeof(bool)).ToString(); + } var bindable = new Bindable(); if (val != null) diff --git a/osu.Game/Tests/Gameplay/TestGameplayState.cs b/osu.Game/Tests/Gameplay/TestGameplayState.cs index 0d00f52d15..f14f8c44ec 100644 --- a/osu.Game/Tests/Gameplay/TestGameplayState.cs +++ b/osu.Game/Tests/Gameplay/TestGameplayState.cs @@ -26,7 +26,10 @@ namespace osu.Game.Tests.Gameplay var workingBeatmap = new TestWorkingBeatmap(beatmap); var playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); - return new GameplayState(playableBeatmap, ruleset, mods, score); + var scoreProcessor = ruleset.CreateScoreProcessor(); + scoreProcessor.ApplyBeatmap(playableBeatmap); + + return new GameplayState(playableBeatmap, ruleset, mods, score, scoreProcessor); } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ca92d06aed..63b8cf4cb5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -31,7 +31,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 68cf8138e2..286a1eb29f 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -790,6 +790,15 @@ See the LICENCE file in the repository root for full licence text. <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True + True + True + True + True + True True True True