diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index ace7db82f8..c4ba6e5143 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2024.517.0", + "version": "2024.802.0", "commands": [ "localisation" ] diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 5e8061bb6a..6452444fed 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Events; -using osu.Framework.Layout; using osu.Framework.Timing; using osuTK; using osuTK.Graphics; @@ -63,8 +62,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor // -1 signals that the part is unusable, and should not be drawn parts[i].InvalidationID = -1; } - - AddLayout(partSizeCache); } [BackgroundDependencyLoader] @@ -95,12 +92,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } } - private readonly LayoutValue partSizeCache = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); - - private Vector2 partSize => partSizeCache.IsValid - ? partSizeCache.Value - : (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy); - /// /// The amount of time to fade the cursor trail pieces. /// @@ -156,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor protected void AddTrail(Vector2 position) { + position = ToLocalSpace(position); + if (InterpolateMovements) { if (!lastPosition.HasValue) @@ -174,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor float distance = diff.Length; Vector2 direction = diff / distance; - float interval = partSize.X / 2.5f * IntervalMultiplier; + float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier; float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0); for (float d = interval; d < stopAt; d += interval) @@ -191,9 +184,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } } - private void addPart(Vector2 screenSpacePosition) + private void addPart(Vector2 localSpacePosition) { - parts[currentIndex].Position = ToLocalSpace(screenSpacePosition); + parts[currentIndex].Position = localSpacePosition; parts[currentIndex].Time = time + 1; ++parts[currentIndex].InvalidationID; diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index a47da4d505..3f1bc58147 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -259,6 +259,44 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestNoChangesAfterDelete() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var rulesets = new RealmRulesetStore(realm, storage); + + using var __ = getBeatmapArchive(out string pathOriginal); + using var _ = getBeatmapArchive(out string pathOriginalSecond); + + var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal)); + + importBeforeUpdate!.PerformWrite(s => s.DeletePending = true); + + var dateBefore = importBeforeUpdate.Value.DateAdded; + + Assert.That(importBeforeUpdate, Is.Not.Null); + Debug.Assert(importBeforeUpdate != null); + + var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value); + + realm.Run(r => r.Refresh()); + + Assert.That(importAfterUpdate, Is.Not.Null); + Debug.Assert(importAfterUpdate != null); + + checkCount(realm, 1); + checkCount(realm, count_beatmaps); + checkCount(realm, count_beatmaps); + + Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1)); + Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore)); + Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore)); + Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID)); + }); + } + [Test] public void TestNoChanges() { @@ -272,21 +310,25 @@ namespace osu.Game.Tests.Database var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal)); + var dateBefore = importBeforeUpdate!.Value.DateAdded; + Assert.That(importBeforeUpdate, Is.Not.Null); Debug.Assert(importBeforeUpdate != null); var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value); + realm.Run(r => r.Refresh()); + Assert.That(importAfterUpdate, Is.Not.Null); Debug.Assert(importAfterUpdate != null); - realm.Run(r => r.Refresh()); - checkCount(realm, 1); checkCount(realm, count_beatmaps); checkCount(realm, count_beatmaps); Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1)); + Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore)); + Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore)); Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID)); }); } diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index cd09a1d20f..e10b3f76e6 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 @@ namespace osu.Game.Tests.Visual.DailyChallenge 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/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs new file mode 100644 index 0000000000..f2135ec992 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -0,0 +1,64 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Profile; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Rulesets.Osu; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene + { + [Cached] + public readonly Bindable User = new Bindable(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + protected override void LoadComplete() + { + base.LoadComplete(); + + DailyChallengeStatsDisplay display = null!; + + AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v)); + AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v)); + AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v)); + AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); + AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); + AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); + AddSliderStep("playcount", 0, 999, 0, v => update(s => s.PlayCount = v)); + AddStep("create", () => + { + Clear(); + Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2, + }); + Add(display = new DailyChallengeStatsDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1f), + User = { BindTarget = User }, + }); + }); + AddStep("hover", () => InputManager.MoveMouseTo(display)); + } + + private void update(Action change) + { + change.Invoke(User.Value!.User.DailyChallengeStatistics); + User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 8dbd493920..3bb38f167f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API; @@ -24,7 +25,17 @@ namespace osu.Game.Tests.Visual.Online [SetUpSteps] public void SetUp() { - AddStep("create profile overlay", () => Child = profile = new UserProfileOverlay()); + AddStep("create profile overlay", () => + { + profile = new UserProfileOverlay(); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(UserProfileOverlay), profile) }, + Child = profile, + }; + }); } [Test] @@ -131,6 +142,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", ProfileHue = hue, + PlayMode = "osu", }); return true; } @@ -174,6 +186,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", ProfileHue = hue, + PlayMode = "osu", })); int hue2 = 0; @@ -189,6 +202,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", ProfileHue = hue2, + PlayMode = "osu", })); } @@ -282,6 +296,15 @@ namespace osu.Game.Tests.Visual.Online ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png", }, }, + DailyChallengeStatistics = new APIUserDailyChallengeStatistics + { + DailyStreakCurrent = 231, + WeeklyStreakCurrent = 18, + DailyStreakBest = 370, + WeeklyStreakBest = 51, + Top10PercentPlacements = 345, + Top50PercentPlacements = 427, + }, Title = "osu!volunteer", Colour = "ff0000", Achievements = Array.Empty(), diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index 5914898cb1..af98aa21db 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.Localisation; 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 @@ namespace osu.Game.Tests.Visual.UserInterface [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 @@ namespace osu.Game.Tests.Visual.UserInterface } }); - AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - ButtonSystemState = ButtonSystemState.TopLevel, - }); + NotificationOverlay notificationOverlay = null!; + DependencyProvidingContainer buttonContainer = null!; AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { 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/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 71aa5b0333..8acaebd1a8 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -43,6 +43,8 @@ namespace osu.Game.Beatmaps public override async Task?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) { + var originalDateAdded = original.DateAdded; + Guid originalId = original.ID; var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false); @@ -57,8 +59,11 @@ namespace osu.Game.Beatmaps // If there were no changes, ensure we don't accidentally nuke ourselves. if (first.ID == originalId) { - first.PerformRead(s => + first.PerformWrite(s => { + // Transfer local values which should be persisted across a beatmap update. + s.DateAdded = originalDateAdded; + // Re-run processing even in this case. We might have outdated metadata. ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst); }); @@ -79,7 +84,7 @@ namespace osu.Game.Beatmaps original.DeletePending = true; // Transfer local values which should be persisted across a beatmap update. - updated.DateAdded = original.DateAdded; + updated.DateAdded = originalDateAdded; transferCollectionReferences(realm, original, updated); @@ -278,6 +283,9 @@ namespace osu.Game.Beatmaps protected override void UndeleteForReuse(BeatmapSetInfo existing) { + if (!existing.DeletePending) + return; + base.UndeleteForReuse(existing); existing.DateAdded = DateTimeOffset.UtcNow; } diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 596bb5d673..e71368c079 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -43,6 +43,9 @@ namespace osu.Game.Collections // // if we want to support user sorting (but changes will need to be made to realm to persist). ShowDragHandle.Value = false; + + Masking = true; + CornerRadius = item_height / 2; } protected override Drawable CreateContent() => new ItemContent(Model); @@ -50,7 +53,7 @@ namespace osu.Game.Collections /// /// The main content of the . /// - private partial class ItemContent : CircularContainer + private partial class ItemContent : CompositeDrawable { private readonly Live collection; @@ -65,13 +68,12 @@ namespace osu.Game.Collections RelativeSizeAxes = Axes.X; Height = item_height; - Masking = true; } [BackgroundDependencyLoader] private void load() { - Children = new[] + InternalChildren = new[] { collection.IsManaged ? new DeleteButton(collection) @@ -132,7 +134,7 @@ namespace osu.Game.Collections } } - public partial class DeleteButton : CompositeDrawable + public partial class DeleteButton : OsuClickableContainer { public Func IsTextBoxHovered = null!; @@ -155,7 +157,7 @@ namespace osu.Game.Collections [BackgroundDependencyLoader] private void load(OsuColour colours) { - InternalChild = fadeContainer = new Container + Child = fadeContainer = new Container { RelativeSizeAxes = Axes.Both, Alpha = 0.1f, @@ -176,6 +178,14 @@ namespace osu.Game.Collections } } }; + + Action = () => + { + if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0) + deleteCollection(); + else + dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection)); + }; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos); @@ -195,12 +205,7 @@ namespace osu.Game.Collections { background.FlashColour(Color4.White, 150); - if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0) - deleteCollection(); - else - dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection)); - - return true; + return base.OnClick(e); } private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); diff --git a/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs index 39a3edb82c..445588d525 100644 --- a/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs +++ b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs @@ -64,6 +64,7 @@ namespace osu.Game.Graphics.Containers { InternalChildren = new Drawable[] { + new HoverClickSounds(), new GridContainer { RelativeSizeAxes = Axes.X, @@ -92,7 +93,6 @@ namespace osu.Game.Graphics.Containers ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }, - new HoverClickSounds() }; } diff --git a/osu.Game/Localisation/DailyChallengeStrings.cs b/osu.Game/Localisation/DailyChallengeStrings.cs new file mode 100644 index 0000000000..32ff98db06 --- /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/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index a2836476c5..c69e45b3fd 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -272,6 +272,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("groups")] public APIUserGroup[] Groups; + [JsonProperty("daily_challenge_user_stats")] + public APIUserDailyChallengeStatistics DailyChallengeStatistics = new APIUserDailyChallengeStatistics(); + public override string ToString() => Username; /// diff --git a/osu.Game/Online/API/Requests/Responses/APIUserDailyChallengeStatistics.cs b/osu.Game/Online/API/Requests/Responses/APIUserDailyChallengeStatistics.cs new file mode 100644 index 0000000000..e77f2b8f68 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIUserDailyChallengeStatistics.cs @@ -0,0 +1,41 @@ +// 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 Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIUserDailyChallengeStatistics + { + [JsonProperty("user_id")] + public int UserID; + + [JsonProperty("daily_streak_best")] + public int DailyStreakBest; + + [JsonProperty("daily_streak_current")] + public int DailyStreakCurrent; + + [JsonProperty("weekly_streak_best")] + public int WeeklyStreakBest; + + [JsonProperty("weekly_streak_current")] + public int WeeklyStreakCurrent; + + [JsonProperty("top_10p_placements")] + public int Top10PercentPlacements; + + [JsonProperty("top_50p_placements")] + public int Top50PercentPlacements; + + [JsonProperty("playcount")] + public int PlayCount; + + [JsonProperty("last_update")] + public DateTimeOffset? LastUpdate; + + [JsonProperty("last_weekly_streak")] + public DateTimeOffset? LastWeeklyStreak; + } +} diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index bf10e13515..540ed8ee94 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; 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; @@ -11,14 +12,16 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osuTK; using osu.Game.Localisation; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.Mods { public partial class ModCustomisationHeader : OsuHoverContainer { private Box background = null!; + private Box backgroundFlash = null!; private SpriteIcon icon = null!; [Resolved] @@ -46,6 +49,13 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.Both, }, + backgroundFlash = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White.Opacity(0.4f), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, new OsuSpriteText { Anchor = Anchor.CentreLeft, @@ -84,6 +94,12 @@ namespace osu.Game.Overlays.Mods TooltipText = e.NewValue ? string.Empty : ModSelectOverlayStrings.CustomisationPanelDisabledReason; + + if (e.NewValue) + { + backgroundFlash.FadeInFromZero(150, Easing.OutQuad).Then() + .FadeOutFromOne(350, Easing.OutQuad); + } }, true); Expanded.BindValueChanged(v => diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 5ffed24e7a..8a499a391c 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -138,6 +138,7 @@ namespace osu.Game.Overlays.Mods }, new GridContainer { + Padding = new MarginPadding { Top = 1, Bottom = 3 }, RelativeSizeAxes = Axes.Both, RowDimensions = new[] { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 7469590895..858992b8ba 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -668,6 +668,8 @@ namespace osu.Game.Overlays.Mods [Cached] internal partial class ColumnScrollContainer : OsuScrollContainer { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public ColumnScrollContainer() : base(Direction.Horizontal) { diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs new file mode 100644 index 0000000000..f55eb595d7 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -0,0 +1,121 @@ +// 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.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Scoring; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class DailyChallengeStatsDisplay : CompositeDrawable, IHasCustomTooltip + { + public readonly Bindable User = new Bindable(); + + public DailyChallengeTooltipData? TooltipContent { get; private set; } + + private OsuSpriteText dailyPlayCount = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + CornerRadius = 5; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + { + AutoSizeAxes = Axes.Both, + // can't use this because osu-web does weird stuff with \\n. + // Text = UsersStrings.ShowDailyChallengeTitle., + Text = "Daily\nChallenge", + Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, + }, + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + CornerRadius = 5f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + dailyPlayCount = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + Colour = colourProvider.Content2, + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + }, + } + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(_ => updateDisplay(), true); + } + + private void updateDisplay() + { + if (User.Value == null || User.Value.Ruleset.OnlineID != 0) + { + Hide(); + return; + } + + APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics; + + dailyPlayCount.Text = UsersStrings.ShowDailyChallengeUnitDay(stats.PlayCount.ToLocalisableString("N0")); + dailyPlayCount.Colour = colours.ForRankingTier(tierForPlayCount(stats.PlayCount)); + + TooltipContent = new DailyChallengeTooltipData(colourProvider, stats); + + Show(); + + static RankingTier tierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily(playCount / 3); + } + + public ITooltip GetCustomTooltip() => new DailyChallengeStatsTooltip(); + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs new file mode 100644 index 0000000000..1b54633b8a --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -0,0 +1,241 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Scoring; +using osuTK; +using Box = osu.Framework.Graphics.Shapes.Box; +using Color4 = osuTK.Graphics.Color4; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class DailyChallengeStatsTooltip : VisibilityContainer, ITooltip + { + private StreakPiece currentDaily = null!; + private StreakPiece currentWeekly = null!; + private StatisticsPiece bestDaily = null!; + private StatisticsPiece bestWeekly = null!; + private StatisticsPiece topTen = null!; + private StatisticsPiece topFifty = null!; + + private Box topBackground = null!; + private Box background = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + CornerRadius = 20f; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 30f, + }; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + topBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding(15f), + Spacing = new Vector2(30f), + Children = new[] + { + currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent), + currentWeekly = new StreakPiece(UsersStrings.ShowDailyChallengeWeeklyStreakCurrent), + } + }, + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(15f), + Spacing = new Vector2(10f), + Children = new[] + { + bestDaily = new StatisticsPiece(UsersStrings.ShowDailyChallengeDailyStreakBest), + bestWeekly = new StatisticsPiece(UsersStrings.ShowDailyChallengeWeeklyStreakBest), + topTen = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop10pPlacements), + topFifty = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop50pPlacements), + } + }, + } + } + }; + } + + public void SetContent(DailyChallengeTooltipData content) + { + var statistics = content.Statistics; + var colourProvider = content.ColourProvider; + + background.Colour = colourProvider.Background4; + topBackground.Colour = colourProvider.Background5; + + currentDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); + currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); + + currentWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0")); + currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent)); + + bestDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0")); + bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakBest)); + + bestWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0")); + bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest)); + + topTen.Value = statistics.Top10PercentPlacements.ToLocalisableString(@"N0"); + topTen.ValueColour = colourProvider.Content2; + + topFifty.Value = statistics.Top50PercentPlacements.ToLocalisableString(@"N0"); + topFifty.ValueColour = colourProvider.Content2; + } + + // reference: https://github.com/ppy/osu-web/blob/8206e0e91eeea80ccf92f0586561346dd40e085e/resources/js/profile-page/daily-challenge.tsx#L13-L43 + public static RankingTier TierForDaily(int daily) + { + if (daily > 360) + return RankingTier.Lustrous; + + if (daily > 240) + return RankingTier.Radiant; + + if (daily > 120) + return RankingTier.Rhodium; + + if (daily > 60) + return RankingTier.Platinum; + + if (daily > 30) + return RankingTier.Gold; + + if (daily > 10) + return RankingTier.Silver; + + if (daily > 5) + return RankingTier.Bronze; + + return RankingTier.Iron; + } + + public static RankingTier TierForWeekly(int weekly) => TierForDaily((weekly - 1) * 7); + + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + + public void Move(Vector2 pos) => Position = pos; + + private partial class StreakPiece : FillFlowContainer + { + private readonly OsuSpriteText valueText; + + public LocalisableString Value + { + set => valueText.Text = value; + } + + public ColourInfo ValueColour + { + set => valueText.Colour = value; + } + + public StreakPiece(LocalisableString title) + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Vertical; + + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12), + Text = title, + }, + valueText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 40, weight: FontWeight.Light), + } + }; + } + } + + private partial class StatisticsPiece : CompositeDrawable + { + private readonly OsuSpriteText valueText; + + public LocalisableString Value + { + set => valueText.Text = value; + } + + public ColourInfo ValueColour + { + set => valueText.Colour = value; + } + + public StatisticsPiece(LocalisableString title) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12), + Text = title, + }, + valueText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.GetFont(size: 12), + } + }; + } + } + } + + public record DailyChallengeTooltipData(OverlayColourProvider ColourProvider, APIUserDailyChallengeStatistics Statistics); +} diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 2505c1bc8c..3d97082230 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -44,22 +44,41 @@ namespace osu.Game.Overlays.Profile.Header.Components Spacing = new Vector2(0, 15), Children = new Drawable[] { - new FillFlowContainer + new GridContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(20), - Children = new Drawable[] + ColumnDimensions = new[] { - detailGlobalRank = new ProfileValueDisplay(true) + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 20), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] { - Title = UsersStrings.ShowRankGlobalSimple, - }, - detailCountryRank = new ProfileValueDisplay(true) - { - Title = UsersStrings.ShowRankCountrySimple, - }, + detailGlobalRank = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankGlobalSimple, + }, + Empty(), + detailCountryRank = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankCountrySimple, + }, + new DailyChallengeStatsDisplay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + User = { BindTarget = User }, + } + } } }, new Container diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index cf6bc30f85..2b74557c1a 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -2,7 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings @@ -10,6 +14,8 @@ namespace osu.Game.Overlays.Settings public partial class SettingsEnumDropdown : SettingsDropdown where T : struct, Enum { + public override IEnumerable FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.GetLocalisableDescription())); + protected override OsuDropdown CreateDropdown() => new DropdownControl(); protected new partial class DropdownControl : OsuEnumDropdown diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 6ce5c06801..7a28f7bbaa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -30,11 +28,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Drawable userContent; - [Resolved] - private EditorClock editorClock { get; set; } + private bool alwaysShowControlPoints; + + public bool AlwaysShowControlPoints + { + get => alwaysShowControlPoints; + set + { + if (value == alwaysShowControlPoints) + return; + + alwaysShowControlPoints = value; + controlPointsVisible.TriggerChange(); + } + } [Resolved] - private EditorBeatmap editorBeatmap { get; set; } + private EditorClock editorClock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; /// /// The timeline's scroll position in the last frame. @@ -61,6 +74,22 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// private float defaultTimelineZoom; + private WaveformGraph waveform = null!; + + private TimelineTickDisplay ticks = null!; + + private TimelineControlPointDisplay controlPoints = null!; + + private Container mainContent = null!; + + private Bindable waveformOpacity = null!; + private Bindable controlPointsVisible = null!; + private Bindable ticksVisible = null!; + + private double trackLengthForZoom; + + private readonly IBindable track = new Bindable(); + public Timeline(Drawable userContent) { this.userContent = userContent; @@ -73,22 +102,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline ScrollbarVisible = false; } - private WaveformGraph waveform; - - private TimelineTickDisplay ticks; - - private TimelineControlPointDisplay controlPoints; - - private Container mainContent; - - private Bindable waveformOpacity; - private Bindable controlPointsVisible; - private Bindable ticksVisible; - - private double trackLengthForZoom; - - private readonly IBindable track = new Bindable(); - [BackgroundDependencyLoader] private void load(IBindable beatmap, OsuColour colours, OsuConfigManager config) { @@ -178,7 +191,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline controlPointsVisible.BindValueChanged(visible => { - if (visible.NewValue) + if (visible.NewValue || alwaysShowControlPoints) { this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint); mainContent.MoveToY(15, 200, Easing.OutQuint); @@ -318,7 +331,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } [Resolved] - private IBeatSnapProvider beatSnapProvider { get; set; } + private IBeatSnapProvider beatSnapProvider { get; set; } = null!; /// /// The total amount of time visible on the timeline. diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index cc33840929..f7e523db25 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -69,19 +69,24 @@ namespace osu.Game.Screens.Edit.Compose if (ruleset == null || composer == null) return base.CreateTimelineContent(); + TimelineBreakDisplay breakDisplay = new TimelineBreakDisplay + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 0.75f, + }; + return wrapSkinnableContent(new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { + // We want to display this below hitobjects to better expose placement objects visually. + // It needs to be above the blueprint container to handle drags on breaks though. + breakDisplay.CreateProxy(), new TimelineBlueprintContainer(composer), - new TimelineBreakDisplay - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Height = 0.75f, - }, + breakDisplay } }); } diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 38d2a1e7e4..01908e45c7 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit @@ -26,7 +25,7 @@ namespace osu.Game.Screens.Edit } [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider colourProvider) + private void load() { // Grid with only two rows. // First is the timeline area, which should be allowed to expand as required. @@ -107,10 +106,18 @@ namespace osu.Game.Screens.Edit MainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); - LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timelineContent.Add); + LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timeline => + { + ConfigureTimeline(timeline); + timelineContent.Add(timeline); + }); }); } + protected virtual void ConfigureTimeline(TimelineArea timelineArea) + { + } + protected abstract Drawable CreateMainContent(); protected virtual Drawable CreateTimelineContent() => new Container(); diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 3f911f5067..67d4429be8 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit.Timing { @@ -53,5 +54,12 @@ namespace osu.Game.Screens.Edit.Timing SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); } } + + protected override void ConfigureTimeline(TimelineArea timelineArea) + { + base.ConfigureTimeline(timelineArea); + + timelineArea.Timeline.AlwaysShowControlPoints = true; + } } } diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index c365994736..a5616b95a0 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -23,6 +23,7 @@ using osu.Game.Online.API.Requests.Responses; 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 @@ namespace osu.Game.Screens.Menu [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) : base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys) { @@ -100,7 +104,8 @@ namespace osu.Game.Screens.Menu { base.LoadComplete(); - info.BindValueChanged(updateDisplay, true); + info.BindValueChanged(_ => dailyChallengeChanged(postNotification: true)); + dailyChallengeChanged(postNotification: false); } protected override void Update() @@ -126,27 +131,30 @@ namespace osu.Game.Screens.Menu } } - 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 322d855cd3..da2d9036c5 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.Metadata; 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 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge 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 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [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 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet; + dailyChallengeInfo.BindTo(metadataClient.DailyChallengeInfo); ((IBindable)breakdown.UserBestScore).BindTo(leaderboard.UserBestScore); } @@ -388,6 +394,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); + + dailyChallengeInfo.BindValueChanged(dailyChallengeChanged); } private void trySetDailyChallengeBeatmap() @@ -405,9 +413,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge 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 0000000000..ea19828a21 --- /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/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 4e49495f47..40fd289be6 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -40,6 +40,7 @@ namespace osu.Game.Screens.Select { case "star": case "stars": + case "sr": return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2); case "ar": diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index c9584b057b..b6508e177a 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Framework.Layout; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Extensions; @@ -38,6 +37,7 @@ using osu.Game.Users.Drawables; using osu.Game.Utils; using osuTK; using osuTK.Graphics; +using CommonStrings = osu.Game.Localisation.CommonStrings; namespace osu.Game.Screens.SelectV2.Leaderboards { @@ -61,7 +61,6 @@ namespace osu.Game.Screens.SelectV2.Leaderboards private const float statistics_regular_min_width = 175; private const float statistics_compact_min_width = 100; private const float rank_label_width = 65; - private const float rank_label_visibility_width_cutoff = rank_label_width + height + username_min_width + statistics_regular_min_width + expanded_right_content_width; private readonly ScoreInfo score; private readonly bool sheared; @@ -560,33 +559,34 @@ namespace osu.Game.Screens.SelectV2.Leaderboards background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint); - if (DrawWidth < rank_label_visibility_width_cutoff && IsHovered) + if (IsHovered && currentMode != DisplayMode.Full) rankLabelOverlay.FadeIn(transition_duration, Easing.OutQuint); else rankLabelOverlay.FadeOut(transition_duration, Easing.OutQuint); } - protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) - { - Scheduler.AddOnce(() => - { - // when width decreases - // - hide rank and show rank overlay on avatar when hovered, then - // - compact statistics, then - // - hide statistics + private DisplayMode? currentMode; - if (DrawWidth >= rank_label_visibility_width_cutoff) + protected override void Update() + { + base.Update(); + + DisplayMode mode = getCurrentDisplayMode(); + + if (currentMode != mode) + { + if (mode >= DisplayMode.Full) rankLabel.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); else rankLabel.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-rankLabel.DrawWidth, transition_duration, Easing.OutQuint); - if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width) + if (mode >= DisplayMode.Regular) { statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Horizontal; statisticsContainer.ScaleTo(1, transition_duration, Easing.OutQuint); } - else if (DrawWidth >= height + username_min_width + statistics_compact_min_width + expanded_right_content_width) + else if (mode >= DisplayMode.Compact) { statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Vertical; @@ -594,13 +594,35 @@ namespace osu.Game.Screens.SelectV2.Leaderboards } else statisticsContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, transition_duration, Easing.OutQuint); - }); - return base.OnInvalidate(invalidation, source); + currentMode = mode; + } + } + + private DisplayMode getCurrentDisplayMode() + { + if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width + rank_label_width) + return DisplayMode.Full; + + if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width) + return DisplayMode.Regular; + + if (DrawWidth >= height + username_min_width + statistics_compact_min_width + expanded_right_content_width) + return DisplayMode.Compact; + + return DisplayMode.Minimal; } #region Subclasses + private enum DisplayMode + { + Minimal, + Compact, + Regular, + Full + } + private partial class DateLabel : DrawableDate { public DateLabel(DateTimeOffset date) @@ -749,8 +771,8 @@ namespace osu.Game.Screens.SelectV2.Leaderboards if (score.Files.Count <= 0) return items.ToArray(); - items.Add(new OsuMenuItem(Localisation.CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); - items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); + items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); + items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); return items.ToArray(); } diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index fa64a83352..c9f2b183e3 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Metadata 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 @@ namespace osu.Game.Tests.Visual.Metadata } 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; } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 95669fc678..c4d76a2441 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,13 +30,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - +