From d3c66e240459f6d563476c5c02c223e12252819c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Jul 2024 12:07:13 +0900 Subject: [PATCH 01/18] Add basic flow for mounting beatmaps for external editing --- osu.Game/Beatmaps/BeatmapManager.cs | 3 ++ osu.Game/Database/ExternalEditOperation.cs | 48 +++++++++++++++++++ osu.Game/Database/IModelImporter.cs | 6 +++ .../Database/RealmArchiveModelImporter.cs | 24 ++++++++++ osu.Game/Scoring/ScoreManager.cs | 3 +- osu.Game/Screens/Edit/Editor.cs | 43 +++++++++++++++++ osu.Game/Skinning/SkinManager.cs | 2 + 7 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Database/ExternalEditOperation.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 0610f7f6fb..e90b3c703f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -415,6 +415,9 @@ namespace osu.Game.Beatmaps public Task?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) => beatmapImporter.ImportAsUpdate(notification, importTask, original); + public Task> BeginExternalEditing(BeatmapSetInfo model) => + beatmapImporter.BeginExternalEditing(model); + public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm)); public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm)); diff --git a/osu.Game/Database/ExternalEditOperation.cs b/osu.Game/Database/ExternalEditOperation.cs new file mode 100644 index 0000000000..ab74cba7d5 --- /dev/null +++ b/osu.Game/Database/ExternalEditOperation.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Threading.Tasks; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + public class ExternalEditOperation where TModel : class, IHasGuidPrimaryKey + { + public readonly string MountedPath; + + private readonly IModelImporter importer; + private readonly TModel original; + + private bool isMounted; + + public ExternalEditOperation(IModelImporter importer, TModel original, string path) + { + this.importer = importer; + this.original = original; + + MountedPath = path; + + isMounted = true; + } + + public async Task?> Finish() + { + if (!Directory.Exists(MountedPath) || !isMounted) + return null; + + Live? imported = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(MountedPath), original) + .ConfigureAwait(false); + + try + { + Directory.Delete(MountedPath, true); + } + catch { } + + isMounted = false; + + return imported; + } + } +} diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index dcbbad0d35..c2e5517f2a 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -34,6 +34,12 @@ namespace osu.Game.Database /// The imported model. Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original); + /// + /// Mount all files for a to a temporary directory to allow for external editing. + /// + /// The to mount. + public Task> BeginExternalEditing(TModel model); + /// /// A user displayable name for the model type associated with this manager. /// diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 0014e246dc..38df2ac1dc 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -179,6 +179,30 @@ namespace osu.Game.Database public virtual Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original) => throw new NotImplementedException(); + public async Task> BeginExternalEditing(TModel model) + { + string mountedPath = Path.Join(Path.GetTempPath(), model.Hash); + + if (Directory.Exists(mountedPath)) + Directory.Delete(mountedPath, true); + + Directory.CreateDirectory(mountedPath); + + foreach (var realmFile in model.Files) + { + string sourcePath = Files.Storage.GetFullPath(realmFile.File.GetStoragePath()); + string destinationPath = Path.Join(mountedPath, realmFile.Filename); + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + + using (var inStream = Files.Storage.GetStream(sourcePath)) + using (var outStream = File.Create(destinationPath)) + await inStream.CopyToAsync(outStream).ConfigureAwait(false); + } + + return new ExternalEditOperation(this, model, mountedPath); + } + /// /// Import one from the filesystem and delete the file on success. /// Note that this bypasses the UI flow and should only be used for special cases or testing. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index df4735b5e6..e3601fe91e 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -15,10 +15,10 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.IO.Archives; +using osu.Game.Online.API; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; -using osu.Game.Online.API; using osu.Game.Scoring.Legacy; namespace osu.Game.Scoring @@ -214,6 +214,7 @@ namespace osu.Game.Scoring } public Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original); + public Task> BeginExternalEditing(ScoreInfo model) => scoreImporter.BeginExternalEditing(model); public Live? Import(ScoreInfo item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => scoreImporter.ImportModel(item, archive, parameters, cancellationToken); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 27d0392b1e..ff8cf3997e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; @@ -13,6 +14,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -22,6 +24,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Threading; @@ -820,6 +823,9 @@ namespace osu.Game.Screens.Edit resetTrack(); + fileMountOperation?.Dispose(); + fileMountOperation = null; + refetchBeatmap(); return base.OnExiting(e); @@ -1095,6 +1101,11 @@ namespace osu.Game.Screens.Edit lastSavedHash = changeHandler?.CurrentStateHash; } + private EditorMenuItem mountFilesItem; + + [CanBeNull] + private Task> fileMountOperation; + private IEnumerable createFileMenuItems() { yield return createDifficultyCreationMenu(); @@ -1112,12 +1123,44 @@ namespace osu.Game.Screens.Edit var export = createExportMenu(); saveRelatedMenuItems.AddRange(export.Items); yield return export; + + yield return mountFilesItem = new EditorMenuItem("Mount files", MenuItemType.Standard, mountFiles); } yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } + [Resolved] + private GameHost gameHost { get; set; } + + private void mountFiles() + { + if (fileMountOperation == null) + { + Save(); + + fileMountOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); + mountFilesItem.Text.Value = "Dismount files"; + + fileMountOperation.ContinueWith(t => + { + var operation = t.GetResultSafely(); + + // Ensure the trailing separator is present in order to show the folder contents. + gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + }); + } + else + { + fileMountOperation.GetResultSafely().Finish().ContinueWith(t => Schedule(() => + { + fileMountOperation = null; + SwitchToDifficulty(t.GetResultSafely().Value.Detach().Beatmaps.First()); + })); + } + } + private EditorMenuItem createExportMenu() { var exportItems = new List diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 59c2a8bca0..4f816d88d2 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -312,6 +312,8 @@ namespace osu.Game.Skinning public Task> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) => skinImporter.ImportAsUpdate(notification, task, original); + public Task> BeginExternalEditing(SkinInfo model) => skinImporter.BeginExternalEditing(model); + public Task> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) => skinImporter.Import(task, parameters, cancellationToken); From 118162c6315a7d93023dac06d8d253e56b0073e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 20:57:29 +0900 Subject: [PATCH 02/18] Add sub screen to limit user interactions --- osu.Game/Screens/Edit/Editor.cs | 59 +++++------------- osu.Game/Screens/Edit/ExternalEditScreen.cs | 68 +++++++++++++++++++++ 2 files changed, 84 insertions(+), 43 deletions(-) create mode 100644 osu.Game/Screens/Edit/ExternalEditScreen.cs diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ff8cf3997e..a675b41833 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; @@ -14,7 +13,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -24,7 +22,6 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Threading; @@ -823,9 +820,6 @@ namespace osu.Game.Screens.Edit resetTrack(); - fileMountOperation?.Dispose(); - fileMountOperation = null; - refetchBeatmap(); return base.OnExiting(e); @@ -1101,11 +1095,6 @@ namespace osu.Game.Screens.Edit lastSavedHash = changeHandler?.CurrentStateHash; } - private EditorMenuItem mountFilesItem; - - [CanBeNull] - private Task> fileMountOperation; - private IEnumerable createFileMenuItems() { yield return createDifficultyCreationMenu(); @@ -1124,43 +1113,15 @@ namespace osu.Game.Screens.Edit saveRelatedMenuItems.AddRange(export.Items); yield return export; - yield return mountFilesItem = new EditorMenuItem("Mount files", MenuItemType.Standard, mountFiles); + var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally); + saveRelatedMenuItems.Add(externalEdit); + yield return externalEdit; } yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } - [Resolved] - private GameHost gameHost { get; set; } - - private void mountFiles() - { - if (fileMountOperation == null) - { - Save(); - - fileMountOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); - mountFilesItem.Text.Value = "Dismount files"; - - fileMountOperation.ContinueWith(t => - { - var operation = t.GetResultSafely(); - - // Ensure the trailing separator is present in order to show the folder contents. - gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); - }); - } - else - { - fileMountOperation.GetResultSafely().Finish().ContinueWith(t => Schedule(() => - { - fileMountOperation = null; - SwitchToDifficulty(t.GetResultSafely().Value.Detach().Beatmaps.First()); - })); - } - } - private EditorMenuItem createExportMenu() { var exportItems = new List @@ -1172,6 +1133,14 @@ namespace osu.Game.Screens.Edit return new EditorMenuItem(CommonStrings.Export) { Items = exportItems }; } + private void editExternally() + { + Save(); + + var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); + this.Push(new ExternalEditScreen(editOperation, this)); + } + private void exportBeatmap(bool legacy) { if (HasUnsavedChanges) @@ -1303,7 +1272,11 @@ namespace osu.Game.Screens.Edit return new EditorMenuItem(EditorStrings.ChangeDifficulty) { Items = difficultyItems }; } - protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); + public void SwitchToDifficulty(BeatmapInfo nextBeatmap) + { + switchingDifficulty = true; + loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); + } private void cancelExit() { diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs new file mode 100644 index 0000000000..79a10c6292 --- /dev/null +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -0,0 +1,68 @@ +#nullable enable +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Screens.Edit +{ + internal partial class ExternalEditScreen : OsuScreen + { + private readonly Task> fileMountOperation; + + [Resolved] + private GameHost gameHost { get; set; } = null!; + + private readonly Editor? editor; + + public ExternalEditScreen(Task> fileMountOperation, Editor editor) + { + this.fileMountOperation = fileMountOperation; + this.editor = editor; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + fileMountOperation.ContinueWith(t => + { + var operation = t.GetResultSafely>(); + + // Ensure the trailing separator is present in order to show the folder contents. + gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + }); + + InternalChildren = new Drawable[] + { + new SettingsButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "end editing", + Action = finish, + } + }; + } + + private void finish() + { + fileMountOperation.GetResultSafely().Finish().ContinueWith(t => + { + Schedule(() => + { + editor?.SwitchToDifficulty(t.GetResultSafely>().Value.Detach().Beatmaps.First()); + }); + }); + } + } +} From 74aa05fa6ed5eec8bad5e2d6b0ccef0788b93677 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 21:20:29 +0900 Subject: [PATCH 03/18] Improve UX and styling of external edit screen --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 108 +++++++++++++++++--- 1 file changed, 92 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index 79a10c6292..047a4d442e 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -1,4 +1,3 @@ -#nullable enable // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. @@ -8,10 +7,17 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Overlays.Settings; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osuTK; namespace osu.Game.Screens.Edit { @@ -22,36 +28,106 @@ namespace osu.Game.Screens.Edit [Resolved] private GameHost gameHost { get; set; } = null!; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + private readonly Editor? editor; + private ExternalEditOperation? operation; + public ExternalEditScreen(Task> fileMountOperation, Editor editor) { this.fileMountOperation = fileMountOperation; this.editor = editor; } + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new Container + { + Masking = true, + CornerRadius = 20, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background5, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Margin = new MarginPadding(20), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Beatmap is mounted externally", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + } + }, + new PurpleRoundedButton + { + Text = "Open folder", + Width = 350, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = open, + }, + new DangerousRoundedButton + { + Text = "Finish editing and import changes", + Width = 350, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = finish, + } + } + } + } + }; + } + protected override void LoadComplete() { base.LoadComplete(); fileMountOperation.ContinueWith(t => { - var operation = t.GetResultSafely>(); - - // Ensure the trailing separator is present in order to show the folder contents. - gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + operation = t.GetResultSafely(); + Schedule(open); }); + } - InternalChildren = new Drawable[] - { - new SettingsButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "end editing", - Action = finish, - } - }; + private void open() + { + if (operation == null) + return; + + // Ensure the trailing separator is present in order to show the folder contents. + gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); } private void finish() From 27ab54882b16e83d3e487da0c19ebdd652d5875c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 21:50:33 +0900 Subject: [PATCH 04/18] Add loading segments and tidy things up --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 158 ++++++++++++++------ 1 file changed, 114 insertions(+), 44 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index 047a4d442e..fd438eacb3 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.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.IO; using System.Linq; using System.Threading.Tasks; @@ -10,11 +11,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; @@ -31,10 +36,14 @@ namespace osu.Game.Screens.Edit [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private readonly Editor? editor; + private readonly Editor editor; private ExternalEditOperation? operation; + private double timeLoaded; + + private FillFlowContainer flow = null!; + public ExternalEditScreen(Task> fileMountOperation, Editor editor) { this.fileMountOperation = fileMountOperation; @@ -60,64 +69,78 @@ namespace osu.Game.Screens.Edit Colour = colourProvider.Background5, RelativeSizeAxes = Axes.Both, }, - new FillFlowContainer + flow = new FillFlowContainer { Margin = new MarginPadding(20), AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Spacing = new Vector2(15), - Children = new Drawable[] - { - new OsuSpriteText - { - Text = "Beatmap is mounted externally", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(15), - Children = new Drawable[] - { - } - }, - new PurpleRoundedButton - { - Text = "Open folder", - Width = 350, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = open, - }, - new DangerousRoundedButton - { - Text = "Finish editing and import changes", - Width = 350, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = finish, - } - } } } }; + + showSpinner("Exporting for edit..."); } protected override void LoadComplete() { base.LoadComplete(); + timeLoaded = Time.Current; + fileMountOperation.ContinueWith(t => { operation = t.GetResultSafely(); - Schedule(open); + + Scheduler.AddDelayed(() => + { + flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Beatmap is mounted externally", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new OsuTextFlowContainer + { + Padding = new MarginPadding(5), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 350, + AutoSizeAxes = Axes.Y, + Text = "Any changes made to the exported folder will be imported to the game, including file additions, modifications and deletions.", + }, + new PurpleRoundedButton + { + Text = "Open folder", + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = open, + Enabled = { Value = false } + }, + new DangerousRoundedButton + { + Text = "Finish editing and import changes", + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = finish, + Enabled = { Value = false } + } + }; + + Scheduler.AddDelayed(() => + { + foreach (var b in flow.ChildrenOfType()) + b.Enabled.Value = true; + open(); + }, 1000); + }, Math.Max(0, 1000 - (Time.Current - timeLoaded))); }); } @@ -130,15 +153,62 @@ namespace osu.Game.Screens.Edit gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); } + public override bool OnExiting(ScreenExitEvent e) + { + if (!fileMountOperation.IsCompleted) + return false; + + if (operation != null) + { + finish(); + return false; + } + + return base.OnExiting(e); + } + private void finish() { - fileMountOperation.GetResultSafely().Finish().ContinueWith(t => + showSpinner("Cleaning up..."); + + EditOperation!.Finish().ContinueWith(t => { Schedule(() => { - editor?.SwitchToDifficulty(t.GetResultSafely>().Value.Detach().Beatmaps.First()); + // Setting to null will allow exit to succeed. + operation = null; + + var beatmap = t.GetResultSafely(); + + if (beatmap == null) + this.Exit(); + else + editor.SwitchToDifficulty(beatmap.Value.Detach().Beatmaps.First()); }); }); } + + private void showSpinner(string text) + { + foreach (var b in flow.ChildrenOfType()) + b.Enabled.Value = false; + + flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = text, + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new LoadingSpinner + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + State = { Value = Visibility.Visible } + }, + }; + } } } From 3beca64cc514f4667340b004e6ee0553ca7cd92c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 21:53:57 +0900 Subject: [PATCH 05/18] Attempt to stay on correct difficulty --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index fd438eacb3..ae5fad3ec0 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -169,6 +169,8 @@ namespace osu.Game.Screens.Edit private void finish() { + string originalDifficulty = editor.Beatmap.Value.Beatmap.BeatmapInfo.DifficultyName; + showSpinner("Cleaning up..."); EditOperation!.Finish().ContinueWith(t => @@ -178,12 +180,18 @@ namespace osu.Game.Screens.Edit // Setting to null will allow exit to succeed. operation = null; - var beatmap = t.GetResultSafely(); + Live? beatmap = t.GetResultSafely(); if (beatmap == null) this.Exit(); else - editor.SwitchToDifficulty(beatmap.Value.Detach().Beatmaps.First()); + { + var closestMatchingBeatmap = + beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty) + ?? beatmap.Value.Beatmaps.First(); + + editor.SwitchToDifficulty(closestMatchingBeatmap); + } }); }); } From 72091b43df03c996b9b5cbb7534e984426c7a29d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 14:34:25 +0900 Subject: [PATCH 06/18] Simplify editor navigation tests --- .../TestSceneBeatmapEditorNavigation.cs | 143 +++++------------- 1 file changed, 40 insertions(+), 103 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 1ac4bb347b..efdcde9161 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -29,30 +29,20 @@ namespace osu.Game.Tests.Visual.Navigation { public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene { + private BeatmapSetInfo beatmapSet = null!; + [Test] public void TestSaveThenDeleteActuallyDeletesAtSongSelect() { - 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); - + prepareBeatmap(); + openEditor(); makeMetadataChange(); - AddAssert("save", () => Game.ChildrenOfType().Single().Save()); + AddAssert("save", () => getEditor().Save()); AddStep("delete beatmap", () => Game.BeatmapManager.Delete(beatmapSet)); - AddStep("exit", () => Game.ChildrenOfType().Single().Exit()); + AddStep("exit", () => getEditor().Exit()); AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.Beatmap.Value is DummyWorkingBeatmap); @@ -61,24 +51,14 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave() { - 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("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); - 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); + prepareBeatmap(); + openEditor(); makeMetadataChange(commit: false); - AddStep("exit", () => Game.ChildrenOfType().Single().Exit()); + AddStep("exit", () => getEditor().Exit()); AddUntilStep("save dialog displayed", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog); } @@ -121,16 +101,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() { - BeatmapSetInfo beatmapSet = null!; + prepareBeatmap(); - 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("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); @@ -183,19 +155,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitEditorWithoutSelection() { - 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); + prepareBeatmap(); + openEditor(); AddStep("escape once", () => InputManager.Key(Key.Escape)); @@ -205,19 +166,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitEditorWithSelection() { - 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); + prepareBeatmap(); + openEditor(); AddStep("make selection", () => { @@ -239,19 +189,8 @@ namespace osu.Game.Tests.Visual.Navigation [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); + prepareBeatmap(); + openEditor(); AddStep("seek to arbitrary time", () => getEditor().ChildrenOfType().First().Seek(1234)); AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234)); @@ -259,32 +198,21 @@ namespace osu.Game.Tests.Visual.Navigation 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()); + openEditor(); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234)); } [Test] public void TestAttemptGlobalMusicOperationFromEditor() { - 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); + prepareBeatmap(); AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying); AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true)); AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying); - 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); + openEditor(); AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying); AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true)); @@ -302,20 +230,10 @@ namespace osu.Game.Tests.Visual.Navigation [TestCase(SortMode.Difficulty)] public void TestSelectionRetainedOnExit(SortMode sortMode) { - 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($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode)); - 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); + prepareBeatmap(); + openEditor(); AddStep("exit editor", () => InputManager.Key(Key.Escape)); AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor); @@ -332,6 +250,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("open editor", () => Game.ChildrenOfType().Single().OnEditBeatmap?.Invoke()); AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); + AddStep("click on file", () => { var item = getEditor().ChildrenOfType().Single(i => i.Item.Text.Value.ToString() == "File"); @@ -354,6 +273,24 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("editor beatmap uses catch ruleset", () => getEditorBeatmap().BeatmapInfo.Ruleset.ShortName == "fruits"); } + private void prepareBeatmap() + { + 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); + } + + private void openEditor() + { + 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); + } + private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single(); private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen; From aa16c72e0661b2a83337461c5b9255ff1ec0bb75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 14:51:34 +0900 Subject: [PATCH 07/18] Add test coverage of external editing --- .../TestSceneBeatmapEditorNavigation.cs | 50 +++++++++++++++++++ osu.Game/Screens/Edit/ExternalEditScreen.cs | 12 ++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index efdcde9161..5d9c3bae97 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.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.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; @@ -13,6 +14,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -31,6 +33,54 @@ namespace osu.Game.Tests.Visual.Navigation { private BeatmapSetInfo beatmapSet = null!; + [Test] + public void TestExternalEditingNoChange() + { + prepareBeatmap(); + openEditor(); + + AddStep("open file menu", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick()); + AddStep("click external edit", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick()); + + AddUntilStep("wait for external edit screen", () => Game.ScreenStack.CurrentScreen is ExternalEditScreen externalEditScreen && externalEditScreen.IsLoaded); + + AddUntilStep("wait for button ready", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().FirstOrDefault()?.Enabled.Value == true); + + AddStep("finish external edit", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().First().TriggerClick()); + + AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddAssert("beatmap didn't change", () => getEditor().Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); + AddAssert("old beatmapset not deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Not.Null); + } + + [Test] + public void TestExternalEditingWithChange() + { + prepareBeatmap(); + openEditor(); + + AddStep("open file menu", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick()); + AddStep("click external edit", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick()); + + AddUntilStep("wait for external edit screen", () => Game.ScreenStack.CurrentScreen is ExternalEditScreen externalEditScreen && externalEditScreen.IsLoaded); + + AddUntilStep("wait for button ready", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().FirstOrDefault()?.Enabled.Value == true); + + AddStep("add file externally", () => + { + var op = ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).EditOperation!; + File.WriteAllText(Path.Combine(op.MountedPath, "test.txt"), "test"); + }); + + AddStep("finish external edit", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().First().TriggerClick()); + + AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddAssert("beatmap changed", () => !getEditor().Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); + AddAssert("old beatmapset deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Null); + } + [Test] public void TestSaveThenDeleteActuallyDeletesAtSongSelect() { diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index ae5fad3ec0..9cae44be78 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Edit private readonly Editor editor; - private ExternalEditOperation? operation; + public ExternalEditOperation? EditOperation; private double timeLoaded; @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Edit fileMountOperation.ContinueWith(t => { - operation = t.GetResultSafely(); + EditOperation = t.GetResultSafely(); Scheduler.AddDelayed(() => { @@ -146,11 +146,11 @@ namespace osu.Game.Screens.Edit private void open() { - if (operation == null) + if (EditOperation == null) return; // Ensure the trailing separator is present in order to show the folder contents. - gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + gameHost.OpenFileExternally(EditOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); } public override bool OnExiting(ScreenExitEvent e) @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Edit if (!fileMountOperation.IsCompleted) return false; - if (operation != null) + if (EditOperation != null) { finish(); return false; @@ -178,7 +178,7 @@ namespace osu.Game.Screens.Edit Schedule(() => { // Setting to null will allow exit to succeed. - operation = null; + EditOperation = null; Live? beatmap = t.GetResultSafely(); From 106d558147124f1e17a8d7b04226a54e2fbddc6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 18:14:54 +0900 Subject: [PATCH 08/18] Add test coverage of difficulty being retained --- .../Navigation/TestSceneBeatmapEditorNavigation.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 5d9c3bae97..1f227520c1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -36,9 +36,13 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExternalEditingNoChange() { + string difficultyName = null!; + prepareBeatmap(); openEditor(); + AddStep("store difficulty name", () => difficultyName = getEditor().Beatmap.Value.BeatmapInfo.DifficultyName); + AddStep("open file menu", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick()); AddStep("click external edit", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick()); @@ -50,16 +54,21 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); - AddAssert("beatmap didn't change", () => getEditor().Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); + AddAssert("beatmapset didn't change", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.EqualTo(beatmapSet)); + AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName)); AddAssert("old beatmapset not deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Not.Null); } [Test] public void TestExternalEditingWithChange() { + string difficultyName = null!; + prepareBeatmap(); openEditor(); + AddStep("store difficulty name", () => difficultyName = getEditor().Beatmap.Value.BeatmapInfo.DifficultyName); + AddStep("open file menu", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick()); AddStep("click external edit", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick()); @@ -77,7 +86,8 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); - AddAssert("beatmap changed", () => !getEditor().Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); + AddAssert("beatmapset changed", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.Not.EqualTo(beatmapSet)); + AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName)); AddAssert("old beatmapset deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Null); } From 704e7e843fc6769b391a3b455b723a9c58d335be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 18:28:11 +0900 Subject: [PATCH 09/18] More xmldoc across new methods and classes --- osu.Game/Database/ExternalEditOperation.cs | 29 +++++++++++++++++----- osu.Game/Database/IModelImporter.cs | 3 +++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/ExternalEditOperation.cs b/osu.Game/Database/ExternalEditOperation.cs index ab74cba7d5..a98d597b3c 100644 --- a/osu.Game/Database/ExternalEditOperation.cs +++ b/osu.Game/Database/ExternalEditOperation.cs @@ -7,15 +7,24 @@ using osu.Game.Overlays.Notifications; namespace osu.Game.Database { + /// + /// Contains information related to an active external edit operation. + /// public class ExternalEditOperation where TModel : class, IHasGuidPrimaryKey { + /// + /// The temporary path at which the model has been exported to for editing. + /// public readonly string MountedPath; + /// + /// Whether the model is still mounted at . + /// + public bool IsMounted { get; private set; } + private readonly IModelImporter importer; private readonly TModel original; - private bool isMounted; - public ExternalEditOperation(IModelImporter importer, TModel original, string path) { this.importer = importer; @@ -23,14 +32,24 @@ namespace osu.Game.Database MountedPath = path; - isMounted = true; + IsMounted = true; } + /// + /// Finish the external edit operation. + /// + /// + /// This will trigger an asynchronous reimport of the model. + /// Subsequent calls will be a no-op. + /// + /// A task which will eventuate in the newly imported model with changes applied. public async Task?> Finish() { - if (!Directory.Exists(MountedPath) || !isMounted) + if (!Directory.Exists(MountedPath) || !IsMounted) return null; + IsMounted = false; + Live? imported = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(MountedPath), original) .ConfigureAwait(false); @@ -40,8 +59,6 @@ namespace osu.Game.Database } catch { } - isMounted = false; - return imported; } } diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index c2e5517f2a..bf19bac5dd 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -37,6 +37,9 @@ namespace osu.Game.Database /// /// Mount all files for a to a temporary directory to allow for external editing. /// + /// + /// When editing is completed, call to begin the import-and-update process. + /// /// The to mount. public Task> BeginExternalEditing(TModel model); From 6cee0210c380e7903873ea4fa4fa29b28d417df2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 18:57:53 +0900 Subject: [PATCH 10/18] Fix(?) xmldoc --- osu.Game/Database/IModelImporter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index bf19bac5dd..ce1563f2df 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -35,12 +35,12 @@ namespace osu.Game.Database Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original); /// - /// Mount all files for a to a temporary directory to allow for external editing. + /// Mount all files for a model to a temporary directory to allow for external editing. /// /// - /// When editing is completed, call to begin the import-and-update process. + /// When editing is completed, call Finish() on the returned operation class to begin the import-and-update process. /// - /// The to mount. + /// The model to mount. public Task> BeginExternalEditing(TModel model); /// From b6741ee4eab7c8bceed3fa1e94e11e60e6a7383b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 20:00:34 +0900 Subject: [PATCH 11/18] Fix back-to-front exit blocking conditionals --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index 9cae44be78..a8a75f22db 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -156,12 +156,12 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(ScreenExitEvent e) { if (!fileMountOperation.IsCompleted) - return false; + return true; if (EditOperation != null) { finish(); - return false; + return true; } return base.OnExiting(e); From b0d6c8ca6d59e3ced2684eafcc9d66135abe5b76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 11:42:06 +0900 Subject: [PATCH 12/18] Abort operation on save failure --- osu.Game/Screens/Edit/Editor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a675b41833..8585aa910f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1135,7 +1135,8 @@ namespace osu.Game.Screens.Edit private void editExternally() { - Save(); + if (!Save()) + return; var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); this.Push(new ExternalEditScreen(editOperation, this)); From cd6b0e875a90ddc0fa5423c57afd3fcad4038d67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 12:15:17 +0900 Subject: [PATCH 13/18] Simplify save dialogs --- osu.Game/Screens/Edit/Editor.cs | 19 ++++++++----------- .../Screens/Edit/SaveRequiredPopupDialog.cs | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8585aa910f..700f355207 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -481,7 +481,7 @@ namespace osu.Game.Screens.Edit { if (HasUnsavedChanges) { - dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () => attemptMutationOperation(() => + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => { if (!Save()) return false; @@ -1146,7 +1146,7 @@ namespace osu.Game.Screens.Edit { if (HasUnsavedChanges) { - dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () => attemptAsyncMutationOperation(() => + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptAsyncMutationOperation(() => { if (!Save()) return Task.CompletedTask; @@ -1224,17 +1224,14 @@ namespace osu.Game.Screens.Edit { if (isNewBeatmap) { - dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () => + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => { - attemptMutationOperation(() => - { - if (!Save()) - return false; + if (!Save()) + return false; - CreateNewDifficulty(rulesetInfo); - return true; - }); - })); + CreateNewDifficulty(rulesetInfo); + return true; + }))); return; } diff --git a/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs b/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs index 3ca92876f1..618efb7cda 100644 --- a/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs +++ b/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs @@ -9,9 +9,9 @@ namespace osu.Game.Screens.Edit { public partial class SaveRequiredPopupDialog : PopupDialog { - public SaveRequiredPopupDialog(string headerText, Action saveAndAction) + public SaveRequiredPopupDialog(Action saveAndAction) { - HeaderText = headerText; + HeaderText = "The beatmap will be saved to continue with this operation."; Icon = FontAwesome.Regular.Save; From 599a765fd18f70e96e23da40462b689e7eca0e66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 12:20:33 +0900 Subject: [PATCH 14/18] Add confirmation before saving for external edit --- osu.Game/Screens/Edit/Editor.cs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 700f355207..7115147d0b 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1135,11 +1135,27 @@ namespace osu.Game.Screens.Edit private void editExternally() { - if (!Save()) - return; + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => + { + if (!Save()) + return false; - var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); - this.Push(new ExternalEditScreen(editOperation, this)); + startEdit(); + return true; + }))); + } + else + { + startEdit(); + } + + void startEdit() + { + var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); + this.Push(new ExternalEditScreen(editOperation, this)); + } } private void exportBeatmap(bool legacy) From bdbdc3592ee8deb9aaeccc23d8bcf13fcfefbe12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 14:27:12 +0900 Subject: [PATCH 15/18] Move full export async flow inside screen and add error handling --- osu.Game/Screens/Edit/Editor.cs | 3 +- osu.Game/Screens/Edit/ExternalEditScreen.cs | 218 +++++++++++--------- 2 files changed, 118 insertions(+), 103 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7115147d0b..d841e68263 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1153,8 +1153,7 @@ namespace osu.Game.Screens.Edit void startEdit() { - var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); - this.Push(new ExternalEditScreen(editOperation, this)); + this.Push(new ExternalEditScreen()); } } diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index a8a75f22db..ef497020f8 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.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.IO; using System.Linq; using System.Threading.Tasks; @@ -20,6 +19,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; @@ -28,28 +28,27 @@ namespace osu.Game.Screens.Edit { internal partial class ExternalEditScreen : OsuScreen { - private readonly Task> fileMountOperation; - [Resolved] private GameHost gameHost { get; set; } = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private readonly Editor editor; + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [Resolved] + private Editor editor { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + private Task? fileMountOperation; public ExternalEditOperation? EditOperation; - private double timeLoaded; - private FillFlowContainer flow = null!; - public ExternalEditScreen(Task> fileMountOperation, Editor editor) - { - this.fileMountOperation = fileMountOperation; - this.editor = editor; - } - [BackgroundDependencyLoader] private void load() { @@ -80,71 +79,98 @@ namespace osu.Game.Screens.Edit } } }; - - showSpinner("Exporting for edit..."); } protected override void LoadComplete() { base.LoadComplete(); - timeLoaded = Time.Current; - - fileMountOperation.ContinueWith(t => - { - EditOperation = t.GetResultSafely(); - - Scheduler.AddDelayed(() => - { - flow.Children = new Drawable[] - { - new OsuSpriteText - { - Text = "Beatmap is mounted externally", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - new OsuTextFlowContainer - { - Padding = new MarginPadding(5), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Width = 350, - AutoSizeAxes = Axes.Y, - Text = "Any changes made to the exported folder will be imported to the game, including file additions, modifications and deletions.", - }, - new PurpleRoundedButton - { - Text = "Open folder", - Width = 350, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Action = open, - Enabled = { Value = false } - }, - new DangerousRoundedButton - { - Text = "Finish editing and import changes", - Width = 350, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Action = finish, - Enabled = { Value = false } - } - }; - - Scheduler.AddDelayed(() => - { - foreach (var b in flow.ChildrenOfType()) - b.Enabled.Value = true; - open(); - }, 1000); - }, Math.Max(0, 1000 - (Time.Current - timeLoaded))); - }); + fileMountOperation = begin(); } - private void open() + public override bool OnExiting(ScreenExitEvent e) + { + // Don't allow exiting until the file mount operation has completed. + // This is mainly to simplify the flow (once the screen is pushed we are guaranteed an attempted mount). + if (fileMountOperation?.IsCompleted == false) + return true; + + // If the operation completed successfully, ensure that we finish the operation before exiting. + // The finish() call will subsequently call Exit() when done. + if (EditOperation != null) + { + finish().FireAndForget(); + return true; + } + + return base.OnExiting(e); + } + + private async Task begin() + { + showSpinner("Exporting for edit..."); + + await Task.Delay(500).ConfigureAwait(true); + + try + { + EditOperation = await beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!).ConfigureAwait(true); + } + catch + { + fileMountOperation = null; + showSpinner("Export failed!"); + await Task.Delay(1000).ConfigureAwait(true); + this.Exit(); + } + + flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Beatmap is mounted externally", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new OsuTextFlowContainer + { + Padding = new MarginPadding(5), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 350, + AutoSizeAxes = Axes.Y, + Text = "Any changes made to the exported folder will be imported to the game, including file additions, modifications and deletions.", + }, + new PurpleRoundedButton + { + Text = "Open folder", + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = openDirectory, + Enabled = { Value = false } + }, + new DangerousRoundedButton + { + Text = "Finish editing and import changes", + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = () => finish().FireAndForget(), + Enabled = { Value = false } + } + }; + + Scheduler.AddDelayed(() => + { + foreach (var b in flow.ChildrenOfType()) + b.Enabled.Value = true; + openDirectory(); + }, 1000); + } + + private void openDirectory() { if (EditOperation == null) return; @@ -153,47 +179,37 @@ namespace osu.Game.Screens.Edit gameHost.OpenFileExternally(EditOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); } - public override bool OnExiting(ScreenExitEvent e) - { - if (!fileMountOperation.IsCompleted) - return true; - - if (EditOperation != null) - { - finish(); - return true; - } - - return base.OnExiting(e); - } - - private void finish() + private async Task finish() { string originalDifficulty = editor.Beatmap.Value.Beatmap.BeatmapInfo.DifficultyName; showSpinner("Cleaning up..."); - EditOperation!.Finish().ContinueWith(t => + Live? beatmap = null; + + try { - Schedule(() => - { - // Setting to null will allow exit to succeed. - EditOperation = null; + beatmap = await EditOperation!.Finish().ConfigureAwait(true); + } + catch + { + showSpinner("Import failed!"); + await Task.Delay(1000).ConfigureAwait(true); + } - Live? beatmap = t.GetResultSafely(); + // Setting to null will allow exit to succeed. + EditOperation = null; - if (beatmap == null) - this.Exit(); - else - { - var closestMatchingBeatmap = - beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty) - ?? beatmap.Value.Beatmaps.First(); + if (beatmap == null) + this.Exit(); + else + { + var closestMatchingBeatmap = + beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty) + ?? beatmap.Value.Beatmaps.First(); - editor.SwitchToDifficulty(closestMatchingBeatmap); - } - }); - }); + editor.SwitchToDifficulty(closestMatchingBeatmap); + } } private void showSpinner(string text) From a859978efddc9e9e4761078fc8daa65ee5cb0b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jul 2024 09:43:00 +0200 Subject: [PATCH 16/18] Add failing test steps for locally modified state not being set --- .../Visual/Navigation/TestSceneBeatmapEditorNavigation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 1f227520c1..b5dfa9a87f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -87,6 +87,8 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); AddAssert("beatmapset changed", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.Not.EqualTo(beatmapSet)); + AddAssert("beatmapset is locally modified", () => getEditor().Beatmap.Value.BeatmapSetInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified)); + AddAssert("all difficulties are locally modified", () => getEditor().Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Status == BeatmapOnlineStatus.LocallyModified)); AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName)); AddAssert("old beatmapset deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Null); } From ac467cf73a2bcd1af6c7e709227b7e5f77180fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jul 2024 09:43:20 +0200 Subject: [PATCH 17/18] Set locally modified state for all externally modified beatmap(sets) that could not be mapped to online --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index ef497020f8..edfaa59e30 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -204,6 +204,16 @@ namespace osu.Game.Screens.Edit this.Exit(); else { + // the `ImportAsUpdate()` flow will yield beatmap(sets) with online status of `None` if online lookup fails. + // coerce such models to `LocallyModified` state instead to unify behaviour with normal editing flow. + beatmap.PerformWrite(s => + { + if (s.Status == BeatmapOnlineStatus.None) + s.Status = BeatmapOnlineStatus.LocallyModified; + foreach (var difficulty in s.Beatmaps.Where(b => b.Status == BeatmapOnlineStatus.None)) + difficulty.Status = BeatmapOnlineStatus.LocallyModified; + }); + var closestMatchingBeatmap = beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty) ?? beatmap.Value.Beatmaps.First(); From cc0d7e99814e70068bf283583efeff864c80834d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jul 2024 09:54:12 +0200 Subject: [PATCH 18/18] Add error logging on failure to begin/end external edit --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index edfaa59e30..8a97e3dcb2 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.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.IO; using System.Linq; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -116,8 +118,9 @@ namespace osu.Game.Screens.Edit { EditOperation = await beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!).ConfigureAwait(true); } - catch + catch (Exception ex) { + Logger.Log($@"Failed to initiate external edit operation: {ex}", LoggingTarget.Database); fileMountOperation = null; showSpinner("Export failed!"); await Task.Delay(1000).ConfigureAwait(true); @@ -191,8 +194,9 @@ namespace osu.Game.Screens.Edit { beatmap = await EditOperation!.Finish().ConfigureAwait(true); } - catch + catch (Exception ex) { + Logger.Log($@"Failed to finish external edit operation: {ex}", LoggingTarget.Database); showSpinner("Import failed!"); await Task.Delay(1000).ConfigureAwait(true); }