diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index cd09a1d20fdf..e10b3f76e68c 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -4,16 +4,34 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Screens; using osu.Game.Online.API; +using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.DailyChallenge { public partial class TestSceneDailyChallenge : OnlinePlayTestScene { + [Cached(typeof(MetadataClient))] + private TestMetadataClient metadataClient = new TestMetadataClient(); + + [Cached(typeof(INotificationOverlay))] + private NotificationOverlay notificationOverlay = new NotificationOverlay(); + + [BackgroundDependencyLoader] + private void load() + { + base.Content.Add(notificationOverlay); + base.Content.Add(metadataClient); + } + [Test] public void TestDailyChallenge() { @@ -36,5 +54,33 @@ public void TestDailyChallenge() AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); } + + [Test] + public void TestNotifications() + { + var room = new Room + { + RoomID = { Value = 1234 }, + Name = { Value = "Daily Challenge: June 4, 2024" }, + Playlist = + { + new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First()) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [new APIMod(new OsuModDoubleTime())] + } + }, + EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, + Category = { Value = RoomCategory.DailyChallenge } + }; + + AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); + AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); + + Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; + AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); + AddUntilStep("wait for screen", () => screen.IsCurrentScreen()); + AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null); + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index 5914898cb1c1..af98aa21db90 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -10,6 +11,7 @@ using osu.Game.Online.API; using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Screens.Menu; using osuTK.Input; using Color4 = osuTK.Graphics.Color4; @@ -39,8 +41,6 @@ public void TestStandardButton() [Test] public void TestDailyChallengeButton() { - AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); - AddStep("set up API", () => dummyAPI.HandleRequest = req => { switch (req) @@ -67,17 +67,45 @@ public void TestDailyChallengeButton() } }); - AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + NotificationOverlay notificationOverlay = null!; + DependencyProvidingContainer buttonContainer = null!; + + AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - ButtonSystemState = ButtonSystemState.TopLevel, + RoomID = 1234, + })); + AddStep("add content", () => + { + notificationOverlay = new NotificationOverlay(); + Children = new Drawable[] + { + notificationOverlay, + buttonContainer = new DependencyProvidingContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)], + Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ButtonSystemState = ButtonSystemState.TopLevel, + }, + }, + }; }); + AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + + AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); + AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + AddStep("hide button's parent", () => buttonContainer.Hide()); AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = 1234, })); + AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); } } } diff --git a/osu.Game/Localisation/DailyChallengeStrings.cs b/osu.Game/Localisation/DailyChallengeStrings.cs new file mode 100644 index 000000000000..32ff98db063d --- /dev/null +++ b/osu.Game/Localisation/DailyChallengeStrings.cs @@ -0,0 +1,29 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class DailyChallengeStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DailyChallenge"; + + /// + /// "Today's daily challenge has concluded – thanks for playing! + /// + /// Tomorrow's challenge is now being prepared and will appear soon." + /// + public static LocalisableString ChallengeEndedNotification => new TranslatableString(getKey(@"todays_daily_challenge_has_concluded"), + @"Today's daily challenge has concluded – thanks for playing! + +Tomorrow's challenge is now being prepared and will appear soon."); + + /// + /// "Today's daily challenge is now live! Click here to play." + /// + public static LocalisableString ChallengeLiveNotification => new TranslatableString(getKey(@"todays_daily_challenge_is_now"), @"Today's daily challenge is now live! Click here to play."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index c365994736bb..a5616b95a0f3 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -23,6 +23,7 @@ using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.DailyChallenge; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -44,6 +45,9 @@ public partial class DailyChallengeButton : MainMenuButton, IHasCustomTooltip? clickAction = null, params Key[] triggerKeys) : base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys) { @@ -100,7 +104,8 @@ protected override void LoadComplete() { base.LoadComplete(); - info.BindValueChanged(updateDisplay, true); + info.BindValueChanged(_ => dailyChallengeChanged(postNotification: true)); + dailyChallengeChanged(postNotification: false); } protected override void Update() @@ -126,27 +131,30 @@ protected override void Update() } } - private void updateDisplay(ValueChangedEvent info) + private void dailyChallengeChanged(bool postNotification) { UpdateState(); scheduledCountdownUpdate?.Cancel(); scheduledCountdownUpdate = null; - if (info.NewValue == null) + if (info.Value == null) { Room = null; cover.OnlineInfo = TooltipContent = null; } else { - var roomRequest = new GetRoomRequest(info.NewValue.Value.RoomID); + var roomRequest = new GetRoomRequest(info.Value.Value.RoomID); roomRequest.Success += room => { Room = room; cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; + if (postNotification) + notificationOverlay?.Post(new NewDailyChallengeNotification(room)); + updateCountdown(); Scheduler.AddDelayed(updateCountdown, 1000, true); }; diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 322d855cd360..da2d9036c59c 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -30,6 +30,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; @@ -54,6 +55,7 @@ public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner private readonly Bindable> userMods = new Bindable>(Array.Empty()); private readonly IBindable apiState = new Bindable(); + private readonly IBindable dailyChallengeInfo = new Bindable(); private OnlinePlayScreenWaveContainer waves = null!; private DailyChallengeLeaderboard leaderboard = null!; @@ -98,6 +100,9 @@ public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner [Resolved] private PreviewTrackManager previewTrackManager { get; set; } = null!; + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool? ApplyModTrackAdjustments => true; @@ -336,6 +341,7 @@ [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }] } metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet; + dailyChallengeInfo.BindTo(metadataClient.DailyChallengeInfo); ((IBindable)breakdown.UserBestScore).BindTo(leaderboard.UserBestScore); } @@ -388,6 +394,8 @@ protected override void LoadComplete() apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); + + dailyChallengeInfo.BindValueChanged(dailyChallengeChanged); } private void trySetDailyChallengeBeatmap() @@ -405,9 +413,17 @@ private void onlineStateChanged(ValueChangedEvent state) => Schedule(( Schedule(forcefullyExit); }); + private void dailyChallengeChanged(ValueChangedEvent change) + { + if (change.OldValue?.RoomID == room.RoomID.Value && change.NewValue == null) + { + notificationOverlay?.Post(new SimpleNotification { Text = DailyChallengeStrings.ChallengeEndedNotification }); + } + } + private void forcefullyExit() { - Logger.Log($"{this} forcefully exiting due to loss of API connection"); + Logger.Log(@$"{this} forcefully exiting due to loss of API connection"); // This is temporary since we don't currently have a way to force screens to be exited // See also: `OnlinePlayScreen.forcefullyExit()` diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs new file mode 100644 index 000000000000..ea19828a2106 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Screens; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Menu; +using osu.Game.Localisation; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class NewDailyChallengeNotification : SimpleNotification + { + private readonly Room room; + + private BeatmapCardNano card = null!; + + public NewDailyChallengeNotification(Room room) + { + this.room = room; + } + + [BackgroundDependencyLoader] + private void load(OsuGame? game) + { + Text = DailyChallengeStrings.ChallengeLiveNotification; + Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); + Activated = () => + { + game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); + return true; + }; + } + + protected override void Update() + { + base.Update(); + card.Width = Content.DrawWidth; + } + } +} diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index fa64a83352f6..c9f2b183e329 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -21,7 +21,7 @@ public partial class TestMetadataClient : MetadataClient public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); - public override IBindable DailyChallengeInfo => dailyChallengeInfo; + public override Bindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); [Resolved] @@ -88,7 +88,14 @@ public override Task DailyChallengeUpdated(DailyChallengeInfo? info) } public override Task BeginWatchingMultiplayerRoom(long id) - => Task.FromResult(new MultiplayerPlaylistItemStats[MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS]); + { + var stats = new MultiplayerPlaylistItemStats[MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS]; + + for (int i = 0; i < stats.Length; i++) + stats[i] = new MultiplayerPlaylistItemStats { PlaylistItemID = i }; + + return Task.FromResult(stats); + } public override Task EndWatchingMultiplayerRoom(long id) => Task.CompletedTask; }