From 013cf7a80aaca9da466063fb969b328d21e32022 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Aug 2022 00:21:13 +0900 Subject: [PATCH 01/46] Fix `DateAdded` not being set to a sane value when creating a new beatmap in the editor --- osu.Game/Beatmaps/BeatmapManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e3d2832108..8ce4e4c2fb 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -94,6 +94,7 @@ namespace osu.Game.Beatmaps var beatmapSet = new BeatmapSetInfo { + DateAdded = DateTimeOffset.UtcNow, Beatmaps = { new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) From d7a06abcab0f697057a4c9d2859faaa3aec95349 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Aug 2022 00:21:28 +0900 Subject: [PATCH 02/46] Add `BeatmapInfo.LastUpdate` to track the time of local changes --- osu.Game/Beatmaps/BeatmapInfo.cs | 8 ++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 1 + osu.Game/Database/RealmAccess.cs | 3 ++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index c53c2f67dd..c0d72fdf8c 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -102,6 +102,14 @@ namespace osu.Game.Beatmaps public string OnlineMD5Hash { get; set; } = string.Empty; + /// + /// The last time of a local modification (via the editor). + /// + public DateTimeOffset? LastUpdated { get; set; } + + /// + /// The last time online metadata was applied to this beatmap. + /// public DateTimeOffset? LastOnlineUpdate { get; set; } /// diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 8ce4e4c2fb..b457fcb387 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -314,6 +314,7 @@ namespace osu.Game.Beatmaps beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash(); beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; + beatmapInfo.LastUpdated = DateTimeOffset.Now; if (setInfo.Beatmaps.All(b => b.Status == BeatmapOnlineStatus.LocallyModified)) setInfo.Status = BeatmapOnlineStatus.LocallyModified; diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 5f0ec67c71..c073415547 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -67,8 +67,9 @@ namespace osu.Game.Database /// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1. /// 21 2022-07-27 Migrate collections to realm (BeatmapCollection). /// 22 2022-07-31 Added ModPreset. + /// 23 2022-08-01 Added LastUpdated to BeatmapInfo. /// - private const int schema_version = 22; + private const int schema_version = 23; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. From efa3c3b75716f3355e9187f23a497267d2680f55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Aug 2022 16:35:15 +0900 Subject: [PATCH 03/46] Fix multiple cases of mutations at editor setup screen not triggering a state save --- osu.Game/Screens/Edit/EditorBeatmap.cs | 3 +++ osu.Game/Screens/Edit/Setup/DesignSection.cs | 2 ++ osu.Game/Screens/Edit/Setup/MetadataSection.cs | 2 ++ 3 files changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 96425e8bc8..afbb5cc018 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -78,7 +78,10 @@ namespace osu.Game.Screens.Edit this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo; if (beatmapSkin is Skin skin) + { BeatmapSkin = new EditorBeatmapSkin(skin); + BeatmapSkin.BeatmapSkinChanged += SaveState; + } beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap); diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index 40bbfeaf7d..5cec730440 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -126,6 +126,8 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning.Current.Value; Beatmap.BeatmapInfo.LetterboxInBreaks = letterboxDuringBreaks.Current.Value; Beatmap.BeatmapInfo.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value; + + Beatmap.SaveState(); } } } diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 854dec2001..33cc88da0a 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -117,6 +117,8 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapInfo.DifficultyName = difficultyTextBox.Current.Value; Beatmap.Metadata.Source = sourceTextBox.Current.Value; Beatmap.Metadata.Tags = tagsTextBox.Current.Value; + + Beatmap.SaveState(); } } } From de186f67e0880aabc8cfb29e2bf019a9e8bd14db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Aug 2022 22:02:30 +0900 Subject: [PATCH 04/46] Limit metadata updates to once per frame --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 33cc88da0a..928e5bc3b6 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Edit.Setup target.Current.Value = value; updateReadOnlyState(); - updateMetadata(); + Scheduler.AddOnce(updateMetadata); } private void updateReadOnlyState() @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Edit.Setup // for now, update on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - updateMetadata(); + Scheduler.AddOnce(updateMetadata); } private void updateMetadata() From 6717f0606cf904e28ebce9156937439fa7e226a5 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Fri, 5 Aug 2022 23:00:37 +0200 Subject: [PATCH 05/46] Add property to SkipOverlay --- osu.Game/Screens/Play/SkipOverlay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 3e2cf9a756..646d2c1762 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -8,6 +8,7 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -40,6 +41,8 @@ namespace osu.Game.Screens.Play private bool isClickable; + public BindableBool IsSkippable = new()!; + [Resolved] private GameplayClock gameplayClock { get; set; } @@ -134,6 +137,7 @@ namespace osu.Game.Screens.Play remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); isClickable = progress > 0; + IsSkippable.Value = isClickable; button.Enabled.Value = isClickable; buttonContainer.State.Value = isClickable ? Visibility.Visible : Visibility.Hidden; } From 99e07aa09a0a0f5176fd9fad7c233405d02cf901 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Fri, 5 Aug 2022 23:01:52 +0200 Subject: [PATCH 06/46] Skip intro if the map gets restarted --- osu.Game/Screens/Play/Player.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9c08c77d91..91c13d95de 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -441,6 +441,12 @@ namespace osu.Game.Screens.Play }, }; + skipIntroOverlay.IsSkippable.ValueChanged += (e) => + { + if (RestartCount > 0 && e.NewValue) + skipIntroOverlay.RequestSkip.Invoke(); + }; + if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) { skipIntroOverlay.Expire(); From d8d7423698c457258797eb63fa8c16a78b101375 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Fri, 5 Aug 2022 23:04:43 +0200 Subject: [PATCH 07/46] Reduce "wait time" in case restarting the map --- osu.Game/Screens/Play/PlayerLoader.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 674490d595..96aefd56ea 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -384,9 +384,10 @@ namespace osu.Game.Screens.Play private void contentIn() { MetadataInfo.Loading = true; + int scaleContentTime = restartCount > 0 ? 65 : 650; content.FadeInFromZero(400); - content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); + content.ScaleTo(1, scaleContentTime, Easing.OutQuint).Then().Schedule(prepareNewPlayer); lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in) @@ -468,7 +469,7 @@ namespace osu.Game.Screens.Play else this.Exit(); }); - }, 500); + }, CurrentPlayer?.RestartCount > 0 ? 50 : 500); } private void cancelLoad() From 445f9217568581fa2ed54f6f1f1727ed7f6f0a0b Mon Sep 17 00:00:00 2001 From: BlauFx Date: Fri, 5 Aug 2022 23:21:03 +0200 Subject: [PATCH 08/46] Move IsSkippable event into load method --- osu.Game/Screens/Play/Player.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 91c13d95de..e7f9e7b0d2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -365,6 +365,12 @@ namespace osu.Game.Screens.Play IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindValueChanged(onBreakTimeChanged, true); + + skipIntroOverlay.IsSkippable.ValueChanged += (e) => + { + if (RestartCount > 0 && e.NewValue) + skipIntroOverlay.RequestSkip.Invoke(); + }; } protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); @@ -441,12 +447,6 @@ namespace osu.Game.Screens.Play }, }; - skipIntroOverlay.IsSkippable.ValueChanged += (e) => - { - if (RestartCount > 0 && e.NewValue) - skipIntroOverlay.RequestSkip.Invoke(); - }; - if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) { skipIntroOverlay.Expire(); From e411a2d18739f958d90dcbedfa38256489a570d7 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sat, 6 Aug 2022 15:12:36 +0200 Subject: [PATCH 09/46] Revert reduced wait time commit --- osu.Game/Screens/Play/PlayerLoader.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 96aefd56ea..674490d595 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -384,10 +384,9 @@ namespace osu.Game.Screens.Play private void contentIn() { MetadataInfo.Loading = true; - int scaleContentTime = restartCount > 0 ? 65 : 650; content.FadeInFromZero(400); - content.ScaleTo(1, scaleContentTime, Easing.OutQuint).Then().Schedule(prepareNewPlayer); + content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in) @@ -469,7 +468,7 @@ namespace osu.Game.Screens.Play else this.Exit(); }); - }, CurrentPlayer?.RestartCount > 0 ? 50 : 500); + }, 500); } private void cancelLoad() From 0d418559bc15b958dac1c75604a6596732f537dd Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sat, 6 Aug 2022 17:02:45 +0200 Subject: [PATCH 10/46] Skip song intro only in case of a quick restart --- osu.Game/Screens/Play/Player.cs | 7 ++++++- osu.Game/Screens/Play/PlayerLoader.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e7f9e7b0d2..a0e513648a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -81,6 +81,10 @@ namespace osu.Game.Screens.Play private bool isRestarting; + public bool RestartedViaHotkey; + + public Bindable IsRestartingFromHotkey = new Bindable(); + private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -287,6 +291,7 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; + IsRestartingFromHotkey.Value = true; fadeOut(true); Restart(); }, @@ -368,7 +373,7 @@ namespace osu.Game.Screens.Play skipIntroOverlay.IsSkippable.ValueChanged += (e) => { - if (RestartCount > 0 && e.NewValue) + if (RestartedViaHotkey && e.NewValue && RestartCount > 0) skipIntroOverlay.RequestSkip.Invoke(); }; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 674490d595..388d7d6ec5 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -123,6 +123,8 @@ namespace osu.Game.Screens.Play private EpilepsyWarning? epilepsyWarning; + private bool isHotKeyRestart; + [Resolved(CanBeNull = true)] private INotificationOverlay? notificationOverlay { get; set; } @@ -363,9 +365,20 @@ namespace osu.Game.Screens.Play CurrentPlayer = createPlayer(); CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.RestartRequested = restartRequested; + CurrentPlayer.IsRestartingFromHotkey.ValueChanged += e => + { + if (e.NewValue) + isHotKeyRestart = true; + }; LoadTask = LoadComponentAsync(CurrentPlayer, _ => { + if (isHotKeyRestart) + { + CurrentPlayer.RestartedViaHotkey = true; + isHotKeyRestart = false; + } + MetadataInfo.Loading = false; OnPlayerLoaded(); }); From fa6d55b5b5e69d4c61c965b02d42e84eec86c911 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sat, 6 Aug 2022 18:47:11 +0200 Subject: [PATCH 11/46] Remove redundant lambda signature parentheses --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a0e513648a..3087b56833 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -371,7 +371,7 @@ namespace osu.Game.Screens.Play IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindValueChanged(onBreakTimeChanged, true); - skipIntroOverlay.IsSkippable.ValueChanged += (e) => + skipIntroOverlay.IsSkippable.ValueChanged += e => { if (RestartedViaHotkey && e.NewValue && RestartCount > 0) skipIntroOverlay.RequestSkip.Invoke(); From bd43a9e96e377f4c57ef639cb180f1a62576880e Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sat, 6 Aug 2022 18:49:07 +0200 Subject: [PATCH 12/46] Add missing type specification --- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 646d2c1762..a5f6bf3fee 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private bool isClickable; - public BindableBool IsSkippable = new()!; + public BindableBool IsSkippable = new BindableBool()!; [Resolved] private GameplayClock gameplayClock { get; set; } From 09230304a4f49df8622391b49bab292fd54123f5 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sun, 7 Aug 2022 13:20:29 +0200 Subject: [PATCH 13/46] Improve implementation --- osu.Game/Screens/Play/Player.cs | 11 +++++------ osu.Game/Screens/Play/PlayerConfiguration.cs | 2 ++ osu.Game/Screens/Play/PlayerLoader.cs | 12 ++++++------ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3087b56833..63e7bcb115 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -81,10 +81,6 @@ namespace osu.Game.Screens.Play private bool isRestarting; - public bool RestartedViaHotkey; - - public Bindable IsRestartingFromHotkey = new Bindable(); - private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -291,7 +287,7 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - IsRestartingFromHotkey.Value = true; + Configuration.AutomaticallySkipIntro = true; fadeOut(true); Restart(); }, @@ -373,8 +369,11 @@ namespace osu.Game.Screens.Play skipIntroOverlay.IsSkippable.ValueChanged += e => { - if (RestartedViaHotkey && e.NewValue && RestartCount > 0) + if (Configuration.AutomaticallySkipIntro && e.NewValue && RestartCount > 0) + { + Configuration.AutomaticallySkipIntro = false; skipIntroOverlay.RequestSkip.Invoke(); + } }; } diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index d11825baee..0624508584 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -31,5 +31,7 @@ namespace osu.Game.Screens.Play /// Whether the player should be allowed to skip intros/outros, advancing to the start of gameplay or the end of a storyboard. /// public bool AllowSkipping { get; set; } = true; + + public bool AutomaticallySkipIntro { get; set; } } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 388d7d6ec5..477811a979 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -365,17 +365,12 @@ namespace osu.Game.Screens.Play CurrentPlayer = createPlayer(); CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.RestartRequested = restartRequested; - CurrentPlayer.IsRestartingFromHotkey.ValueChanged += e => - { - if (e.NewValue) - isHotKeyRestart = true; - }; LoadTask = LoadComponentAsync(CurrentPlayer, _ => { if (isHotKeyRestart) { - CurrentPlayer.RestartedViaHotkey = true; + CurrentPlayer.Configuration.AutomaticallySkipIntro = true; isHotKeyRestart = false; } @@ -390,6 +385,11 @@ namespace osu.Game.Screens.Play private void restartRequested() { + if (CurrentPlayer != null) + { + isHotKeyRestart = CurrentPlayer.Configuration.AutomaticallySkipIntro; + } + hideOverlays = true; ValidForResume = true; } From bb344e064fafb25878e006e30c5e77afd9df432b Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sun, 7 Aug 2022 13:31:26 +0200 Subject: [PATCH 14/46] Add xml docs --- osu.Game/Screens/Play/PlayerConfiguration.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index 0624508584..b1b0e01d80 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Play /// public bool AllowSkipping { get; set; } = true; + /// + /// Whether the intro should be skipped by default. + /// public bool AutomaticallySkipIntro { get; set; } } } From cd68134565d6a02d014b8d80ae37738cf737741a Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 13:10:28 +0200 Subject: [PATCH 15/46] Call skip method directly --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 63e7bcb115..b648c918e1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -372,7 +372,7 @@ namespace osu.Game.Screens.Play if (Configuration.AutomaticallySkipIntro && e.NewValue && RestartCount > 0) { Configuration.AutomaticallySkipIntro = false; - skipIntroOverlay.RequestSkip.Invoke(); + performUserRequestedSkip(); } }; } From fac2596eee86b21e49fc9e065258208009aa16d2 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 13:38:52 +0200 Subject: [PATCH 16/46] Change type from BindableBool to IBindable --- osu.Game/Screens/Play/SkipOverlay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index a5f6bf3fee..227038253d 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private bool isClickable; - public BindableBool IsSkippable = new BindableBool()!; + public IBindable IsSkippable = new Bindable(); [Resolved] private GameplayClock gameplayClock { get; set; } @@ -92,6 +92,8 @@ namespace osu.Game.Screens.Play } } }; + + IsSkippable.BindTo(button.Enabled); } private const double fade_time = 300; @@ -137,7 +139,6 @@ namespace osu.Game.Screens.Play remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); isClickable = progress > 0; - IsSkippable.Value = isClickable; button.Enabled.Value = isClickable; buttonContainer.State.Value = isClickable ? Visibility.Visible : Visibility.Hidden; } From f6e65cf1af289afdb6a3706321c16435057bcdda Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 20:53:05 +0200 Subject: [PATCH 17/46] Improve implementation --- osu.Game/Screens/Play/Player.cs | 10 ++++++---- osu.Game/Screens/Play/PlayerLoader.cs | 4 +--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b648c918e1..f2b3bfc090 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -81,6 +81,11 @@ namespace osu.Game.Screens.Play private bool isRestarting; + /// + /// Is set to true when pressed the via the quick retry hotkey. + /// + public Bindable IsQuickRestart = new Bindable(); + private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -287,7 +292,7 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - Configuration.AutomaticallySkipIntro = true; + IsQuickRestart.Value = true; fadeOut(true); Restart(); }, @@ -370,10 +375,7 @@ namespace osu.Game.Screens.Play skipIntroOverlay.IsSkippable.ValueChanged += e => { if (Configuration.AutomaticallySkipIntro && e.NewValue && RestartCount > 0) - { - Configuration.AutomaticallySkipIntro = false; performUserRequestedSkip(); - } }; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 477811a979..76092fbaa3 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -386,9 +386,7 @@ namespace osu.Game.Screens.Play private void restartRequested() { if (CurrentPlayer != null) - { - isHotKeyRestart = CurrentPlayer.Configuration.AutomaticallySkipIntro; - } + isHotKeyRestart = CurrentPlayer.IsQuickRestart.Value; hideOverlays = true; ValidForResume = true; From e4879aa4509dfae0942989d2ca67cddfb7338743 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 21:12:38 +0200 Subject: [PATCH 18/46] Add test --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 05474e3d39..b0308dcd22 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -353,6 +353,22 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } + [Test] + public void TestQuickRetry() + { + AddStep("load dummy beatmap", () => resetPlayer(false)); + AddUntilStep("wait for current", () => player.IsCurrentScreen()); + + AddStep("Restart map normally", () => player.Restart()); + AddUntilStep("wait for current", () => player.IsCurrentScreen()); + + AddStep("Restart map with quick retry hotkey", () => + { + InputManager.UseParentInput = true; + InputManager.PressKey(Key.Tilde); + }); + } + private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); private class TestPlayerLoader : PlayerLoader From 0afa3a5ec8eb62e4bda6c8476fa03808af736259 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 21:20:09 +0200 Subject: [PATCH 19/46] Fix xml doc --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f2b3bfc090..1dd4006450 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Play private bool isRestarting; /// - /// Is set to true when pressed the via the quick retry hotkey. + /// Is set to true when the quick retry hotkey has been pressed. /// public Bindable IsQuickRestart = new Bindable(); From 3525dfb0f15242880092fde6ddc361b7e40b6c1d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 12 Aug 2022 01:17:33 +0200 Subject: [PATCH 20/46] added merging feature --- .../Edit/OsuSelectionHandler.cs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 4a6d94f303..f9d4fbfc72 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -6,13 +6,16 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Game.Extensions; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -23,6 +26,12 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } + [Resolved(CanBeNull = true)] + private EditorBeatmap? editorBeatmap { get; set; } + + [Resolved(CanBeNull = true)] + private IEditorChangeHandler? changeHandler { get; set; } + /// /// During a transform, the initial origin is stored so it can be used throughout the operation. /// @@ -322,5 +331,105 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType() .Where(h => !(h is Spinner)) .ToArray(); + + /// + /// All osu! hitobjects which can be merged. + /// + private OsuHitObject[] selectedMergeableObjects => SelectedItems.OfType() + .Where(h => h is HitCircle or Slider) + .OrderBy(h => h.StartTime) + .ToArray(); + + private void mergeSelection() + { + if (editorBeatmap == null || changeHandler == null || selectedMergeableObjects.Length < 2) + return; + + changeHandler.BeginChange(); + + // Have an initial slider object. + var firstHitObject = selectedMergeableObjects[0]; + var mergedHitObject = firstHitObject as Slider ?? new Slider + { + StartTime = firstHitObject.StartTime, + Position = firstHitObject.Position, + NewCombo = firstHitObject.NewCombo, + SampleControlPoint = firstHitObject.SampleControlPoint, + }; + + if (mergedHitObject.Path.ControlPoints.Count == 0) + { + mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear)); + } + + // Merge all the selected hit objects into one slider path. + bool lastCircle = firstHitObject is HitCircle; + + foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) + { + if (selectedMergeableObject is IHasPath hasPath) + { + var offset = lastCircle ? selectedMergeableObject.Position - mergedHitObject.Position : mergedHitObject.Path.ControlPoints[^1].Position; + float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset); + + // Calculate the distance required to travel to the expected distance of the merging slider. + mergedHitObject.Path.ExpectedDistance.Value = mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance; + + // Remove the last control point if it sits exactly on the start of the next control point. + if (Precision.AlmostEquals(distanceToLastControlPoint, 0)) + { + mergedHitObject.Path.ControlPoints.RemoveAt(mergedHitObject.Path.ControlPoints.Count - 1); + } + + mergedHitObject.Path.ControlPoints.AddRange(hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type))); + lastCircle = false; + } + else + { + // Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type. + if (!lastCircle) + { + mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear; + } + + mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position)); + mergedHitObject.Path.ExpectedDistance.Value = null; + lastCircle = true; + } + } + + // Make sure only the merged hit object is in the beatmap. + if (firstHitObject is Slider) + { + foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) + { + editorBeatmap.Remove(selectedMergeableObject); + } + } + else + { + foreach (var selectedMergeableObject in selectedMergeableObjects) + { + editorBeatmap.Remove(selectedMergeableObject); + } + + editorBeatmap.Add(mergedHitObject); + } + + // Make sure the merged hitobject is selected. + SelectedItems.Clear(); + SelectedItems.Add(mergedHitObject); + + changeHandler.EndChange(); + } + + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + { + foreach (var item in base.GetContextMenuItemsForSelection(selection)) + yield return item; + + if (selection.Count() > 1 && selection.All(o => o.Item is HitCircle or Slider)) + yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection); + } } } From 3abc333813218a832ef3f2c229af4da5c52abbbf Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 17:18:55 +0200 Subject: [PATCH 21/46] added hotkey for merging selection --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index f9d4fbfc72..e3cd31f6a3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; @@ -18,6 +19,7 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit { @@ -62,6 +64,17 @@ namespace osu.Game.Rulesets.Osu.Edit referencePathTypes = null; } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed) + { + mergeSelection(); + return true; + } + + return false; + } + public override bool HandleMovement(MoveSelectionEvent moveEvent) { var hitObjects = selectedMovableObjects; From d261be873439cf0618188c25784bfdf38c8a7452 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 17:19:04 +0200 Subject: [PATCH 22/46] added visual tests --- .../Editor/TestSceneObjectMerging.cs | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs new file mode 100644 index 0000000000..8387f9c74c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -0,0 +1,179 @@ +// 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.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneObjectMerging : TestSceneOsuEditor + { + [Test] + public void TestSimpleMerge() + { + HitCircle circle1 = null; + HitCircle circle2 = null; + + AddStep("select first two circles", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1); + EditorClock.Seek(circle1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(circle2); + }); + + mergeSelection(); + + AddAssert("slider created", () => sliderCreatedFor( + (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle2.Position, pathType: null))); + + AddStep("undo", () => Editor.Undo()); + AddAssert("merged objects restored", () => objectsRestored(circle1, circle2)); + } + + [Test] + public void TestMergeCircleSlider() + { + HitCircle circle1 = null; + Slider slider = null; + HitCircle circle2 = null; + + AddStep("select a circle, slider, circle", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); + EditorClock.Seek(circle1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(slider); + EditorBeatmap.SelectedHitObjects.Add(circle2); + }); + + mergeSelection(); + + AddAssert("slider created", () => + { + var controlPoints = slider.Path.ControlPoints; + (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; + args[0] = (circle1.Position, PathType.Linear); + + for (int i = 0; i < controlPoints.Count; i++) + { + args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type); + } + + args[^1] = (circle2.Position, null); + return sliderCreatedFor(args); + }); + + AddStep("undo", () => Editor.Undo()); + AddAssert("merged objects restored", () => objectsRestored(circle1, slider, circle2)); + } + + [Test] + public void TestMergeSliderSlider() + { + Slider slider1 = null; + SliderPath slider1Path = null; + Slider slider2 = null; + + AddStep("select two sliders", () => + { + slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider); + slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value); + slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime); + EditorClock.Seek(slider1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(slider1); + EditorBeatmap.SelectedHitObjects.Add(slider2); + }); + + mergeSelection(); + + AddAssert("slider created", () => + { + var controlPoints1 = slider1Path.ControlPoints; + var controlPoints2 = slider2.Path.ControlPoints; + (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1]; + + for (int i = 0; i < controlPoints1.Count - 1; i++) + { + args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type); + } + + for (int i = 0; i < controlPoints2.Count; i++) + { + args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type); + } + + return sliderCreatedFor(args); + }); + + AddAssert("merged slider matches first slider", () => + { + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + return mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) + && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) + && mergedSlider.Samples.SequenceEqual(slider1.Samples) + && mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint); + }); + + AddAssert("slider end is at same completion for last slider", () => + { + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance); + }); + } + + private void mergeSelection() + { + AddStep("merge selection", () => + { + InputManager.PressKey(Key.LControl); + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.M); + InputManager.ReleaseKey(Key.LShift); + InputManager.ReleaseKey(Key.LControl); + }); + } + + private bool sliderCreatedFor(params (Vector2 pos, PathType? pathType)[] expectedControlPoints) + { + if (EditorBeatmap.SelectedHitObjects.Count != 1) + return false; + + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + int i = 0; + + foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints) + { + var controlPoint = mergedSlider.Path.ControlPoints[i++]; + + if (!Precision.AlmostEquals(controlPoint.Position + mergedSlider.Position, pos) || controlPoint.Type != pathType) + return false; + } + + return true; + } + + private bool objectsRestored(params HitObject[] objects) + { + foreach (var hitObject in objects) + { + if (EditorBeatmap.HitObjects.Contains(hitObject)) + return false; + } + + return true; + } + } +} From b5e54113485d5c511bc179a67a2f0794cd78ec55 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 18:07:55 +0200 Subject: [PATCH 23/46] remove copyright notice from new file --- osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 8387f9c74c..9b90c0ef05 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -1,6 +1,3 @@ -// 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.Linq; From 7c2ada9b209f1a887e026aac48c9cadeb3d7905f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 21:11:06 +0200 Subject: [PATCH 24/46] Revert "remove copyright notice from new file" This reverts commit b5e54113485d5c511bc179a67a2f0794cd78ec55. --- osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 9b90c0ef05..8387f9c74c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -1,3 +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.Linq; From d140e0df14224da4163c9e5049aa3eec0bf5ce1d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 21:15:23 +0200 Subject: [PATCH 25/46] remove nullable disable annotation --- .../Editor/TestSceneObjectMerging.cs | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 8387f9c74c..63e6760514 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.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.Linq; using NUnit.Framework; using osu.Framework.Utils; @@ -19,8 +17,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestSimpleMerge() { - HitCircle circle1 = null; - HitCircle circle2 = null; + HitCircle? circle1 = null; + HitCircle? circle2 = null; AddStep("select first two circles", () => { @@ -33,20 +31,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor mergeSelection(); - AddAssert("slider created", () => sliderCreatedFor( + AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( (pos: circle1.Position, pathType: PathType.Linear), (pos: circle2.Position, pathType: null))); AddStep("undo", () => Editor.Undo()); - AddAssert("merged objects restored", () => objectsRestored(circle1, circle2)); + AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); } [Test] public void TestMergeCircleSlider() { - HitCircle circle1 = null; - Slider slider = null; - HitCircle circle2 = null; + HitCircle? circle1 = null; + Slider? slider = null; + HitCircle? circle2 = null; AddStep("select a circle, slider, circle", () => { @@ -63,6 +61,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider created", () => { + if (circle1 is null || circle2 is null || slider is null) + return false; + var controlPoints = slider.Path.ControlPoints; (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; args[0] = (circle1.Position, PathType.Linear); @@ -77,15 +78,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); AddStep("undo", () => Editor.Undo()); - AddAssert("merged objects restored", () => objectsRestored(circle1, slider, circle2)); + AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2)); } [Test] public void TestMergeSliderSlider() { - Slider slider1 = null; - SliderPath slider1Path = null; - Slider slider2 = null; + Slider? slider1 = null; + SliderPath? slider1Path = null; + Slider? slider2 = null; AddStep("select two sliders", () => { @@ -101,6 +102,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider created", () => { + if (slider1 is null || slider2 is null || slider1Path is null) + return false; + var controlPoints1 = slider1Path.ControlPoints; var controlPoints2 = slider2.Path.ControlPoints; (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1]; @@ -121,14 +125,17 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("merged slider matches first slider", () => { var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); - return mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) - && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) - && mergedSlider.Samples.SequenceEqual(slider1.Samples) - && mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint); + return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) + && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) + && mergedSlider.Samples.SequenceEqual(slider1.Samples) + && mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint); }); AddAssert("slider end is at same completion for last slider", () => { + if (slider1Path is null || slider2 is null) + return false; + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance); }); From a15e6f19aa48d2ce7ec39387f0373cc4d7bde144 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 13:35:57 +0900 Subject: [PATCH 26/46] Fix running `TestScenePlayerLoader` interactively leaving volume in a bad state --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 81 +++++++++++++++---- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 05474e3d39..dbbedf37ae 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -56,6 +56,10 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly ChangelogOverlay changelogOverlay; + private double savedTrackVolume; + private double savedMasterVolume; + private bool savedMutedState; + public TestScenePlayerLoader() { AddRange(new Drawable[] @@ -75,11 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [SetUp] - public void Setup() => Schedule(() => - { - player = null; - audioManager.Volume.SetDefault(); - }); + public void Setup() => Schedule(() => player = null); /// /// Sets the input manager child to a new test player loader container instance. @@ -147,6 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay moveMouse(); return player?.LoadState == LoadState.Ready; }); + AddRepeatStep("move mouse", moveMouse, 20); AddAssert("loader still active", () => loader.IsCurrentScreen()); @@ -154,6 +155,8 @@ namespace osu.Game.Tests.Visual.Gameplay void moveMouse() { + notificationOverlay.State.Value = Visibility.Hidden; + InputManager.MoveMouseTo( loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft) @@ -274,6 +277,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("load player", () => resetPlayer(false, beforeLoad)); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); + saveVolumes(); + AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1); AddStep("click notification", () => { @@ -287,6 +292,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("check " + volumeName, assert); + restoreVolumes(); + AddUntilStep("wait for player load", () => player.IsLoaded); } @@ -294,6 +301,9 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(false)] public void TestEpilepsyWarning(bool warning) { + saveVolumes(); + setFullVolume(); + AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("load dummy beatmap", () => resetPlayer(false)); @@ -306,6 +316,30 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25); AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } + + restoreVolumes(); + } + + [Test] + public void TestEpilepsyWarningEarlyExit() + { + saveVolumes(); + setFullVolume(); + + AddStep("set epilepsy warning", () => epilepsyWarning = true); + AddStep("load dummy beatmap", () => resetPlayer(false)); + + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); + AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible); + + AddStep("exit early", () => loader.Exit()); + + AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); + AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); + + restoreVolumes(); } [TestCase(true, 1.0, false)] // on battery, above cutoff --> no warning @@ -336,21 +370,34 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for player load", () => player.IsLoaded); } - [Test] - public void TestEpilepsyWarningEarlyExit() + private void restoreVolumes() { - AddStep("set epilepsy warning", () => epilepsyWarning = true); - AddStep("load dummy beatmap", () => resetPlayer(false)); + AddStep("restore previous volumes", () => + { + audioManager.VolumeTrack.Value = savedTrackVolume; + audioManager.Volume.Value = savedMasterVolume; + volumeOverlay.IsMuted.Value = savedMutedState; + }); + } - AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + private void setFullVolume() + { + AddStep("set volumes to 100%", () => + { + audioManager.VolumeTrack.Value = 1; + audioManager.Volume.Value = 1; + volumeOverlay.IsMuted.Value = false; + }); + } - AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); - AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible); - - AddStep("exit early", () => loader.Exit()); - - AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); - AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); + private void saveVolumes() + { + AddStep("save previous volumes", () => + { + savedTrackVolume = audioManager.VolumeTrack.Value; + savedMasterVolume = audioManager.Volume.Value; + savedMutedState = volumeOverlay.IsMuted.Value; + }); } private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); From 6761f869f993493598f9de7d2d192b1ba7cef8ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 13:04:56 +0900 Subject: [PATCH 27/46] Modify flow to avoid weird bindable and value resetting --- .../Visual/Mods/TestSceneModFailCondition.cs | 2 +- osu.Game/Screens/Play/Player.cs | 21 +++++++------------ osu.Game/Screens/Play/PlayerLoader.cs | 15 ++++--------- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 4 files changed, 14 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs index 91fa09b414..23c1eda7f7 100644 --- a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs +++ b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Mods protected override TestPlayer CreateModPlayer(Ruleset ruleset) { var player = base.CreateModPlayer(ruleset); - player.RestartRequested = () => restartRequested = true; + player.RestartRequested = _ => restartRequested = true; return player; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 7ed68aa5b7..4f6bb1d37a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -77,15 +77,10 @@ namespace osu.Game.Screens.Play /// protected virtual bool PauseOnFocusLost => true; - public Action RestartRequested; + public Action RestartRequested; private bool isRestarting; - /// - /// Is set to true when the quick retry hotkey has been pressed. - /// - public Bindable IsQuickRestart = new Bindable(); - private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -272,7 +267,7 @@ namespace osu.Game.Screens.Play FailOverlay = new FailOverlay { SaveReplay = prepareAndImportScore, - OnRetry = Restart, + OnRetry = () => Restart(), OnQuit = () => PerformExit(true), }, new HotkeyExitOverlay @@ -298,9 +293,8 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - IsQuickRestart.Value = true; fadeOut(true); - Restart(); + Restart(true); }, }); } @@ -453,7 +447,7 @@ namespace osu.Game.Screens.Play { OnResume = Resume, Retries = RestartCount, - OnRetry = Restart, + OnRetry = () => Restart(), OnQuit = () => PerformExit(true), }, }, @@ -660,7 +654,8 @@ namespace osu.Game.Screens.Play /// Restart gameplay via a parent . /// This can be called from a child screen in order to trigger the restart process. /// - public void Restart() + /// Whether a quick restart was requested (skipping intro etc.). + public void Restart(bool quickRestart = false) { if (!Configuration.AllowRestart) return; @@ -672,7 +667,7 @@ namespace osu.Game.Screens.Play musicController.Stop(); sampleRestart?.Play(); - RestartRequested?.Invoke(); + RestartRequested?.Invoke(quickRestart); PerformExit(false); } @@ -852,7 +847,7 @@ namespace osu.Game.Screens.Play failAnimationLayer.Start(); if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) - Restart(); + Restart(true); return true; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 76092fbaa3..e6bd1367ef 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Play private EpilepsyWarning? epilepsyWarning; - private bool isHotKeyRestart; + private bool quickRestart; [Resolved(CanBeNull = true)] private INotificationOverlay? notificationOverlay { get; set; } @@ -363,17 +363,12 @@ namespace osu.Game.Screens.Play return; CurrentPlayer = createPlayer(); + CurrentPlayer.Configuration.AutomaticallySkipIntro = quickRestart; CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.RestartRequested = restartRequested; LoadTask = LoadComponentAsync(CurrentPlayer, _ => { - if (isHotKeyRestart) - { - CurrentPlayer.Configuration.AutomaticallySkipIntro = true; - isHotKeyRestart = false; - } - MetadataInfo.Loading = false; OnPlayerLoaded(); }); @@ -383,11 +378,9 @@ namespace osu.Game.Screens.Play { } - private void restartRequested() + private void restartRequested(bool quickRestartRequested) { - if (CurrentPlayer != null) - isHotKeyRestart = CurrentPlayer.IsQuickRestart.Value; - + quickRestart = quickRestartRequested; hideOverlays = true; ValidForResume = true; } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index c530febcae..226216b0f0 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -177,7 +177,7 @@ namespace osu.Game.Screens.Ranking { if (!this.IsCurrentScreen()) return; - player?.Restart(); + player?.Restart(true); }, }); } From 9a1a7bae891c1b13a90ccf72354ee1fad6a1e6fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 14:17:28 +0900 Subject: [PATCH 28/46] Make test actually test things --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 38 +++++++++++++++---- osu.Game/Screens/Play/SkipOverlay.cs | 2 + 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index b0308dcd22..1fd517d1a6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -29,6 +29,7 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Utils; using osuTK.Input; +using SkipOverlay = osu.Game.Screens.Play.SkipOverlay; namespace osu.Game.Tests.Visual.Gameplay { @@ -98,7 +99,13 @@ namespace osu.Game.Tests.Visual.Gameplay private void prepareBeatmap() { var workingBeatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + + // Add intro time to test quick retry skipping (TestQuickRetry). + workingBeatmap.BeatmapInfo.AudioLeadIn = 60000; + + // Turn on epilepsy warning to test warning display (TestEpilepsyWarning). workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning; + Beatmap.Value = workingBeatmap; foreach (var mod in SelectedMods.Value.OfType()) @@ -356,17 +363,32 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickRetry() { + TestPlayer getCurrentPlayer() => loader.CurrentPlayer as TestPlayer; + bool checkSkipButtonVisible() => player.ChildrenOfType().FirstOrDefault()?.IsButtonVisible == true; + + TestPlayer previousPlayer = null; + AddStep("load dummy beatmap", () => resetPlayer(false)); - AddUntilStep("wait for current", () => player.IsCurrentScreen()); - AddStep("Restart map normally", () => player.Restart()); - AddUntilStep("wait for current", () => player.IsCurrentScreen()); + AddUntilStep("wait for current", () => getCurrentPlayer()?.IsCurrentScreen() == true); + AddStep("store previous player", () => previousPlayer = getCurrentPlayer()); - AddStep("Restart map with quick retry hotkey", () => - { - InputManager.UseParentInput = true; - InputManager.PressKey(Key.Tilde); - }); + AddStep("Restart map normally", () => getCurrentPlayer().Restart()); + AddUntilStep("wait for load", () => getCurrentPlayer()?.LoadedBeatmapSuccessfully == true); + + AddUntilStep("restart completed", () => getCurrentPlayer() != null && getCurrentPlayer() != previousPlayer); + AddStep("store previous player", () => previousPlayer = getCurrentPlayer()); + + AddUntilStep("skip button visible", checkSkipButtonVisible); + + AddStep("press quick retry key", () => InputManager.PressKey(Key.Tilde)); + AddUntilStep("restart completed", () => getCurrentPlayer() != null && getCurrentPlayer() != previousPlayer); + AddStep("release quick retry key", () => InputManager.ReleaseKey(Key.Tilde)); + + AddUntilStep("wait for load", () => getCurrentPlayer()?.LoadedBeatmapSuccessfully == true); + + AddUntilStep("time reached zero", () => getCurrentPlayer()?.GameplayClockContainer.CurrentTime > 0); + AddUntilStep("skip button not visible", () => !checkSkipButtonVisible()); } private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 70a8345c1f..0629d0ae23 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -46,6 +46,8 @@ namespace osu.Game.Screens.Play [Resolved] private IGameplayClock gameplayClock { get; set; } + internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// From 63819648dfda22157edd7c029cea5a6db06b8235 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 14:27:02 +0900 Subject: [PATCH 29/46] Fix up flow of actual skip operation --- osu.Game/Screens/Play/Player.cs | 7 ++----- osu.Game/Screens/Play/SkipOverlay.cs | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4f6bb1d37a..6827ff04d3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -372,11 +372,8 @@ namespace osu.Game.Screens.Play IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindValueChanged(onBreakTimeChanged, true); - skipIntroOverlay.IsSkippable.ValueChanged += e => - { - if (Configuration.AutomaticallySkipIntro && e.NewValue && RestartCount > 0) - performUserRequestedSkip(); - }; + if (Configuration.AutomaticallySkipIntro) + skipIntroOverlay.SkipWhenReady(); } protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 0629d0ae23..5c9a706549 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -8,7 +8,6 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -40,8 +39,7 @@ namespace osu.Game.Screens.Play private double displayTime; private bool isClickable; - - public IBindable IsSkippable = new Bindable(); + private bool skipQueued; [Resolved] private IGameplayClock gameplayClock { get; set; } @@ -94,8 +92,6 @@ namespace osu.Game.Screens.Play } } }; - - IsSkippable.BindTo(button.Enabled); } private const double fade_time = 300; @@ -130,6 +126,20 @@ namespace osu.Game.Screens.Play displayTime = gameplayClock.CurrentTime; fadeContainer.TriggerShow(); + + if (skipQueued) + { + Scheduler.AddDelayed(() => button.TriggerClick(), 200); + skipQueued = false; + } + } + + public void SkipWhenReady() + { + if (IsLoaded) + button.TriggerClick(); + else + skipQueued = true; } protected override void Update() From ea50936d718bd21a78fea574811cc7b560a651f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:26:29 +0900 Subject: [PATCH 30/46] Fix slider ticks playing back at infinite rate while making changes to a slider in the editor --- osu.Game/Screens/Edit/Editor.cs | 19 ++++++------------- osu.Game/Screens/Edit/EditorBeatmap.cs | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 89f9aec5ee..90e912bd6d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -225,8 +225,6 @@ namespace osu.Game.Screens.Edit dependencies.CacheAs(clock); AddInternal(clock); - clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState()); - // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); @@ -428,7 +426,12 @@ namespace osu.Game.Screens.Edit protected override void Update() { base.Update(); + clock.ProcessFrame(); + + samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value + || currentScreen is not ComposeScreen + || editorBeatmap.UpdateInProgress; } public bool OnPressed(KeyBindingPressEvent e) @@ -822,16 +825,10 @@ namespace osu.Game.Screens.Edit } finally { - updateSampleDisabledState(); rebindClipboardBindables(); } } - private void updateSampleDisabledState() - { - samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value || !(currentScreen is ComposeScreen); - } - private void seek(UIEvent e, int direction) { double amount = e.ShiftPressed ? 4 : 1; @@ -936,11 +933,7 @@ namespace osu.Game.Screens.Edit protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); - private void cancelExit() - { - updateSampleDisabledState(); - loader?.CancelPendingDifficultySwitch(); - } + private void cancelExit() => loader?.CancelPendingDifficultySwitch(); public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 96425e8bc8..b9b0879fa4 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -10,6 +10,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -22,6 +23,15 @@ namespace osu.Game.Screens.Edit { public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapProvider { + /// + /// While performing updates on hitobjects, this will momentarily become true. + /// + /// + /// This is intended to be used to avoid performing operations (like playback of samples) + /// while mutating hitobjects. + /// + public bool UpdateInProgress { get; private set; } + /// /// Invoked when a is added to this . /// @@ -225,8 +235,12 @@ namespace osu.Game.Screens.Edit { // updates are debounced regardless of whether a batch is active. batchPendingUpdates.Add(hitObject); + + advertiseUpdateInProgress(); } + private ScheduledDelegate updateCompleteDelegate; + /// /// Update all hit objects with potentially changed difficulty or control point data. /// @@ -234,6 +248,8 @@ namespace osu.Game.Screens.Edit { foreach (var h in HitObjects) batchPendingUpdates.Add(h); + + advertiseUpdateInProgress(); } /// @@ -333,6 +349,15 @@ namespace osu.Game.Screens.Edit /// public void Clear() => RemoveRange(HitObjects.ToArray()); + private void advertiseUpdateInProgress() + { + UpdateInProgress = true; + + // Debounce is arbitrarily high enough to avoid flip-flopping the value each other frame. + updateCompleteDelegate?.Cancel(); + updateCompleteDelegate = Scheduler.AddDelayed(() => UpdateInProgress = false, 50); + } + private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, PlayableBeatmap.Difficulty); private void trackStartTime(HitObject hitObject) From e636fcd9b8312be89cf884ca98066becf4720288 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:35:32 +0900 Subject: [PATCH 31/46] Use DI'd components from parent class rather than fetching locally --- .../Edit/OsuSelectionHandler.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index e3cd31f6a3..44fbbd9591 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -16,7 +16,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; using osuTK.Input; @@ -28,12 +27,6 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } - [Resolved(CanBeNull = true)] - private EditorBeatmap? editorBeatmap { get; set; } - - [Resolved(CanBeNull = true)] - private IEditorChangeHandler? changeHandler { get; set; } - /// /// During a transform, the initial origin is stored so it can be used throughout the operation. /// @@ -355,10 +348,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void mergeSelection() { - if (editorBeatmap == null || changeHandler == null || selectedMergeableObjects.Length < 2) + if (EditorBeatmap == null || ChangeHandler == null || selectedMergeableObjects.Length < 2) return; - changeHandler.BeginChange(); + ChangeHandler.BeginChange(); // Have an initial slider object. var firstHitObject = selectedMergeableObjects[0]; @@ -416,24 +409,24 @@ namespace osu.Game.Rulesets.Osu.Edit { foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) { - editorBeatmap.Remove(selectedMergeableObject); + EditorBeatmap.Remove(selectedMergeableObject); } } else { foreach (var selectedMergeableObject in selectedMergeableObjects) { - editorBeatmap.Remove(selectedMergeableObject); + EditorBeatmap.Remove(selectedMergeableObject); } - editorBeatmap.Add(mergedHitObject); + EditorBeatmap.Add(mergedHitObject); } // Make sure the merged hitobject is selected. SelectedItems.Clear(); SelectedItems.Add(mergedHitObject); - changeHandler.EndChange(); + ChangeHandler.EndChange(); } protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) From ecb9351babd55217c0b28a5e20c3274be44669b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:36:43 +0900 Subject: [PATCH 32/46] Remove unnecessary null pre-checks --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 44fbbd9591..363a191e34 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -348,10 +348,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void mergeSelection() { - if (EditorBeatmap == null || ChangeHandler == null || selectedMergeableObjects.Length < 2) + if (selectedMergeableObjects.Length < 2) return; - ChangeHandler.BeginChange(); + ChangeHandler?.BeginChange(); // Have an initial slider object. var firstHitObject = selectedMergeableObjects[0]; @@ -426,7 +426,7 @@ namespace osu.Game.Rulesets.Osu.Edit SelectedItems.Clear(); SelectedItems.Add(mergedHitObject); - ChangeHandler.EndChange(); + ChangeHandler?.EndChange(); } protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) From 7cc9fdbaa0d19f847cdc57264bc6652746ac91b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:37:38 +0900 Subject: [PATCH 33/46] Simplify context menu check by using existing mergeable object list --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 363a191e34..663bf03a00 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -335,7 +335,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// All osu! hitobjects which can be moved/rotated/scaled. /// private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType() - .Where(h => !(h is Spinner)) + .Where(h => h is not Spinner) .ToArray(); /// @@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - if (selection.Count() > 1 && selection.All(o => o.Item is HitCircle or Slider)) + if (selectedMergeableObjects.Length > 1) yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection); } } From 0833a6fb9a3dec55c464f211340f1d51a1bbdb97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:38:13 +0900 Subject: [PATCH 34/46] Avoid multiple iteration of LINQ query using a local --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 663bf03a00..f3c0a05bc2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -348,13 +348,15 @@ namespace osu.Game.Rulesets.Osu.Edit private void mergeSelection() { - if (selectedMergeableObjects.Length < 2) + var mergeableObjects = selectedMergeableObjects; + + if (mergeableObjects.Length < 2) return; ChangeHandler?.BeginChange(); // Have an initial slider object. - var firstHitObject = selectedMergeableObjects[0]; + var firstHitObject = mergeableObjects[0]; var mergedHitObject = firstHitObject as Slider ?? new Slider { StartTime = firstHitObject.StartTime, @@ -371,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Edit // Merge all the selected hit objects into one slider path. bool lastCircle = firstHitObject is HitCircle; - foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) + foreach (var selectedMergeableObject in mergeableObjects.Skip(1)) { if (selectedMergeableObject is IHasPath hasPath) { @@ -407,14 +409,14 @@ namespace osu.Game.Rulesets.Osu.Edit // Make sure only the merged hit object is in the beatmap. if (firstHitObject is Slider) { - foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) + foreach (var selectedMergeableObject in mergeableObjects.Skip(1)) { EditorBeatmap.Remove(selectedMergeableObject); } } else { - foreach (var selectedMergeableObject in selectedMergeableObjects) + foreach (var selectedMergeableObject in mergeableObjects) { EditorBeatmap.Remove(selectedMergeableObject); } From 11f38e539f6571ff1811db64883a219e2930f5c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 16:01:19 +0900 Subject: [PATCH 35/46] Rename property to `LastLocalUpdate` --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Database/RealmAccess.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 29f04e5a54..32b7f0b29b 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -105,7 +105,7 @@ namespace osu.Game.Beatmaps /// /// The last time of a local modification (via the editor). /// - public DateTimeOffset? LastUpdated { get; set; } + public DateTimeOffset? LastLocalUpdate { get; set; } /// /// The last time online metadata was applied to this beatmap. diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d6ef1e0173..d736765dd9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -314,7 +314,7 @@ namespace osu.Game.Beatmaps beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash(); - beatmapInfo.LastUpdated = DateTimeOffset.Now; + beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 40ac3b7422..cdaf35a1fb 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -68,7 +68,7 @@ namespace osu.Game.Database /// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1. /// 21 2022-07-27 Migrate collections to realm (BeatmapCollection). /// 22 2022-07-31 Added ModPreset. - /// 23 2022-08-01 Added LastUpdated to BeatmapInfo. + /// 23 2022-08-01 Added LastLocalUpdate to BeatmapInfo. /// private const int schema_version = 23; From ee153a345c34c825852883723a8339a2593c062a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 16:31:56 +0900 Subject: [PATCH 36/46] Add a few more overlooked beatmap save states on setup screen modifications --- osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs | 1 + osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs | 1 + osu.Game/Screens/Edit/Setup/DifficultySection.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs index b602a67baa..81c41ab628 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs @@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup private void updateBeatmap() { Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value; + Beatmap.SaveState(); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs index d8471aca1a..2889832ff1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs +++ b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs @@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup private void updateBeatmap() { Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; + Beatmap.SaveState(); } } } diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 5ce5d05d64..ce44445683 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -96,6 +96,7 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.UpdateAllHitObjects(); + Beatmap.SaveState(); } } } From 1a7ddc004075c0ebb9774a64080314dfe1a3e7a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 16:43:05 +0900 Subject: [PATCH 37/46] Fix re-importing existing collections not correctly adding new items --- osu.Game/Database/LegacyCollectionImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyCollectionImporter.cs b/osu.Game/Database/LegacyCollectionImporter.cs index 4bb28bf731..6d3e3fb76a 100644 --- a/osu.Game/Database/LegacyCollectionImporter.cs +++ b/osu.Game/Database/LegacyCollectionImporter.cs @@ -89,7 +89,7 @@ namespace osu.Game.Database if (existing != null) { - foreach (string newBeatmap in existing.BeatmapMD5Hashes) + foreach (string newBeatmap in collection.BeatmapMD5Hashes) { if (!existing.BeatmapMD5Hashes.Contains(newBeatmap)) existing.BeatmapMD5Hashes.Add(newBeatmap); From 5ac314077a706610c86ff9e3926fe57f0d46df10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 18:09:04 +0900 Subject: [PATCH 38/46] Improve intro timings when not using osu! theme Roughly as proposed in https://github.com/ppy/osu/discussions/19687. --- osu.Game/Screens/Menu/IntroCircles.cs | 11 ++++++++--- osu.Game/Screens/Menu/IntroScreen.cs | 14 ++++++++++---- osu.Game/Screens/Menu/IntroTriangles.cs | 15 +++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index faa9a267ce..5f481ed00e 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -19,8 +19,11 @@ namespace osu.Game.Screens.Menu protected override string BeatmapFile => "circles.osz"; + public const double TRACK_START_DELAY_NON_THEMED = 1000; + private const double track_start_delay_themed = 600; + + private const double delay_for_menu = 2900; private const double delay_step_one = 2300; - private const double delay_step_two = 600; private Sample welcome; @@ -44,14 +47,16 @@ namespace osu.Game.Screens.Menu { welcome?.Play(); + double trackStartDelay = UsingThemedIntro ? track_start_delay_themed : TRACK_START_DELAY_NON_THEMED; + Scheduler.AddDelayed(delegate { StartTrack(); PrepareMenuLoad(); - Scheduler.AddDelayed(LoadMenu, delay_step_one); - }, delay_step_two); + Scheduler.AddDelayed(LoadMenu, delay_for_menu - trackStartDelay); + }, trackStartDelay); logo.ScaleTo(1); logo.FadeIn(); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index a2ecd7eacb..bf713997f7 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -272,11 +272,17 @@ namespace osu.Game.Screens.Menu FadeInBackground(200); } - protected virtual void StartTrack() + protected void StartTrack() { - // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. - if (UsingThemedIntro) - Track.Start(); + var drawableTrack = musicController.CurrentTrack; + + drawableTrack.Start(); + + if (!UsingThemedIntro) + { + drawableTrack.VolumeTo(0).Then() + .VolumeTo(1, 2000, Easing.OutQuint); + } } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 6ad0350e43..4ec79b852a 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -84,9 +84,17 @@ namespace osu.Game.Screens.Menu return; if (!UsingThemedIntro) + { + // If the user has requested no theme, fallback to the same intro voice and delay as IntroCircles. + // The triangles intro voice and theme are combined which makes it impossible to use. welcome?.Play(); + Scheduler.AddDelayed(StartTrack, IntroCircles.TRACK_START_DELAY_NON_THEMED); + } + else + StartTrack(); - StartTrack(); + // no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure. + decoupledClock.Start(); }); } } @@ -99,11 +107,6 @@ namespace osu.Game.Screens.Menu intro.Expire(); } - protected override void StartTrack() - { - decoupledClock.Start(); - } - private class TrianglesIntroSequence : CompositeDrawable { private readonly OsuLogo logo; From d021218d78cf102b45bf52d3670e5be9fc507a64 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 16 Aug 2022 23:05:35 +0200 Subject: [PATCH 39/46] added test for objects which cant be merged --- .../Editor/TestSceneObjectMerging.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 63e6760514..b68231ce64 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -141,6 +141,33 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } + [Test] + public void TestNonMerge() + { + HitCircle? circle1 = null; + HitCircle? circle2 = null; + Spinner? spinner = null; + + AddStep("select first two circles and spinner", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1); + spinner = (Spinner)EditorBeatmap.HitObjects.First(h => h is Spinner); + EditorClock.Seek(spinner.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(circle2); + EditorBeatmap.SelectedHitObjects.Add(spinner); + }); + + mergeSelection(); + + AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( + (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle2.Position, pathType: null))); + + AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner)); + } + private void mergeSelection() { AddStep("merge selection", () => From 37799e3b31edc2fe4b71ce8308fbceecb2ec8646 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 13:20:24 +0900 Subject: [PATCH 40/46] Allow preparing preview point without looping --- osu.Game/Beatmaps/IWorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 4 ++-- osu.Game/Screens/Menu/MainMenu.cs | 2 +- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 548341cc77..a39766abe1 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps /// /// Reads the correct track restart point from beatmap metadata and sets looping to enabled. /// - void PrepareTrackForPreviewLooping(); + void PrepareTrackForPreview(bool looping); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 16464932e0..301610ee58 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -110,9 +110,9 @@ namespace osu.Game.Beatmaps public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000); - public void PrepareTrackForPreviewLooping() + public void PrepareTrackForPreview(bool looping) { - Track.Looping = true; + Track.Looping = looping; Track.RestartPoint = Metadata.PreviewTime; if (Track.RestartPoint == -1) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 066a37055c..0071ada05a 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -192,7 +192,7 @@ namespace osu.Game.Screens.Menu // presume the track is the current beatmap's track. not sure how correct this assumption is but it has worked until now. if (!track.IsRunning) { - Beatmap.Value.PrepareTrackForPreviewLooping(); + Beatmap.Value.PrepareTrackForPreview(false); track.Restart(); } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 25f2a94a3c..03216180fb 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -485,7 +485,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (track != null) { - Beatmap.Value.PrepareTrackForPreviewLooping(); + Beatmap.Value.PrepareTrackForPreview(true); music?.EnsurePlayingSomething(); } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 33ff31857f..0c2ca6d4af 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -683,7 +683,7 @@ namespace osu.Game.Screens.Select } private void ensureTrackLooping(IWorkingBeatmap beatmap, TrackChangeDirection changeDirection) - => beatmap.PrepareTrackForPreviewLooping(); + => beatmap.PrepareTrackForPreview(true); public override bool OnBackButton() { From d9346abb9c3b390357d060441fc7020915573c9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 13:20:40 +0900 Subject: [PATCH 41/46] Tweak intro timings more and ensure non-theme tracks play from preview point --- osu.Game/Screens/Menu/IntroCircles.cs | 10 +++------- osu.Game/Screens/Menu/IntroScreen.cs | 14 ++++++++++---- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index 5f481ed00e..7a4bdb231f 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -19,11 +19,9 @@ namespace osu.Game.Screens.Menu protected override string BeatmapFile => "circles.osz"; - public const double TRACK_START_DELAY_NON_THEMED = 1000; - private const double track_start_delay_themed = 600; + public const double TRACK_START_DELAY = 600; private const double delay_for_menu = 2900; - private const double delay_step_one = 2300; private Sample welcome; @@ -47,16 +45,14 @@ namespace osu.Game.Screens.Menu { welcome?.Play(); - double trackStartDelay = UsingThemedIntro ? track_start_delay_themed : TRACK_START_DELAY_NON_THEMED; - Scheduler.AddDelayed(delegate { StartTrack(); PrepareMenuLoad(); - Scheduler.AddDelayed(LoadMenu, delay_for_menu - trackStartDelay); - }, trackStartDelay); + Scheduler.AddDelayed(LoadMenu, delay_for_menu - TRACK_START_DELAY); + }, TRACK_START_DELAY); logo.ScaleTo(1); logo.FadeIn(); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index bf713997f7..bd49eba663 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -276,12 +276,18 @@ namespace osu.Game.Screens.Menu { var drawableTrack = musicController.CurrentTrack; - drawableTrack.Start(); - if (!UsingThemedIntro) { - drawableTrack.VolumeTo(0).Then() - .VolumeTo(1, 2000, Easing.OutQuint); + initialBeatmap.PrepareTrackForPreview(false); + + drawableTrack.VolumeTo(0); + drawableTrack.Restart(); + drawableTrack.VolumeTo(1, 2200, Easing.InCubic); + } + else + + { + drawableTrack.Restart(); } } diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 4ec79b852a..d777f78df2 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -88,7 +88,7 @@ namespace osu.Game.Screens.Menu // If the user has requested no theme, fallback to the same intro voice and delay as IntroCircles. // The triangles intro voice and theme are combined which makes it impossible to use. welcome?.Play(); - Scheduler.AddDelayed(StartTrack, IntroCircles.TRACK_START_DELAY_NON_THEMED); + Scheduler.AddDelayed(StartTrack, IntroCircles.TRACK_START_DELAY); } else StartTrack(); From 0ff5547b8356fe9e06f1c3dc1325f2073bdfd608 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 13:35:44 +0900 Subject: [PATCH 42/46] Make `changeHandler` optional in `convertToStream` method to match new implementation --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index bd11cc826f..794551dab7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -251,13 +251,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void convertToStream() { - if (editorBeatmap == null || changeHandler == null || beatDivisor == null) + if (editorBeatmap == null || beatDivisor == null) return; var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime); double streamSpacing = timingPoint.BeatLength / beatDivisor.Value; - changeHandler.BeginChange(); + changeHandler?.BeginChange(); int i = 0; double time = HitObject.StartTime; @@ -292,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders editorBeatmap.Remove(HitObject); - changeHandler.EndChange(); + changeHandler?.EndChange(); } public override MenuItem[] ContextMenuItems => new MenuItem[] From 8b5ac55fca7b2cb3a5bf6a826de7f461aee14b3e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 17 Aug 2022 13:48:06 +0900 Subject: [PATCH 43/46] Remove newline --- osu.Game/Screens/Menu/IntroScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index bd49eba663..a07ebb463a 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -285,7 +285,6 @@ namespace osu.Game.Screens.Menu drawableTrack.VolumeTo(1, 2200, Easing.InCubic); } else - { drawableTrack.Restart(); } From 8ce50e98a6ef52132350f77f186d9c01dec2b681 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 14:04:55 +0900 Subject: [PATCH 44/46] Move delegate debounce logic to `Editor` itself --- osu.Game/Screens/Edit/Editor.cs | 21 ++++++++++++++++++++- osu.Game/Screens/Edit/EditorBeatmap.cs | 24 ++++++++---------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 90e912bd6d..9cf0a0fc0c 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -24,6 +24,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Threading; using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -231,6 +232,8 @@ namespace osu.Game.Screens.Edit AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo)); dependencies.CacheAs(editorBeatmap); + editorBeatmap.UpdateInProgress.BindValueChanged(updateInProgress); + canSave = editorBeatmap.BeatmapInfo.Ruleset.CreateInstance() is ILegacyRuleset; if (canSave) @@ -431,7 +434,7 @@ namespace osu.Game.Screens.Edit samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value || currentScreen is not ComposeScreen - || editorBeatmap.UpdateInProgress; + || temporaryMuteFromUpdateInProgress; } public bool OnPressed(KeyBindingPressEvent e) @@ -717,6 +720,22 @@ namespace osu.Game.Screens.Edit this.Exit(); } + #region Mute from update application + + private ScheduledDelegate temporaryMuteRestorationDelegate; + private bool temporaryMuteFromUpdateInProgress; + + private void updateInProgress(ValueChangedEvent obj) + { + temporaryMuteFromUpdateInProgress = true; + + // Debounce is arbitrarily high enough to avoid flip-flopping the value each other frame. + temporaryMuteRestorationDelegate?.Cancel(); + temporaryMuteRestorationDelegate = Scheduler.AddDelayed(() => temporaryMuteFromUpdateInProgress = false, 50); + } + + #endregion + #region Clipboard support private EditorMenuItem cutMenuItem; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index b9b0879fa4..6154485bc2 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -10,7 +10,6 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -24,13 +23,15 @@ namespace osu.Game.Screens.Edit public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapProvider { /// - /// While performing updates on hitobjects, this will momentarily become true. + /// Will become true when a new update is queued, and false when all updates have been applied. /// /// /// This is intended to be used to avoid performing operations (like playback of samples) /// while mutating hitobjects. /// - public bool UpdateInProgress { get; private set; } + public IBindable UpdateInProgress => updateInProgress; + + private readonly BindableBool updateInProgress = new BindableBool(); /// /// Invoked when a is added to this . @@ -236,11 +237,9 @@ namespace osu.Game.Screens.Edit // updates are debounced regardless of whether a batch is active. batchPendingUpdates.Add(hitObject); - advertiseUpdateInProgress(); + updateInProgress.Value = true; } - private ScheduledDelegate updateCompleteDelegate; - /// /// Update all hit objects with potentially changed difficulty or control point data. /// @@ -249,7 +248,7 @@ namespace osu.Game.Screens.Edit foreach (var h in HitObjects) batchPendingUpdates.Add(h); - advertiseUpdateInProgress(); + updateInProgress.Value = true; } /// @@ -342,6 +341,8 @@ namespace osu.Game.Screens.Edit foreach (var h in deletes) HitObjectRemoved?.Invoke(h); foreach (var h in inserts) HitObjectAdded?.Invoke(h); foreach (var h in updates) HitObjectUpdated?.Invoke(h); + + updateInProgress.Value = false; } /// @@ -349,15 +350,6 @@ namespace osu.Game.Screens.Edit /// public void Clear() => RemoveRange(HitObjects.ToArray()); - private void advertiseUpdateInProgress() - { - UpdateInProgress = true; - - // Debounce is arbitrarily high enough to avoid flip-flopping the value each other frame. - updateCompleteDelegate?.Cancel(); - updateCompleteDelegate = Scheduler.AddDelayed(() => UpdateInProgress = false, 50); - } - private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, PlayableBeatmap.Difficulty); private void trackStartTime(HitObject hitObject) From 6b9dec599698d8ee371837eb47b84e6b182d165a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 14:32:21 +0900 Subject: [PATCH 45/46] Restore original event flow to allow for `OnSuspend` case to work correctly --- osu.Game/Screens/Edit/Editor.cs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9cf0a0fc0c..a7cbe1f1ad 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -226,6 +226,8 @@ namespace osu.Game.Screens.Edit dependencies.CacheAs(clock); AddInternal(clock); + clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState()); + // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); @@ -429,12 +431,7 @@ namespace osu.Game.Screens.Edit protected override void Update() { base.Update(); - clock.ProcessFrame(); - - samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value - || currentScreen is not ComposeScreen - || temporaryMuteFromUpdateInProgress; } public bool OnPressed(KeyBindingPressEvent e) @@ -728,10 +725,15 @@ namespace osu.Game.Screens.Edit private void updateInProgress(ValueChangedEvent obj) { temporaryMuteFromUpdateInProgress = true; + updateSampleDisabledState(); // Debounce is arbitrarily high enough to avoid flip-flopping the value each other frame. temporaryMuteRestorationDelegate?.Cancel(); - temporaryMuteRestorationDelegate = Scheduler.AddDelayed(() => temporaryMuteFromUpdateInProgress = false, 50); + temporaryMuteRestorationDelegate = Scheduler.AddDelayed(() => + { + temporaryMuteFromUpdateInProgress = false; + updateSampleDisabledState(); + }, 50); } #endregion @@ -844,10 +846,18 @@ namespace osu.Game.Screens.Edit } finally { + updateSampleDisabledState(); rebindClipboardBindables(); } } + private void updateSampleDisabledState() + { + samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value + || currentScreen is not ComposeScreen + || temporaryMuteFromUpdateInProgress; + } + private void seek(UIEvent e, int direction) { double amount = e.ShiftPressed ? 4 : 1; @@ -952,7 +962,11 @@ namespace osu.Game.Screens.Edit protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); - private void cancelExit() => loader?.CancelPendingDifficultySwitch(); + private void cancelExit() + { + updateSampleDisabledState(); + loader?.CancelPendingDifficultySwitch(); + } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); From 21b364cd7758f9574b94f3bdd97970e905c6bd18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 14:48:00 +0900 Subject: [PATCH 46/46] Fix nullref in tests as `initialBeatmap` may be null --- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index a07ebb463a..409c7d6c8d 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -278,7 +278,7 @@ namespace osu.Game.Screens.Menu if (!UsingThemedIntro) { - initialBeatmap.PrepareTrackForPreview(false); + initialBeatmap?.PrepareTrackForPreview(false); drawableTrack.VolumeTo(0); drawableTrack.Restart();