From bdbdc3592ee8deb9aaeccc23d8bcf13fcfefbe12 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 11 Jul 2024 14:27:12 +0900
Subject: [PATCH] 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 <contact@ppy.sh>. 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<ExternalEditOperation<BeatmapSetInfo>> 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<BeatmapSetInfo>? EditOperation;
 
-        private double timeLoaded;
-
         private FillFlowContainer flow = null!;
 
-        public ExternalEditScreen(Task<ExternalEditOperation<BeatmapSetInfo>> 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<RoundedButton>())
-                            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<RoundedButton>())
+                    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<BeatmapSetInfo>? 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<BeatmapSetInfo>? 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)