From 0bd34a4f13eb79a0dba578f5a17c20e872976a09 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 25 Jul 2023 21:14:21 -0700 Subject: [PATCH 001/103] Always show supporter-only playlist durations --- osu.Game/Localisation/OnlinePlayStrings.cs | 19 +++++++ .../Match/Components/RoomSettingsOverlay.cs | 5 +- .../Playlists/PlaylistsRoomSettingsOverlay.cs | 55 +++++++++++++------ 3 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Localisation/OnlinePlayStrings.cs diff --git a/osu.Game/Localisation/OnlinePlayStrings.cs b/osu.Game/Localisation/OnlinePlayStrings.cs new file mode 100644 index 0000000000..e9f17cddc0 --- /dev/null +++ b/osu.Game/Localisation/OnlinePlayStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class OnlinePlayStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.OnlinePlay"; + + /// + /// "This duration is only available for osu!supporters." + /// + public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"This duration is only available for osu!supporters."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs index 05232fe0e2..916b799d50 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs @@ -113,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected partial class Section : Container { - private readonly Container content; + private readonly ReverseChildIDFillFlowContainer content; protected override Container Content => content; @@ -135,10 +135,11 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), Text = title.ToUpperInvariant(), }, - content = new Container + content = new ReverseChildIDFillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical }, }, }; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index e93f56c2e2..c69ccb7b1c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -23,6 +23,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -80,6 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private IBindable localUser = null!; private readonly Room room; + private OsuSpriteText durationNoticeText = null!; public MatchSettings(Room room) { @@ -141,14 +143,22 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }, new Section("Duration") { - Child = new Container + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - Height = 40, - Child = DurationField = new DurationDropdown + new Container { - RelativeSizeAxes = Axes.X - } + RelativeSizeAxes = Axes.X, + Height = 40, + Child = DurationField = new DurationDropdown + { + RelativeSizeAxes = Axes.X + }, + }, + durationNoticeText = new OsuSpriteText + { + Alpha = 0, + Colour = colours.Yellow, + }, } }, new Section("Allowed attempts (across all playlist items)") @@ -305,6 +315,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true); Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); + DurationField.Current.BindValueChanged(duration => + { + if (selectedAvailableDuration) + durationNoticeText.Hide(); + else + { + durationNoticeText.Show(); + durationNoticeText.Text = OnlinePlayStrings.SupporterOnlyDurationNotice; + } + }); + localUser = api.LocalUser.GetBoundCopy(); localUser.BindValueChanged(populateDurations, true); @@ -314,6 +335,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void populateDurations(ValueChangedEvent user) { + // roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427) + // if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though. + const int days_in_month = 31; + DurationField.Items = new[] { TimeSpan.FromMinutes(30), @@ -326,18 +351,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists TimeSpan.FromDays(3), TimeSpan.FromDays(7), TimeSpan.FromDays(14), + TimeSpan.FromDays(days_in_month), + TimeSpan.FromDays(days_in_month * 3), }; - - // TODO: show these in the interface at all times. - if (user.NewValue.IsSupporter) - { - // roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427) - // if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though. - const int days_in_month = 31; - - DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month)); - DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month * 3)); - } } protected override void Update() @@ -352,7 +368,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) => playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; - private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; + private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0 + && selectedAvailableDuration; + + private bool selectedAvailableDuration => DurationField.Current.Value < TimeSpan.FromDays(31) || localUser.Value.IsSupporter; private void apply() { From 30e0e00c520d8072eb9cfe8fca2c0e2a14fd06e9 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 25 Jul 2023 22:57:01 -0700 Subject: [PATCH 002/103] Rename `selectedAvailableDuration` to `hasValidDuration` --- .../OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index c69ccb7b1c..f7bd7fc530 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -317,7 +317,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists DurationField.Current.BindValueChanged(duration => { - if (selectedAvailableDuration) + if (hasValidDuration) durationNoticeText.Hide(); else { @@ -369,9 +369,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0 - && selectedAvailableDuration; + && hasValidDuration; - private bool selectedAvailableDuration => DurationField.Current.Value < TimeSpan.FromDays(31) || localUser.Value.IsSupporter; + private bool hasValidDuration => DurationField.Current.Value < TimeSpan.FromDays(31) || localUser.Value.IsSupporter; private void apply() { From c1ba8fe175c9bff9e018f2409fbc3cfa041e4902 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 16:07:45 +0900 Subject: [PATCH 003/103] Rename `BackgroundBeatmapProcessor` to `BackgroundDataStoreProcessor` --- ...orTests.cs => BackgroundDataStoreProcessorTests.cs} | 8 ++++---- ...mapProcessor.cs => BackgroundDataStoreProcessor.cs} | 10 +++++++--- osu.Game/OsuGame.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) rename osu.Game.Tests/Database/{BackgroundBeatmapProcessorTests.cs => BackgroundDataStoreProcessorTests.cs} (92%) rename osu.Game/{BackgroundBeatmapProcessor.cs => BackgroundDataStoreProcessor.cs} (97%) diff --git a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs similarity index 92% rename from osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs rename to osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index c876316be4..15bb0fc09c 100644 --- a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -15,7 +15,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Database { [HeadlessTest] - public partial class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo + public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo { public IBindable IsPlaying => isPlaying; @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => { - Add(new TestBackgroundBeatmapProcessor()); + Add(new TestBackgroundDataStoreProcessor()); }); AddUntilStep("wait for difficulties repopulated", () => @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => { - Add(new TestBackgroundBeatmapProcessor()); + Add(new TestBackgroundDataStoreProcessor()); }); AddWaitStep("wait some", 500); @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Database }); } - public partial class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor + public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor { protected override int TimeToSleepDuringGameplay => 10; } diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundDataStoreProcessor.cs similarity index 97% rename from osu.Game/BackgroundBeatmapProcessor.cs rename to osu.Game/BackgroundDataStoreProcessor.cs index b553fee503..c475a07c79 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundDataStoreProcessor.cs @@ -24,7 +24,10 @@ using osu.Game.Screens.Play; namespace osu.Game { - public partial class BackgroundBeatmapProcessor : Component + /// + /// Performs background updating of data stores at startup. + /// + public partial class BackgroundDataStoreProcessor : Component { [Resolved] private RulesetStore rulesetStore { get; set; } = null!; @@ -61,7 +64,8 @@ namespace osu.Game Task.Factory.StartNew(() => { - Logger.Log("Beginning background beatmap processing.."); + Logger.Log("Beginning background data store processing.."); + checkForOutdatedStarRatings(); processBeatmapSetsWithMissingMetrics(); processScoresWithMissingStatistics(); @@ -74,7 +78,7 @@ namespace osu.Game return; } - Logger.Log("Finished background beatmap processing!"); + Logger.Log("Finished background data store processing!"); }); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1a40bb8e3d..c60bff9e4c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1025,7 +1025,7 @@ namespace osu.Game loadComponentSingleFile(CreateHighPerformanceSession(), Add); - loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add); + loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); Add(difficultyRecommender); Add(externalLinkOpener = new ExternalLinkOpener()); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index c6f4433824..a3bbb2c09e 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -66,7 +66,7 @@ namespace osu.Game.Scoring /// If this does not match , /// the total score has not yet been updated to reflect the current scoring values. /// - /// See 's conversion logic. + /// See 's conversion logic. /// /// /// This may not match the version stored in the replay files. From 30baac0f3db82d4df246c75bb458113be90c210e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 16:08:02 +0900 Subject: [PATCH 004/103] Avoid reprocessing scores which already failed an upgrade previously Closes https://github.com/ppy/osu/issues/24301. --- osu.Game/BackgroundDataStoreProcessor.cs | 6 ++++-- osu.Game/Database/RealmAccess.cs | 3 ++- osu.Game/Scoring/ScoreInfo.cs | 9 +++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/BackgroundDataStoreProcessor.cs b/osu.Game/BackgroundDataStoreProcessor.cs index c475a07c79..ae9e9527de 100644 --- a/osu.Game/BackgroundDataStoreProcessor.cs +++ b/osu.Game/BackgroundDataStoreProcessor.cs @@ -186,7 +186,7 @@ namespace osu.Game realmAccess.Run(r => { - foreach (var score in r.All()) + foreach (var score in r.All().Where(s => !s.TotalScoreUpgradeFailed)) { if (score.BeatmapInfo != null && score.Statistics.Sum(kvp => kvp.Value) > 0 @@ -225,6 +225,7 @@ namespace osu.Game catch (Exception e) { Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); + realmAccess.Write(r => r.Find(id)!.TotalScoreUpgradeFailed = true); } } } @@ -234,7 +235,7 @@ namespace osu.Game Logger.Log("Querying for scores that need total score conversion..."); HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All() - .Where(s => s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) + .Where(s => !s.TotalScoreUpgradeFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) .AsEnumerable().Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); @@ -283,6 +284,7 @@ namespace osu.Game catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); + realmAccess.Write(r => r.Find(id)!.TotalScoreUpgradeFailed = true); ++failedCount; } } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f32b161bb6..04a4b28fa4 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -82,8 +82,9 @@ namespace osu.Game.Database /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. + /// 33 2023-07-26 Add TotalScoreUpgradeFailed flag to ScoreInfo to track upgrade failures. /// - private const int schema_version = 32; + private const int schema_version = 33; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index a3bbb2c09e..c3a45332e4 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -81,6 +81,15 @@ namespace osu.Game.Scoring /// public long? LegacyTotalScore { get; set; } + /// + /// If an reprocess of total score failed to update this score to the latest version, this flag will become true. + /// Should be used to ensure we don't repeatedly attempt to update the same scores each startup even though we already know they will fail. + /// + /// + /// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur(missing beatmap file on disk). + /// + public bool TotalScoreUpgradeFailed { get; set; } + public int MaxCombo { get; set; } public double Accuracy { get; set; } From a2f1ced5a224e6ff1c239c430be62793b3b24fe0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 16:22:21 +0900 Subject: [PATCH 005/103] Add test coverage of failed-to-upgrade flag --- .../BackgroundDataStoreProcessorTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 15bb0fc09c..d7ec572026 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -8,6 +8,9 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Visual; @@ -124,6 +127,32 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestScoreUpgradeFailed() + { + ScoreInfo scoreInfo = null!; + + AddStep("Add score which requires upgrade (but has no beatmap)", () => + { + Realm.Write(r => + { + r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo(), + Ruleset = r.All().First(), + }) + { + TotalScoreVersion = 30000002, + IsLegacyScore = true, + }); + }); + }); + + AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); + + AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreUpgradeFailed)); + } + public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor { protected override int TimeToSleepDuringGameplay => 10; From 235cb2d73fcc64ffb2c85d1dafb2a03486557276 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 16:28:38 +0900 Subject: [PATCH 006/103] Add test coverage of upgrade success for good measure --- .../BackgroundDataStoreProcessorTests.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index d7ec572026..23b88b7395 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -127,6 +127,30 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestScoreUpgradeSuccess() + { + ScoreInfo scoreInfo = null!; + + AddStep("Add score which requires upgrade (but has no beatmap)", () => + { + Realm.Write(r => + { + r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: r.All().First()) + { + TotalScoreVersion = 30000002, + LegacyTotalScore = 123456, + IsLegacyScore = true, + }); + }); + }); + + AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); + + AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION)); + AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreUpgradeFailed), () => Is.False); + } + [Test] public void TestScoreUpgradeFailed() { @@ -150,7 +174,8 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); - AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreUpgradeFailed)); + AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreUpgradeFailed), () => Is.True); + AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002)); } public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor From 9d87ff2986f3430b4c44b03d895720f5c8b57bd3 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 26 Jul 2023 01:37:55 -0700 Subject: [PATCH 007/103] Improve supporter-only duration notice and fix max duration of non-supporter See https://github.com/ppy/osu-web/blob/3c542292569b336c4dff38e2b52a823149ec1a81/.env.example#L230. Might have more duration options in the future. --- osu.Game/Localisation/OnlinePlayStrings.cs | 4 ++-- .../OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/OnlinePlayStrings.cs b/osu.Game/Localisation/OnlinePlayStrings.cs index e9f17cddc0..1853cb753a 100644 --- a/osu.Game/Localisation/OnlinePlayStrings.cs +++ b/osu.Game/Localisation/OnlinePlayStrings.cs @@ -10,9 +10,9 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.OnlinePlay"; /// - /// "This duration is only available for osu!supporters." + /// "Playlist durations longer than 2 weeks require an active osu!supporter tag." /// - public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"This duration is only available for osu!supporters."); + public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag."); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index f7bd7fc530..84e419d67a 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -371,7 +371,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0 && hasValidDuration; - private bool hasValidDuration => DurationField.Current.Value < TimeSpan.FromDays(31) || localUser.Value.IsSupporter; + private bool hasValidDuration => DurationField.Current.Value <= TimeSpan.FromDays(14) || localUser.Value.IsSupporter; private void apply() { From d2f5e696e77072138dc0bafd5e62b3567d165f07 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 11 Aug 2023 22:34:04 +0200 Subject: [PATCH 008/103] Fix DrawableHitObject state not synchronizing with hitobject edits --- .../Objects/Drawables/DrawableHitObject.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index bf649a0a15..f141263e27 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -260,7 +260,6 @@ namespace osu.Game.Rulesets.Objects.Drawables } StartTimeBindable.BindTo(HitObject.StartTimeBindable); - StartTimeBindable.BindValueChanged(onStartTimeChanged); if (HitObject is IHasComboInformation combo) { @@ -311,9 +310,6 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.UnbindFrom(HitObject.SamplesBindable); - // Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway. - StartTimeBindable.ValueChanged -= onStartTimeChanged; - // When a new hitobject is applied, the samples will be cleared before re-populating. // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). samplesBindable.CollectionChanged -= onSamplesChanged; @@ -333,6 +329,8 @@ namespace osu.Game.Rulesets.Objects.Drawables Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry); ClearNestedHitObjects(); + // Changes in state trigger defaults applied trigger state updates. + // When a new hitobject is applied, OnApply() automatically performs a state update anyway. HitObject.DefaultsApplied -= onDefaultsApplied; entry.RevertResult -= onRevertResult; @@ -375,8 +373,6 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); - private void onStartTimeChanged(ValueChangedEvent startTime) => updateState(State.Value, true); - private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onRevertResult() @@ -394,6 +390,13 @@ namespace osu.Game.Rulesets.Objects.Drawables Debug.Assert(Entry != null); Apply(Entry); + // Applied defaults indicate a change in hit object state. + if (Result is not null) + { + Result.TimeOffset = 0; + updateState(State.Value, true); + } + DefaultsApplied?.Invoke(this); } From 5d1ccc2601a739e2d8ed61b8d9b64a0efa580f7f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 11 Aug 2023 23:39:28 +0200 Subject: [PATCH 009/103] Ensure invariant of monotone time --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 30 ++++++++++++++++++- osu.Game/Rulesets/UI/Playfield.cs | 35 ++++++++++++++++++---- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 099be486b3..e919e4d088 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -51,6 +51,11 @@ namespace osu.Game.Rulesets.UI [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } + /// + /// Invoked when a is updated. + /// + public event Action HitObjectUpdated; + public HitObjectContainer() { RelativeSizeAxes = Axes.Both; @@ -108,6 +113,7 @@ namespace osu.Game.Rulesets.UI drawable.OnNewResult += onNewResult; bindStartTime(drawable); + bindUpdated(drawable); AddInternal(drawable); } @@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.UI drawable.OnNewResult -= onNewResult; unbindStartTime(drawable); - + unbindUpdated(drawable); RemoveInternal(drawable, false); } @@ -176,6 +182,27 @@ namespace osu.Game.Rulesets.UI startTimeMap.Clear(); } + private void bindUpdated(DrawableHitObject hitObject) + { + hitObject.DefaultsApplied += onDefaultsApplied; + } + + private void unbindUpdated(DrawableHitObject hitObject) + { + hitObject.DefaultsApplied += onDefaultsApplied; + } + + private void unbindAllUpdated() + { + foreach (var h in AliveObjects) + unbindUpdated(h); + } + + private void onDefaultsApplied(DrawableHitObject obj) + { + HitObjectUpdated?.Invoke(obj.HitObject); + } + protected override int Compare(Drawable x, Drawable y) { if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) @@ -192,6 +219,7 @@ namespace osu.Game.Rulesets.UI { base.Dispose(isDisposing); unbindAllStartTimes(); + unbindAllUpdated(); } } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e9c35555c8..9a4d082a06 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.UI private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager(); - private readonly Stack judgedEntries; + private readonly LinkedList judgedEntries; /// /// Creates a new . @@ -125,12 +125,13 @@ namespace osu.Game.Rulesets.UI h.NewResult += onNewResult; h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); + h.HitObjectUpdated += onHitObjectUpdated; })); entryManager.OnEntryAdded += onEntryAdded; entryManager.OnEntryRemoved += onEntryRemoved; - judgedEntries = new Stack(); + judgedEntries = new LinkedList(); } [BackgroundDependencyLoader] @@ -270,15 +271,16 @@ namespace osu.Game.Rulesets.UI } // When rewinding, revert future judgements in the reverse order. - while (judgedEntries.Count > 0) + while (judgedEntries.Last is not null) { - var result = judgedEntries.Peek().Result; + var result = judgedEntries.Last.Value.Result; Debug.Assert(result?.RawTime != null); if (Time.Current >= result.RawTime.Value) break; - revertResult(judgedEntries.Pop()); + revertResult(judgedEntries.Last.Value); + judgedEntries.RemoveLast(); } } @@ -471,10 +473,31 @@ namespace osu.Game.Rulesets.UI #endregion + private void onHitObjectUpdated(HitObject _) + { + // The time of judged entries may have changed, so we need to re-sort the list to preserve the invariant of monotone time. + // Insertion sort on linked-list is O(n) for nearly-sorted lists, which is the case here. + var current = judgedEntries.First; + + while (current?.Next is not null) + { + var next = current.Next; + + if (current.Value.Result?.RawTime > next.Value.Result?.RawTime) + { + judgedEntries.Remove(next); + judgedEntries.AddBefore(current, next); + current = next.Previous; + } + else + current = next; + } + } + private void onNewResult(DrawableHitObject drawable, JudgementResult result) { Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null); - judgedEntries.Push(drawable.Entry.AsNonNull()); + judgedEntries.AddLast(drawable.Entry.AsNonNull()); NewResult?.Invoke(drawable, result); } From 90f2acaf0a9d1bfa5ac8d4cc653798604a3fdf21 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 11 Aug 2023 23:50:00 +0200 Subject: [PATCH 010/103] Fix typo --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index e919e4d088..76cfc049e3 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.UI private void unbindUpdated(DrawableHitObject hitObject) { - hitObject.DefaultsApplied += onDefaultsApplied; + hitObject.DefaultsApplied -= onDefaultsApplied; } private void unbindAllUpdated() From 8912a0e91e79e05264b56754493fe4be5e355b40 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sat, 12 Aug 2023 20:30:48 +0200 Subject: [PATCH 011/103] Fix sliders being reversed incorrectly in the editor. --- osu.Game/Rulesets/Objects/SliderPath.cs | 2 +- .../Rulesets/Objects/SliderPathExtensions.cs | 57 ++++++++++++++++++- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 028f8b6839..f48ffbe5a4 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -202,7 +202,7 @@ namespace osu.Game.Rulesets.Objects { ensureValid(); - return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength); + return segmentEnds.Select(i => cumulativeLength[Math.Clamp(i, 0, cumulativeLength.Count - 1)] / calculatedLength); } private void invalidate() diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 92a3b570fb..a312f872b8 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -1,7 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -26,10 +29,58 @@ namespace osu.Game.Rulesets.Objects /// The positional offset of the resulting path. It should be added to the start position of this path. public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset) { - var points = sliderPath.ControlPoints.ToArray(); + var controlPoints = sliderPath.ControlPoints; + double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); + double[] distinctSegmentEnds = segmentEnds.Distinct().ToArray(); + + // Remove control points at the end which do not affect the visual slider path ("invisible" control points). + if (segmentEnds[^1] == segmentEnds[^2] && distinctSegmentEnds.Length > 1) + { + int numVisibleSegments = distinctSegmentEnds.Length - 2; + var nonInheritedControlPoints = controlPoints.Where(p => p.Type is not null).ToList(); + + var lastVisibleControlPoint = nonInheritedControlPoints[numVisibleSegments]; + int lastVisibleControlPointIndex = controlPoints.IndexOf(lastVisibleControlPoint); + + if (controlPoints.Count > lastVisibleControlPointIndex + 1) + { + // Make sure to include all inherited control points directly after the last visible non-inherited control point. + do + { + lastVisibleControlPointIndex++; + } while (lastVisibleControlPointIndex + 1 < controlPoints.Count && controlPoints[lastVisibleControlPointIndex].Type is null); + } + + // Remove all control points after the first invisible non-inherited control point. + controlPoints.RemoveRange(lastVisibleControlPointIndex + 1, controlPoints.Count - lastVisibleControlPointIndex - 1); + } + + // Recalculate perfect curve at the end of the slider path. + if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && distinctSegmentEnds.Length > 1) + { + double lastSegmentStart = distinctSegmentEnds[^2]; + double lastSegmentEnd = distinctSegmentEnds[^1]; + + var oldCircleArcPath = new List(); + sliderPath.GetPathToProgress(oldCircleArcPath, lastSegmentStart / lastSegmentEnd, 1); + + var newCircleArcPoints = new[] + { + oldCircleArcPath[0], + oldCircleArcPath[oldCircleArcPath.Count / 2], + oldCircleArcPath[^1] + }; + + var newCircleArcPath = PathApproximator.ApproximateCircularArc(newCircleArcPoints.AsSpan()); + controlPoints[^2].Position = newCircleArcPath[newCircleArcPath.Count / 2]; + } + + // Reverse the control points. + + var points = controlPoints.ToArray(); positionalOffset = sliderPath.PositionAt(1); - sliderPath.ControlPoints.Clear(); + controlPoints.Clear(); PathType? lastType = null; @@ -47,7 +98,7 @@ namespace osu.Game.Rulesets.Objects else if (p.Type != null) (p.Type, lastType) = (lastType, p.Type); - sliderPath.ControlPoints.Insert(0, p); + controlPoints.Insert(0, p); } } } From b9d0a8a9f69fadacf1d1578fccd9101798583d47 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 13 Aug 2023 15:15:37 +0200 Subject: [PATCH 012/103] Fix TestSceneFruitRandomness --- osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index de3d9d6530..8e7f77285c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddSliderStep("start time", 500, 600, 0, x => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); } @@ -44,6 +46,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("Initialize start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); fruitRotation = drawableFruit.DisplayRotation; bananaRotation = drawableBanana.DisplayRotation; @@ -54,6 +58,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("change start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation); @@ -64,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("reset start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); AddAssert("rotation and size restored", () => From f42b3603b34546609ed9cd829053b770510e9847 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 14 Aug 2023 14:08:02 +0200 Subject: [PATCH 013/103] Fix linear sliders sometimes being reversed incorrectly Extract control point reversing to separate method --- .../Rulesets/Objects/SliderPathExtensions.cs | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index a312f872b8..2c8a6393af 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; @@ -30,31 +31,40 @@ namespace osu.Game.Rulesets.Objects public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset) { var controlPoints = sliderPath.ControlPoints; + var originalControlPointTypes = controlPoints.Select(p => p.Type).ToArray(); + + controlPoints[0].Type ??= PathType.Linear; + + // Inherited points after a linear point should be treated as linear points. + controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear).ForEach(p => p.Type = PathType.Linear); + double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); double[] distinctSegmentEnds = segmentEnds.Distinct().ToArray(); // Remove control points at the end which do not affect the visual slider path ("invisible" control points). - if (segmentEnds[^1] == segmentEnds[^2] && distinctSegmentEnds.Length > 1) + if (Math.Abs(segmentEnds[^1] - segmentEnds[^2]) < 1e-10 && distinctSegmentEnds.Length > 1) { int numVisibleSegments = distinctSegmentEnds.Length - 2; var nonInheritedControlPoints = controlPoints.Where(p => p.Type is not null).ToList(); - var lastVisibleControlPoint = nonInheritedControlPoints[numVisibleSegments]; - int lastVisibleControlPointIndex = controlPoints.IndexOf(lastVisibleControlPoint); + int lastVisibleControlPointIndex = controlPoints.IndexOf(nonInheritedControlPoints[numVisibleSegments]); - if (controlPoints.Count > lastVisibleControlPointIndex + 1) + // Make sure to include all inherited control points directly after the last visible non-inherited control point. + while (lastVisibleControlPointIndex + 1 < controlPoints.Count) { - // Make sure to include all inherited control points directly after the last visible non-inherited control point. - do - { - lastVisibleControlPointIndex++; - } while (lastVisibleControlPointIndex + 1 < controlPoints.Count && controlPoints[lastVisibleControlPointIndex].Type is null); + lastVisibleControlPointIndex++; + + if (controlPoints[lastVisibleControlPointIndex].Type is not null) + break; } // Remove all control points after the first invisible non-inherited control point. controlPoints.RemoveRange(lastVisibleControlPointIndex + 1, controlPoints.Count - lastVisibleControlPointIndex - 1); } + // Restore original control point types. + controlPoints.Zip(originalControlPointTypes).ForEach(x => x.First.Type = x.Second); + // Recalculate perfect curve at the end of the slider path. if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && distinctSegmentEnds.Length > 1) { @@ -75,12 +85,20 @@ namespace osu.Game.Rulesets.Objects controlPoints[^2].Position = newCircleArcPath[newCircleArcPath.Count / 2]; } - // Reverse the control points. + sliderPath.reverseControlPoints(out positionalOffset); + } - var points = controlPoints.ToArray(); + /// + /// Reverses the order of the provided 's s. + /// + /// The . + /// The positional offset of the resulting path. It should be added to the start position of this path. + private static void reverseControlPoints(this SliderPath sliderPath, out Vector2 positionalOffset) + { + var points = sliderPath.ControlPoints.ToArray(); positionalOffset = sliderPath.PositionAt(1); - controlPoints.Clear(); + sliderPath.ControlPoints.Clear(); PathType? lastType = null; @@ -98,7 +116,7 @@ namespace osu.Game.Rulesets.Objects else if (p.Type != null) (p.Type, lastType) = (lastType, p.Type); - controlPoints.Insert(0, p); + sliderPath.ControlPoints.Insert(0, p); } } } From 55ac942e7abe42d60958007b8819ebf58f72df2d Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 14 Aug 2023 14:09:08 +0200 Subject: [PATCH 014/103] Fix `IndexOutOfRangeException` when trying to reverse a zero-length slider --- osu.Game/Rulesets/Objects/SliderPathExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 2c8a6393af..1bc9668258 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Objects double[] distinctSegmentEnds = segmentEnds.Distinct().ToArray(); // Remove control points at the end which do not affect the visual slider path ("invisible" control points). - if (Math.Abs(segmentEnds[^1] - segmentEnds[^2]) < 1e-10 && distinctSegmentEnds.Length > 1) + if (segmentEnds.Length >= 2 && Math.Abs(segmentEnds[^1] - segmentEnds[^2]) < 1e-10 && distinctSegmentEnds.Length > 1) { int numVisibleSegments = distinctSegmentEnds.Length - 2; var nonInheritedControlPoints = controlPoints.Where(p => p.Type is not null).ToList(); From bbd91e3ee47a66e7107b398db60b716eacae3c67 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 14 Aug 2023 14:14:11 +0200 Subject: [PATCH 015/103] Add `TestSceneSliderReversal` --- .../Editor/TestSceneSliderReversal.cs | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs new file mode 100644 index 0000000000..ddef74deb1 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs @@ -0,0 +1,102 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +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.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public partial class TestSceneSliderReversal : TestSceneOsuEditor + { + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false); + + private readonly PathControlPoint[][] paths = + { + createPathSegment( + PathType.PerfectCurve, + new Vector2(200, -50), + new Vector2(250, 0) + ), + createPathSegment( + PathType.Linear, + new Vector2(100, 0), + new Vector2(100, 100) + ) + }; + + private static PathControlPoint[] createPathSegment(PathType type, params Vector2[] positions) + { + return new[] + { + new PathControlPoint + { + Type = type + }, + }.Concat(positions.Select(p => new PathControlPoint + { + Position = p + })).ToArray(); + } + + [TestCase(0, 250)] + [TestCase(0, 200)] + [TestCase(1, 80)] + [TestCase(1, 120)] + public void TestSliderReversal(int pathIndex, double length) + { + var controlPoints = paths[pathIndex]; + + Vector2 oldStartPos = default; + Vector2 oldEndPos = default; + double oldDistance = default; + + AddStep("Add slider", () => + { + var slider = new Slider + { + Position = new Vector2(OsuPlayfield.BASE_SIZE.X / 2, OsuPlayfield.BASE_SIZE.Y / 2), + Path = new SliderPath(controlPoints) + { + ExpectedDistance = { Value = length } + } + }; + + EditorBeatmap.Add(slider); + + oldStartPos = slider.Position; + oldEndPos = slider.EndPosition; + oldDistance = slider.Path.Distance; + }); + + AddStep("Select slider", () => + { + var slider = (Slider)EditorBeatmap.HitObjects[0]; + EditorBeatmap.SelectedHitObjects.Add(slider); + }); + + AddStep("Reverse slider", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.G); + InputManager.ReleaseKey(Key.LControl); + }); + + AddAssert("Slider was reversed correctly", () => + { + var slider = (Slider)EditorBeatmap.SelectedHitObjects[0]; + return Vector2.Distance(slider.Position, oldEndPos) < 1 + && Vector2.Distance(slider.EndPosition, oldStartPos) < 1 + && Math.Abs(slider.Path.Distance - oldDistance) < 1e-10; + }); + } + } +} From 19c8b74a47fbbb001bc5acf57d3ad921482ec597 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 14 Aug 2023 21:09:58 +0200 Subject: [PATCH 016/103] Remove unnecessary circle arc approximation --- osu.Game/Rulesets/Objects/SliderPathExtensions.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 1bc9668258..cb753229ae 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -71,18 +70,10 @@ namespace osu.Game.Rulesets.Objects double lastSegmentStart = distinctSegmentEnds[^2]; double lastSegmentEnd = distinctSegmentEnds[^1]; - var oldCircleArcPath = new List(); - sliderPath.GetPathToProgress(oldCircleArcPath, lastSegmentStart / lastSegmentEnd, 1); + var circleArcPath = new List(); + sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1); - var newCircleArcPoints = new[] - { - oldCircleArcPath[0], - oldCircleArcPath[oldCircleArcPath.Count / 2], - oldCircleArcPath[^1] - }; - - var newCircleArcPath = PathApproximator.ApproximateCircularArc(newCircleArcPoints.AsSpan()); - controlPoints[^2].Position = newCircleArcPath[newCircleArcPath.Count / 2]; + controlPoints[^2].Position = circleArcPath[circleArcPath.Count / 2]; } sliderPath.reverseControlPoints(out positionalOffset); From 449bee98cc1c1194d0fd111ae36afadb75fed117 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 14 Aug 2023 21:56:08 +0200 Subject: [PATCH 017/103] Code cleanup --- .../Editor/TestSceneSliderReversal.cs | 13 +++++-------- osu.Game/Rulesets/Objects/SliderPathExtensions.cs | 9 ++++++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs index ddef74deb1..f26a889ec6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs @@ -35,16 +35,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private static PathControlPoint[] createPathSegment(PathType type, params Vector2[] positions) { - return new[] - { - new PathControlPoint - { - Type = type - }, - }.Concat(positions.Select(p => new PathControlPoint + return positions.Select(p => new PathControlPoint { Position = p - })).ToArray(); + }).Prepend(new PathControlPoint + { + Type = type + }).ToArray(); } [TestCase(0, 250)] diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index cb753229ae..9e9b542961 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Objects double[] distinctSegmentEnds = segmentEnds.Distinct().ToArray(); // Remove control points at the end which do not affect the visual slider path ("invisible" control points). - if (segmentEnds.Length >= 2 && Math.Abs(segmentEnds[^1] - segmentEnds[^2]) < 1e-10 && distinctSegmentEnds.Length > 1) + if (segmentEnds.Length >= 2 && Precision.AlmostEquals(segmentEnds[^1], segmentEnds[^2]) && distinctSegmentEnds.Length > 1) { int numVisibleSegments = distinctSegmentEnds.Length - 2; var nonInheritedControlPoints = controlPoints.Where(p => p.Type is not null).ToList(); @@ -62,7 +62,10 @@ namespace osu.Game.Rulesets.Objects } // Restore original control point types. - controlPoints.Zip(originalControlPointTypes).ForEach(x => x.First.Type = x.Second); + for (int i = 0; i < controlPoints.Count; i++) + { + controlPoints[i].Type = originalControlPointTypes[i]; + } // Recalculate perfect curve at the end of the slider path. if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && distinctSegmentEnds.Length > 1) From 28994166bca85c7b2c83ec779d896d25b281cf6c Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 14 Aug 2023 22:09:12 +0200 Subject: [PATCH 018/103] Split up assertions --- .../Editor/TestSceneSliderReversal.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs index f26a889ec6..9f6b278609 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -44,6 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }).ToArray(); } + private Slider SelectedSlider => (Slider)EditorBeatmap.SelectedHitObjects[0]; + [TestCase(0, 250)] [TestCase(0, 200)] [TestCase(1, 80)] @@ -87,13 +90,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.ReleaseKey(Key.LControl); }); - AddAssert("Slider was reversed correctly", () => - { - var slider = (Slider)EditorBeatmap.SelectedHitObjects[0]; - return Vector2.Distance(slider.Position, oldEndPos) < 1 - && Vector2.Distance(slider.EndPosition, oldStartPos) < 1 - && Math.Abs(slider.Path.Distance - oldDistance) < 1e-10; - }); + AddAssert("Slider has correct length", () => + Precision.AlmostEquals(SelectedSlider.Path.Distance, oldDistance)); + + AddAssert("Slider has correct start position", () => + Vector2.Distance(SelectedSlider.Position, oldEndPos) < 1); + + AddAssert("Slider has correct end position", () => + Vector2.Distance(SelectedSlider.EndPosition, oldStartPos) < 1); } } } From cd706734636cd3d8da5bef4e9078c4b4d70cd5f9 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 14 Aug 2023 22:41:25 +0200 Subject: [PATCH 019/103] Fix property name --- .../Editor/TestSceneSliderReversal.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs index 9f6b278609..b804680f0d 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Utils; @@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }).ToArray(); } - private Slider SelectedSlider => (Slider)EditorBeatmap.SelectedHitObjects[0]; + private Slider selectedSlider => (Slider)EditorBeatmap.SelectedHitObjects[0]; [TestCase(0, 250)] [TestCase(0, 200)] @@ -91,13 +90,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); AddAssert("Slider has correct length", () => - Precision.AlmostEquals(SelectedSlider.Path.Distance, oldDistance)); + Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance)); AddAssert("Slider has correct start position", () => - Vector2.Distance(SelectedSlider.Position, oldEndPos) < 1); + Vector2.Distance(selectedSlider.Position, oldEndPos) < 1); AddAssert("Slider has correct end position", () => - Vector2.Distance(SelectedSlider.EndPosition, oldStartPos) < 1); + Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1); } } } From 6346872c3931b795fd20bad5326b1d62acff776e Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Tue, 15 Aug 2023 23:27:12 +0200 Subject: [PATCH 020/103] Improve code readability and add assertion to test scene --- .../Editor/TestSceneSliderReversal.cs | 10 +++++++++- .../Rulesets/Objects/SliderPathExtensions.cs | 19 ++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs index b804680f0d..9c5eb83e3c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs @@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [TestCase(0, 250)] [TestCase(0, 200)] - [TestCase(1, 80)] [TestCase(1, 120)] + [TestCase(1, 80)] public void TestSliderReversal(int pathIndex, double length) { var controlPoints = paths[pathIndex]; @@ -57,6 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Vector2 oldStartPos = default; Vector2 oldEndPos = default; double oldDistance = default; + var oldControlPointTypes = controlPoints.Select(p => p.Type); AddStep("Add slider", () => { @@ -97,6 +98,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("Slider has correct end position", () => Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1); + + AddAssert("Control points have correct types", () => + { + var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray(); + + return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes); + }); } } } diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 9e9b542961..682e31fa96 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; @@ -30,12 +29,17 @@ namespace osu.Game.Rulesets.Objects public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset) { var controlPoints = sliderPath.ControlPoints; - var originalControlPointTypes = controlPoints.Select(p => p.Type).ToArray(); - controlPoints[0].Type ??= PathType.Linear; + var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear && p.Type is null).ToList(); - // Inherited points after a linear point should be treated as linear points. - controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear).ForEach(p => p.Type = PathType.Linear); + if (controlPoints[0].Type == null) + { + inheritedLinearPoints.Add(controlPoints[0]); + } + + // Inherited points after a linear point, as well as the first control point if it inherited, + // should be treated as linear points, so their types are temporarily changed to linear. + inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear); double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); double[] distinctSegmentEnds = segmentEnds.Distinct().ToArray(); @@ -62,10 +66,7 @@ namespace osu.Game.Rulesets.Objects } // Restore original control point types. - for (int i = 0; i < controlPoints.Count; i++) - { - controlPoints[i].Type = originalControlPointTypes[i]; - } + inheritedLinearPoints.ForEach(p => p.Type = null); // Recalculate perfect curve at the end of the slider path. if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && distinctSegmentEnds.Length > 1) From e7e0c49f428075debc22c7607fcf44697e28c4ac Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Wed, 16 Aug 2023 01:14:25 +0200 Subject: [PATCH 021/103] Replace `.Distinct()` with `truncateEndingDuplicates()` --- .../Rulesets/Objects/SliderPathExtensions.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 682e31fa96..2fa460ec01 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Objects inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear); double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); - double[] distinctSegmentEnds = segmentEnds.Distinct().ToArray(); + double[] distinctSegmentEnds = truncateEndingDuplicates(segmentEnds); // Remove control points at the end which do not affect the visual slider path ("invisible" control points). if (segmentEnds.Length >= 2 && Precision.AlmostEquals(segmentEnds[^1], segmentEnds[^2]) && distinctSegmentEnds.Length > 1) @@ -83,6 +83,24 @@ namespace osu.Game.Rulesets.Objects sliderPath.reverseControlPoints(out positionalOffset); } + /// + /// Keeps removing the last element of the provided array until the last two elements are not equal. + /// + /// The array to truncate. + /// The truncated array. + private static double[] truncateEndingDuplicates(double[] arr) + { + if (arr.Length < 2) + return arr; + + var result = arr.ToList(); + + while (Precision.AlmostEquals(result[^1], result[^2])) + result.RemoveAt(result.Count - 1); + + return result.ToArray(); + } + /// /// Reverses the order of the provided 's s. /// From 58bffa13cd284891ebd735c216ca9bdab94ca8c8 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Wed, 16 Aug 2023 01:19:41 +0200 Subject: [PATCH 022/103] Fix possible `IndexOutOfRangeException` --- osu.Game/Rulesets/Objects/SliderPathExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 2fa460ec01..21448404ef 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Objects var result = arr.ToList(); - while (Precision.AlmostEquals(result[^1], result[^2])) + while (Precision.AlmostEquals(result[^1], result[^2]) && result.Count > 1) result.RemoveAt(result.Count - 1); return result.ToArray(); From 19f892687a0607afbe4e0d010366dc2a66236073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 21:00:35 +0200 Subject: [PATCH 023/103] Add precise rotation control to osu! editor --- .../Edit/OsuHitObjectComposer.cs | 5 + .../Edit/PreciseRotationPopover.cs | 107 ++++++++++++++++++ .../Edit/TransformToolboxGroup.cs | 80 +++++++++++++ .../UserInterfaceV2/LabelledTextBox.cs | 1 + .../UserInterfaceV2/SliderWithTextBoxInput.cs | 2 + .../Input/Bindings/GlobalActionContainer.cs | 4 + .../GlobalActionKeyBindingStrings.cs | 5 + .../Edit/Components/EditorToolButton.cs | 107 ++++++++++++++++++ .../Compose/Components/BlueprintContainer.cs | 2 +- .../Compose/Components/SelectionHandler.cs | 2 +- 10 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs create mode 100644 osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs create mode 100644 osu.Game/Screens/Edit/Components/EditorToolButton.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0b80750a02..cff2171cbd 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -85,6 +85,11 @@ namespace osu.Game.Rulesets.Osu.Edit // we may be entering the screen with a selection already active updateDistanceSnapGrid(); + + RightToolbox.Add(new TransformToolboxGroup + { + RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler + }); } protected override ComposeBlueprintContainer CreateBlueprintContainer() diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs new file mode 100644 index 0000000000..0bc7e72751 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -0,0 +1,107 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit.Components.RadioButtons; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class PreciseRotationPopover : OsuPopover + { + private readonly SelectionRotationHandler rotationHandler; + + private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre)); + + private SliderWithTextBoxInput angleInput = null!; + private EditorRadioButtonCollection rotationOrigin = null!; + + public PreciseRotationPopover(SelectionRotationHandler rotationHandler) + { + this.rotationHandler = rotationHandler; + + AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 220, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Children = new Drawable[] + { + angleInput = new SliderWithTextBoxInput("Angle (degrees):") + { + Current = new BindableNumber + { + MinValue = -180, + MaxValue = 180, + Precision = 1 + }, + Instantaneous = true + }, + rotationOrigin = new EditorRadioButtonCollection + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + new RadioButton("Playfield centre", + () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre }, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + new RadioButton("Selection centre", + () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre }, + () => new SpriteIcon { Icon = FontAwesome.Solid.ObjectGroup }) + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => angleInput.TakeFocus()); + angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); + rotationOrigin.Items.First().Select(); + + rotationInfo.BindValueChanged(rotation => + { + rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null); + }); + } + + protected override void PopIn() + { + base.PopIn(); + rotationHandler.Begin(); + } + + protected override void PopOut() + { + base.PopOut(); + + if (IsLoaded) + rotationHandler.Commit(); + } + } + + public enum RotationOrigin + { + PlayfieldCentre, + SelectionCentre + } + + public record PreciseRotationInfo(float Degrees, RotationOrigin Origin); +} diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs new file mode 100644 index 0000000000..3da9f5b69b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler + { + private readonly Bindable canRotate = new BindableBool(); + + private EditorToolButton rotateButton = null!; + + public SelectionRotationHandler RotationHandler { get; init; } = null!; + + public TransformToolboxGroup() + : base("transform") + { + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Children = new Drawable[] + { + rotateButton = new EditorToolButton("Rotate", + () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, + () => new PreciseRotationPopover(RotationHandler)), + // TODO: scale + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // bindings to `Enabled` on the buttons are decoupled on purpose + // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. + canRotate.BindTo(RotationHandler.CanRotate); + canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) return false; + + switch (e.Action) + { + case GlobalAction.EditorToggleRotateControl: + { + rotateButton.TriggerClick(); + return true; + } + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 454be02d0b..8b9d35e343 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -35,6 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public string Text { + get => Component.Text; set => Component.Text = value; } diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index fc0e4d2083..37ea2a3f96 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -85,6 +85,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 Current.BindValueChanged(updateTextBoxFromSlider, true); } + public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox); + private bool updatingFromTextBox; private void textChanged(ValueChangedEvent change) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 1090eeb462..9a0a2d5c15 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -105,6 +105,7 @@ namespace osu.Game.Input.Bindings // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38. new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), }; public IEnumerable InGameKeyBindings => new[] @@ -378,5 +379,8 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))] ToggleInGameLeaderboard, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] + EditorToggleRotateControl, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index ceefc27968..8356c480dd 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -344,6 +344,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay"); + /// + /// "Toggle rotate control" + /// + public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Components/EditorToolButton.cs b/osu.Game/Screens/Edit/Components/EditorToolButton.cs new file mode 100644 index 0000000000..6550362687 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/EditorToolButton.cs @@ -0,0 +1,107 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Components +{ + public partial class EditorToolButton : OsuButton, IHasPopover + { + public BindableBool Selected { get; } = new BindableBool(); + + private readonly Func createIcon; + private readonly Func createPopover; + + private Color4 defaultBackgroundColour; + private Color4 defaultIconColour; + private Color4 selectedBackgroundColour; + private Color4 selectedIconColour; + + private Drawable icon = null!; + + public EditorToolButton(LocalisableString text, Func createIcon, Func createPopover) + { + Text = text; + this.createIcon = createIcon; + this.createPopover = createPopover; + + RelativeSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + defaultBackgroundColour = colourProvider.Background3; + selectedBackgroundColour = colourProvider.Background1; + + defaultIconColour = defaultBackgroundColour.Darken(0.5f); + selectedIconColour = selectedBackgroundColour.Lighten(0.5f); + + Add(icon = createIcon().With(b => + { + b.Blending = BlendingParameters.Additive; + b.Anchor = Anchor.CentreLeft; + b.Origin = Anchor.CentreLeft; + b.Size = new Vector2(20); + b.X = 10; + })); + + Action = Selected.Toggle; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Selected.BindValueChanged(_ => updateSelectionState(), true); + } + + private void updateSelectionState() + { + if (!IsLoaded) + return; + + BackgroundColour = Selected.Value ? selectedBackgroundColour : defaultBackgroundColour; + icon.Colour = Selected.Value ? selectedIconColour : defaultIconColour; + + if (Selected.Value) + this.ShowPopover(); + else + this.HidePopover(); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40f + }; + + public Popover? GetPopover() => Enabled.Value + ? createPopover()?.With(p => + { + p.State.BindValueChanged(state => + { + if (state.NewValue == Visibility.Hidden) + Selected.Value = false; + }); + }) + : null; + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 1de6c8364c..110beb0fa6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public Container> SelectionBlueprints { get; private set; } - protected SelectionHandler SelectionHandler { get; private set; } + public SelectionHandler SelectionHandler { get; private set; } private readonly Dictionary> blueprintMap = new Dictionary>(); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 158b4066bc..3c859c65ff 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved(CanBeNull = true)] protected IEditorChangeHandler ChangeHandler { get; private set; } - protected SelectionRotationHandler RotationHandler { get; private set; } + public SelectionRotationHandler RotationHandler { get; private set; } protected SelectionHandler() { From 5219b8a13b328341d1afad565f6638cceedbdbca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Aug 2023 11:35:09 +0200 Subject: [PATCH 024/103] Add test coverage for precise rotation control --- .../Editor/TestScenePreciseRotation.cs | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs new file mode 100644 index 0000000000..d7dd30d608 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Edit; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit.Components.RadioButtons; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public partial class TestScenePreciseRotation : TestSceneOsuEditor + { + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset); + + [Test] + public void TestHotkeyHandling() + { + AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First())); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero); + + AddStep("select first three objects", () => + { + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Take(3)); + }); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("popover present", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero); + } + + [Test] + public void TestRotateCorrectness() + { + AddStep("replace objects", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.AddRange(new HitObject[] + { + new HitCircle { Position = new Vector2(100) }, + new HitCircle { Position = new Vector2(200) }, + }); + }); + AddStep("select both circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("popover present", getPopover, () => Is.Not.Null); + + AddStep("rotate by 180deg", () => getPopover().ChildrenOfType().Single().Current.Value = "180"); + AddAssert("first object rotated 180deg around playfield centre", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, + () => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(100))); + AddAssert("second object rotated 180deg around playfield centre", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, + () => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200))); + + AddStep("change rotation origin", () => getPopover().ChildrenOfType().ElementAt(1).TriggerClick()); + AddAssert("first object rotated 90deg around selection centre", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200))); + AddAssert("second object rotated 90deg around selection centre", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, () => Is.EqualTo(new Vector2(100, 100))); + + PreciseRotationPopover? getPopover() => this.ChildrenOfType().SingleOrDefault(); + } + } +} From 49fc9655d28d3ec6da285ac4f2fdec609893163a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Aug 2023 19:34:37 +0900 Subject: [PATCH 025/103] Apply NRT to `osu.Game.Betamaps.Formats` namespace --- osu.Game/Beatmaps/Formats/Decoder.cs | 7 ++----- osu.Game/Beatmaps/Formats/IHasComboColours.cs | 4 +--- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 10 ++++------ .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 16 ++++++---------- .../Beatmaps/Formats/LegacyStoryboardDecoder.cs | 8 +++----- osu.Game/Skinning/ArgonSkin.cs | 2 +- osu.Game/Skinning/TrianglesSkin.cs | 2 +- 7 files changed, 18 insertions(+), 31 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs index 4f0f11d053..c007f5dcdc 100644 --- a/osu.Game/Beatmaps/Formats/Decoder.cs +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -1,13 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.IO; using System.Linq; -using JetBrains.Annotations; using osu.Game.IO; using osu.Game.Rulesets; @@ -45,7 +42,7 @@ namespace osu.Game.Beatmaps.Formats /// Register dependencies for use with static decoder classes. /// /// A store containing all available rulesets (used by ). - public static void RegisterDependencies([NotNull] RulesetStore rulesets) + public static void RegisterDependencies(RulesetStore rulesets) { LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets)); } @@ -63,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats throw new IOException(@"Unknown decoder type"); // start off with the first line of the file - string line = stream.PeekLine()?.Trim(); + string? line = stream.PeekLine()?.Trim(); while (line != null && line.Length == 0) { diff --git a/osu.Game/Beatmaps/Formats/IHasComboColours.cs b/osu.Game/Beatmaps/Formats/IHasComboColours.cs index 1d9cc0be65..1608adee7d 100644 --- a/osu.Game/Beatmaps/Formats/IHasComboColours.cs +++ b/osu.Game/Beatmaps/Formats/IHasComboColours.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.Collections.Generic; using osuTK.Graphics; @@ -13,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats /// /// Retrieves the list of combo colours for presentation only. /// - IReadOnlyList ComboColours { get; } + IReadOnlyList? ComboColours { get; } /// /// The list of custom combo colours. diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 65a01befb4..5da51f6c8e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.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 - #pragma warning disable 618 using System; @@ -36,11 +34,11 @@ namespace osu.Game.Beatmaps.Formats /// private const double control_point_leniency = 1; - internal static RulesetStore RulesetStore; + internal static RulesetStore? RulesetStore; - private Beatmap beatmap; + private Beatmap beatmap = null!; - private ConvertHitObjectParser parser; + private ConvertHitObjectParser? parser; private LegacySampleBank defaultSampleBank; private int defaultSampleVolume = 100; @@ -222,7 +220,7 @@ namespace osu.Game.Beatmaps.Formats case @"Mode": int rulesetID = Parsing.ParseInt(pair.Value); - beatmap.BeatmapInfo.Ruleset = RulesetStore.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally."); + beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally."); switch (rulesetID) { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 041b00c7e1..fc9de13c89 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; -using JetBrains.Annotations; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -34,8 +31,7 @@ namespace osu.Game.Beatmaps.Formats private readonly IBeatmap beatmap; - [CanBeNull] - private readonly ISkin skin; + private readonly ISkin? skin; private readonly int onlineRulesetID; @@ -44,7 +40,7 @@ namespace osu.Game.Beatmaps.Formats /// /// The beatmap to encode. /// The beatmap's skin, used for encoding combo colours. - public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin) + public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin) { this.beatmap = beatmap; this.skin = skin; @@ -180,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[TimingPoints]"); - SampleControlPoint lastRelevantSamplePoint = null; - DifficultyControlPoint lastRelevantDifficultyPoint = null; + SampleControlPoint? lastRelevantSamplePoint = null; + DifficultyControlPoint? lastRelevantDifficultyPoint = null; // In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats. // In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored. @@ -585,7 +581,7 @@ namespace osu.Game.Beatmaps.Formats return type; } - private LegacySampleBank toLegacySampleBank(string sampleBank) + private LegacySampleBank toLegacySampleBank(string? sampleBank) { switch (sampleBank?.ToLowerInvariant()) { @@ -603,7 +599,7 @@ namespace osu.Game.Beatmaps.Formats } } - private int toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) + private int toLegacyCustomSampleBank(HitSampleInfo? hitSampleInfo) { if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy) return legacy.CustomSampleBank; diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index df5d3edb55..cf4700bf85 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -19,10 +17,10 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyStoryboardDecoder : LegacyDecoder { - private StoryboardSprite storyboardSprite; - private CommandTimelineGroup timelineGroup; + private StoryboardSprite? storyboardSprite; + private CommandTimelineGroup? timelineGroup; - private Storyboard storyboard; + private Storyboard storyboard = null!; private readonly Dictionary variables = new Dictionary(); diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index ba392386de..61b0dc5aa1 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -232,6 +232,6 @@ namespace osu.Game.Skinning } private static Color4 getComboColour(IHasComboColours source, int colourIndex) - => source.ComboColours[colourIndex % source.ComboColours.Count]; + => source.ComboColours![colourIndex % source.ComboColours.Count]; } } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index a68a7fd5b9..0957f60579 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -203,6 +203,6 @@ namespace osu.Game.Skinning } private static Color4 getComboColour(IHasComboColours source, int colourIndex) - => source.ComboColours[colourIndex % source.ComboColours.Count]; + => source.ComboColours![colourIndex % source.ComboColours.Count]; } } From 8686b6b1e6ea2a7e3fd096ec838ec0e344f55acd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 16 Aug 2023 16:44:08 +0200 Subject: [PATCH 026/103] fix The last slider point has effect on previous inherited --- osu.Game/Database/LegacyBeatmapExporter.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index a874353f73..c00977d072 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -70,7 +70,13 @@ namespace osu.Game.Database hitObject.StartTime = Math.Floor(hitObject.StartTime); - if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; + if (hitObject is not IHasPath hasPath) continue; + + // Make sure the last control point is inherit type + if (hasPath.Path.ControlPoints.Count > 1) + hasPath.Path.ControlPoints[^1].Type = null; + + if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); From 59abb59ee862cc83145e7af2efb6f2e17dfcb407 Mon Sep 17 00:00:00 2001 From: Krzysztof Gutkowski Date: Thu, 17 Aug 2023 00:49:48 +0200 Subject: [PATCH 027/103] Set correct date added value when importing stable beatmapsets --- .../Database/LegacyBeatmapImporterTest.cs | 31 ++++++++++++++- osu.Game/Beatmaps/BeatmapImporter.cs | 38 ++++++++++++++++++- .../Archives/LegacyDirectoryArchiveReader.cs | 4 +- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs index b237556d11..638ad94472 100644 --- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs +++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs @@ -1,19 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Database { [TestFixture] - public class LegacyBeatmapImporterTest + public class LegacyBeatmapImporterTest : RealmTest { private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter(); @@ -60,6 +63,32 @@ namespace osu.Game.Tests.Database } } + [Test] + public void TestStableDateAddedApplied() + { + RunTestWithRealmAsync(async (realm, storage) => + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder")) + { + var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host); + var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH); + + System.IO.Compression.ZipFile.ExtractToDirectory(TestResources.GetQuickTestBeatmapForImport(), songsStorage.GetFullPath("renatus")); + + string[] beatmaps = Directory.GetFiles(songsStorage.GetFullPath("renatus"), "*.osu", SearchOption.TopDirectoryOnly); + + File.SetLastWriteTimeUtc(beatmaps[beatmaps.Length / 2], new DateTime(2000, 1, 1, 12, 0, 0)); + + await new LegacyBeatmapImporter(new BeatmapImporter(storage, realm)).ImportFromStableAsync(stableStorage); + + var set = realm.Realm.All(); + + Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Utc)), set.First().DateAdded); + } + }); + } + private class TestLegacyBeatmapImporter : LegacyBeatmapImporter { public TestLegacyBeatmapImporter() diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index c840b4fa94..6c3f393069 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -302,14 +302,50 @@ namespace osu.Game.Beatmaps beatmap = Decoder.GetDecoder(stream).Decode(stream); } + var dateAdded = DateTimeOffset.UtcNow; + + // Apply proper date added for a beatmapset when importing from stable. + // Stable tracks said date using the filesystem last modified date on the .osu file. + if (reader is LegacyDirectoryArchiveReader legacyReader) + { + dateAdded = determineDateAdded(legacyReader); + } + return new BeatmapSetInfo { OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1, // Metadata = beatmap.Metadata, - DateAdded = DateTimeOffset.UtcNow + DateAdded = dateAdded }; } + /// + /// Used for beatmapsets in legacy (stable) storage. + /// Determine the date a given beatmapset has been added to the game. + /// The specific date is determined based on the oldest `.osu` file existing + /// in the beatmapset directory. + /// + /// + /// + private DateTimeOffset determineDateAdded(LegacyDirectoryArchiveReader reader) + { + var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); + + var dateAdded = File.GetLastWriteTimeUtc(reader.GetPath(beatmaps.First())); + + foreach (string beatmapName in beatmaps) + { + var currentDateAdded = File.GetLastWriteTimeUtc(reader.GetPath(beatmapName)); + + if (currentDateAdded < dateAdded) + { + dateAdded = currentDateAdded; + } + } + + return new DateTimeOffset(dateAdded); + } + /// /// Create all required s for the provided archive. /// diff --git a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs b/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs index dfae58aed7..8a576e33d8 100644 --- a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs +++ b/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs @@ -21,7 +21,9 @@ namespace osu.Game.IO.Archives this.path = Path.GetFullPath(path); } - public override Stream GetStream(string name) => File.OpenRead(Path.Combine(path, name)); + public override Stream GetStream(string name) => File.OpenRead(GetPath(name)); + + public string GetPath(string name) => Path.Combine(path, name); public override void Dispose() { From 046cc62db2b3c58096a3a9697c4f6a3ed76a232f Mon Sep 17 00:00:00 2001 From: Krzysztof Gutkowski Date: Thu, 17 Aug 2023 01:11:09 +0200 Subject: [PATCH 028/103] Cleanup tests --- osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs index 638ad94472..d6056fa71b 100644 --- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs +++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs @@ -82,9 +82,10 @@ namespace osu.Game.Tests.Database await new LegacyBeatmapImporter(new BeatmapImporter(storage, realm)).ImportFromStableAsync(stableStorage); - var set = realm.Realm.All(); + var beatmapset = realm.Realm.All().First(); - Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Utc)), set.First().DateAdded); + Assert.NotNull(beatmapset); + Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Utc)), beatmapset.DateAdded); } }); } From e2cb0d7afb1223eb8b0f85bdd585a8211a431fb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Aug 2023 17:59:22 +0900 Subject: [PATCH 029/103] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d64855e5c1..8ebfde8047 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml index 4a1545a423..f5a49210ea 100644 --- a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml index 45d27dda70..ed4725dd94 100644 --- a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml index 452b9683ec..cc88d3080a 100644 --- a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Tests.Android/AndroidManifest.xml b/osu.Game.Tests.Android/AndroidManifest.xml index f25b2e5328..6f91fb928c 100644 --- a/osu.Game.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file From 912f31dabc7160fbd32ccb0a47fd733e427274e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Aug 2023 23:37:11 +0200 Subject: [PATCH 058/103] Declare media permissions in game project for editor usage --- osu.Android/AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Android/AndroidManifest.xml b/osu.Android/AndroidManifest.xml index fb54c8e151..af102a1e4e 100644 --- a/osu.Android/AndroidManifest.xml +++ b/osu.Android/AndroidManifest.xml @@ -2,4 +2,7 @@ + + + \ No newline at end of file From a942b6ff745193ff9b3b0f8c624d5905a4495225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 07:27:02 +0200 Subject: [PATCH 059/103] Replace inline comment with actual explanation of what's happening --- osu.Game/Database/LegacyBeatmapExporter.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index c00977d072..ece705f685 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -72,7 +72,16 @@ namespace osu.Game.Database if (hitObject is not IHasPath hasPath) continue; - // Make sure the last control point is inherit type + // stable's hit object parsing expects the entire slider to use only one type of curve, + // and happens to use the last non-empty curve type read for the entire slider. + // this clear of the last control point type handles an edge case + // wherein the last control point of an otherwise-single-segment slider path has a different type than previous, + // which would lead to sliders being mangled when exported back to stable. + // normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below, + // which outputs a slider path containing only Bezier control points, + // but a non-inherited last control point is (rightly) not considered to be starting a new segment, + // therefore it would fail to clear the `CountSegments() <= 1` check. + // by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier. if (hasPath.Path.ControlPoints.Count > 1) hasPath.Path.ControlPoints[^1].Type = null; From dd1ac461db2c779c5f9a24bfedbdb3437f503c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 08:23:58 +0200 Subject: [PATCH 060/103] Reformat xmldoc --- osu.Game/Rulesets/Objects/SliderPath.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 1c02b18a0f..34113285a4 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -201,13 +201,20 @@ namespace osu.Game.Rulesets.Objects /// Returns the progress values at which (control point) segments of the path end. /// Ranges from 0 (beginning of the path) to 1 (end of the path) to infinity (beyond the end of the path). /// - /// In case is less than , + /// + /// truncates the progression values to [0,1], + /// so you can't use this method in conjunction with that one to retrieve the positions of segment ends beyond the end of the path. + /// + /// + /// + /// In case is less than , /// the last segment ends after the end of the path, hence it returns a value greater than 1. - /// + /// + /// /// In case is greater than , - /// the last segment ends before the end of the path, hence it returns a value less than 1. - /// truncates the progression values to [0,1], - /// so you can't use this method to retrieve the positions of segment ends beyond the end of the path. + /// the last segment ends before the end of the path, hence it returns a value less than 1. + /// + /// public IEnumerable GetSegmentEnds() { ensureValid(); From 479c463751e0159049bb63fe27937072a9b836c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 08:27:08 +0200 Subject: [PATCH 061/103] Explain why segment end positions are not recovered in test --- osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs index 16e4ae13d9..635d9f9604 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs @@ -207,6 +207,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150); AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 150))); + // see remarks in `GetSegmentEnds()` xmldoc (`SliderPath.PositionAt()` clamps progress to [0,1]). AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(new[] { positions[1], From 5009fd379421f928bca35028b00e6b88374bdb5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 20 Aug 2023 17:57:45 +0900 Subject: [PATCH 062/103] Add test coverage of song bar crash --- .../Components/TestSceneSongBar.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index 0f31192a9c..d52b453185 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -2,27 +2,34 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps.Legacy; -using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Tests.Components { [TestFixture] - public partial class TestSceneSongBar : OsuTestScene + public partial class TestSceneSongBar : TournamentTestScene { - [Cached] - private readonly LadderInfo ladder = new LadderInfo(); - private SongBar songBar = null!; [SetUpSteps] - public void SetUpSteps() + public override void SetUpSteps() { + base.SetUpSteps(); + + AddStep("setup picks bans", () => + { + Ladder.CurrentMatch.Value!.PicksBans.Add(new BeatmapChoice + { + BeatmapID = CreateSampleBeatmap().OnlineID, + Team = TeamColour.Red, + Type = ChoiceType.Pick, + }); + }); + AddStep("create bar", () => Child = songBar = new SongBar { RelativeSizeAxes = Axes.X, @@ -38,12 +45,14 @@ namespace osu.Game.Tournament.Tests.Components AddStep("set beatmap", () => { var beatmap = CreateAPIBeatmap(Ruleset.Value); + beatmap.CircleSize = 3.4f; beatmap.ApproachRate = 6.8f; beatmap.OverallDifficulty = 5.5f; beatmap.StarRating = 4.56f; beatmap.Length = 123456; beatmap.BPM = 133; + beatmap.OnlineID = CreateSampleBeatmap().OnlineID; songBar.Beatmap = new TournamentBeatmap(beatmap); }); From 1067769b24e924bb34d4cc8975e94e162c7198c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 15:33:58 +0900 Subject: [PATCH 063/103] Remove masking on song bar Turns out this breaks when a border style is applied for picks/bans, and it wasn't doing much for visuals anyway. --- osu.Game.Tournament/Components/SongBar.cs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 3d060600f7..cde826628e 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Components } } }, - new UnmaskedTournamentBeatmapPanel(beatmap) + new TournamentBeatmapPanel(beatmap) { RelativeSizeAxes = Axes.X, Width = 0.5f, @@ -277,18 +277,4 @@ namespace osu.Game.Tournament.Components } } } - - internal partial class UnmaskedTournamentBeatmapPanel : TournamentBeatmapPanel - { - public UnmaskedTournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "") - : base(beatmap, mod) - { - } - - [BackgroundDependencyLoader] - private void load() - { - Masking = false; - } - } } From f03c64462e5bd6a21626e7030cfaaf13a50c2e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 08:58:49 +0200 Subject: [PATCH 064/103] Better convey meaning of zero last year placement via tooltip --- osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 241692d515..250d5acaae 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -10,7 +10,9 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Tournament.Models; @@ -128,7 +130,7 @@ namespace osu.Game.Tournament.Screens.Editors Width = 0.2f, Current = Model.Seed }, - new SettingsSlider + new SettingsSlider { LabelText = "Last Year Placement", Width = 0.33f, @@ -175,6 +177,11 @@ namespace osu.Game.Tournament.Screens.Editors }; } + private partial class LastYearPlacementSlider : RoundedSliderBar + { + public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText; + } + public partial class PlayerEditor : CompositeDrawable { private readonly TournamentTeam team; From 827d48adcc5fb3d334c5175879898b604871b4d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 16:10:48 +0900 Subject: [PATCH 065/103] Fix test coverage not actually covering crash --- osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index d52b453185..e0444b6126 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -14,6 +14,7 @@ namespace osu.Game.Tournament.Tests.Components public partial class TestSceneSongBar : TournamentTestScene { private SongBar songBar = null!; + private TournamentBeatmap ladderBeatmap = null!; [SetUpSteps] public override void SetUpSteps() @@ -22,9 +23,10 @@ namespace osu.Game.Tournament.Tests.Components AddStep("setup picks bans", () => { + ladderBeatmap = CreateSampleBeatmap(); Ladder.CurrentMatch.Value!.PicksBans.Add(new BeatmapChoice { - BeatmapID = CreateSampleBeatmap().OnlineID, + BeatmapID = ladderBeatmap.OnlineID, Team = TeamColour.Red, Type = ChoiceType.Pick, }); @@ -52,7 +54,7 @@ namespace osu.Game.Tournament.Tests.Components beatmap.StarRating = 4.56f; beatmap.Length = 123456; beatmap.BPM = 133; - beatmap.OnlineID = CreateSampleBeatmap().OnlineID; + beatmap.OnlineID = ladderBeatmap.OnlineID; songBar.Beatmap = new TournamentBeatmap(beatmap); }); From e7d61e00022a4a09e8164eb809e2a1dc6b34dfa6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 17:59:24 +0900 Subject: [PATCH 066/103] Fix star fountain directions not matching stable --- .../Visual/Menus/TestSceneStarFountain.cs | 2 +- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 30 +++++++++++++++---- osu.Game/Screens/Menu/StarFountain.cs | 6 ++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index b12f3e7946..bb327e5962 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Menus foreach (var fountain in Children.OfType()) { if (RNG.NextSingle() > 0.8f) - fountain.Shoot(); + fountain.Shoot(RNG.Next(-1, 2)); } }, 150); } diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index a4d58e398a..07c06dcdb9 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -2,10 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics; +using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; @@ -13,6 +13,9 @@ namespace osu.Game.Screens.Menu { public partial class KiaiMenuFountains : BeatSyncedContainer { + private StarFountain leftFountain = null!; + private StarFountain rightFountain = null!; + [BackgroundDependencyLoader] private void load() { @@ -20,13 +23,13 @@ namespace osu.Game.Screens.Menu Children = new[] { - new StarFountain + leftFountain = new StarFountain { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, X = 250, }, - new StarFountain + rightFountain = new StarFountain { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -58,8 +61,25 @@ namespace osu.Game.Screens.Menu if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) return; - foreach (var fountain in Children.OfType()) - fountain.Shoot(); + int direction = RNG.Next(-1, 2); + + switch (direction) + { + case -1: + leftFountain.Shoot(1); + rightFountain.Shoot(-1); + break; + + case 0: + leftFountain.Shoot(0); + rightFountain.Shoot(0); + break; + + case 1: + leftFountain.Shoot(-1); + rightFountain.Shoot(1); + break; + } lastTrigger = Clock.CurrentTime; } diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 0d35f6e0e0..fd59ec3573 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Menu InternalChild = spewer = new StarFountainSpewer(); } - public void Shoot() => spewer.Shoot(); + public void Shoot(int direction) => spewer.Shoot(direction); protected override void SkinChanged(ISkinSource skin) { @@ -81,10 +81,10 @@ namespace osu.Game.Screens.Menu return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); } - public void Shoot() + public void Shoot(int direction) { lastShootTime = Clock.CurrentTime; - lastShootDirection = RNG.Next(-1, 2); + lastShootDirection = direction; } private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); From 5f040a991b3f1492c97d247799a3245b2b825ee1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 19:05:20 +0900 Subject: [PATCH 067/103] Fix potential crash when loading menu items due to cross-thread ops --- osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index eb046932e6..2f2cb7e5f8 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -40,8 +40,14 @@ namespace osu.Game.Graphics.UserInterface AddInternal(hoverClickSounds = new HoverClickSounds()); updateTextColour(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); Item.Action.BindDisabledChanged(_ => updateState(), true); + FinishTransforms(); } private void updateTextColour() From 662073c47220d513ed3ef510d70ab0ccb850e334 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 19:35:04 +0900 Subject: [PATCH 068/103] Fix some incorrect comments / test step descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 23b88b7395..f766faec9a 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Database { ScoreInfo scoreInfo = null!; - AddStep("Add score which requires upgrade (but has no beatmap)", () => + AddStep("Add score which requires upgrade (and has beatmap)", () => { Realm.Write(r => { diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index c3a45332e4..526c4217ef 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -86,7 +86,7 @@ namespace osu.Game.Scoring /// Should be used to ensure we don't repeatedly attempt to update the same scores each startup even though we already know they will fail. /// /// - /// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur(missing beatmap file on disk). + /// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk). /// public bool TotalScoreUpgradeFailed { get; set; } From b3e7416972f097242bf8c9fe407dec3567d489d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 19:36:22 +0900 Subject: [PATCH 069/103] Rename new flag and update xmldoc to match --- .../Database/BackgroundDataStoreProcessorTests.cs | 4 ++-- osu.Game/BackgroundDataStoreProcessor.cs | 8 ++++---- osu.Game/Scoring/ScoreInfo.cs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index f766faec9a..da46392e4b 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION)); - AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreUpgradeFailed), () => Is.False); + AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); } [Test] @@ -174,7 +174,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); - AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreUpgradeFailed), () => Is.True); + AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002)); } diff --git a/osu.Game/BackgroundDataStoreProcessor.cs b/osu.Game/BackgroundDataStoreProcessor.cs index ae9e9527de..f29b100ee8 100644 --- a/osu.Game/BackgroundDataStoreProcessor.cs +++ b/osu.Game/BackgroundDataStoreProcessor.cs @@ -186,7 +186,7 @@ namespace osu.Game realmAccess.Run(r => { - foreach (var score in r.All().Where(s => !s.TotalScoreUpgradeFailed)) + foreach (var score in r.All().Where(s => !s.BackgroundReprocessingFailed)) { if (score.BeatmapInfo != null && score.Statistics.Sum(kvp => kvp.Value) > 0 @@ -225,7 +225,7 @@ namespace osu.Game catch (Exception e) { Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); - realmAccess.Write(r => r.Find(id)!.TotalScoreUpgradeFailed = true); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); } } } @@ -235,7 +235,7 @@ namespace osu.Game Logger.Log("Querying for scores that need total score conversion..."); HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All() - .Where(s => !s.TotalScoreUpgradeFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) + .Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) .AsEnumerable().Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); @@ -284,7 +284,7 @@ namespace osu.Game catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); - realmAccess.Write(r => r.Find(id)!.TotalScoreUpgradeFailed = true); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); ++failedCount; } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 526c4217ef..2efea2105c 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -82,13 +82,13 @@ namespace osu.Game.Scoring public long? LegacyTotalScore { get; set; } /// - /// If an reprocess of total score failed to update this score to the latest version, this flag will become true. - /// Should be used to ensure we don't repeatedly attempt to update the same scores each startup even though we already know they will fail. + /// If background processing of this beatmap failed in some way, this flag will become true. + /// Should be used to ensure we don't repeatedly attempt to reprocess the same scores each startup even though we already know they will fail. /// /// /// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk). /// - public bool TotalScoreUpgradeFailed { get; set; } + public bool BackgroundReprocessingFailed { get; set; } public int MaxCombo { get; set; } From 82de7385d1c07dc0bd3e21a46ee6220e82a59244 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 12:59:58 +0200 Subject: [PATCH 070/103] Revert "Fix TestSceneFruitRandomness" This reverts commit b9d0a8a9f69fadacf1d1578fccd9101798583d47. --- osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 8e7f77285c..de3d9d6530 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddSliderStep("start time", 500, 600, 0, x => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x; - drawableFruit.RefreshStateTransforms(); - drawableBanana.RefreshStateTransforms(); }); } @@ -46,8 +44,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("Initialize start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; - drawableFruit.RefreshStateTransforms(); - drawableBanana.RefreshStateTransforms(); fruitRotation = drawableFruit.DisplayRotation; bananaRotation = drawableBanana.DisplayRotation; @@ -58,8 +54,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("change start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; - drawableFruit.RefreshStateTransforms(); - drawableBanana.RefreshStateTransforms(); }); AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation); @@ -70,8 +64,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("reset start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; - drawableFruit.RefreshStateTransforms(); - drawableBanana.RefreshStateTransforms(); }); AddAssert("rotation and size restored", () => From c7b1c75379983860a2dec8bd2de92d4331a0fc75 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 13:00:01 +0200 Subject: [PATCH 071/103] Revert "Fix typo" This reverts commit 90f2acaf0a9d1bfa5ac8d4cc653798604a3fdf21. --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 76cfc049e3..e919e4d088 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.UI private void unbindUpdated(DrawableHitObject hitObject) { - hitObject.DefaultsApplied -= onDefaultsApplied; + hitObject.DefaultsApplied += onDefaultsApplied; } private void unbindAllUpdated() From 5bc11ed35806ac8fac87c82118ff904905df3127 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 13:02:23 +0200 Subject: [PATCH 072/103] Revert "Ensure invariant of monotone time" This reverts commit 5d1ccc2601a739e2d8ed61b8d9b64a0efa580f7f. --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 30 +------------------ osu.Game/Rulesets/UI/Playfield.cs | 35 ++++------------------ 2 files changed, 7 insertions(+), 58 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index e919e4d088..099be486b3 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -51,11 +51,6 @@ namespace osu.Game.Rulesets.UI [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } - /// - /// Invoked when a is updated. - /// - public event Action HitObjectUpdated; - public HitObjectContainer() { RelativeSizeAxes = Axes.Both; @@ -113,7 +108,6 @@ namespace osu.Game.Rulesets.UI drawable.OnNewResult += onNewResult; bindStartTime(drawable); - bindUpdated(drawable); AddInternal(drawable); } @@ -122,7 +116,7 @@ namespace osu.Game.Rulesets.UI drawable.OnNewResult -= onNewResult; unbindStartTime(drawable); - unbindUpdated(drawable); + RemoveInternal(drawable, false); } @@ -182,27 +176,6 @@ namespace osu.Game.Rulesets.UI startTimeMap.Clear(); } - private void bindUpdated(DrawableHitObject hitObject) - { - hitObject.DefaultsApplied += onDefaultsApplied; - } - - private void unbindUpdated(DrawableHitObject hitObject) - { - hitObject.DefaultsApplied += onDefaultsApplied; - } - - private void unbindAllUpdated() - { - foreach (var h in AliveObjects) - unbindUpdated(h); - } - - private void onDefaultsApplied(DrawableHitObject obj) - { - HitObjectUpdated?.Invoke(obj.HitObject); - } - protected override int Compare(Drawable x, Drawable y) { if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) @@ -219,7 +192,6 @@ namespace osu.Game.Rulesets.UI { base.Dispose(isDisposing); unbindAllStartTimes(); - unbindAllUpdated(); } } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 9a4d082a06..e9c35555c8 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.UI private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager(); - private readonly LinkedList judgedEntries; + private readonly Stack judgedEntries; /// /// Creates a new . @@ -125,13 +125,12 @@ namespace osu.Game.Rulesets.UI h.NewResult += onNewResult; h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); - h.HitObjectUpdated += onHitObjectUpdated; })); entryManager.OnEntryAdded += onEntryAdded; entryManager.OnEntryRemoved += onEntryRemoved; - judgedEntries = new LinkedList(); + judgedEntries = new Stack(); } [BackgroundDependencyLoader] @@ -271,16 +270,15 @@ namespace osu.Game.Rulesets.UI } // When rewinding, revert future judgements in the reverse order. - while (judgedEntries.Last is not null) + while (judgedEntries.Count > 0) { - var result = judgedEntries.Last.Value.Result; + var result = judgedEntries.Peek().Result; Debug.Assert(result?.RawTime != null); if (Time.Current >= result.RawTime.Value) break; - revertResult(judgedEntries.Last.Value); - judgedEntries.RemoveLast(); + revertResult(judgedEntries.Pop()); } } @@ -473,31 +471,10 @@ namespace osu.Game.Rulesets.UI #endregion - private void onHitObjectUpdated(HitObject _) - { - // The time of judged entries may have changed, so we need to re-sort the list to preserve the invariant of monotone time. - // Insertion sort on linked-list is O(n) for nearly-sorted lists, which is the case here. - var current = judgedEntries.First; - - while (current?.Next is not null) - { - var next = current.Next; - - if (current.Value.Result?.RawTime > next.Value.Result?.RawTime) - { - judgedEntries.Remove(next); - judgedEntries.AddBefore(current, next); - current = next.Previous; - } - else - current = next; - } - } - private void onNewResult(DrawableHitObject drawable, JudgementResult result) { Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null); - judgedEntries.AddLast(drawable.Entry.AsNonNull()); + judgedEntries.Push(drawable.Entry.AsNonNull()); NewResult?.Invoke(drawable, result); } From c82e997644529adb475bec516ead328d260c1fda Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 13:02:41 +0200 Subject: [PATCH 073/103] Revert "Revert "Fix TestSceneFruitRandomness"" This reverts commit 82de7385d1c07dc0bd3e21a46ee6220e82a59244. --- osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index de3d9d6530..8e7f77285c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddSliderStep("start time", 500, 600, 0, x => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); } @@ -44,6 +46,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("Initialize start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); fruitRotation = drawableFruit.DisplayRotation; bananaRotation = drawableBanana.DisplayRotation; @@ -54,6 +58,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("change start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation); @@ -64,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("reset start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); AddAssert("rotation and size restored", () => From e283aa2843104bf58bf027486cee0d595c65fa3a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 13:09:31 +0200 Subject: [PATCH 074/103] Update inline comments --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index f141263e27..c442fac0b8 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -330,7 +330,7 @@ namespace osu.Game.Rulesets.Objects.Drawables ClearNestedHitObjects(); // Changes in state trigger defaults applied trigger state updates. - // When a new hitobject is applied, OnApply() automatically performs a state update anyway. + // When a new hitobject is applied, OnApply() automatically performs a state update. HitObject.DefaultsApplied -= onDefaultsApplied; entry.RevertResult -= onRevertResult; @@ -391,6 +391,8 @@ namespace osu.Game.Rulesets.Objects.Drawables Apply(Entry); // Applied defaults indicate a change in hit object state. + // We need to update the judgement result time to the new end time + // and update state to ensure the hit object fades out at the correct time. if (Result is not null) { Result.TimeOffset = 0; From bdac05263164d51af00fa23d4fe84f1e1fcf905c Mon Sep 17 00:00:00 2001 From: tsrk Date: Mon, 21 Aug 2023 15:29:41 +0200 Subject: [PATCH 075/103] refactor(MessageNotifier): apply changes required by framework --- osu.Game/Online/Chat/MessageNotifier.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index b91cf06847..de38d3ef26 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -127,7 +127,7 @@ namespace osu.Game.Online.Chat if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM) return false; - (host as DesktopGameHost)?.FlashWindow(); + host.Window?.Flash(); notifications.Post(new PrivateMessageNotification(message, channel)); return true; @@ -137,7 +137,7 @@ namespace osu.Game.Online.Chat { if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return; - (host as DesktopGameHost)?.FlashWindow(); + host.Window?.Flash(); notifications.Post(new MentionNotification(message, channel)); } From 8533cba0bf6a02b05b8e9bfe99aaee2822dfe5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 17:27:05 +0200 Subject: [PATCH 076/103] Fix mismatching schema version in comment --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 36781b0454..db4f0d9864 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -83,7 +83,7 @@ namespace osu.Game.Database /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding. - /// 35 2023-08-21 Add TotalScoreUpgradeFailed flag to ScoreInfo to track upgrade failures. + /// 34 2023-08-21 Add TotalScoreUpgradeFailed flag to ScoreInfo to track upgrade failures. /// private const int schema_version = 34; From 273dcf9150f441c475c524455b12376a96d3930f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 17:44:35 +0200 Subject: [PATCH 077/103] Also update the reference to added flag in schema change breakdown --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index db4f0d9864..cd97bb6430 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -83,7 +83,7 @@ namespace osu.Game.Database /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding. - /// 34 2023-08-21 Add TotalScoreUpgradeFailed flag to ScoreInfo to track upgrade failures. + /// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures. /// private const int schema_version = 34; From 5454d1caa1927428fe339e3d5b47ddfe45f9dfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Aug 2023 17:40:55 +0200 Subject: [PATCH 078/103] Remove global action container input queue workaround As described in #24248, the workaround employed by `GlobalActionContainer`, wherein it tried to handle actions with priority before its children by being placed in front of the children and not _actually containing_ said children, is blocking the resolution of some rather major input handling issues that allow key releases to be received by deparented drawables. To resolve, migrate `GlobalActionContainer` to use `Prioritised`, which can be done without regressing certain mouse button flows after ppy/osu-framework#5966. --- .../Input/Bindings/GlobalActionContainer.cs | 34 +++++-------------- osu.Game/OsuGameBase.cs | 19 ++++++----- .../Visual/OsuManualInputManagerTestScene.cs | 8 ++++- 3 files changed, 25 insertions(+), 36 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 9a0a2d5c15..296232d9ea 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -3,33 +3,26 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Localisation; namespace osu.Game.Input.Bindings { - public partial class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput + public partial class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput, IKeyBindingHandler { - private readonly Drawable? handler; - - private InputManager? parentInputManager; + private readonly IKeyBindingHandler? handler; public GlobalActionContainer(OsuGameBase? game) : base(matchingMode: KeyCombinationMatchingMode.Modifiers) { - if (game is IKeyBindingHandler) - handler = game; + if (game is IKeyBindingHandler h) + handler = h; } - protected override void LoadComplete() - { - base.LoadComplete(); - - parentInputManager = GetContainingInputManager(); - } + protected override bool Prioritised => true; // IMPORTANT: Take care when changing order of the items in the enumerable. // It is used to decide the order of precedence, with the earlier items having higher precedence. @@ -161,20 +154,9 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.F3, GlobalAction.MusicPlay) }; - protected override IEnumerable KeyBindingInputQueue - { - get - { - // To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content. - // It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly - // allow the whole game to handle these actions. + public bool OnPressed(KeyBindingPressEvent e) => handler?.OnPressed(e) == true; - // An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging. - var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue; - - return handler != null ? inputQueue.Prepend(handler) : inputQueue; - } - } + public void OnReleased(KeyBindingReleaseEvent e) => handler?.OnReleased(e); } public enum GlobalAction diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 6737caa5f9..75b46a0a4d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -392,17 +392,18 @@ namespace osu.Game { SafeAreaOverrideEdges = SafeAreaOverrideEdges, RelativeSizeAxes = Axes.Both, - Child = CreateScalingContainer().WithChildren(new Drawable[] + Child = CreateScalingContainer().WithChild(globalBindings = new GlobalActionContainer(this) { - (GlobalCursorDisplay = new GlobalCursorDisplay + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor) - { - RelativeSizeAxes = Axes.Both - }), - // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. - globalBindings = new GlobalActionContainer(this) + (GlobalCursorDisplay = new GlobalCursorDisplay + { + RelativeSizeAxes = Axes.Both + }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor) + { + RelativeSizeAxes = Axes.Both + }), + } }) }); diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 37260b3b13..ffe40243ab 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -57,7 +57,13 @@ namespace osu.Game.Tests.Visual } if (CreateNestedActionContainer) - mainContent.Add(new GlobalActionContainer(null)); + { + var globalActionContainer = new GlobalActionContainer(null) + { + Child = mainContent + }; + mainContent = globalActionContainer; + } base.Content.AddRange(new Drawable[] { From 96c58c86ea85c1e2e85cb578a730be649993351d Mon Sep 17 00:00:00 2001 From: tsrk Date: Mon, 21 Aug 2023 23:36:54 +0200 Subject: [PATCH 079/103] refactor: make flashing available in `Notifications` This will be used in `NotificationOverlay` when a `Notification` is posted. --- osu.Game/Online/Chat/MessageNotifier.cs | 4 ---- osu.Game/Overlays/NotificationOverlay.cs | 9 +++++++++ osu.Game/Overlays/Notifications/Notification.cs | 5 +++++ osu.Game/Screens/Play/PlayerLoader.cs | 2 ++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index de38d3ef26..65aac723da 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -127,8 +127,6 @@ namespace osu.Game.Online.Chat if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM) return false; - host.Window?.Flash(); - notifications.Post(new PrivateMessageNotification(message, channel)); return true; } @@ -137,8 +135,6 @@ namespace osu.Game.Online.Chat { if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return; - host.Window?.Flash(); - notifications.Post(new MentionNotification(message, channel)); } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index c9d09848f8..08c567af82 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -43,6 +43,9 @@ namespace osu.Game.Overlays [Resolved] private AudioManager audio { get; set; } = null!; + [Resolved] + private OsuGame game { get; set; } = null!; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -176,6 +179,12 @@ namespace osu.Game.Overlays playDebouncedSample(notification.PopInSampleName); + if (notification.FlashTaskbar) + { + game.Window?.Flash(notification.IsImportant); + notification.Closed += () => game.Window?.CancelFlash(); + } + if (State.Value == Visibility.Hidden) { notification.IsInToastTray = true; diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 8cdc373417..53fc152c96 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -38,6 +38,11 @@ namespace osu.Game.Overlays.Notifications /// public virtual bool IsImportant => true; + /// + /// Whether this notification should trigger a taskbar flash if the window is un-focused when posted. + /// + public bool FlashTaskbar { get; init; } = true; + /// /// Run on user activating the notification. Return true to close. /// diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 872425e3fd..eccfc4dc7b 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -568,6 +568,7 @@ namespace osu.Game.Screens.Play public MutedNotification() { Text = NotificationsStrings.GameVolumeTooLow; + FlashTaskbar = false; } [BackgroundDependencyLoader] @@ -623,6 +624,7 @@ namespace osu.Game.Screens.Play public BatteryWarningNotification() { Text = NotificationsStrings.BatteryLow; + FlashTaskbar = false; } [BackgroundDependencyLoader] From e8337c592a69e1905e75d459bf5ff6aad6c0d47f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 22 Aug 2023 12:50:13 +0900 Subject: [PATCH 080/103] Update framework and apply changes to support masking SSBO --- .../UI/Cursor/CursorTrail.cs | 20 ++++++++++++++----- .../Resources/Shaders/sh_TestVertex.vs | 2 +- osu.Game/Screens/Loader.cs | 5 +---- osu.Game/osu.Game.csproj | 2 +- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index a29faac5a0..0774d34488 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor if (time - part.Time >= 1) continue; - vertexBatch.Add(new TexturedTrailVertex + vertexBatch.Add(new TexturedTrailVertex(renderer) { Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)), TexturePosition = textureRect.BottomLeft, @@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Time = part.Time }); - vertexBatch.Add(new TexturedTrailVertex + vertexBatch.Add(new TexturedTrailVertex(renderer) { Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)), TexturePosition = textureRect.BottomRight, @@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Time = part.Time }); - vertexBatch.Add(new TexturedTrailVertex + vertexBatch.Add(new TexturedTrailVertex(renderer) { Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y), TexturePosition = textureRect.TopRight, @@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Time = part.Time }); - vertexBatch.Add(new TexturedTrailVertex + vertexBatch.Add(new TexturedTrailVertex(renderer) { Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y), TexturePosition = textureRect.TopLeft, @@ -362,12 +362,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor [VertexMember(1, VertexAttribPointerType.Float)] public float Time; + [VertexMember(1, VertexAttribPointerType.Int)] + private readonly int maskingIndex; + + public TexturedTrailVertex(IRenderer renderer) + { + this = default; + maskingIndex = renderer.CurrentMaskingIndex; + } + public bool Equals(TexturedTrailVertex other) { return Position.Equals(other.Position) && TexturePosition.Equals(other.TexturePosition) && Colour.Equals(other.Colour) - && Time.Equals(other.Time); + && Time.Equals(other.Time) + && maskingIndex == other.maskingIndex; } } } diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs index 505554bb33..80ed686ba5 100644 --- a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs +++ b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs @@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange; void main(void) { // Transform from screen space to masking space. - highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0); + highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0); v_MaskingPosition = maskingPos.xy / maskingPos.z; v_Colour = m_Colour; diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 372cfe748e..962c7d9d14 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -126,12 +126,9 @@ namespace osu.Game.Screens private void load(ShaderManager manager) { loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); - + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2_NO_MASKING, FragmentShaderDescriptor.BLUR)); loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder")); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7d4a721c91..08107c2fad 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + From f09b81841810df3e63a9bc6bfa11a369803f0d89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Aug 2023 13:17:12 +0900 Subject: [PATCH 081/103] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 08107c2fad..4c3205178a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From be1a712f33bfa755a049d9b2763a22f45f8a1498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 08:54:41 +0200 Subject: [PATCH 082/103] Make `OsuGame` dependency nullable --- osu.Game/Overlays/NotificationOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 08c567af82..6dd344ca99 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays private AudioManager audio { get; set; } = null!; [Resolved] - private OsuGame game { get; set; } = null!; + private OsuGame? game { get; set; } [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -181,8 +181,8 @@ namespace osu.Game.Overlays if (notification.FlashTaskbar) { - game.Window?.Flash(notification.IsImportant); - notification.Closed += () => game.Window?.CancelFlash(); + game?.Window?.Flash(notification.IsImportant); + notification.Closed += () => game?.Window?.CancelFlash(); } if (State.Value == Visibility.Hidden) From aa29e00578a01768d1ee7bf537e5b2a335c46b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 08:58:10 +0200 Subject: [PATCH 083/103] Remove `FlashTaskbar` and use `IsImportant` directly instead --- osu.Game/Overlays/NotificationOverlay.cs | 4 ++-- osu.Game/Overlays/Notifications/Notification.cs | 5 ----- osu.Game/Screens/Play/PlayerLoader.cs | 2 -- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 6dd344ca99..b93d5f1e12 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -179,9 +179,9 @@ namespace osu.Game.Overlays playDebouncedSample(notification.PopInSampleName); - if (notification.FlashTaskbar) + if (notification.IsImportant) { - game?.Window?.Flash(notification.IsImportant); + game?.Window?.Flash(); notification.Closed += () => game?.Window?.CancelFlash(); } diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 53fc152c96..8cdc373417 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -38,11 +38,6 @@ namespace osu.Game.Overlays.Notifications /// public virtual bool IsImportant => true; - /// - /// Whether this notification should trigger a taskbar flash if the window is un-focused when posted. - /// - public bool FlashTaskbar { get; init; } = true; - /// /// Run on user activating the notification. Return true to close. /// diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index eccfc4dc7b..872425e3fd 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -568,7 +568,6 @@ namespace osu.Game.Screens.Play public MutedNotification() { Text = NotificationsStrings.GameVolumeTooLow; - FlashTaskbar = false; } [BackgroundDependencyLoader] @@ -624,7 +623,6 @@ namespace osu.Game.Screens.Play public BatteryWarningNotification() { Text = NotificationsStrings.BatteryLow; - FlashTaskbar = false; } [BackgroundDependencyLoader] From 142abe1fd0a87a2144ee4f61c40cff1c96ce4605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 09:00:59 +0200 Subject: [PATCH 084/103] Make highlight messages important in order to trigger window flash --- osu.Game/Online/Chat/MessageNotifier.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 65aac723da..56f490cb21 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -182,8 +182,6 @@ namespace osu.Game.Online.Chat private readonly Message message; private readonly Channel channel; - public override bool IsImportant => false; - [BackgroundDependencyLoader] private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay) { From 5be533578499d25c54e18ea76ec2aae2a32a9b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 09:37:54 +0200 Subject: [PATCH 085/103] Reword comment to be better --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index c442fac0b8..e31656e0ff 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -329,8 +329,8 @@ namespace osu.Game.Rulesets.Objects.Drawables Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry); ClearNestedHitObjects(); - // Changes in state trigger defaults applied trigger state updates. - // When a new hitobject is applied, OnApply() automatically performs a state update. + // Changes to `HitObject` properties trigger default application, which triggers `State` updates. + // When a new hitobject is applied, `OnApply()` automatically performs a state update. HitObject.DefaultsApplied -= onDefaultsApplied; entry.RevertResult -= onRevertResult; From c9f611a713916ea36f9a98eb93de4c02c11cbfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 14:30:55 +0200 Subject: [PATCH 086/103] Enable NRT in `TestSceneObjectOrderedHitPolicy` --- .../TestSceneObjectOrderedHitPolicy.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index ee70441688..8c2d8ea84e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -380,7 +378,7 @@ namespace osu.Game.Rulesets.Osu.Tests () => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result)); } - private void addJudgementAssert(string name, Func hitObject, HitResult result) + private void addJudgementAssert(string name, Func hitObject, HitResult result) { AddAssert($"{name} judgement is {result}", () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); @@ -392,8 +390,8 @@ namespace osu.Game.Rulesets.Osu.Tests () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); } - private ScoreAccessibleReplayPlayer currentPlayer; - private List judgementResults; + private ScoreAccessibleReplayPlayer currentPlayer = null!; + private List judgementResults = null!; private void performTest(List hitObjects, List frames) { From ab4d47b594dd45358110db8b68a76705e1089b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 14:37:58 +0200 Subject: [PATCH 087/103] Rewrite assertions to use nunit constraints --- .../TestSceneObjectOrderedHitPolicy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 8c2d8ea84e..9ee55d95a5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -381,13 +381,13 @@ namespace osu.Game.Rulesets.Osu.Tests private void addJudgementAssert(string name, Func hitObject, HitResult result) { AddAssert($"{name} judgement is {result}", - () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); + () => judgementResults.Single(r => r.HitObject == hitObject()).Type, () => Is.EqualTo(result)); } private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", - () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); + () => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(100)); } private ScoreAccessibleReplayPlayer currentPlayer = null!; From 9fd59b807f23c27f298d146c25f737305d73ecfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 15:06:06 +0200 Subject: [PATCH 088/103] Rewrite `TestSceneObjectOrderedHitPolicy` to not rely on custom hitwindows --- .../TestSceneObjectOrderedHitPolicy.cs | 117 +++++++----------- 1 file changed, 47 insertions(+), 70 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 9ee55d95a5..1dd9116da2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -7,7 +7,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Screens; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; @@ -17,6 +16,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -28,8 +28,13 @@ namespace osu.Game.Rulesets.Osu.Tests { public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene { - private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss - private const double late_miss_window = 500; // time after +500 is considered a miss + private readonly OsuHitWindows referenceHitWindows; + + public TestSceneObjectOrderedHitPolicy() + { + referenceHitWindows = new OsuHitWindows(); + referenceHitWindows.SetDifficulty(0); + } /// /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. @@ -44,12 +49,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -63,7 +68,8 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss); - addJudgementOffsetAssert(hitObjects[0], late_miss_window); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); } /// @@ -79,12 +85,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -98,7 +104,8 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss); - addJudgementOffsetAssert(hitObjects[0], late_miss_window); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); } /// @@ -114,12 +121,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -133,7 +140,8 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss); - addJudgementOffsetAssert(hitObjects[0], late_miss_window); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); } /// @@ -149,12 +157,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -167,8 +175,8 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); - addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[0], HitResult.Meh); + addJudgementAssert(hitObjects[1], HitResult.Meh); addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } @@ -186,12 +194,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -204,8 +212,8 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); - addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[0], HitResult.Meh); + addJudgementAssert(hitObjects[1], HitResult.Ok); addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time } @@ -223,19 +231,19 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_circle, Position = positionCircle }, - new TestSlider + new Slider { StartTime = time_slider, Position = positionSlider, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, - new Vector2(25, 0), + new Vector2(50, 0), }) } }; @@ -265,19 +273,19 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_circle, Position = positionCircle }, - new TestSlider + new Slider { StartTime = time_slider, Position = positionSlider, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, - new Vector2(25, 0), + new Vector2(50, 0), }) } }; @@ -285,11 +293,11 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, }); - addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[0], HitResult.Ok); addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); @@ -302,7 +310,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestHitCircleBeforeSpinner() { const double time_spinner = 1500; - const double time_circle = 1800; + const double time_circle = 1600; Vector2 positionCircle = Vector2.Zero; var hitObjects = new List @@ -313,7 +321,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), EndTime = time_spinner + 1000, }, - new TestHitCircle + new HitCircle { StartTime = time_circle, Position = positionCircle @@ -331,7 +339,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Meh); } [Test] @@ -344,12 +352,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_circle, Position = positionCircle }, - new TestSlider + new Slider { StartTime = time_slider, Position = positionSlider, @@ -400,7 +408,11 @@ namespace osu.Game.Rulesets.Osu.Tests Beatmap.Value = CreateWorkingBeatmap(new Beatmap { HitObjects = hitObjects, - Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = 0, + SliderTickRate = 3 + }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo @@ -428,28 +440,6 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } - private class TestHitCircle : HitCircle - { - protected override HitWindows CreateHitWindows() => new TestHitWindows(); - } - - private class TestSlider : Slider - { - public TestSlider() - { - SliderVelocity = 0.1f; - - DefaultsApplied += _ => - { - HeadCircle.HitWindows = new TestHitWindows(); - TailCircle.HitWindows = new TestHitWindows(); - - HeadCircle.HitWindows.SetDifficulty(0); - TailCircle.HitWindows.SetDifficulty(0); - }; - } - } - private class TestSpinner : Spinner { protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) @@ -459,19 +449,6 @@ namespace osu.Game.Rulesets.Osu.Tests } } - private class TestHitWindows : HitWindows - { - private static readonly DifficultyRange[] ranges = - { - new DifficultyRange(HitResult.Great, 500, 500, 500), - new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window), - }; - - public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; - - protected override DifficultyRange[] GetRanges() => ranges; - } - private partial class ScoreAccessibleReplayPlayer : ReplayPlayer { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; From a1b4a5621592234f4787d26a88d1b03830661c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 18:04:20 +0200 Subject: [PATCH 089/103] Add capability to export ordered object policy test cases for stable crosscheck --- .../TestSceneObjectOrderedHitPolicy.cs | 86 ++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 1dd9116da2..5205400a7e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -3,12 +3,17 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -20,6 +25,7 @@ using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; using osuTK; @@ -30,6 +36,13 @@ namespace osu.Game.Rulesets.Osu.Tests { private readonly OsuHitWindows referenceHitWindows; + /// + /// This is provided as a convenience for testing note lock behaviour against osu!stable. + /// Setting this field to a non-null path will cause beatmap files and replays used in all test cases + /// to be exported to disk so that they can be cross-checked against stable. + /// + private readonly string? exportLocation = null; + public TestSceneObjectOrderedHitPolicy() { referenceHitWindows = new OsuHitWindows(); @@ -401,12 +414,21 @@ namespace osu.Game.Rulesets.Osu.Tests private ScoreAccessibleReplayPlayer currentPlayer = null!; private List judgementResults = null!; - private void performTest(List hitObjects, List frames) + private void performTest(List hitObjects, List frames, [CallerMemberName] string testCaseName = "") { - AddStep("load player", () => + IBeatmap playableBeatmap = null!; + Score score = null!; + + AddStep("create beatmap", () => { + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); Beatmap.Value = CreateWorkingBeatmap(new Beatmap { + Metadata = + { + Title = testCaseName + }, HitObjects = hitObjects, Difficulty = new BeatmapDifficulty { @@ -417,11 +439,69 @@ namespace osu.Game.Rulesets.Osu.Tests { Ruleset = new OsuRuleset().RulesetInfo }, + ControlPointInfo = cpi + }); + playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo); + }); + + AddStep("create score", () => + { + score = new Score + { + Replay = new Replay + { + Frames = new List + { + // required for correct playback in stable + new OsuReplayFrame(0, new Vector2(256, -500)), + new OsuReplayFrame(0, new Vector2(256, -500)) + }.Concat(frames).ToList() + }, + ScoreInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = playableBeatmap.BeatmapInfo + } + }; + }); + + if (exportLocation != null) + { + AddStep("create beatmap", () => + { + var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null); + + using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create)) + { + var memoryStream = new MemoryStream(); + using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) + beatmapEncoder.Encode(writer); + + memoryStream.Seek(0, SeekOrigin.Begin); + memoryStream.CopyTo(stream); + memoryStream.Seek(0, SeekOrigin.Begin); + playableBeatmap.BeatmapInfo.MD5Hash = memoryStream.ComputeMD5Hash(); + } }); + AddStep("export score", () => + { + var scoreToEncode = score.DeepClone(); + scoreToEncode.Replay.Frames = scoreToEncode.Replay.Frames.Cast() + .Select(frame => new OsuReplayFrame(frame.Time + LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET, frame.Position, frame.Actions.ToArray())) + .ToList(); + + using var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osr"), FileMode.Create); + var encoder = new LegacyScoreEncoder(scoreToEncode, playableBeatmap); + encoder.Encode(stream); + }); + } + + AddStep("load player", () => + { SelectedMods.Value = new[] { new OsuModClassic() }; - var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + var p = new ScoreAccessibleReplayPlayer(score); p.OnLoadComplete += _ => { From 64786aaee8d99de8757648635cf1daa3d7ec75d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Aug 2023 10:43:48 +0200 Subject: [PATCH 090/103] Adjust test cases slightly to avoid running into hitwindow edge issue Some note lock test cases do not play out correctly when exported out to stable due to a completely separate issue, namely #11311. Adjust the test cases for now to isolate failure vectors. --- .../TestSceneObjectOrderedHitPolicy.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 5205400a7e..fd8973c375 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -184,14 +184,14 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } + new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 90, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); addJudgementAssert(hitObjects[0], HitResult.Meh); addJudgementAssert(hitObjects[1], HitResult.Meh); - addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 - addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 + addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190 + addJudgementOffsetAssert(hitObjects[0], -90); // time_second_circle - first_circle_time - 90 } /// @@ -221,13 +221,13 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); addJudgementAssert(hitObjects[0], HitResult.Meh); addJudgementAssert(hitObjects[1], HitResult.Ok); - addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 + addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190 addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time } @@ -343,7 +343,7 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, From 91c2cadb4737839e36d087c02658b4872cd2456a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Aug 2023 19:13:32 +0900 Subject: [PATCH 091/103] Add missing colon in mod settings tooltip --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f9812d6c00..6d40826afe 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mods var bindable = (IBindable)property.GetValue(this)!; if (!bindable.IsDefault) - tooltipTexts.Add($"{attr.Label} {bindable}"); + tooltipTexts.Add($"{attr.Label}: {bindable}"); } return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); From d6aded3ac31b232884f0642bedc793f41b377b2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Aug 2023 20:11:55 +0900 Subject: [PATCH 092/103] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8ebfde8047..2d15bce85a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - +