From 9351fd2f166eeb616e196a5362d6bf02eb6e2e5f Mon Sep 17 00:00:00 2001 From: jupahe64 Date: Wed, 6 Dec 2023 17:14:26 +0100 Subject: [PATCH] make more stuff async and show tasks/progress in UI --- Fushigi/Program.cs | 3 + Fushigi/param/ParamDB.cs | 15 ++- Fushigi/ui/MainWindow.cs | 150 +++++++++++++++--------- Fushigi/ui/modal/OkDialog.cs | 37 ++++++ Fushigi/ui/modal/PopupModalHost.cs | 33 ++++++ Fushigi/ui/widgets/CourseScene.cs | 18 +-- Fushigi/ui/widgets/ParamDBDialog.cs | 42 ------- Fushigi/ui/widgets/Preferences.cs | 11 +- Fushigi/ui/widgets/ProgressBarDialog.cs | 106 +++++++++++++++++ 9 files changed, 290 insertions(+), 125 deletions(-) create mode 100644 Fushigi/ui/modal/OkDialog.cs delete mode 100644 Fushigi/ui/widgets/ParamDBDialog.cs create mode 100644 Fushigi/ui/widgets/ProgressBarDialog.cs diff --git a/Fushigi/Program.cs b/Fushigi/Program.cs index a4c38273..f2836835 100644 --- a/Fushigi/Program.cs +++ b/Fushigi/Program.cs @@ -2,6 +2,7 @@ using Fushigi.param; using Fushigi.ui; using System.Runtime.InteropServices; +using Fushigi.windowing; FileStream outputStream = new FileStream("output.log", FileMode.Create); @@ -39,9 +40,11 @@ }; MainWindow window = new MainWindow(); +WindowManager.Run(); outputStream.Close(); + void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) { Exception? ex = e.ExceptionObject as Exception; diff --git a/Fushigi/param/ParamDB.cs b/Fushigi/param/ParamDB.cs index acb0866d..0c585877 100644 --- a/Fushigi/param/ParamDB.cs +++ b/Fushigi/param/ParamDB.cs @@ -69,7 +69,7 @@ public static bool TryGetRailPointComponent(string railName, [NotNullWhen(true)] public static string[] GetActors() => sActors.Keys.ToArray(); - public static void Load() + public static void Load(IProgress<(string operationName, float? progress)> progress) { /* if we have already been initialized, we skip this process */ if (sIsInit) @@ -77,13 +77,18 @@ public static void Load() return; } + progress.Report(("Gathering Actor packs", null)); /* the files in /Pack/Actor in the RomFS contain the PACK files that contain our parameters */ - // string[] files = RomFS.GetFiles("/Pack/Actor"); string[] files = RomFS.GetFiles(Path.Combine("Pack", "Actor")); + /* iterate through each file */ - foreach (string file in files) + for (int i = 0; i < files.Length; i++) { + string file = files[i]; + + progress.Report(("Loading Parameters from Actor packs", i / (float)files.Length)); + /* the actor name in question is at the beginning of the file name */ string actorName = Path.GetFileNameWithoutExtension(file).Split(".pack")[0]; ParamList param = new ParamList(); @@ -193,14 +198,14 @@ public static void Load() sIsInit = true; } - public static void Reload() + public static void Reload(IProgress<(string operationName, float? progress)> progress) { sActors.Clear(); sComponents.Clear(); sRails.Clear(); sRailParamList.Clear(); sIsInit = false; - Load(); + Load(progress); } static Component ReadByml(Byml.Byml byml) diff --git a/Fushigi/ui/MainWindow.cs b/Fushigi/ui/MainWindow.cs index d0e02acb..ac0d86eb 100644 --- a/Fushigi/ui/MainWindow.cs +++ b/Fushigi/ui/MainWindow.cs @@ -1,3 +1,4 @@ +using Fushigi.course; using Fushigi.param; using Fushigi.ui.modal; using Fushigi.ui.widgets; @@ -19,11 +20,14 @@ public class MainWindow : IPopupModalHost private ImFontPtr mDefaultFont; private ImFontPtr mIconFont; + private (Course course, TaskCompletionSource promise)? mCourseLoadRequest = null; + public MainWindow() { WindowManager.CreateWindow(out mWindow, onConfigureIO: () => { + Console.WriteLine("Initializing Window"); unsafe { var io = ImGui.GetIO(); @@ -87,8 +91,6 @@ public MainWindow() }); mWindow.Load += () => WindowManager.RegisterRenderDelegate(mWindow, Render); mWindow.Closing += Close; - mWindow.Run(); - mWindow.Dispose(); } public async Task TryCloseCourse() @@ -135,8 +137,27 @@ public void Close() }).ConfigureAwait(false); //fire and forget } - void LoadFromSettings(GL gl) + //TODO put this somewhere else + public static Task LoadParamDBWithProgressBar(IPopupModalHost modalHost) + { + return ProgressBarDialog.ShowDialogForAsyncAction(modalHost, + "Loading ParamDB", + async (p) => + { + p.Report(("Creating task", 0)); + await modalHost.WaitTick(); + var task = ParamDB.sIsInit ? + Task.Run(() => ParamDB.Reload(p)) : + Task.Run(() => ParamDB.Load(p)); + await task; + }); + } + + async Task StartupRoutine(GL gl) { + await WaitTick(); + bool shouldShowPreferenceWindow = true; + bool shouldShowWelcomeDialog = true; string romFSPath = UserSettings.GetRomFSPath(); if (RomFS.IsValidRoot(romFSPath)) { @@ -146,16 +167,21 @@ void LoadFromSettings(GL gl) if (!ParamDB.sIsInit) { Console.WriteLine("Parameter database needs to be initialized..."); - mIsGeneratingParamDB = true; + + await LoadParamDBWithProgressBar(this); + await Task.Delay(500); + } string? latestCourse = UserSettings.GetLatestCourse(); if (latestCourse != null && ParamDB.sIsInit) { - mCurrentCourseName = latestCourse; - mSelectedCourseScene = new(new(mCurrentCourseName), gl, this); - mIsChoosingPreferences = false; - mIsWelcome = false; + //wait for other pending dialogs to close + await mModalHost.WaitTick(); + + await LoadCourseWithProgressBar(latestCourse); + shouldShowPreferenceWindow = false; + shouldShowWelcomeDialog = false; } } @@ -164,11 +190,33 @@ void LoadFromSettings(GL gl) if (!string.IsNullOrEmpty(RomFS.GetRoot()) && !string.IsNullOrEmpty(UserSettings.GetModRomFSPath())) { - mIsChoosingPreferences = false; - mIsWelcome = false; + shouldShowPreferenceWindow = false; + shouldShowWelcomeDialog = false; } - + if(shouldShowPreferenceWindow) + mIsShowPreferenceWindow = true; + + if(shouldShowWelcomeDialog) + await WelcomeMessage.ShowDialog(this); + } + + Task LoadCourseWithProgressBar(string name) + { + return ProgressBarDialog.ShowDialogForAsyncAction(this, + $"Loading {name}", + async (p) => + { + var promise = new TaskCompletionSource(); + p.Report(("Loading course files", null)); + await mModalHost.WaitTick(); + var course = new Course(name); + p.Report(("Loading other resources (this temporarily freezes the app)", null)); + await mModalHost.WaitTick(); + + mCourseLoadRequest = (course, promise); + await promise.Task; + }); } void DrawMainMenu(GL gl) @@ -194,7 +242,7 @@ void DrawMainMenu(GL gl) { mCurrentCourseName = selectedCourse; Console.WriteLine($"Selected course {mCurrentCourseName}!"); - mSelectedCourseScene = new(new(mCurrentCourseName), gl, this); + await LoadCourseWithProgressBar(mCurrentCourseName); UserSettings.AppendRecentCourse(mCurrentCourseName); } }).ConfigureAwait(false); //fire and forget @@ -265,12 +313,12 @@ void DrawMainMenu(GL gl) { if (ImGui.MenuItem("Preferences")) { - mIsChoosingPreferences = true; + mIsShowPreferenceWindow = true; } if (ImGui.MenuItem("Regenerate Parameter Database", ParamDB.sIsInit)) { - mIsGeneratingParamDB = true; + _ = LoadParamDBWithProgressBar(this); } if (ImGui.MenuItem("Undo")) @@ -292,26 +340,17 @@ void DrawMainMenu(GL gl) } } - void DrawWelcome() + public void Render(GL gl, double delta, ImGuiController controller) { - if (!ImGui.Begin("Welcome")) + //for now (makes sure we have atleast one frame rendered before the course get's loaded) + if(mCourseLoadRequest.TryGetValue(out var request)) { - return; - } - - ImGui.Text("Welcome to Fushigi! Set the RomFS game path and save directory to get started."); - - if (ImGui.Button("Close")) - { - mIsWelcome = false; + mSelectedCourseScene = new(request.course, gl, this); + mCurrentCourseName = request.course.GetName(); + request.promise.SetResult(); + mCourseLoadRequest = null; } - ImGui.End(); - } - - public void Render(GL gl, double delta, ImGuiController controller) - { - /* keep OpenGLs viewport size in sync with the window's size */ gl.Viewport(mWindow.FramebufferSize); @@ -325,34 +364,21 @@ public void Render(GL gl, double delta, ImGuiController controller) if (ImGui.GetFrameCount() == 2) { ImGui.LoadIniSettingsFromDisk("imgui.ini"); - LoadFromSettings(gl); + _ = StartupRoutine(gl); } DrawMainMenu(gl); - // ImGui settings are available frame 3 - if (ImGui.GetFrameCount() > 2) + + if (!string.IsNullOrEmpty(RomFS.GetRoot()) && + !string.IsNullOrEmpty(UserSettings.GetModRomFSPath())) { - if (!string.IsNullOrEmpty(RomFS.GetRoot()) && - !string.IsNullOrEmpty(UserSettings.GetModRomFSPath())) - { - mSelectedCourseScene?.DrawUI(gl, delta); - } - - if (mIsChoosingPreferences) - { - Preferences.Draw(ref mIsChoosingPreferences, gl); - } - - if (mIsWelcome) - { - DrawWelcome(); - } + mSelectedCourseScene?.DrawUI(gl, delta); + } - if (mIsGeneratingParamDB) - { - ParamDBDialog.Draw(ref mIsGeneratingParamDB); - } + if (mIsShowPreferenceWindow) + { + Preferences.Draw(ref mIsShowPreferenceWindow, gl, this); } mModalHost.DrawHostedModals(); @@ -372,11 +398,25 @@ public void Render(GL gl, double delta, ImGuiController controller) return mModalHost.ShowPopUp(modal, title, windowFlags, minWindowSize); } + public Task WaitTick() + { + return ((IPopupModalHost)mModalHost).WaitTick(); + } + readonly IWindow mWindow; string? mCurrentCourseName; CourseScene? mSelectedCourseScene; - bool mIsChoosingPreferences = true; - bool mIsWelcome = true; - bool mIsGeneratingParamDB = false; + bool mIsShowPreferenceWindow = false; + + + class WelcomeMessage : OkDialog + { + protected override string Title => "Welcome"; + + protected override void DrawBody() + { + ImGui.Text("Welcome to Fushigi! Set the RomFS game path and save directory to get started."); + } + } } } diff --git a/Fushigi/ui/modal/OkDialog.cs b/Fushigi/ui/modal/OkDialog.cs new file mode 100644 index 00000000..300a25cd --- /dev/null +++ b/Fushigi/ui/modal/OkDialog.cs @@ -0,0 +1,37 @@ +using Fushigi.util; +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Fushigi.ui.modal +{ + public abstract class OkDialog : IPopupModal.Void> + where TDialog : OkDialog, new() + { + private struct Void { } + + protected abstract string Title { get; } + + public static async Task ShowDialog(IPopupModalHost modalHost) + { + var dialog = new TDialog(); + await modalHost.ShowPopUp(dialog, dialog.Title, + ImGuiWindowFlags.AlwaysAutoResize); + } + + protected abstract void DrawBody(); + + void IPopupModal.DrawModalContent(Promise promise) + { + DrawBody(); + + if (ImGui.Button("OK")) + { + promise.SetResult(new Void()); + } + } + } +} diff --git a/Fushigi/ui/modal/PopupModalHost.cs b/Fushigi/ui/modal/PopupModalHost.cs index bcd0c8c8..d29c4684 100644 --- a/Fushigi/ui/modal/PopupModalHost.cs +++ b/Fushigi/ui/modal/PopupModalHost.cs @@ -16,6 +16,8 @@ public interface IPopupModalHost string title, ImGuiWindowFlags windowFlags = ImGuiWindowFlags.None, Vector2? minWindowSize = null); + + Task WaitTick(); } public class PopupModalHost : IPopupModalHost @@ -36,6 +38,9 @@ private struct ModalMethods private readonly Stack<(PopupInfo info, ModalMethods methods, Task resultTask)> mPopupStack = []; private readonly List<(PopupInfo info, ModalMethods methods, Task resultTask)> mNewPopups = []; + private ulong mTicks = 0; + private List<(ulong targetTick, TaskCompletionSource promise)> mTickWaiters = []; + public Task<(bool wasClosed, TResult result)> ShowPopUp(IPopupModal modal, string title, ImGuiWindowFlags windowFlags = ImGuiWindowFlags.None, @@ -70,10 +75,35 @@ private struct ModalMethods return completionSource.Task; } + public Task WaitTick() + { + lock (mTickWaiters) + { + var promise = new TaskCompletionSource(); + + mTickWaiters.Add((mTicks+2, promise)); + + return promise.Task; + } + } + private readonly List mModalsToClose = []; public void DrawHostedModals() { + lock (mTickWaiters) + { + for (int i = mTickWaiters.Count - 1; i >= 0; i--) + { + if (mTickWaiters[i].targetTick == mTicks) + { + mTickWaiters[i].promise.SetResult(); + mTickWaiters.RemoveAt(i); + } + + } + } + lock (mNewPopups) { foreach (var item in mNewPopups) @@ -131,6 +161,9 @@ public void DrawHostedModals() { mPopupStack.Pop(); } + + lock (mTickWaiters) + mTicks++; } } } diff --git a/Fushigi/ui/widgets/CourseScene.cs b/Fushigi/ui/widgets/CourseScene.cs index 1bee6a92..7d082ffb 100644 --- a/Fushigi/ui/widgets/CourseScene.cs +++ b/Fushigi/ui/widgets/CourseScene.cs @@ -2126,26 +2126,14 @@ private void SelectActorToAddLayer() private string mAddLayerSearchQuery = ""; } - class SaveFailureAlert : IPopupModal + class SaveFailureAlert : OkDialog { - private struct Void { } + protected override string Title => "Saving failed"; - public static async Task ShowDialog(IPopupModalHost modalHost) - { - - await modalHost.ShowPopUp(new SaveFailureAlert(), "Saving failed", - ImGuiWindowFlags.AlwaysAutoResize); - } - - void IPopupModal.DrawModalContent(Promise promise) + protected override void DrawBody() { ImGui.Text("The course files may be open in an external app, or Super Mario Bros. Wonder may currently be running in an emulator. \n" + "Close the emulator or external app and try again."); - - if (ImGui.Button("Okay")) - { - promise.SetResult(new Void()); - } } } } diff --git a/Fushigi/ui/widgets/ParamDBDialog.cs b/Fushigi/ui/widgets/ParamDBDialog.cs deleted file mode 100644 index 6e04c83b..00000000 --- a/Fushigi/ui/widgets/ParamDBDialog.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Fushigi.param; -using ImGuiNET; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Text; -using System.Threading.Tasks; - -namespace Fushigi.ui.widgets -{ - class ParamDBDialog - { - static Task? mLoadParamDB; - - public static void Draw(ref bool shouldDraw) - { - mLoadParamDB ??= ParamDB.sIsInit ? Task.Run(ParamDB.Reload) : Task.Run(ParamDB.Load); - - if (mLoadParamDB.IsCompleted) - { - shouldDraw = false; - mLoadParamDB = null; - return; - } - - Vector2 center = ImGui.GetMainViewport().GetCenter(); - ImGui.SetNextWindowPos(center, ImGuiCond.Appearing, new Vector2(0.5f, 0.5f)); - - ImGui.OpenPopup("ParamDB"); - - if (ImGui.BeginPopupModal("ParamDB", ref shouldDraw, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration)) - { - ImGui.Text("Generating ParamDB..."); - // TODO: replace this progress bar with an animated loading bar - ImGui.ProgressBar(0.33f, new Vector2(0, 0), "Loading..."); - - ImGui.EndPopup(); - } - } - } -} diff --git a/Fushigi/ui/widgets/Preferences.cs b/Fushigi/ui/widgets/Preferences.cs index 12ecb9dd..3b36e335 100644 --- a/Fushigi/ui/widgets/Preferences.cs +++ b/Fushigi/ui/widgets/Preferences.cs @@ -1,4 +1,5 @@ using Fushigi.param; +using Fushigi.ui.modal; using Fushigi.util; using ImGuiNET; using Silk.NET.OpenGL; @@ -11,15 +12,9 @@ class Preferences static readonly Vector4 errCol = new Vector4(1f, 0, 0, 1); static bool romfsTouched = false; static bool modRomfsTouched = false; - static bool mIsGeneratingParamDB = false; - public static void Draw(ref bool continueDisplay, GL gl) + public static void Draw(ref bool continueDisplay, GL gl, IPopupModalHost modalHost) { - if (mIsGeneratingParamDB) - { - ParamDBDialog.Draw(ref mIsGeneratingParamDB); - } - ImGui.SetNextWindowSize(new Vector2(700, 250), ImGuiCond.Once); if (ImGui.Begin("Preferences", ImGuiWindowFlags.NoDocking)) { @@ -50,7 +45,7 @@ public static void Draw(ref bool continueDisplay, GL gl) /* if our parameter database isn't set, set it */ if (!ParamDB.sIsInit) { - mIsGeneratingParamDB = true; + MainWindow.LoadParamDBWithProgressBar(modalHost); } } diff --git a/Fushigi/ui/widgets/ProgressBarDialog.cs b/Fushigi/ui/widgets/ProgressBarDialog.cs new file mode 100644 index 00000000..84667bb7 --- /dev/null +++ b/Fushigi/ui/widgets/ProgressBarDialog.cs @@ -0,0 +1,106 @@ +using Fushigi.param; +using Fushigi.ui.modal; +using Fushigi.util; +using ImGuiNET; +using NativeFileDialogSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace Fushigi.ui.widgets +{ + public class ProgressBarDialog : IPopupModal + { + private struct Void { } + + private class Progress : IProgress<(string operationName, float? progress)> + { + public event Action<(string operationName, float? progress)>? ProgressChanged; + public void Report((string operationName, float? progress) value) + { + lock (this) //just in case + { + ProgressChanged?.Invoke(value); + } + } + } + + public static async Task ShowDialogForAsyncAction(IPopupModalHost modalHost, + string text, Func, Task> asyncAction) + { + var progress = new Progress(); + var dialog = new ProgressBarDialog(progress, text); + dialog.mTask = asyncAction(progress); + await modalHost.ShowPopUp(dialog, "", + ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoTitleBar, + minWindowSize: new Vector2(300, 150)); + } + + public static async Task ShowDialogForAsyncFunc(IPopupModalHost modalHost, + string text, Func, Task> asyncFunc) + { + var progress = new Progress(); + var dialog = new ProgressBarDialog(progress, text); + var task = asyncFunc(progress); + dialog.mTask = task; + await modalHost.ShowPopUp(dialog, "", + ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoTitleBar); + return task.Result; + } + + private ProgressBarDialog(Progress progress, string text) + { + mTask = null!; //asyncAction/asyncFunc needs to be executed AFTER this constructor + //otherwise we might miss a progress report, therefore we don't have a task yet + mText = text; + progress.ProgressChanged += p => + { + mProgressValue = p.progress; + mOperationName = p.operationName; + }; + } + + void IPopupModal.DrawModalContent(Promise promise) + { + ImGui.GetFont().Scale = 1.2f; + ImGui.PushFont(ImGui.GetFont()); + + ImGui.Dummy(Vector2.Zero with { X = ImGui.CalcTextSize(mText + sDots[^1]).X }); + ImGui.Text($"{mText}{sDots[(int)ImGui.GetTime() % sDots.Length]}"); + + ImGui.GetFont().Scale = 1; + ImGui.PopFont(); + + ImGui.Spacing(); + + if(mOperationName is not null) + { + ImGui.Text(mOperationName); + if(mProgressValue.TryGetValue(out float value)) + ImGui.ProgressBar(value, new Vector2(0, 0)); + } + else + { + ImGui.NewLine(); + } + + + if (mTask.IsCompleted) + promise.SetResult(new Void()); + } + + private static readonly string[] sDots = [ + ".", + "..", + "...", + ]; + + private float? mProgressValue = 0; + private string? mOperationName; + private Task mTask; + private readonly string mText; + } +}