diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 1edd21b5a9..3a6051ed43 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -794,5 +794,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(path.Distance, Is.EqualTo(1)); } } + + [Test] + public void TestLegacyDefaultsPreserved() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var memoryStream = new MemoryStream()) + using (var stream = new LineBufferedReader(memoryStream)) + { + var decoded = decoder.Decode(stream); + + Assert.Multiple(() => + { + Assert.That(decoded.BeatmapInfo.AudioLeadIn, Is.EqualTo(0)); + Assert.That(decoded.BeatmapInfo.StackLeniency, Is.EqualTo(0.7f)); + Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False); + Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False); + Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); + Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); + Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); + Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); + Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); + Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); + Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); + }); + } + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs index 235842acc9..ddb0872541 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs @@ -3,9 +3,9 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Input; @@ -19,28 +19,35 @@ namespace osu.Game.Tests.Visual.Gameplay protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms - [BackgroundDependencyLoader] - private void load() + private HoldForMenuButton holdForMenuButton; + + [SetUpSteps] + public void SetUpSteps() { - HoldForMenuButton holdForMenuButton; - - Add(holdForMenuButton = new HoldForMenuButton + AddStep("create button", () => { - Origin = Anchor.BottomRight, - Anchor = Anchor.BottomRight, - Action = () => exitAction = true + exitAction = false; + + Child = holdForMenuButton = new HoldForMenuButton + { + Scale = new Vector2(2), + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Action = () => exitAction = true + }; }); + } - var text = holdForMenuButton.Children.OfType().First(); - + [Test] + public void TestMovementAndTrigger() + { AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(holdForMenuButton)); - AddUntilStep("Text visible", () => text.IsPresent && !exitAction); + AddUntilStep("Text visible", () => getSpriteText().IsPresent && !exitAction); AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One)); - AddUntilStep("Text is not visible", () => !text.IsPresent && !exitAction); + AddUntilStep("Text is not visible", () => !getSpriteText().IsPresent && !exitAction); AddStep("Trigger exit action", () => { - exitAction = false; InputManager.MoveMouseTo(holdForMenuButton); InputManager.PressButton(MouseButton.Left); }); @@ -50,6 +57,17 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Trigger exit action", () => InputManager.PressButton(MouseButton.Left)); AddUntilStep($"{nameof(holdForMenuButton.Action)} was triggered", () => exitAction); + AddStep("Release", () => InputManager.ReleaseButton(MouseButton.Left)); } + + [Test] + public void TestFadeOnNoInput() + { + AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.One)); + AddUntilStep("wait for text fade out", () => !getSpriteText().IsPresent); + AddUntilStep("wait for button fade out", () => holdForMenuButton.Alpha < 0.1f); + } + + private SpriteText getSpriteText() => holdForMenuButton.Children.OfType().First(); } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 89dca77af4..3be7cf7c5c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -1,6 +1,7 @@ // 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.Allocation; @@ -10,16 +11,19 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; using osuTK; @@ -96,35 +100,81 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestRetryFromResults() { - Player player = null; - ResultsScreen results = null; + var getOriginalPlayer = playToResults(); - IWorkingBeatmap beatmap() => Game.Beatmap.Value; + AddStep("attempt to retry", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().First().Action()); + AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); + } - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + [Test] + public void TestDeleteAllScoresAfterPlaying() + { + playToResults(); - AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + ScoreInfo score = null; + LeaderboardScore scorePanel = null; - AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score); - AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } }); + AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == false)); - AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddStep("press back button", () => Game.ChildrenOfType().First().Action()); - AddUntilStep("wait for player", () => + AddStep("show local scores", () => Game.ChildrenOfType().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local)); + + AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); + + AddStep("open options", () => InputManager.Key(Key.F3)); + + AddStep("choose clear all scores", () => InputManager.Key(Key.Number4)); + + AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); + + AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true)); + + AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null); + } + + [Test] + public void TestDeleteScoreAfterPlaying() + { + playToResults(); + + ScoreInfo score = null; + LeaderboardScore scorePanel = null; + + AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score); + + AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == false)); + + AddStep("press back button", () => Game.ChildrenOfType().First().Action()); + + AddStep("show local scores", () => Game.ChildrenOfType().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local)); + + AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); + + AddStep("right click panel", () => { - // dismiss any notifications that may appear (ie. muted notification). - clickMouseInCentre(); - return (player = Game.ScreenStack.CurrentScreen as Player) != null; + InputManager.MoveMouseTo(scorePanel); + InputManager.Click(MouseButton.Right); }); - AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning); - AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000)); - AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded); - AddStep("attempt to retry", () => results.ChildrenOfType().First().Action()); - AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player); + AddStep("click delete", () => + { + var dropdownItem = Game + .ChildrenOfType().First() + .ChildrenOfType().First() + .ChildrenOfType().First(i => i.Item.Text.ToString() == "Delete"); + + InputManager.MoveMouseTo(dropdownItem); + InputManager.Click(MouseButton.Left); + }); + + AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); + + AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true)); + + AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null); } [TestCase(true)] @@ -432,6 +482,37 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("test dispose doesn't crash", () => Game.Dispose()); } + private Func playToResults() + { + Player player = null; + + IWorkingBeatmap beatmap() => Game.Beatmap.Value; + + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } }); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + // dismiss any notifications that may appear (ie. muted notification). + clickMouseInCentre(); + return (player = Game.ScreenStack.CurrentScreen as Player) != null; + }); + + AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning); + AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000)); + AddUntilStep("wait for pass", () => (Game.ScreenStack.CurrentScreen as ResultsScreen)?.IsLoaded == true); + return () => player; + } + private void clickMouseInCentre() { InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 830631ebad..a23bc620ec 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets; @@ -40,6 +41,36 @@ namespace osu.Game.Tests.Visual.SongSelect this.rulesets = rulesets; } + [Test] + public void TestScrollPositionMaintainedOnAdd() + { + loadBeatmaps(count: 1, randomDifficulties: false); + + for (int i = 0; i < 10; i++) + { + AddRepeatStep("Add some sets", () => carousel.UpdateBeatmapSet(TestResources.CreateTestBeatmapSetInfo()), 4); + + checkSelectionIsCentered(); + } + } + + [Test] + public void TestScrollPositionMaintainedOnDelete() + { + loadBeatmaps(count: 50, randomDifficulties: false); + + for (int i = 0; i < 10; i++) + { + AddRepeatStep("Remove some sets", () => + carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item) + .OfType() + .OrderBy(item => item.GetHashCode()) + .First(item => item.State.Value != CarouselItemState.Selected && item.Visible).BeatmapSet), 4); + + checkSelectionIsCentered(); + } + } + [Test] public void TestManyPanels() { @@ -813,6 +844,18 @@ namespace osu.Game.Tests.Visual.SongSelect carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count); } + private void checkSelectionIsCentered() + { + AddAssert("Selected panel is centered", () => + { + return Precision.AlmostEquals( + carousel.ScreenSpaceDrawQuad.Centre, + carousel.Items + .First(i => i.Item.State.Value == CarouselItemState.Selected) + .ScreenSpaceDrawQuad.Centre, 100); + }); + } + private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null); private void nextRandom() => diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 5d613894d4..7967f54b49 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -61,18 +61,15 @@ namespace osu.Game.Tournament loadingSpinner.Show(); - BracketLoadTask.ContinueWith(t => + BracketLoadTask.ContinueWith(t => Schedule(() => { if (t.IsFaulted) { - Schedule(() => - { - loadingSpinner.Hide(); - loadingSpinner.Expire(); + loadingSpinner.Hide(); + loadingSpinner.Expire(); - Logger.Error(t.Exception, "Couldn't load bracket with error"); - Add(new WarningBox($"Your {BRACKET_FILENAME} file could not be parsed. Please check runtime.log for more details.")); - }); + Logger.Error(t.Exception, "Couldn't load bracket with error"); + Add(new WarningBox($"Your {BRACKET_FILENAME} file could not be parsed. Please check runtime.log for more details.")); return; } @@ -143,7 +140,7 @@ namespace osu.Game.Tournament windowMode.Value = WindowMode.Windowed; }), true); }); - }); + })); } } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 3e6e33f1d0..e4bfd768b7 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -99,11 +99,11 @@ namespace osu.Game.Beatmaps public bool LetterboxInBreaks { get; set; } - public bool WidescreenStoryboard { get; set; } + public bool WidescreenStoryboard { get; set; } = true; public bool EpilepsyWarning { get; set; } - public bool SamplesMatchPlaybackRate { get; set; } + public bool SamplesMatchPlaybackRate { get; set; } = true; public double DistanceSpacing { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 414b7cd12b..e4fdb3d471 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -90,14 +90,7 @@ namespace osu.Game.Beatmaps { Beatmaps = { - new BeatmapInfo - { - Difficulty = new BeatmapDifficulty(), - Ruleset = ruleset, - Metadata = metadata, - WidescreenStoryboard = true, - SamplesMatchPlaybackRate = true, - } + new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) } }; diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index cb38373bd3..1514d3af7a 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps /// The time in milliseconds to begin playing the track for preview purposes. /// If -1, the track should begin playing at 40% of its length. /// - public int PreviewTime { get; set; } + public int PreviewTime { get; set; } = -1; public string AudioFile { get; set; } = string.Empty; public string BackgroundFile { get; set; } = string.Empty; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 35d1cefeca..01ba0fcc9f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -56,6 +56,8 @@ namespace osu.Game.Beatmaps.Formats this.beatmap = beatmap; this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; + applyLegacyDefaults(this.beatmap.BeatmapInfo); + base.ParseStreamInto(stream, beatmap); flushPendingPoints(); @@ -70,6 +72,19 @@ namespace osu.Game.Beatmaps.Formats hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.Difficulty); } + /// + /// Some `BeatmapInfo` members have default values that differ from the default values used by stable. + /// In addition, legacy beatmaps will sometimes not contain some configuration keys, in which case + /// the legacy default values should be used. + /// This method's intention is to restore those legacy defaults. + /// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29 + /// + private void applyLegacyDefaults(BeatmapInfo beatmapInfo) + { + beatmapInfo.WidescreenStoryboard = false; + beatmapInfo.SamplesMatchPlaybackRate = false; + } + protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(' ') || line.StartsWith('_'); protected override void ParseLine(Beatmap beatmap, Section section, string line) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 9fb0130619..a53704df9d 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -374,12 +374,12 @@ namespace osu.Game.Database log($"Migrated {written}/{count} scores..."); } - var beatmap = r.All().First(b => b.Hash == score.BeatmapInfo.Hash); + var beatmap = r.All().FirstOrDefault(b => b.Hash == score.BeatmapInfo.Hash); var ruleset = r.Find(score.Ruleset.ShortName); - if (ruleset == null) + if (beatmap == null || ruleset == null) { - log($"Skipping {++missing} scores with missing ruleset"); + log($"Skipping {++missing} scores with missing ruleset or beatmap"); continue; } diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index 243be8da44..5eddb3b49d 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; @@ -66,6 +68,8 @@ namespace osu.Game.Online private void doPoll() { + Debug.Assert(ThreadSafety.IsUpdateThread); + scheduledPoll = null; pollingActive = true; Poll().ContinueWith(_ => pollComplete()); @@ -96,13 +100,13 @@ namespace osu.Game.Online if (!lastTimePolled.HasValue) { - doPoll(); + Scheduler.AddOnce(doPoll); return; } if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value) { - doPoll(); + Scheduler.AddOnce(doPoll); return; } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 1f3f73a60a..ec795cf6b2 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -107,6 +107,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { set { + if (score == null && value == null) + return; + if (score?.Equals(value) == true) return; diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 8f665224ee..6f9cce2d3c 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -266,6 +266,15 @@ namespace osu.Game.Scoring }); } + public void Delete(BeatmapInfo beatmap, bool silent = false) + { + realm.Run(r => + { + var beatmapScores = r.Find(beatmap.ID).Scores.ToList(); + scoreModelManager.Delete(beatmapScores, silent); + }); + } + public void Delete(List items, bool silent = false) { scoreModelManager.Delete(items, silent); diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 2bdf59b21c..2cde962b12 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -82,6 +82,11 @@ namespace osu.Game.Screens.Edit.Compose protected override void LoadComplete() { base.LoadComplete(); + + // May be null in the case of a ruleset that doesn't have editor support, see CreateMainContent(). + if (composer == null) + return; + EditorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => updateClipboardActionAvailability()); clipboard.BindValueChanged(_ => updateClipboardActionAvailability()); composer.OnLoadComplete += _ => updateClipboardActionAvailability(); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 4812da98f4..2fead84deb 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -28,6 +28,8 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; @@ -61,7 +63,16 @@ namespace osu.Game.Screens.Edit public override bool? AllowTrackAdjustments => false; - protected bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash; + protected bool HasUnsavedChanges + { + get + { + if (!canSave) + return false; + + return lastSavedHash != changeHandler?.CurrentStateHash; + } + } [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -72,10 +83,15 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private DialogOverlay dialogOverlay { get; set; } + [Resolved(canBeNull: true)] + private NotificationOverlay notifications { get; set; } + public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; private readonly Bindable samplePlaybackDisabled = new Bindable(); + private bool canSave; + private bool exitConfirmed; private string lastSavedHash; @@ -92,6 +108,8 @@ namespace osu.Game.Screens.Edit private IBeatmap playableBeatmap; private EditorBeatmap editorBeatmap; + + [CanBeNull] // Should be non-null once it can support custom rulesets. private EditorChangeHandler changeHandler; private EditorMenuBar menuBar; @@ -172,8 +190,14 @@ namespace osu.Game.Screens.Edit AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo)); dependencies.CacheAs(editorBeatmap); - changeHandler = new EditorChangeHandler(editorBeatmap); - dependencies.CacheAs(changeHandler); + + canSave = editorBeatmap.BeatmapInfo.Ruleset.CreateInstance() is ILegacyRuleset; + + if (canSave) + { + changeHandler = new EditorChangeHandler(editorBeatmap); + dependencies.CacheAs(changeHandler); + } beatDivisor.Value = editorBeatmap.BeatmapInfo.BeatDivisor; beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); @@ -311,8 +335,8 @@ namespace osu.Game.Screens.Edit } }); - changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); - changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); + changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); + changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); menuBar.Mode.ValueChanged += onModeChanged; } @@ -353,6 +377,12 @@ namespace osu.Game.Screens.Edit protected void Save() { + if (!canSave) + { + notifications?.Post(new SimpleErrorNotification { Text = "Saving is not supported for this ruleset yet, sorry!" }); + return; + } + // no longer new after first user-triggered save. isNewBeatmap = false; @@ -648,9 +678,9 @@ namespace osu.Game.Screens.Edit #endregion - protected void Undo() => changeHandler.RestoreState(-1); + protected void Undo() => changeHandler?.RestoreState(-1); - protected void Redo() => changeHandler.RestoreState(1); + protected void Redo() => changeHandler?.RestoreState(1); private void resetTrack(bool seekToStart = false) { @@ -761,7 +791,7 @@ namespace osu.Game.Screens.Edit private void updateLastSavedHash() { - lastSavedHash = changeHandler.CurrentStateHash; + lastSavedHash = changeHandler?.CurrentStateHash; } private List createFileMenuItems() diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index fcf7767958..666d425f62 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Components }); } - private GetRoomsRequest pollReq; + private GetRoomsRequest lastPollRequest; protected override Task Poll() { @@ -45,10 +45,11 @@ namespace osu.Game.Screens.OnlinePlay.Components var tcs = new TaskCompletionSource(); - pollReq?.Cancel(); - pollReq = new GetRoomsRequest(Filter.Value.Status, Filter.Value.Category); + lastPollRequest?.Cancel(); - pollReq.Success += result => + var req = new GetRoomsRequest(Filter.Value.Status, Filter.Value.Category); + + req.Success += result => { foreach (var existing in RoomManager.Rooms.ToArray()) { @@ -66,10 +67,11 @@ namespace osu.Game.Screens.OnlinePlay.Components tcs.SetResult(true); }; - pollReq.Failure += _ => tcs.SetResult(false); + req.Failure += _ => tcs.SetResult(false); - API.Queue(pollReq); + API.Queue(req); + lastPollRequest = req; return tcs.Task; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index 22842fbb9e..e05bdf8c8e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay.Components this.room = room; } - private GetRoomRequest pollReq; + private GetRoomRequest lastPollRequest; protected override Task Poll() { @@ -30,19 +30,22 @@ namespace osu.Game.Screens.OnlinePlay.Components var tcs = new TaskCompletionSource(); - pollReq?.Cancel(); - pollReq = new GetRoomRequest(room.RoomID.Value.Value); + lastPollRequest?.Cancel(); - pollReq.Success += result => + var req = new GetRoomRequest(room.RoomID.Value.Value); + + req.Success += result => { result.RemoveExpiredPlaylistItems(); RoomManager.AddOrUpdateRoom(result); tcs.SetResult(true); }; - pollReq.Failure += _ => tcs.SetResult(false); + req.Failure += _ => tcs.SetResult(false); - API.Queue(pollReq); + API.Queue(req); + + lastPollRequest = req; return tcs.Task; } diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 5a7ef786d3..430f001427 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); } - private float positionalAdjust; + private float positionalAdjust = 1; // Start at 1 to handle the case where a user never send positional input. protected override bool OnMouseMove(MouseMoveEvent e) { diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index dff2c598c3..c27915c383 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -286,6 +286,9 @@ namespace osu.Game.Screens.Select root.RemoveChild(existingSet); itemsCache.Invalidate(); + + if (!Scroll.UserScrolling) + ScrollToSelected(true); }); public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => @@ -311,13 +314,10 @@ namespace osu.Game.Screens.Select itemsCache.Invalidate(); - Schedule(() => - { - if (!Scroll.UserScrolling) - ScrollToSelected(true); + if (!Scroll.UserScrolling) + ScrollToSelected(true); - BeatmapSetsChanged?.Invoke(); - }); + BeatmapSetsChanged?.Invoke(); }); /// diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index 774d3b4b28..4a16be4a3a 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select Text = @"Yes. Please.", Action = () => { - Task.Run(() => scoreManager.Delete(s => !s.DeletePending && s.BeatmapInfo.ID == beatmapInfo.ID)) + Task.Run(() => scoreManager.Delete(beatmapInfo)) .ContinueWith(_ => onCompletion); } }, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2070e53257..31cbe91f5c 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -38,6 +38,14 @@ namespace osu.Game.Screens.Select.Leaderboards get => beatmapInfo; set { + if (beatmapInfo == null && value == null) + { + // always null scores to ensure a correct initial display. + // see weird `scoresLoadedOnce` logic in base implementation. + Scores = null; + return; + } + if (beatmapInfo?.Equals(value) == true) return; diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index ebbd9bb5dd..3b8d9a4cd1 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -14,6 +14,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays; @@ -111,6 +112,8 @@ namespace osu.Game.Tests.Visual public new ScreenStack ScreenStack => base.ScreenStack; + public RealmAccess Realm => Dependencies.Get(); + public new BackButton BackButton => base.BackButton; public new BeatmapManager BeatmapManager => base.BeatmapManager; @@ -158,6 +161,14 @@ namespace osu.Game.Tests.Visual Dependencies.Get().SetValue(Static.MutedAudioNotificationShownOnce, true); } + + protected override void Update() + { + base.Update(); + + // when running in visual tests and the window loses focus, we generally don't want the game to pause. + ((Bindable)IsActive).Value = true; + } } public class TestLoader : Loader