diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 2250868a39..007716bd6c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -209,10 +209,14 @@ namespace osu.Game.Tests.Visual.Editing public override void TearDownSteps() { base.TearDownSteps(); - AddStep("delete imported", () => + AddStep("delete imported", () => Realm.Write(r => { - beatmaps.Delete(importedBeatmapSet); - }); + // delete from realm directly rather than via `BeatmapManager` to avoid cross-test pollution + // (`BeatmapManager.Delete()` uses soft deletion, which can lead to beatmap reuse between test cases). + r.RemoveAll(); + r.RemoveAll(); + r.RemoveAll(); + })); } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 1b2bb57b84..5483be5676 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); - AddStep("test gameplay", () => ((Editor)Game.ScreenStack.CurrentScreen).TestGameplay()); + AddStep("test gameplay", () => getEditor().TestGameplay()); AddUntilStep("wait for player", () => { @@ -141,6 +141,37 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor); } - private EditorBeatmap getEditorBeatmap() => ((Editor)Game.ScreenStack.CurrentScreen).ChildrenOfType().Single(); + [Test] + public void TestLastTimestampRememberedOnExit() + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); + AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); + + AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("wait for song select", + () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded); + + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddStep("seek to arbitrary time", () => getEditor().ChildrenOfType().First().Seek(1234)); + AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234)); + + AddStep("exit editor", () => InputManager.Key(Key.Escape)); + AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor); + + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit()); + + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234)); + } + + private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single(); + + private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen; } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 393feff087..5019d64276 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -171,6 +171,11 @@ namespace osu.Game.Beatmaps public double TimelineZoom { get; set; } = 1.0; + /// + /// The time in milliseconds when last exiting the editor with this beatmap loaded. + /// + public double? EditorTimestamp { get; set; } + [Ignored] public CountdownType Countdown { get; set; } = CountdownType.Normal; diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 94108531e8..f4c6c802f1 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -71,8 +71,9 @@ namespace osu.Game.Database /// 24 2022-08-22 Added MaximumStatistics to ScoreInfo. /// 25 2022-09-18 Remove skins to add with new naming. /// 26 2023-02-05 Added BeatmapHash to ScoreInfo. + /// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo. /// - private const int schema_version = 26; + private const int schema_version = 27; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b8fa7f6579..bb052b1d22 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -28,6 +28,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -92,6 +93,9 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private INotificationOverlay notifications { get; set; } + [Resolved] + private RealmAccess realm { get; set; } + public readonly Bindable Mode = new Bindable(); public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; @@ -700,6 +704,13 @@ namespace osu.Game.Screens.Edit } } + realm.Write(r => + { + var beatmap = r.Find(editorBeatmap.BeatmapInfo.ID); + if (beatmap != null) + beatmap.EditorTimestamp = clock.CurrentTime; + }); + ApplyToBackground(b => { b.DimWhenUserSettingsIgnored.Value = 0; @@ -833,7 +844,11 @@ namespace osu.Game.Screens.Edit { double targetTime = 0; - if (Beatmap.Value.Beatmap.HitObjects.Count > 0) + if (editorBeatmap.BeatmapInfo.EditorTimestamp != null) + { + targetTime = editorBeatmap.BeatmapInfo.EditorTimestamp.Value; + } + else if (Beatmap.Value.Beatmap.HitObjects.Count > 0) { // seek to one beat length before the first hitobject targetTime = Beatmap.Value.Beatmap.HitObjects[0].StartTime;