From 9845b0a889808116b28061675ef4e0f66c8e90d4 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Fri, 10 Oct 2025 10:18:43 +0100 Subject: [PATCH 01/14] chore: attach to console on windows if possible --- src/ModMan/main.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ModMan/main.cpp b/src/ModMan/main.cpp index a6afc7bb..66780792 100644 --- a/src/ModMan/main.cpp +++ b/src/ModMan/main.cpp @@ -5,13 +5,16 @@ #include "Web/ViteAssets.h" #include "Web/WebViewLib.h" -#include #include #include #include #include #include +#ifdef _WIN32 +#include +#endif + using namespace std::string_literals; using namespace PLATFORM_NAMESPACE; @@ -105,10 +108,23 @@ namespace #ifdef _WIN32 int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/, LPSTR /*lpCmdLine*/, int /*nCmdShow*/) -{ #else int main() +#endif { +#ifdef _WIN32 + // Attach console if possible on Windows for stdout/stderr in console + if (AttachConsole(-1)) + { + FILE* fDummy; + (void)freopen_s(&fDummy, "CONOUT$", "w", stdout); + (void)freopen_s(&fDummy, "CONOUT$", "w", stderr); + (void)freopen_s(&fDummy, "CONIN$", "r", stdin); + std::cout.clear(); + std::clog.clear(); + std::cerr.clear(); + std::cin.clear(); + } #endif con::info("Starting ModMan " GIT_VERSION); From 219f0c1c85d8f98cdbc92d9f322d88a30868fad0 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Fri, 10 Oct 2025 10:57:07 +0100 Subject: [PATCH 02/14] chore: save shared modman info in context --- src/ModMan/Context/ModManContext.cpp | 7 ++++ src/ModMan/Context/ModManContext.h | 14 +++++++ src/ModMan/Web/Binds/Binds.cpp | 11 +++++ src/ModMan/Web/Binds/Binds.h | 8 ++++ src/ModMan/main.cpp | 61 +++++++++++----------------- src/ModManUi/src/App.vue | 25 +----------- src/ModManUi/src/native/index.ts | 6 +-- 7 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 src/ModMan/Context/ModManContext.cpp create mode 100644 src/ModMan/Context/ModManContext.h create mode 100644 src/ModMan/Web/Binds/Binds.cpp create mode 100644 src/ModMan/Web/Binds/Binds.h diff --git a/src/ModMan/Context/ModManContext.cpp b/src/ModMan/Context/ModManContext.cpp new file mode 100644 index 00000000..ff4bbe9e --- /dev/null +++ b/src/ModMan/Context/ModManContext.cpp @@ -0,0 +1,7 @@ +#include "ModManContext.h" + +ModManContext& ModManContext::Get() +{ + static ModManContext context; + return context; +} diff --git a/src/ModMan/Context/ModManContext.h b/src/ModMan/Context/ModManContext.h new file mode 100644 index 00000000..05f72099 --- /dev/null +++ b/src/ModMan/Context/ModManContext.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Web/WebViewLib.h" + +#include + +class ModManContext +{ +public: + static ModManContext& Get(); + + std::unique_ptr m_main_webview; + std::unique_ptr m_dev_tools_webview; +}; diff --git a/src/ModMan/Web/Binds/Binds.cpp b/src/ModMan/Web/Binds/Binds.cpp new file mode 100644 index 00000000..9e41b3a0 --- /dev/null +++ b/src/ModMan/Web/Binds/Binds.cpp @@ -0,0 +1,11 @@ +#include "Binds.h" + +#include "Web/Binds/DialogBinds.h" + +namespace ui +{ + void RegisterAllBinds(webview::webview& wv) + { + RegisterDialogHandlerBinds(wv); + } +} // namespace ui diff --git a/src/ModMan/Web/Binds/Binds.h b/src/ModMan/Web/Binds/Binds.h new file mode 100644 index 00000000..3dd717ba --- /dev/null +++ b/src/ModMan/Web/Binds/Binds.h @@ -0,0 +1,8 @@ +#pragma once + +#include "Web/WebViewLib.h" + +namespace ui +{ + void RegisterAllBinds(webview::webview& wv); +} diff --git a/src/ModMan/main.cpp b/src/ModMan/main.cpp index 66780792..9321760e 100644 --- a/src/ModMan/main.cpp +++ b/src/ModMan/main.cpp @@ -1,5 +1,6 @@ -#include "GitVersion.h" -#include "Web/Binds/DialogBinds.h" +#include "Context/ModManContext.h" +#include "GitVersion.h" +#include "Web/Binds/Binds.h" #include "Web/Platform/AssetHandler.h" #include "Web/UiCommunication.h" #include "Web/ViteAssets.h" @@ -7,7 +8,6 @@ #include #include -#include #include #include @@ -21,15 +21,17 @@ using namespace PLATFORM_NAMESPACE; namespace { #ifdef _DEBUG - std::optional devToolWindow; - - void RunDevToolsWindow() + void SpawnDevToolsWindow() { con::debug("Creating dev tools window"); + auto& context = ModManContext::Get(); + try { - auto& newWindow = devToolWindow.emplace(false, nullptr); + context.m_dev_tools_webview = std::make_unique(false, nullptr); + auto& newWindow = *context.m_dev_tools_webview; + newWindow.set_title("Devtools"); newWindow.set_size(640, 480, WEBVIEW_HINT_NONE); newWindow.set_size(480, 320, WEBVIEW_HINT_MIN); @@ -42,59 +44,44 @@ namespace } #endif - int RunMainWindow() + int SpawnMainWindow() { con::debug("Creating main window"); + auto& context = ModManContext::Get(); try { - webview::webview w( + context.m_main_webview = std::make_unique( #ifdef _DEBUG true, #else false, #endif nullptr); - w.set_title("OpenAssetTools ModMan"); - w.set_size(1280, 640, WEBVIEW_HINT_NONE); - w.set_size(480, 320, WEBVIEW_HINT_MIN); + auto& newWindow = *context.m_main_webview; - ui::RegisterDialogHandlerBinds(w); + newWindow.set_title("OpenAssetTools ModMan"); + newWindow.set_size(1280, 640, WEBVIEW_HINT_NONE); + newWindow.set_size(480, 320, WEBVIEW_HINT_MIN); - // A binding that counts up or down and immediately returns the new value. - ui::Bind(w, - "greet", - [&w](std::string name) -> std::string - { - ui::Notify(w, "greeting", name); - return std::format("Hello from C++ {}!", name); - }); - -#if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) - InstallAssetHandler(w); - constexpr auto urlPrefix = URL_PREFIX; -#elif defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK) - InstallAssetHandler(w); - constexpr auto urlPrefix = URL_PREFIX; -#else -#error Unsupported platform -#endif + InstallAssetHandler(newWindow); + ui::RegisterAllBinds(newWindow); #ifdef _DEBUG - w.navigate(VITE_DEV_SERVER ? std::format("http://localhost:{}", VITE_DEV_SERVER_PORT) : std::format("{}index.html", urlPrefix)); + newWindow.navigate(VITE_DEV_SERVER ? std::format("http://localhost:{}", VITE_DEV_SERVER_PORT) : std::format("{}index.html", URL_PREFIX)); if (VITE_DEV_SERVER) { - w.dispatch( + newWindow.dispatch( [] { - RunDevToolsWindow(); + SpawnDevToolsWindow(); }); } #else - w.navigate(std::format("{}index.html", urlPrefix)); + newWindow.navigate(std::format("{}index.html", URL_PREFIX)); #endif - w.run(); + newWindow.run(); } catch (const webview::exception& e) { @@ -129,7 +116,7 @@ int main() con::info("Starting ModMan " GIT_VERSION); - const auto result = RunMainWindow(); + const auto result = SpawnMainWindow(); return result; } diff --git a/src/ModManUi/src/App.vue b/src/ModManUi/src/App.vue index 1ff66ed6..0db0d725 100644 --- a/src/ModManUi/src/App.vue +++ b/src/ModManUi/src/App.vue @@ -1,28 +1,13 @@ diff --git a/src/ModManUi/src/native/FastFileBinds.ts b/src/ModManUi/src/native/FastFileBinds.ts new file mode 100644 index 00000000..4f417902 --- /dev/null +++ b/src/ModManUi/src/native/FastFileBinds.ts @@ -0,0 +1,3 @@ +export interface FastFileBinds { + loadFastFile(path: string): Promise; +} diff --git a/src/ModManUi/src/native/index.ts b/src/ModManUi/src/native/index.ts index 9893cbbe..9ab6257a 100644 --- a/src/ModManUi/src/native/index.ts +++ b/src/ModManUi/src/native/index.ts @@ -1,7 +1,8 @@ import type { DialogBinds } from "./DialogBinds"; +import type { FastFileBinds } from "./FastFileBinds"; -export type NativeMethods = DialogBinds; +export type NativeMethods = DialogBinds & FastFileBinds; interface NativeEventMap { From 2037cf3258fb9ed03b390f1e80f12df4075ec857 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sat, 11 Oct 2025 14:15:04 +0100 Subject: [PATCH 06/14] chore: add ModMan args --- src/ModMan/ModManArgs.cpp | 105 ++++++++++++++++++++++++++++++++++++++ src/ModMan/ModManArgs.h | 19 +++++++ src/ModMan/main.cpp | 15 +++++- 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/ModMan/ModManArgs.cpp create mode 100644 src/ModMan/ModManArgs.h diff --git a/src/ModMan/ModManArgs.cpp b/src/ModMan/ModManArgs.cpp new file mode 100644 index 00000000..b5b1f88c --- /dev/null +++ b/src/ModMan/ModManArgs.cpp @@ -0,0 +1,105 @@ +#include "ModManArgs.h" + +#include "GitVersion.h" +#include "Utils/Arguments/UsageInformation.h" +#include "Utils/Logging/Log.h" + +#include +#include + +namespace +{ + // clang-format off + const CommandLineOption* const OPTION_HELP = + CommandLineOption::Builder::Create() + .WithShortName("?") + .WithLongName("help") + .WithDescription("Displays usage information.") + .Build(); + + const CommandLineOption* const OPTION_VERSION = + CommandLineOption::Builder::Create() + .WithLongName("version") + .WithDescription("Prints the application version.") + .Build(); + + const CommandLineOption* const OPTION_VERBOSE = + CommandLineOption::Builder::Create() + .WithShortName("v") + .WithLongName("verbose") + .WithDescription("Outputs a lot more and more detailed messages.") + .Build(); + + const CommandLineOption* const OPTION_NO_COLOR = + CommandLineOption::Builder::Create() + .WithLongName("no-color") + .WithDescription("Disables colored terminal output.") + .Build(); + // clang-format on + + const CommandLineOption* const COMMAND_LINE_OPTIONS[]{ + OPTION_HELP, + OPTION_VERSION, + OPTION_VERBOSE, + OPTION_NO_COLOR, + }; +} // namespace + +ModManArgs::ModManArgs() + : m_argument_parser(COMMAND_LINE_OPTIONS, std::extent_v) +{ +} + +void ModManArgs::PrintUsage() const +{ + UsageInformation usage(m_argument_parser.GetExecutableName()); + + for (const auto* commandLineOption : COMMAND_LINE_OPTIONS) + { + usage.AddCommandLineOption(commandLineOption); + } + + usage.Print(); +} + +void ModManArgs::PrintVersion() +{ + con::info("OpenAssetTools ModMan {}", GIT_VERSION); +} + +bool ModManArgs::ParseArgs(const int argc, const char** argv, bool& shouldContinue) +{ + shouldContinue = true; + if (!m_argument_parser.ParseArguments(argc, argv)) + { + PrintUsage(); + return false; + } + + // Check if the user requested help + if (m_argument_parser.IsOptionSpecified(OPTION_HELP)) + { + PrintUsage(); + shouldContinue = false; + return true; + } + + // Check if the user wants to see the version + if (m_argument_parser.IsOptionSpecified(OPTION_VERSION)) + { + PrintVersion(); + shouldContinue = false; + return true; + } + + // -v; --verbose + if (m_argument_parser.IsOptionSpecified(OPTION_VERBOSE)) + con::globalLogLevel = con::LogLevel::DEBUG; + else + con::globalLogLevel = con::LogLevel::INFO; + + // --no-color + con::globalUseColor = !m_argument_parser.IsOptionSpecified(OPTION_NO_COLOR); + + return true; +} diff --git a/src/ModMan/ModManArgs.h b/src/ModMan/ModManArgs.h new file mode 100644 index 00000000..feb19628 --- /dev/null +++ b/src/ModMan/ModManArgs.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Utils/Arguments/ArgumentParser.h" + +class ModManArgs +{ +public: + ModManArgs(); + bool ParseArgs(int argc, const char** argv, bool& shouldContinue); + +private: + /** + * \brief Prints a command line usage help text for the ModMan tool to stdout. + */ + void PrintUsage() const; + static void PrintVersion(); + + ArgumentParser m_argument_parser; +}; diff --git a/src/ModMan/main.cpp b/src/ModMan/main.cpp index 5c34be42..4b4f69ae 100644 --- a/src/ModMan/main.cpp +++ b/src/ModMan/main.cpp @@ -1,5 +1,6 @@ #include "Context/ModManContext.h" #include "GitVersion.h" +#include "ModManArgs.h" #include "Web/Binds/Binds.h" #include "Web/Platform/AssetHandler.h" #include "Web/UiCommunication.h" @@ -94,9 +95,13 @@ namespace } // namespace #ifdef _WIN32 +#define MODMAN_ARGC __argc +#define MODMAN_ARGV const_cast(__argv) int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/, LPSTR /*lpCmdLine*/, int /*nCmdShow*/) #else -int main() +#define MODMAN_ARGC argc +#define MODMAN_ARGV argv +int main(int argc, const char** argv) #endif { #ifdef _WIN32 @@ -114,6 +119,14 @@ int main() } #endif + ModManArgs args; + auto shouldContinue = true; + if (!args.ParseArgs(MODMAN_ARGC, MODMAN_ARGV, shouldContinue)) + return false; + + if (!shouldContinue) + return true; + con::info("Starting ModMan " GIT_VERSION); ModManContext::Get().Startup(); From 098be5355911f3572644b590004fe331052e9145 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sat, 11 Oct 2025 14:42:52 +0100 Subject: [PATCH 07/14] chore: add error handling for fastfile bind --- src/ModMan/Context/FastFileContext.cpp | 9 ++++++--- src/ModMan/Context/FastFileContext.h | 3 ++- src/ModMan/Web/Binds/FastFileBinds.cpp | 28 ++++++++++++++++++-------- src/ModManUi/src/App.vue | 10 +++++++-- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/ModMan/Context/FastFileContext.cpp b/src/ModMan/Context/FastFileContext.cpp index 1ea213e1..51d0814e 100644 --- a/src/ModMan/Context/FastFileContext.cpp +++ b/src/ModMan/Context/FastFileContext.cpp @@ -2,8 +2,11 @@ #include "ZoneLoading.h" -bool FastFileContext::LoadFastFile(const std::string& path) +std::optional FastFileContext::LoadFastFile(const std::string& path) { - m_loaded_zones.emplace_back(ZoneLoading::LoadZone(path)); - return true; + auto zone = ZoneLoading::LoadZone(path); + if (!zone) + return std::nullopt; + + return m_loaded_zones.emplace_back(std::move(zone)).get(); } diff --git a/src/ModMan/Context/FastFileContext.h b/src/ModMan/Context/FastFileContext.h index 9aed2d8c..54fa0114 100644 --- a/src/ModMan/Context/FastFileContext.h +++ b/src/ModMan/Context/FastFileContext.h @@ -2,12 +2,13 @@ #include "Zone/Zone.h" #include +#include #include class FastFileContext { public: - bool LoadFastFile(const std::string& path); + std::optional LoadFastFile(const std::string& path); std::vector> m_loaded_zones; }; diff --git a/src/ModMan/Web/Binds/FastFileBinds.cpp b/src/ModMan/Web/Binds/FastFileBinds.cpp index 3bfc74cb..a86ea0bf 100644 --- a/src/ModMan/Web/Binds/FastFileBinds.cpp +++ b/src/ModMan/Web/Binds/FastFileBinds.cpp @@ -5,8 +5,26 @@ namespace { + void LoadFastFile(webview::webview& wv, std::string id, std::string path) // NOLINT(performance-unnecessary-value-param) Copy is made for thread safety + { + ModManContext::Get().m_db_thread.Dispatch( + [&wv, id, path] + { + const auto maybeZone = ModManContext::Get().m_fast_file.LoadFastFile(path); -} + if (maybeZone) + { + ui::PromiseResolve(wv, id, true); + con::debug("Loaded zone \"{}\"", maybeZone.value()->m_name); + } + else + { + con::warn("Failed to load zone \"{}\"", path); + ui::PromiseReject(wv, id, false); + } + }); + } +} // namespace namespace ui { @@ -16,13 +34,7 @@ namespace ui "loadFastFile", [&wv](const std::string& id, std::string path) { - std::string idMove(id); - ModManContext::Get().m_db_thread.Dispatch( - [&wv, idMove, path] - { - ModManContext::Get().m_fast_file.LoadFastFile(path); - PromiseResolve(wv, idMove, true); - }); + LoadFastFile(wv, id, std::move(path)); }); } } // namespace ui diff --git a/src/ModManUi/src/App.vue b/src/ModManUi/src/App.vue index b360774f..3e15c414 100644 --- a/src/ModManUi/src/App.vue +++ b/src/ModManUi/src/App.vue @@ -9,9 +9,15 @@ async function onOpenFastfileClick() { lastPath.value = (await webviewBinds.openFileDialog({ filters: [{ name: "Fastfiles", filter: "*.ff" }] })) ?? ""; - loadingFastFile.value = true; - await webviewBinds.loadFastFile(lastPath.value); + loadingFastFile.value = true; + + webviewBinds.loadFastFile(lastPath.value) + .catch((e) => { + console.error("Failed to load fastfile", e); + }) + .finally(() => { loadingFastFile.value = false; + }); } From b27b7e77bdc99aab9d859390e62f3e213d4067fa Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sat, 11 Oct 2025 15:28:19 +0100 Subject: [PATCH 08/14] chore: pass ZoneLoading error as result --- src/Linker/Linker.cpp | 10 +- src/ModMan/Context/FastFileContext.cpp | 6 +- src/ModMan/Context/FastFileContext.h | 5 +- src/ModMan/Web/Binds/FastFileBinds.cpp | 6 +- src/ModManUi/src/App.vue | 4 +- src/Unlinker/Unlinker.cpp | 22 ++- src/Utils/Utils/Result.h | 227 ++++++++++++++++--------- src/ZoneLoading/ZoneLoading.cpp | 18 +- src/ZoneLoading/ZoneLoading.h | 4 +- 9 files changed, 181 insertions(+), 121 deletions(-) diff --git a/src/Linker/Linker.cpp b/src/Linker/Linker.cpp index 61050cf0..3da46b23 100644 --- a/src/Linker/Linker.cpp +++ b/src/Linker/Linker.cpp @@ -364,14 +364,16 @@ class LinkerImpl final : public Linker zoneDirectory = fs::current_path(); auto absoluteZoneDirectory = absolute(zoneDirectory).string(); - auto zone = ZoneLoading::LoadZone(zonePath); - if (!zone) + auto maybeZone = ZoneLoading::LoadZone(zonePath); + if (!maybeZone) { - con::error("Failed to load zone \"{}\".", zonePath); + con::error("Failed to load zone \"{}\": {}", zonePath, maybeZone.error()); return false; } - con::debug("Load zone \"{}\"", zone->m_name); + auto zone = std::move(*maybeZone); + + con::debug("Loaded zone \"{}\"", zone->m_name); m_loaded_zones.emplace_back(std::move(zone)); } diff --git a/src/ModMan/Context/FastFileContext.cpp b/src/ModMan/Context/FastFileContext.cpp index 51d0814e..823d595c 100644 --- a/src/ModMan/Context/FastFileContext.cpp +++ b/src/ModMan/Context/FastFileContext.cpp @@ -2,11 +2,11 @@ #include "ZoneLoading.h" -std::optional FastFileContext::LoadFastFile(const std::string& path) +result::Expected FastFileContext::LoadFastFile(const std::string& path) { auto zone = ZoneLoading::LoadZone(path); if (!zone) - return std::nullopt; + return result::Unexpected(std::move(zone.error())); - return m_loaded_zones.emplace_back(std::move(zone)).get(); + return m_loaded_zones.emplace_back(std::move(*zone)).get(); } diff --git a/src/ModMan/Context/FastFileContext.h b/src/ModMan/Context/FastFileContext.h index 54fa0114..74b0b3af 100644 --- a/src/ModMan/Context/FastFileContext.h +++ b/src/ModMan/Context/FastFileContext.h @@ -1,14 +1,15 @@ #pragma once + +#include "Utils/Result.h" #include "Zone/Zone.h" #include -#include #include class FastFileContext { public: - std::optional LoadFastFile(const std::string& path); + result::Expected LoadFastFile(const std::string& path); std::vector> m_loaded_zones; }; diff --git a/src/ModMan/Web/Binds/FastFileBinds.cpp b/src/ModMan/Web/Binds/FastFileBinds.cpp index a86ea0bf..fab40843 100644 --- a/src/ModMan/Web/Binds/FastFileBinds.cpp +++ b/src/ModMan/Web/Binds/FastFileBinds.cpp @@ -10,7 +10,7 @@ namespace ModManContext::Get().m_db_thread.Dispatch( [&wv, id, path] { - const auto maybeZone = ModManContext::Get().m_fast_file.LoadFastFile(path); + auto maybeZone = ModManContext::Get().m_fast_file.LoadFastFile(path); if (maybeZone) { @@ -19,8 +19,8 @@ namespace } else { - con::warn("Failed to load zone \"{}\"", path); - ui::PromiseReject(wv, id, false); + con::warn("Failed to load zone \"{}\": {}", path, maybeZone.error()); + ui::PromiseReject(wv, id, std::move(maybeZone).error()); } }); } diff --git a/src/ModManUi/src/App.vue b/src/ModManUi/src/App.vue index 3e15c414..85054db9 100644 --- a/src/ModManUi/src/App.vue +++ b/src/ModManUi/src/App.vue @@ -12,8 +12,8 @@ async function onOpenFastfileClick() { loadingFastFile.value = true; webviewBinds.loadFastFile(lastPath.value) - .catch((e) => { - console.error("Failed to load fastfile", e); + .catch((e: string) => { + console.error("Failed to load fastfile:", e); }) .finally(() => { loadingFastFile.value = false; diff --git a/src/Unlinker/Unlinker.cpp b/src/Unlinker/Unlinker.cpp index 790e75c0..1503d215 100644 --- a/src/Unlinker/Unlinker.cpp +++ b/src/Unlinker/Unlinker.cpp @@ -227,13 +227,15 @@ private: auto absoluteZoneDirectory = absolute(std::filesystem::path(zonePath).remove_filename()).string(); auto searchPathsForZone = paths.GetSearchPathsForZone(absoluteZoneDirectory); - auto zone = ZoneLoading::LoadZone(zonePath); - if (zone == nullptr) + auto maybeZone = ZoneLoading::LoadZone(zonePath); + if (!maybeZone) { - con::error("Failed to load zone \"{}\".", zonePath); + con::error("Failed to load zone \"{}\": {}", zonePath, maybeZone.error()); return false; } + auto zone = std::move(*maybeZone); + con::debug("Loaded zone \"{}\"", zone->m_name); if (ShouldLoadObj()) @@ -287,16 +289,16 @@ private: auto searchPathsForZone = paths.GetSearchPathsForZone(absoluteZoneDirectory); - std::string zoneName; - auto zone = ZoneLoading::LoadZone(zonePath); - if (zone == nullptr) + auto maybeZone = ZoneLoading::LoadZone(zonePath); + if (!maybeZone) { - con::error("Failed to load zone \"{}\".", zonePath); + con::error("Failed to load zone \"{}\": {}", zonePath, maybeZone.error()); return false; } - zoneName = zone->m_name; - con::debug("Loaded zone \"{}\"", zoneName); + auto zone = std::move(*maybeZone); + + con::debug("Loaded zone \"{}\"", zone->m_name); const auto* objLoader = IObjLoader::GetObjLoaderForGame(zone->m_game_id); if (ShouldLoadObj()) @@ -308,6 +310,8 @@ private: if (ShouldLoadObj()) objLoader->UnloadContainersOfZone(*zone); + // Copy zone name for using it after freeing the zone + std::string zoneName = zone->m_name; zone.reset(); con::debug("Unloaded zone \"{}\"", zoneName); } diff --git a/src/Utils/Utils/Result.h b/src/Utils/Utils/Result.h index 2e932393..b5d870c0 100644 --- a/src/Utils/Utils/Result.h +++ b/src/Utils/Utils/Result.h @@ -6,112 +6,171 @@ using NoResult = std::monostate; // Can be replaced by std::expected with c++23 -template class Result +namespace result { -public: - Result(TResult result) - : m_data(std::variant(std::in_place_index<0>, std::move(result))) + template class Unexpected { - } + public: + Unexpected(TError result) + : m_data(std::move(result)) + { + } - static Result Ok(TResult result) + constexpr std::add_lvalue_reference_t value() & + { + return m_data; + } + + constexpr std::add_const_t> value() const& + { + return m_data; + } + + constexpr std::add_rvalue_reference_t value() && + { + return std::move(m_data); + } + + constexpr std::add_const_t> value() const&& + { + return std::move(m_data); + } + + constexpr std::add_lvalue_reference_t operator*() & + { + return m_data; + } + + constexpr std::add_const_t> operator*() const& + { + return m_data; + } + + constexpr std::add_rvalue_reference_t operator*() && + { + return std::move(m_data); + } + + constexpr std::add_const_t> operator*() const&& + { + return std::move(m_data); + } + + constexpr std::add_pointer_t operator->() + { + return m_data; + } + + constexpr std::add_const_t> operator->() const + { + return m_data; + } + + private: + TError m_data; + }; + + template class Expected { - return Result(std::in_place_index<0>, std::move(result)); - } + public: + Expected(TResult result) + : m_data(std::variant(std::in_place_index<0>, std::move(result))) + { + } - static Result Bad(TError error) - { - return Result(std::in_place_index<1>, std::move(error)); - } + Expected(Unexpected unexpected) + : m_data(std::variant(std::in_place_index<1>, std::move(*unexpected))) + { + } - constexpr operator bool() const noexcept - { - return m_data.index() == 0; - } + constexpr operator bool() const noexcept + { + return m_data.index() == 0; + } - constexpr bool has_value() const noexcept - { - return m_data.index() == 0; - } + constexpr bool has_value() const noexcept + { + return m_data.index() == 0; + } - constexpr std::add_lvalue_reference_t value() & - { - return std::get<0, TResult>(m_data); - } + constexpr std::add_lvalue_reference_t value() & + { + return std::get<0>(m_data); + } - constexpr std::add_const_t> value() const& - { - return std::get<0, TResult>(m_data); - } + constexpr std::add_const_t> value() const& + { + return std::get<0>(m_data); + } - constexpr std::add_rvalue_reference_t value() && - { - return std::move(std::get<0, TResult>(m_data)); - } + constexpr std::add_rvalue_reference_t value() && + { + return std::move(std::get<0>(m_data)); + } - constexpr std::add_const_t> value() const&& - { - return std::move(std::get<0, TResult>(m_data)); - } + constexpr std::add_const_t> value() const&& + { + return std::move(std::get<0>(m_data)); + } - constexpr std::add_lvalue_reference_t operator*() & - { - return std::get<0, TResult>(m_data); - } + constexpr std::add_lvalue_reference_t operator*() & + { + return std::get<0>(m_data); + } - constexpr std::add_const_t> operator*() const& - { - return std::get<0, TResult>(m_data); - } + constexpr std::add_const_t> operator*() const& + { + return std::get<0>(m_data); + } - constexpr std::add_rvalue_reference_t operator*() && - { - return std::move(std::get<0, TResult>(m_data)); - } + constexpr std::add_rvalue_reference_t operator*() && + { + return std::move(std::get<0>(m_data)); + } - constexpr std::add_const_t> operator*() const&& - { - return std::move(std::get<0, TResult>(m_data)); - } + constexpr std::add_const_t> operator*() const&& + { + return std::move(std::get<0>(m_data)); + } - constexpr std::add_pointer_t operator->() - { - return std::get<0, TResult>(m_data); - } + constexpr std::add_pointer_t operator->() + { + return std::get<0>(m_data); + } - constexpr std::add_const_t> operator->() const - { - return std::get<0, TResult>(m_data); - } + constexpr std::add_const_t> operator->() const + { + return std::get<0>(m_data); + } - constexpr std::add_lvalue_reference_t error() & - { - return std::get<1, TError>(m_data); - } + constexpr std::add_lvalue_reference_t error() & + { + return std::get<1>(m_data); + } - constexpr std::add_const_t> error() const& - { - return std::get<1, TError>(m_data); - } + constexpr std::add_const_t> error() const& + { + return std::get<1>(m_data); + } - constexpr std::add_rvalue_reference_t error() && - { - return std::move(std::get<1, TError>(m_data)); - } + constexpr std::add_rvalue_reference_t error() && + { + return std::move(std::get<1>(m_data)); + } - constexpr std::add_const_t> error() const&& - { - return std::move(std::get<1, TError>(m_data)); - } + constexpr std::add_const_t> error() const&& + { + return std::move(std::get<1>(m_data)); + } -private: - explicit Result(std::variant data) - : m_data(std::move(data)) - { - } + private: + explicit Expected(std::variant data) + : m_data(std::move(data)) + { + } - std::variant m_data; -}; + std::variant m_data; + }; #define ENSURE_RESULT_VAR(var) \ if (!(var)) \ @@ -122,3 +181,5 @@ private: if (!result) \ return result; \ } + +} // namespace result diff --git a/src/ZoneLoading/ZoneLoading.cpp b/src/ZoneLoading/ZoneLoading.cpp index 3d15b80c..36e2a2ed 100644 --- a/src/ZoneLoading/ZoneLoading.cpp +++ b/src/ZoneLoading/ZoneLoading.cpp @@ -2,7 +2,6 @@ #include "Loading/IZoneLoaderFactory.h" #include "Loading/ZoneLoader.h" -#include "Utils/Logging/Log.h" #include "Utils/ObjFileStream.h" #include @@ -12,24 +11,18 @@ namespace fs = std::filesystem; -std::unique_ptr ZoneLoading::LoadZone(const std::string& path) +result::Expected, std::string> ZoneLoading::LoadZone(const std::string& path) { auto zoneName = fs::path(path).filename().replace_extension().string(); std::ifstream file(path, std::fstream::in | std::fstream::binary); if (!file.is_open()) - { - con::error("Could not open file '{}'.", path); - return nullptr; - } + return result::Unexpected(std::format("Could not open file '{}'.", path)); ZoneHeader header{}; file.read(reinterpret_cast(&header), sizeof(header)); if (file.gcount() != sizeof(header)) - { - con::error("Failed to read zone header from file '{}'.", path); - return nullptr; - } + return result::Unexpected(std::format("Failed to read zone header from file '{}'.", path)); std::unique_ptr zoneLoader; for (auto game = 0u; game < static_cast(GameId::COUNT); game++) @@ -42,10 +35,7 @@ std::unique_ptr ZoneLoading::LoadZone(const std::string& path) } if (!zoneLoader) - { - con::error("Could not create factory for zone '{}'.", zoneName); - return nullptr; - } + return result::Unexpected(std::format("Could not create factory for zone '{}'.", zoneName)); auto loadedZone = zoneLoader->LoadZone(file); diff --git a/src/ZoneLoading/ZoneLoading.h b/src/ZoneLoading/ZoneLoading.h index 749db050..330b3421 100644 --- a/src/ZoneLoading/ZoneLoading.h +++ b/src/ZoneLoading/ZoneLoading.h @@ -1,4 +1,6 @@ #pragma once + +#include "Utils/Result.h" #include "Zone/Zone.h" #include @@ -6,5 +8,5 @@ class ZoneLoading { public: - static std::unique_ptr LoadZone(const std::string& path); + static result::Expected, std::string> LoadZone(const std::string& path); }; From 7cefaee41cbcd3303775824f85d45de5164a03c8 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sat, 11 Oct 2025 15:30:19 +0100 Subject: [PATCH 09/14] fix: dialog binds not returning a promise --- src/ModManUi/src/native/DialogBinds.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ModManUi/src/native/DialogBinds.ts b/src/ModManUi/src/native/DialogBinds.ts index 8c7e66b7..97ddce0b 100644 --- a/src/ModManUi/src/native/DialogBinds.ts +++ b/src/ModManUi/src/native/DialogBinds.ts @@ -12,7 +12,7 @@ export interface SaveFileDialogDto { } export interface DialogBinds { - openFileDialog(options?: OpenFileDialogDto): string | null; - saveFileDialog(options?: SaveFileDialogDto): string | null; - folderSelectDialog(): string | null; + openFileDialog(options?: OpenFileDialogDto): Promise; + saveFileDialog(options?: SaveFileDialogDto): Promise; + folderSelectDialog(): Promise; } From 49f2000bad94324c1280cddafe3065e4d8c93adf Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sat, 11 Oct 2025 16:25:14 +0100 Subject: [PATCH 10/14] chore: track loaded zones in ui --- src/ModMan/Context/FastFileContext.cpp | 32 +++++++++++- src/ModMan/Context/FastFileContext.h | 3 ++ src/ModMan/Context/ModManContext.cpp | 1 + src/ModMan/Web/Binds/Binds.cpp | 2 +- src/ModMan/Web/Binds/FastFileBinds.cpp | 64 +++++++++++++++++++++++- src/ModMan/Web/Binds/FastFileBinds.h | 7 ++- src/ModManUi/src/App.vue | 31 +++++++++--- src/ModManUi/src/native/FastFileBinds.ts | 15 ++++++ src/ModManUi/src/native/index.ts | 7 +-- src/ModManUi/src/stores/ZoneStore.ts | 20 ++++++++ src/ModManUi/src/stores/counter.ts | 12 ----- src/ZoneCommon/Zone/Zone.cpp | 3 +- 12 files changed, 166 insertions(+), 31 deletions(-) create mode 100644 src/ModManUi/src/stores/ZoneStore.ts delete mode 100644 src/ModManUi/src/stores/counter.ts diff --git a/src/ModMan/Context/FastFileContext.cpp b/src/ModMan/Context/FastFileContext.cpp index 823d595c..6e9319ee 100644 --- a/src/ModMan/Context/FastFileContext.cpp +++ b/src/ModMan/Context/FastFileContext.cpp @@ -1,12 +1,42 @@ #include "FastFileContext.h" +#include "Web/Binds/FastFileBinds.h" +#include "Web/UiCommunication.h" #include "ZoneLoading.h" +void FastFileContext::Destroy() +{ + // Unload all zones + m_loaded_zones.clear(); +} + result::Expected FastFileContext::LoadFastFile(const std::string& path) { auto zone = ZoneLoading::LoadZone(path); if (!zone) return result::Unexpected(std::move(zone.error())); - return m_loaded_zones.emplace_back(std::move(*zone)).get(); + auto* result = m_loaded_zones.emplace_back(std::move(*zone)).get(); + + ui::NotifyZoneLoaded(result->m_name, path); + + return result; +} + +result::Expected FastFileContext::UnloadZone(const std::string& zoneName) +{ + const auto existingZone = std::ranges::find_if(m_loaded_zones, + [&zoneName](const std::unique_ptr& zone) + { + return zone->m_name == zoneName; + }); + + if (existingZone != m_loaded_zones.end()) + { + m_loaded_zones.erase(existingZone); + ui::NotifyZoneUnloaded(zoneName); + return NoResult(); + } + + return result::Unexpected(std::format("No zone with name {} loaded", zoneName)); } diff --git a/src/ModMan/Context/FastFileContext.h b/src/ModMan/Context/FastFileContext.h index 74b0b3af..a3f87a42 100644 --- a/src/ModMan/Context/FastFileContext.h +++ b/src/ModMan/Context/FastFileContext.h @@ -9,7 +9,10 @@ class FastFileContext { public: + void Destroy(); + result::Expected LoadFastFile(const std::string& path); + result::Expected UnloadZone(const std::string& zoneName); std::vector> m_loaded_zones; }; diff --git a/src/ModMan/Context/ModManContext.cpp b/src/ModMan/Context/ModManContext.cpp index cf01802f..221ebbf6 100644 --- a/src/ModMan/Context/ModManContext.cpp +++ b/src/ModMan/Context/ModManContext.cpp @@ -13,5 +13,6 @@ void ModManContext::Startup() void ModManContext::Destroy() { + m_fast_file.Destroy(); m_db_thread.Terminate(); } diff --git a/src/ModMan/Web/Binds/Binds.cpp b/src/ModMan/Web/Binds/Binds.cpp index d75e5026..ab1668f5 100644 --- a/src/ModMan/Web/Binds/Binds.cpp +++ b/src/ModMan/Web/Binds/Binds.cpp @@ -8,6 +8,6 @@ namespace ui void RegisterAllBinds(webview::webview& wv) { RegisterDialogHandlerBinds(wv); - RegisterFastFileBinds(wv); + RegisterZoneBinds(wv); } } // namespace ui diff --git a/src/ModMan/Web/Binds/FastFileBinds.cpp b/src/ModMan/Web/Binds/FastFileBinds.cpp index fab40843..b2cf0d50 100644 --- a/src/ModMan/Web/Binds/FastFileBinds.cpp +++ b/src/ModMan/Web/Binds/FastFileBinds.cpp @@ -3,8 +3,27 @@ #include "Context/ModManContext.h" #include "Web/UiCommunication.h" +#include "Json/JsonExtension.h" + namespace { + class ZoneLoadedDto + { + public: + std::string zoneName; + std::string filePath; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneLoadedDto, zoneName, filePath); + + class ZoneUnloadedDto + { + public: + std::string zoneName; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneUnloadedDto, zoneName); + void LoadFastFile(webview::webview& wv, std::string id, std::string path) // NOLINT(performance-unnecessary-value-param) Copy is made for thread safety { ModManContext::Get().m_db_thread.Dispatch( @@ -24,11 +43,47 @@ namespace } }); } + + void UnloadZone(webview::webview& wv, std::string id, std::string zoneName) // NOLINT(performance-unnecessary-value-param) Copy is made for thread safety + { + ModManContext::Get().m_db_thread.Dispatch( + [&wv, id, zoneName] + { + auto result = ModManContext::Get().m_fast_file.UnloadZone(zoneName); + if (result) + { + con::debug("Unloaded zone \"{}\"", zoneName); + ui::PromiseResolve(wv, id, true); + } + else + { + con::warn("Failed unloading zone {}: {}", zoneName, result.error()); + ui::PromiseReject(wv, id, std::move(result).error()); + } + }); + } } // namespace namespace ui { - void RegisterFastFileBinds(webview::webview& wv) + void NotifyZoneLoaded(std::string zoneName, std::string fastFilePath) + { + const ZoneLoadedDto dto{ + .zoneName = std::move(zoneName), + .filePath = std::move(fastFilePath), + }; + Notify(*ModManContext::Get().m_main_webview, "zoneLoaded", dto); + } + + void NotifyZoneUnloaded(std::string zoneName) + { + const ZoneUnloadedDto dto{ + .zoneName = std::move(zoneName), + }; + Notify(*ModManContext::Get().m_main_webview, "zoneUnloaded", dto); + } + + void RegisterZoneBinds(webview::webview& wv) { BindAsync(wv, "loadFastFile", @@ -36,5 +91,12 @@ namespace ui { LoadFastFile(wv, id, std::move(path)); }); + + BindAsync(wv, + "unloadZone", + [&wv](const std::string& id, std::string zoneName) + { + UnloadZone(wv, id, std::move(zoneName)); + }); } } // namespace ui diff --git a/src/ModMan/Web/Binds/FastFileBinds.h b/src/ModMan/Web/Binds/FastFileBinds.h index dc45ad9a..cf11f10d 100644 --- a/src/ModMan/Web/Binds/FastFileBinds.h +++ b/src/ModMan/Web/Binds/FastFileBinds.h @@ -4,5 +4,8 @@ namespace ui { - void RegisterFastFileBinds(webview::webview& wv); -} + void NotifyZoneLoaded(std::string zoneName, std::string fastFilePath); + void NotifyZoneUnloaded(std::string zoneName); + + void RegisterZoneBinds(webview::webview& wv); +} // namespace ui diff --git a/src/ModManUi/src/App.vue b/src/ModManUi/src/App.vue index 85054db9..4a6a761c 100644 --- a/src/ModManUi/src/App.vue +++ b/src/ModManUi/src/App.vue @@ -1,7 +1,9 @@ @@ -28,9 +37,15 @@ async function onOpenFastfileClick() {

- The last path: {{ lastPath }} Loading: {{ loadingFastFile }}

+
+

Loaded zones:

+
+ {{ zone }} + +
+
diff --git a/src/ModManUi/src/native/FastFileBinds.ts b/src/ModManUi/src/native/FastFileBinds.ts index 4f417902..556d3210 100644 --- a/src/ModManUi/src/native/FastFileBinds.ts +++ b/src/ModManUi/src/native/FastFileBinds.ts @@ -1,3 +1,18 @@ +export interface ZoneLoadedDto { + zoneName: string; + filePath: string; +} + +export interface ZoneUnloadedDto { + zoneName: string; +} + export interface FastFileBinds { loadFastFile(path: string): Promise; + unloadZone(zoneName: string): Promise; +} + +export interface FastFileEventMap { + zoneLoaded: ZoneLoadedDto; + zoneUnloaded: ZoneUnloadedDto; } diff --git a/src/ModManUi/src/native/index.ts b/src/ModManUi/src/native/index.ts index 9ab6257a..d57dd38d 100644 --- a/src/ModManUi/src/native/index.ts +++ b/src/ModManUi/src/native/index.ts @@ -1,12 +1,9 @@ import type { DialogBinds } from "./DialogBinds"; -import type { FastFileBinds } from "./FastFileBinds"; - +import type { FastFileBinds, FastFileEventMap } from "./FastFileBinds"; export type NativeMethods = DialogBinds & FastFileBinds; -interface NativeEventMap { - -} +type NativeEventMap = FastFileEventMap; type WebViewExtensions = { webviewBinds: NativeMethods; diff --git a/src/ModManUi/src/stores/ZoneStore.ts b/src/ModManUi/src/stores/ZoneStore.ts new file mode 100644 index 00000000..3877ab87 --- /dev/null +++ b/src/ModManUi/src/stores/ZoneStore.ts @@ -0,0 +1,20 @@ +import { readonly, ref } from "vue"; +import { defineStore } from "pinia"; +import { webviewAddEventListener } from "@/native"; + +export const useZoneStore = defineStore("zone", () => { + const loadedZones = ref([]); + + webviewAddEventListener("zoneLoaded", (dto) => { + loadedZones.value.push(dto.zoneName); + }); + + webviewAddEventListener("zoneUnloaded", (dto) => { + const index = loadedZones.value.indexOf(dto.zoneName); + if (index >= 0) { + loadedZones.value.splice(index, 1); + } + }); + + return { loadedZones: readonly(loadedZones) }; +}); diff --git a/src/ModManUi/src/stores/counter.ts b/src/ModManUi/src/stores/counter.ts deleted file mode 100644 index 374b4d03..00000000 --- a/src/ModManUi/src/stores/counter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ref, computed } from "vue"; -import { defineStore } from "pinia"; - -export const useCounterStore = defineStore("counter", () => { - const count = ref(0); - const doubleCount = computed(() => count.value * 2); - function increment() { - count.value++; - } - - return { count, doubleCount, increment }; -}); diff --git a/src/ZoneCommon/Zone/Zone.cpp b/src/ZoneCommon/Zone/Zone.cpp index deae50d3..8c160d89 100644 --- a/src/ZoneCommon/Zone/Zone.cpp +++ b/src/ZoneCommon/Zone/Zone.cpp @@ -8,7 +8,8 @@ Zone::Zone(std::string name, const zone_priority_t priority, GameId gameId) m_language(GameLanguage::LANGUAGE_NONE), m_game_id(gameId), m_pools(ZoneAssetPools::CreateForGame(gameId, this, priority)), - m_memory(std::make_unique()) + m_memory(std::make_unique()), + m_registered(false) { } From c791034562b963fc69ccdb3dace6cfe37221aa60 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sat, 11 Oct 2025 16:55:32 +0100 Subject: [PATCH 11/14] chore: rename FastFileBinds to ZoneBinds --- src/ModMan/Context/FastFileContext.cpp | 2 +- src/ModMan/Web/Binds/Binds.cpp | 2 +- src/ModMan/Web/Binds/{FastFileBinds.cpp => ZoneBinds.cpp} | 2 +- src/ModMan/Web/Binds/{FastFileBinds.h => ZoneBinds.h} | 0 src/ModManUi/src/native/{FastFileBinds.ts => ZoneBinds.ts} | 4 ++-- src/ModManUi/src/native/index.ts | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) rename src/ModMan/Web/Binds/{FastFileBinds.cpp => ZoneBinds.cpp} (99%) rename src/ModMan/Web/Binds/{FastFileBinds.h => ZoneBinds.h} (100%) rename src/ModManUi/src/native/{FastFileBinds.ts => ZoneBinds.ts} (80%) diff --git a/src/ModMan/Context/FastFileContext.cpp b/src/ModMan/Context/FastFileContext.cpp index 6e9319ee..3af0d92d 100644 --- a/src/ModMan/Context/FastFileContext.cpp +++ b/src/ModMan/Context/FastFileContext.cpp @@ -1,6 +1,6 @@ #include "FastFileContext.h" -#include "Web/Binds/FastFileBinds.h" +#include "Web/Binds/ZoneBinds.h" #include "Web/UiCommunication.h" #include "ZoneLoading.h" diff --git a/src/ModMan/Web/Binds/Binds.cpp b/src/ModMan/Web/Binds/Binds.cpp index ab1668f5..94bd2ec5 100644 --- a/src/ModMan/Web/Binds/Binds.cpp +++ b/src/ModMan/Web/Binds/Binds.cpp @@ -1,7 +1,7 @@ #include "Binds.h" -#include "FastFileBinds.h" #include "Web/Binds/DialogBinds.h" +#include "ZoneBinds.h" namespace ui { diff --git a/src/ModMan/Web/Binds/FastFileBinds.cpp b/src/ModMan/Web/Binds/ZoneBinds.cpp similarity index 99% rename from src/ModMan/Web/Binds/FastFileBinds.cpp rename to src/ModMan/Web/Binds/ZoneBinds.cpp index b2cf0d50..4291a1d1 100644 --- a/src/ModMan/Web/Binds/FastFileBinds.cpp +++ b/src/ModMan/Web/Binds/ZoneBinds.cpp @@ -1,4 +1,4 @@ -#include "FastFileBinds.h" +#include "ZoneBinds.h" #include "Context/ModManContext.h" #include "Web/UiCommunication.h" diff --git a/src/ModMan/Web/Binds/FastFileBinds.h b/src/ModMan/Web/Binds/ZoneBinds.h similarity index 100% rename from src/ModMan/Web/Binds/FastFileBinds.h rename to src/ModMan/Web/Binds/ZoneBinds.h diff --git a/src/ModManUi/src/native/FastFileBinds.ts b/src/ModManUi/src/native/ZoneBinds.ts similarity index 80% rename from src/ModManUi/src/native/FastFileBinds.ts rename to src/ModManUi/src/native/ZoneBinds.ts index 556d3210..b95307bf 100644 --- a/src/ModManUi/src/native/FastFileBinds.ts +++ b/src/ModManUi/src/native/ZoneBinds.ts @@ -7,12 +7,12 @@ export interface ZoneUnloadedDto { zoneName: string; } -export interface FastFileBinds { +export interface ZoneBinds { loadFastFile(path: string): Promise; unloadZone(zoneName: string): Promise; } -export interface FastFileEventMap { +export interface ZoneEventMap { zoneLoaded: ZoneLoadedDto; zoneUnloaded: ZoneUnloadedDto; } diff --git a/src/ModManUi/src/native/index.ts b/src/ModManUi/src/native/index.ts index d57dd38d..c055a7c5 100644 --- a/src/ModManUi/src/native/index.ts +++ b/src/ModManUi/src/native/index.ts @@ -1,9 +1,9 @@ import type { DialogBinds } from "./DialogBinds"; -import type { FastFileBinds, FastFileEventMap } from "./FastFileBinds"; +import type { ZoneBinds, ZoneEventMap } from "./ZoneBinds"; -export type NativeMethods = DialogBinds & FastFileBinds; +export type NativeMethods = DialogBinds & ZoneBinds; -type NativeEventMap = FastFileEventMap; +type NativeEventMap = ZoneEventMap; type WebViewExtensions = { webviewBinds: NativeMethods; From f53195d4bd77a4f8be38c2f7e0ce3ff75bab9cbb Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sat, 11 Oct 2025 17:59:34 +0100 Subject: [PATCH 12/14] chore: update testing style --- src/ModManUi/src/App.vue | 29 ++++++++++++------ .../src/components/SpinningLoader.vue | 30 +++++++++++++++++++ src/ModManUi/src/main.scss | 10 +++++-- 3 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 src/ModManUi/src/components/SpinningLoader.vue diff --git a/src/ModManUi/src/App.vue b/src/ModManUi/src/App.vue index 4a6a761c..0da45521 100644 --- a/src/ModManUi/src/App.vue +++ b/src/ModManUi/src/App.vue @@ -2,6 +2,7 @@ import { ref } from "vue"; import { webviewBinds } from "@/native"; import { useZoneStore } from "@/stores/ZoneStore"; +import SpinningLoader from "@/components/SpinningLoader.vue"; const zoneStore = useZoneStore(); const lastPath = ref(""); @@ -36,25 +37,35 @@ function onUnloadClicked(zoneName: string) { Nothing to see here yet, this is mainly for testing

- - Loading: {{ loadingFastFile }} +

Loaded zones:

-
- {{ zone }} - +
+
+ {{ zone }} + +
diff --git a/src/ModManUi/src/components/SpinningLoader.vue b/src/ModManUi/src/components/SpinningLoader.vue new file mode 100644 index 00000000..4161cd84 --- /dev/null +++ b/src/ModManUi/src/components/SpinningLoader.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/ModManUi/src/main.scss b/src/ModManUi/src/main.scss index 20d4d9c6..37c9c0b0 100644 --- a/src/ModManUi/src/main.scss +++ b/src/ModManUi/src/main.scss @@ -71,13 +71,17 @@ button { cursor: pointer; } -button:hover { +button:not(:disabled):hover { border-color: #396cd8; } -button:active { +button:not(:disabled):active { border-color: #396cd8; background-color: #e8e8e8; } +button:disabled { + opacity: 0.6; + cursor: not-allowed; +} input, button { @@ -103,7 +107,7 @@ button { color: #ffffff; background-color: #0f0f0f98; } - button:active { + button:not(:disabled):active { background-color: #0f0f0f69; } } \ No newline at end of file From d86b70e0063743a9b7107e4712f7456594405e7b Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sat, 11 Oct 2025 18:56:11 +0100 Subject: [PATCH 13/14] feat: unlink fastfiles via ModMan --- src/ModMan/Web/Binds/Binds.cpp | 2 + src/ModMan/Web/Binds/UnlinkingBinds.cpp | 93 +++++++++++++++++++++++ src/ModMan/Web/Binds/UnlinkingBinds.h | 8 ++ src/ModMan/Web/Binds/ZoneBinds.cpp | 7 +- src/ModManUi/src/App.vue | 69 ++++++++++++++--- src/ModManUi/src/native/UnlinkingBinds.ts | 3 + src/ModManUi/src/native/ZoneBinds.ts | 2 +- src/ModManUi/src/native/index.ts | 3 +- 8 files changed, 174 insertions(+), 13 deletions(-) create mode 100644 src/ModMan/Web/Binds/UnlinkingBinds.cpp create mode 100644 src/ModMan/Web/Binds/UnlinkingBinds.h create mode 100644 src/ModManUi/src/native/UnlinkingBinds.ts diff --git a/src/ModMan/Web/Binds/Binds.cpp b/src/ModMan/Web/Binds/Binds.cpp index 94bd2ec5..7ae5ab9c 100644 --- a/src/ModMan/Web/Binds/Binds.cpp +++ b/src/ModMan/Web/Binds/Binds.cpp @@ -1,5 +1,6 @@ #include "Binds.h" +#include "UnlinkingBinds.h" #include "Web/Binds/DialogBinds.h" #include "ZoneBinds.h" @@ -8,6 +9,7 @@ namespace ui void RegisterAllBinds(webview::webview& wv) { RegisterDialogHandlerBinds(wv); + RegisterUnlinkingBinds(wv); RegisterZoneBinds(wv); } } // namespace ui diff --git a/src/ModMan/Web/Binds/UnlinkingBinds.cpp b/src/ModMan/Web/Binds/UnlinkingBinds.cpp new file mode 100644 index 00000000..bc281dcf --- /dev/null +++ b/src/ModMan/Web/Binds/UnlinkingBinds.cpp @@ -0,0 +1,93 @@ +#include "UnlinkingBinds.h" + +#include "Context/ModManContext.h" +#include "IObjWriter.h" +#include "SearchPath/OutputPathFilesystem.h" +#include "SearchPath/SearchPaths.h" +#include "Utils/PathUtils.h" +#include "Web/UiCommunication.h" + +#include "Json/JsonExtension.h" +#include + +namespace fs = std::filesystem; + +namespace +{ + class ZoneLoadedDto + { + public: + std::string zoneName; + std::string filePath; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneLoadedDto, zoneName, filePath); + + class ZoneUnloadedDto + { + public: + std::string zoneName; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneUnloadedDto, zoneName); + + result::Expected UnlinkZoneInDbThread(const std::string& zoneName) + { + const auto& context = ModManContext::Get().m_fast_file; + const auto existingZone = std::ranges::find_if(context.m_loaded_zones, + [&zoneName](const std::unique_ptr& zone) + { + return zone->m_name == zoneName; + }); + + if (existingZone == context.m_loaded_zones.end()) + return result::Unexpected(std::format("No zone with name {} loaded", zoneName)); + + const auto& zone = *existingZone->get(); + + const auto* objWriter = IObjWriter::GetObjWriterForGame(zone.m_game_id); + + const auto outputFolderPath = fs::path(utils::GetExecutablePath()).parent_path() / "zone_dump" / zoneName; + const auto outputFolderPathStr = outputFolderPath.string(); + + OutputPathFilesystem outputFolderOutputPath(outputFolderPath); + SearchPaths searchPaths; + AssetDumpingContext dumpingContext(zone, outputFolderPathStr, outputFolderOutputPath, searchPaths); + objWriter->DumpZone(dumpingContext); + + return NoResult(); + } + + void UnlinkZone(webview::webview& wv, std::string id, std::string zoneName) // NOLINT(performance-unnecessary-value-param) Copy is made for thread safety + { + ModManContext::Get().m_db_thread.Dispatch( + [&wv, id, zoneName] + { + auto result = UnlinkZoneInDbThread(zoneName); + + if (result) + { + con::debug("Unlinked zone \"{}\"", zoneName); + ui::PromiseResolve(wv, id, true); + } + else + { + con::warn("Failed to unlink zone \"{}\": {}", zoneName, result.error()); + ui::PromiseReject(wv, id, std::move(result).error()); + } + }); + } +} // namespace + +namespace ui +{ + void RegisterUnlinkingBinds(webview::webview& wv) + { + BindAsync(wv, + "unlinkZone", + [&wv](const std::string& id, std::string zoneName) + { + UnlinkZone(wv, id, std::move(zoneName)); + }); + } +} // namespace ui diff --git a/src/ModMan/Web/Binds/UnlinkingBinds.h b/src/ModMan/Web/Binds/UnlinkingBinds.h new file mode 100644 index 00000000..4169a780 --- /dev/null +++ b/src/ModMan/Web/Binds/UnlinkingBinds.h @@ -0,0 +1,8 @@ +#pragma once + +#include "Web/WebViewLib.h" + +namespace ui +{ + void RegisterUnlinkingBinds(webview::webview& wv); +} // namespace ui diff --git a/src/ModMan/Web/Binds/ZoneBinds.cpp b/src/ModMan/Web/Binds/ZoneBinds.cpp index 4291a1d1..8882a7d0 100644 --- a/src/ModMan/Web/Binds/ZoneBinds.cpp +++ b/src/ModMan/Web/Binds/ZoneBinds.cpp @@ -33,7 +33,12 @@ namespace if (maybeZone) { - ui::PromiseResolve(wv, id, true); + ui::PromiseResolve(wv, + id, + ZoneLoadedDto{ + .zoneName = maybeZone.value()->m_name, + .filePath = path, + }); con::debug("Loaded zone \"{}\"", maybeZone.value()->m_name); } else diff --git a/src/ModManUi/src/App.vue b/src/ModManUi/src/App.vue index 0da45521..8c4a4dc1 100644 --- a/src/ModManUi/src/App.vue +++ b/src/ModManUi/src/App.vue @@ -1,21 +1,29 @@