From 82c34789ff69252d0b1368adc81eadccf6f4e0f8 Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Mon, 25 Nov 2024 10:13:50 -0600 Subject: [PATCH 1/9] Implemented otr extraction --- CMakeLists.txt | 11 +- config.yml | 2 +- include/portable-file-dialogs.h | 1770 ++++++++++++++++++++++++++ src/port/Engine.cpp | 69 +- src/port/Engine.h | 8 +- src/port/extractor/GameExtractor.cpp | 62 + src/port/extractor/GameExtractor.h | 22 + tools/Torch | 2 +- 8 files changed, 1919 insertions(+), 27 deletions(-) create mode 100644 include/portable-file-dialogs.h create mode 100644 src/port/extractor/GameExtractor.cpp create mode 100644 src/port/extractor/GameExtractor.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d99cfe9e..b054a67e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -321,16 +321,23 @@ endif() target_link_libraries(${PROJECT_NAME} PRIVATE "${ADDITIONAL_LIBRARY_DEPENDENCIES}") +set(USE_STANDALONE OFF) +set(BUILD_STORMLIB OFF) +set(BUILD_SM64 OFF) +set(BUILD_MK64 OFF) +add_subdirectory(tools/Torch) +target_link_libraries(${PROJECT_NAME} PRIVATE torch) + if(CMAKE_SYSTEM_NAME MATCHES "NintendoSwitch") -nx_generate_nacp(Lylat.nacp +nx_generate_nacp(Starship.nacp NAME "${PROJECT_NAME}" AUTHOR "${PROJECT_TEAM}" VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" ) nx_create_nro(${PROJECT_NAME} - NACP Lylat.nacp + NACP Starship.nacp ) INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.nro DESTINATION . COMPONENT ${PROJECT_NAME}) diff --git a/config.yml b/config.yml index 6e7b92c6..562fddeb 100644 --- a/config.yml +++ b/config.yml @@ -6,7 +6,7 @@ f7475fb11e7e6830f82883412638e8390791ab87: config: gbi: F3DEX sort: OFFSET - # logging: ERROR + logging: ERROR output: binary: sf64.otr code: src/assets diff --git a/include/portable-file-dialogs.h b/include/portable-file-dialogs.h new file mode 100644 index 00000000..28fc65f6 --- /dev/null +++ b/include/portable-file-dialogs.h @@ -0,0 +1,1770 @@ +// +// Portable File Dialogs +// +// Copyright © 2018–2022 Sam Hocevar +// +// This library is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#pragma once + +#if _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#include +#include +#include // IFileDialog +#include +#include +#include // std::async +#include // GetUserProfileDirectory() + +#elif __EMSCRIPTEN__ +#include + +#else +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 2 // for popen() +#endif +#ifdef __APPLE__ +#ifndef _DARWIN_C_SOURCE +#define _DARWIN_C_SOURCE +#endif +#endif +#include // popen() +#include // std::getenv() +#include // fcntl() +#include // read(), pipe(), dup2(), getuid() +#include // ::kill, std::signal +#include // stat() +#include // waitpid() +#include // getpwnam() +#endif + +#include // std::string +#include // std::shared_ptr +#include // std::ostream +#include // std::map +#include // std::set +#include // std::regex +#include // std::mutex, std::this_thread +#include // std::chrono + +// Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog +#ifndef PFD_HAS_IFILEDIALOG +#define PFD_HAS_IFILEDIALOG 1 +#if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION +#if __GXX_ABI_VERSION <= 1013 +#undef PFD_HAS_IFILEDIALOG +#define PFD_HAS_IFILEDIALOG 0 +#endif +#endif +#endif + +namespace pfd { + +enum class button { + cancel = -1, + ok, + yes, + no, + abort, + retry, + ignore, +}; + +enum class choice { + ok = 0, + ok_cancel, + yes_no, + yes_no_cancel, + retry_cancel, + abort_retry_ignore, +}; + +enum class icon { + info = 0, + warning, + error, + question, +}; + +// Additional option flags for various dialog constructors +enum class opt : uint8_t { + none = 0, + // For file open, allow multiselect. + multiselect = 0x1, + // For file save, force overwrite and disable the confirmation dialog. + force_overwrite = 0x2, + // For folder select, force path to be the provided argument instead + // of the last opened directory, which is the Microsoft-recommended, + // user-friendly behaviour. + force_path = 0x4, +}; + +inline opt operator|(opt a, opt b) { + return opt(uint8_t(a) | uint8_t(b)); +} +inline bool operator&(opt a, opt b) { + return bool(uint8_t(a) & uint8_t(b)); +} + +// The settings class, only exposing to the user a way to set verbose mode +// and to force a rescan of installed desktop helpers (zenity, kdialog…). +class settings { + public: + static bool available(); + + static void verbose(bool value); + static void rescan(); + + protected: + explicit settings(bool resync = false); + + bool check_program(std::string const& program); + + inline bool is_osascript() const; + inline bool is_zenity() const; + inline bool is_kdialog() const; + + enum class flag { + is_scanned = 0, + is_verbose, + + has_zenity, + has_matedialog, + has_qarma, + has_kdialog, + is_vista, + + max_flag, + }; + + // Static array of flags for internal state + bool const& flags(flag in_flag) const; + + // Non-const getter for the static array of flags + bool& flags(flag in_flag); +}; + +// Internal classes, not to be used by client applications +namespace internal { + +// Process wait timeout, in milliseconds +static int const default_wait_timeout = 20; + +class executor { + friend class dialog; + + public: + // High level function to get the result of a command + std::string result(int* exit_code = nullptr); + + // High level function to abort + bool kill(); + +#if _WIN32 + void start_func(std::function const& fun); + static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam); +#elif __EMSCRIPTEN__ + void start(int exit_code); +#else + void start_process(std::vector const& command); +#endif + + ~executor(); + + protected: + bool ready(int timeout = default_wait_timeout); + void stop(); + + private: + bool m_running = false; + std::string m_stdout; + int m_exit_code = -1; +#if _WIN32 + std::future m_future; + std::set m_windows; + std::condition_variable m_cond; + std::mutex m_mutex; + DWORD m_tid; +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something +#else + pid_t m_pid = 0; + int m_fd = -1; +#endif +}; + +class platform { + protected: +#if _WIN32 + // Helper class around LoadLibraryA() and GetProcAddress() with some safety + class dll { + public: + dll(std::string const& name); + ~dll(); + + template class proc { + public: + proc(dll const& lib, std::string const& sym) + : m_proc(reinterpret_cast((void*)::GetProcAddress(lib.handle, sym.c_str()))) { + } + + operator bool() const { + return m_proc != nullptr; + } + operator T*() const { + return m_proc; + } + + private: + T* m_proc; + }; + + private: + HMODULE handle; + }; + + // Helper class around CoInitialize() and CoUnInitialize() + class ole32_dll : public dll { + public: + ole32_dll(); + ~ole32_dll(); + bool is_initialized(); + + private: + HRESULT m_state; + }; + + // Helper class around CreateActCtx() and ActivateActCtx() + class new_style_context { + public: + new_style_context(); + ~new_style_context(); + + private: + HANDLE create(); + ULONG_PTR m_cookie = 0; + }; +#endif +}; + +class dialog : protected settings, protected platform { + public: + bool ready(int timeout = default_wait_timeout) const; + bool kill() const; + + protected: + explicit dialog(); + + std::vector desktop_helper() const; + static std::string buttons_to_name(choice _choice); + static std::string get_icon_name(icon _icon); + + std::string powershell_quote(std::string const& str) const; + std::string osascript_quote(std::string const& str) const; + std::string shell_quote(std::string const& str) const; + + // Keep handle to executing command + std::shared_ptr m_async; +}; + +class file_dialog : public dialog { + protected: + enum type { + open, + save, + folder, + }; + + file_dialog(type in_type, std::string const& title, std::string const& default_path = "", + std::vector const& filters = {}, opt options = opt::none); + + protected: + std::string string_result(); + std::vector vector_result(); + +#if _WIN32 + static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData); +#if PFD_HAS_IFILEDIALOG + std::string select_folder_vista(IFileDialog* ifd, bool force_path); +#endif + + std::wstring m_wtitle; + std::wstring m_wdefault_path; + + std::vector m_vector_result; +#endif +}; + +} // namespace internal + +// +// The path class provides some platform-specific path constants +// + +class path : protected internal::platform { + public: + static std::string home(); + static std::string separator(); +}; + +// +// The notify widget +// + +class notify : public internal::dialog { + public: + notify(std::string const& title, std::string const& message, icon _icon = icon::info); +}; + +// +// The message widget +// + +class message : public internal::dialog { + public: + message(std::string const& title, std::string const& text, choice _choice = choice::ok_cancel, + icon _icon = icon::info); + + button result(); + + private: + // Some extra logic to map the exit code to button number + std::map m_mappings; +}; + +// +// The open_file, save_file, and open_folder widgets +// + +class open_file : public internal::file_dialog { + public: + open_file(std::string const& title, std::string const& default_path = "", + std::vector const& filters = { "All Files", "*" }, opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]] +#endif +#endif + open_file(std::string const& title, std::string const& default_path, std::vector const& filters, + bool allow_multiselect); + + std::vector result(); +}; + +class save_file : public internal::file_dialog { + public: + save_file(std::string const& title, std::string const& default_path = "", + std::vector const& filters = { "All Files", "*" }, opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]] +#endif +#endif + save_file(std::string const& title, std::string const& default_path, std::vector const& filters, + bool confirm_overwrite); + + std::string result(); +}; + +class select_folder : public internal::file_dialog { + public: + select_folder(std::string const& title, std::string const& default_path = "", opt options = opt::none); + + std::string result(); +}; + +// +// Below this are all the method implementations. You may choose to define the +// macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except +// in one place. This may reduce compilation times. +// + +#if !defined PFD_SKIP_IMPLEMENTATION + +// internal free functions implementations + +namespace internal { + +#if _WIN32 +static inline std::wstring str2wstr(std::string const& str) { + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0); + std::wstring ret(len, '\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size()); + return ret; +} + +static inline std::string wstr2str(std::wstring const& str) { + int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr); + std::string ret(len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr); + return ret; +} + +static inline bool is_vista() { + OSVERSIONINFOEXW osvi; + memset(&osvi, 0, sizeof(osvi)); + DWORDLONG const mask = + VerSetConditionMask(VerSetConditionMask(VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL), + VER_MINORVERSION, VER_GREATER_EQUAL), + VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); + osvi.dwOSVersionInfoSize = sizeof(osvi); + osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA); + osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA); + osvi.wServicePackMajor = 0; + + return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE; +} +#endif + +// This is necessary until C++20 which will have std::string::ends_with() etc. + +static inline bool ends_with(std::string const& str, std::string const& suffix) { + return suffix.size() <= str.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +static inline bool starts_with(std::string const& str, std::string const& prefix) { + return prefix.size() <= str.size() && str.compare(0, prefix.size(), prefix) == 0; +} + +// This is necessary until C++17 which will have std::filesystem::is_directory + +static inline bool is_directory(std::string const& path) { +#if _WIN32 + auto attr = GetFileAttributesA(path.c_str()); + return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY); +#elif __EMSCRIPTEN__ + // TODO + return false; +#else + struct stat s; + return stat(path.c_str(), &s) == 0 && S_ISDIR(s.st_mode); +#endif +} + +// This is necessary because getenv is not thread-safe + +static inline std::string getenv(std::string const& str) { +#if _MSC_VER + char* buf = nullptr; + size_t size = 0; + if (_dupenv_s(&buf, &size, str.c_str()) == 0 && buf) { + std::string ret(buf); + free(buf); + return ret; + } + return ""; +#else + auto buf = std::getenv(str.c_str()); + return buf ? buf : ""; +#endif +} + +} // namespace internal + +// settings implementation + +inline settings::settings(bool resync) { + flags(flag::is_scanned) &= !resync; + + if (flags(flag::is_scanned)) + return; + + auto pfd_verbose = internal::getenv("PFD_VERBOSE"); + auto match_no = std::regex("(|0|no|false)", std::regex_constants::icase); + if (!std::regex_match(pfd_verbose, match_no)) + flags(flag::is_verbose) = true; + +#if _WIN32 + flags(flag::is_vista) = internal::is_vista(); +#elif !__APPLE__ + flags(flag::has_zenity) = check_program("zenity"); + flags(flag::has_matedialog) = check_program("matedialog"); + flags(flag::has_qarma) = check_program("qarma"); + flags(flag::has_kdialog) = check_program("kdialog"); + + // If multiple helpers are available, try to default to the best one + if (flags(flag::has_zenity) && flags(flag::has_kdialog)) { + auto desktop_name = internal::getenv("XDG_SESSION_DESKTOP"); + if (desktop_name == std::string("gnome")) + flags(flag::has_kdialog) = false; + else if (desktop_name == std::string("KDE")) + flags(flag::has_zenity) = false; + } +#endif + + flags(flag::is_scanned) = true; +} + +inline bool settings::available() { +#if _WIN32 + return true; +#elif __APPLE__ + return true; +#elif __EMSCRIPTEN__ + // FIXME: Return true after implementation is complete. + return false; +#else + settings tmp; + return tmp.flags(flag::has_zenity) || tmp.flags(flag::has_matedialog) || tmp.flags(flag::has_qarma) || + tmp.flags(flag::has_kdialog); +#endif +} + +inline void settings::verbose(bool value) { + settings().flags(flag::is_verbose) = value; +} + +inline void settings::rescan() { + settings(/* resync = */ true); +} + +// Check whether a program is present using “which”. +inline bool settings::check_program(std::string const& program) { +#if _WIN32 + (void)program; + return false; +#elif __EMSCRIPTEN__ + (void)program; + return false; +#else + int exit_code = -1; + internal::executor async; + async.start_process({ "/bin/sh", "-c", "which " + program }); + async.result(&exit_code); + return exit_code == 0; +#endif +} + +inline bool settings::is_osascript() const { +#if __APPLE__ + return true; +#else + return false; +#endif +} + +inline bool settings::is_zenity() const { + return flags(flag::has_zenity) || flags(flag::has_matedialog) || flags(flag::has_qarma); +} + +inline bool settings::is_kdialog() const { + return flags(flag::has_kdialog); +} + +inline bool const& settings::flags(flag in_flag) const { + static bool flags[size_t(flag::max_flag)]; + return flags[size_t(in_flag)]; +} + +inline bool& settings::flags(flag in_flag) { + return const_cast(static_cast(this)->flags(in_flag)); +} + +// path implementation +inline std::string path::home() { +#if _WIN32 + // First try the USERPROFILE environment variable + auto user_profile = internal::getenv("USERPROFILE"); + if (user_profile.size() > 0) + return user_profile; + // Otherwise, try GetUserProfileDirectory() + HANDLE token = nullptr; + DWORD len = MAX_PATH; + char buf[MAX_PATH] = { '\0' }; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + dll userenv("userenv.dll"); + dll::proc get_user_profile_directory(userenv, "GetUserProfileDirectoryA"); + get_user_profile_directory(token, buf, &len); + CloseHandle(token); + if (*buf) + return buf; + } +#elif __EMSCRIPTEN__ + return "/"; +#else + // First try the HOME environment variable + auto home = internal::getenv("HOME"); + if (home.size() > 0) + return home; + // Otherwise, try getpwuid_r() + size_t len = 4096; +#if defined(_SC_GETPW_R_SIZE_MAX) + auto size_max = sysconf(_SC_GETPW_R_SIZE_MAX); + if (size_max != -1) + len = size_t(size_max); +#endif + std::vector buf(len); + struct passwd pwd, *result; + if (getpwuid_r(getuid(), &pwd, buf.data(), buf.size(), &result) == 0) + return result->pw_dir; +#endif + return "/"; +} + +inline std::string path::separator() { +#if _WIN32 + return "\\"; +#else + return "/"; +#endif +} + +// executor implementation + +inline std::string internal::executor::result(int* exit_code /* = nullptr */) { + stop(); + if (exit_code) + *exit_code = m_exit_code; + return m_stdout; +} + +inline bool internal::executor::kill() { +#if _WIN32 + if (m_future.valid()) { + // Close all windows that weren’t open when we started the future + auto previous_windows = m_windows; + EnumWindows(&enum_windows_callback, (LPARAM)this); + for (auto hwnd : m_windows) + if (previous_windows.find(hwnd) == previous_windows.end()) { + SendMessage(hwnd, WM_CLOSE, 0, 0); + // Also send IDNO in case of a Yes/No or Abort/Retry/Ignore messagebox + SendMessage(hwnd, WM_COMMAND, IDNO, 0); + } + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + return false; // cannot kill +#else + ::kill(m_pid, SIGKILL); +#endif + stop(); + return true; +} + +#if _WIN32 +inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam) { + auto that = (executor*)lParam; + + DWORD pid; + auto tid = GetWindowThreadProcessId(hwnd, &pid); + if (tid == that->m_tid) + that->m_windows.insert(hwnd); + return TRUE; +} +#endif + +#if _WIN32 +inline void internal::executor::start_func(std::function const& fun) { + stop(); + + auto trampoline = [fun, this]() { + // Save our thread id so that the caller can cancel us + m_tid = GetCurrentThreadId(); + EnumWindows(&enum_windows_callback, (LPARAM)this); + m_cond.notify_all(); + return fun(&m_exit_code); + }; + + std::unique_lock lock(m_mutex); + m_future = std::async(std::launch::async, trampoline); + m_cond.wait(lock); + m_running = true; +} + +#elif __EMSCRIPTEN__ +inline void internal::executor::start(int exit_code) { + m_exit_code = exit_code; +} + +#else +inline void internal::executor::start_process(std::vector const& command) { + stop(); + m_stdout.clear(); + m_exit_code = -1; + + int in[2], out[2]; + if (pipe(in) != 0 || pipe(out) != 0) + return; + + m_pid = fork(); + if (m_pid < 0) + return; + + close(in[m_pid ? 0 : 1]); + close(out[m_pid ? 1 : 0]); + + if (m_pid == 0) { + dup2(in[0], STDIN_FILENO); + dup2(out[1], STDOUT_FILENO); + + // Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity) + int fd = open("/dev/null", O_WRONLY); + dup2(fd, STDERR_FILENO); + close(fd); + + std::vector args; + std::transform(command.cbegin(), command.cend(), std::back_inserter(args), + [](std::string const& s) { return const_cast(s.c_str()); }); + args.push_back(nullptr); // null-terminate argv[] + + execvp(args[0], args.data()); + exit(1); + } + + close(in[1]); + m_fd = out[0]; + auto flags = fcntl(m_fd, F_GETFL); + fcntl(m_fd, F_SETFL, flags | O_NONBLOCK); + + m_running = true; +} +#endif + +inline internal::executor::~executor() { + stop(); +} + +inline bool internal::executor::ready(int timeout /* = default_wait_timeout */) { + if (!m_running) + return true; + +#if _WIN32 + if (m_future.valid()) { + auto status = m_future.wait_for(std::chrono::milliseconds(timeout)); + if (status != std::future_status::ready) { + // On Windows, we need to run the message pump. If the async + // thread uses a Windows API dialog, it may be attached to the + // main thread and waiting for messages that only we can dispatch. + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return false; + } + + m_stdout = m_future.get(); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; +#else + char buf[BUFSIZ]; + ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore + if (received > 0) { + m_stdout += std::string(buf, received); + return false; + } + + // Reap child process if it is dead. It is possible that the system has already reaped it + // (this happens when the calling application handles or ignores SIG_CHLD) and results in + // waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for + // a little while. + int status; + pid_t child = waitpid(m_pid, &status, WNOHANG); + if (child != m_pid && (child >= 0 || errno != ECHILD)) { + // FIXME: this happens almost always at first iteration + std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + return false; + } + + close(m_fd); + m_exit_code = WEXITSTATUS(status); +#endif + + m_running = false; + return true; +} + +inline void internal::executor::stop() { + // Loop until the user closes the dialog + while (!ready()) + ; +} + +// dll implementation + +#if _WIN32 +inline internal::platform::dll::dll(std::string const& name) : handle(::LoadLibraryA(name.c_str())) { +} + +inline internal::platform::dll::~dll() { + if (handle) + ::FreeLibrary(handle); +} +#endif // _WIN32 + +// ole32_dll implementation + +#if _WIN32 +inline internal::platform::ole32_dll::ole32_dll() : dll("ole32.dll") { + // Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes. + // See https://github.com/samhocevar/portable-file-dialogs/issues/51 + auto coinit = proc(*this, "CoInitializeEx"); + m_state = coinit(nullptr, COINIT_MULTITHREADED); +} + +inline internal::platform::ole32_dll::~ole32_dll() { + if (is_initialized()) + proc(*this, "CoUninitialize")(); +} + +inline bool internal::platform::ole32_dll::is_initialized() { + return m_state == S_OK || m_state == S_FALSE; +} +#endif + +// new_style_context implementation + +#if _WIN32 +inline internal::platform::new_style_context::new_style_context() { + // Only create one activation context for the whole app lifetime. + static HANDLE hctx = create(); + + if (hctx != INVALID_HANDLE_VALUE) + ActivateActCtx(hctx, &m_cookie); +} + +inline internal::platform::new_style_context::~new_style_context() { + DeactivateActCtx(0, m_cookie); +} + +inline HANDLE internal::platform::new_style_context::create() { + // This “hack” seems to be necessary for this code to work on windows XP. + // Without it, dialogs do not show and close immediately. GetError() + // returns 0 so I don’t know what causes this. I was not able to reproduce + // this behavior on Windows 7 and 10 but just in case, let it be here for + // those versions too. + // This hack is not required if other dialogs are used (they load comdlg32 + // automatically), only if message boxes are used. + dll comdlg32("comdlg32.dll"); + + // Using approach as shown here: https://stackoverflow.com/a/10444161 + UINT len = ::GetSystemDirectoryA(nullptr, 0); + std::string sys_dir(len, '\0'); + ::GetSystemDirectoryA(&sys_dir[0], len); + + ACTCTXA act_ctx = { + // Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a + // crash with error “default context is already set”. + sizeof(act_ctx), + ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, + "shell32.dll", + 0, + 0, + sys_dir.c_str(), + (LPCSTR)124, + nullptr, + 0, + }; + + return ::CreateActCtxA(&act_ctx); +} +#endif // _WIN32 + +// dialog implementation + +inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const { + return m_async->ready(timeout); +} + +inline bool internal::dialog::kill() const { + return m_async->kill(); +} + +inline internal::dialog::dialog() : m_async(std::make_shared()) { +} + +inline std::vector internal::dialog::desktop_helper() const { +#if __APPLE__ + return { "osascript" }; +#else + return { flags(flag::has_zenity) ? "zenity" + : flags(flag::has_matedialog) ? "matedialog" + : flags(flag::has_qarma) ? "qarma" + : flags(flag::has_kdialog) ? "kdialog" + : "echo" }; +#endif +} + +inline std::string internal::dialog::buttons_to_name(choice _choice) { + switch (_choice) { + case choice::ok_cancel: + return "okcancel"; + case choice::yes_no: + return "yesno"; + case choice::yes_no_cancel: + return "yesnocancel"; + case choice::retry_cancel: + return "retrycancel"; + case choice::abort_retry_ignore: + return "abortretryignore"; + /* case choice::ok: */ default: + return "ok"; + } +} + +inline std::string internal::dialog::get_icon_name(icon _icon) { + switch (_icon) { + case icon::warning: + return "warning"; + case icon::error: + return "error"; + case icon::question: + return "question"; + // Zenity wants "information" but WinForms wants "info" + /* case icon::info: */ default: +#if _WIN32 + return "info"; +#else + return "information"; +#endif + } +} + +// This is only used for debugging purposes +inline std::ostream& operator<<(std::ostream& s, std::vector const& v) { + int not_first = 0; + for (auto& e : v) + s << (not_first++ ? " " : "") << e; + return s; +} + +// Properly quote a string for Powershell: replace ' or " with '' or "" +// FIXME: we should probably get rid of newlines! +// FIXME: the \" sequence seems unsafe, too! +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::powershell_quote(std::string const& str) const { + return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'"; +} + +// Properly quote a string for osascript: replace \ or " with \\ or \" +// XXX: this also used to replace ' with \' when popen was used, but it would be +// smarter to do shell_quote(osascript_quote(...)) if this is needed again. +inline std::string internal::dialog::osascript_quote(std::string const& str) const { + return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\""; +} + +// Properly quote a string for the shell: just replace ' with '\'' +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::shell_quote(std::string const& str) const { + return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; +} + +// file_dialog implementation + +inline internal::file_dialog::file_dialog(type in_type, std::string const& title, + std::string const& default_path /* = "" */, + std::vector const& filters /* = {} */, + opt options /* = opt::none */) { +#if _WIN32 + std::string filter_list; + std::regex whitespace(" *"); + for (size_t i = 0; i + 1 < filters.size(); i += 2) { + filter_list += filters[i] + '\0'; + filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0'; + } + filter_list += '\0'; + + m_async->start_func([this, in_type, title, default_path, filter_list, options](int* exit_code) -> std::string { + (void)exit_code; + m_wtitle = internal::str2wstr(title); + m_wdefault_path = internal::str2wstr(default_path); + auto wfilter_list = internal::str2wstr(filter_list); + + // Initialise COM. This is required for the new folder selection window, + // (see https://github.com/samhocevar/portable-file-dialogs/pull/21) + // and to avoid random crashes with GetOpenFileNameW() (see + // https://github.com/samhocevar/portable-file-dialogs/issues/51) + ole32_dll ole32; + + // Folder selection uses a different method + if (in_type == type::folder) { +#if PFD_HAS_IFILEDIALOG + if (flags(flag::is_vista)) { + // On Vista and higher we should be able to use IFileDialog for folder selection + IFileDialog* ifd; + HRESULT hr = dll::proc( + ole32, "CoCreateInstance")(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd)); + + // In case CoCreateInstance fails (which it should not), try legacy approach + if (SUCCEEDED(hr)) + return select_folder_vista(ifd, options & opt::force_path); + } +#endif + + BROWSEINFOW bi; + memset(&bi, 0, sizeof(bi)); + + bi.lpfn = &bffcallback; + bi.lParam = (LPARAM)this; + + if (flags(flag::is_vista)) { + if (ole32.is_initialized()) + bi.ulFlags |= BIF_NEWDIALOGSTYLE; + bi.ulFlags |= BIF_EDITBOX; + bi.ulFlags |= BIF_STATUSTEXT; + } + + auto* list = SHBrowseForFolderW(&bi); + std::string ret; + if (list) { + auto buffer = new wchar_t[MAX_PATH]; + SHGetPathFromIDListW(list, buffer); + dll::proc(ole32, "CoTaskMemFree")(list); + ret = internal::wstr2str(buffer); + delete[] buffer; + } + return ret; + } + + OPENFILENAMEW ofn; + memset(&ofn, 0, sizeof(ofn)); + ofn.lStructSize = sizeof(OPENFILENAMEW); + ofn.hwndOwner = GetActiveWindow(); + + ofn.lpstrFilter = wfilter_list.c_str(); + + auto woutput = std::wstring(MAX_PATH * 256, L'\0'); + ofn.lpstrFile = (LPWSTR)woutput.data(); + ofn.nMaxFile = (DWORD)woutput.size(); + if (!m_wdefault_path.empty()) { + // If a directory was provided, use it as the initial directory. If + // a valid path was provided, use it as the initial file. Otherwise, + // let the Windows API decide. + auto path_attr = GetFileAttributesW(m_wdefault_path.c_str()); + if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY)) + ofn.lpstrInitialDir = m_wdefault_path.c_str(); + else if (m_wdefault_path.size() <= woutput.size()) + // second argument is size of buffer, not length of string + StringCchCopyW(ofn.lpstrFile, MAX_PATH * 256 + 1, m_wdefault_path.c_str()); + else { + ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data(); + ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size(); + } + } + ofn.lpstrTitle = m_wtitle.c_str(); + ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER; + + dll comdlg32("comdlg32.dll"); + + // Apply new visual style (required for windows XP) + new_style_context ctx; + + if (in_type == type::save) { + if (!(options & opt::force_overwrite)) + ofn.Flags |= OFN_OVERWRITEPROMPT; + + dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); + if (get_save_file_name(&ofn) == 0) + return ""; + return internal::wstr2str(woutput.c_str()); + } else { + if (options & opt::multiselect) + ofn.Flags |= OFN_ALLOWMULTISELECT; + ofn.Flags |= OFN_PATHMUSTEXIST; + + dll::proc get_open_file_name(comdlg32, "GetOpenFileNameW"); + if (get_open_file_name(&ofn) == 0) + return ""; + } + + std::string prefix; + for (wchar_t const* p = woutput.c_str(); *p;) { + auto filename = internal::wstr2str(p); + p += wcslen(p); + // In multiselect mode, we advance p one wchar further and + // check for another filename. If there is one and the + // prefix is empty, it means we just read the prefix. + if ((options & opt::multiselect) && *++p && prefix.empty()) { + prefix = filename + "/"; + continue; + } + + m_vector_result.push_back(prefix + filename); + } + + return ""; + }); +#elif __EMSCRIPTEN__ + // FIXME: do something + (void)in_type; + (void)title; + (void)default_path; + (void)filters; + (void)options; +#else + auto command = desktop_helper(); + + if (is_osascript()) { + std::string script = "set ret to choose"; + switch (in_type) { + case type::save: + script += " file name"; + break; + case type::open: + default: + script += " file"; + if (options & opt::multiselect) + script += " with multiple selections allowed"; + break; + case type::folder: + script += " folder"; + break; + } + + if (default_path.size()) { + if (in_type == type::folder || is_directory(default_path)) + script += " default location "; + else + script += " default name "; + script += osascript_quote(default_path); + } + + script += " with prompt " + osascript_quote(title); + + if (in_type == type::open) { + // Concatenate all user-provided filter patterns + std::string patterns; + for (size_t i = 0; i < filters.size() / 2; ++i) + patterns += " " + filters[2 * i + 1]; + + // Split the pattern list to check whether "*" is in there; if it + // is, we have to disable filters because there is no mechanism in + // OS X for the user to override the filter. + std::regex sep("\\s+"); + std::string filter_list; + bool has_filter = true; + std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1); + std::sregex_token_iterator end; + for (; iter != end; ++iter) { + auto pat = iter->str(); + if (pat == "*" || pat == "*.*") + has_filter = false; + else if (internal::starts_with(pat, "*.")) + filter_list += "," + osascript_quote(pat.substr(2, pat.size() - 2)); + } + + if (has_filter && filter_list.size() > 0) { + // There is a weird AppleScript bug where file extensions of length != 3 are + // ignored, e.g. type{"txt"} works, but type{"json"} does not. Fortunately if + // the whole list starts with a 3-character extension, everything works again. + // We use "///" for such an extension because we are sure it cannot appear in + // an actual filename. + script += " of type {\"///\"" + filter_list + "}"; + } + } + + if (in_type == type::open && (options & opt::multiselect)) { + script += "\nset s to \"\""; + script += "\nrepeat with i in ret"; + script += "\n set s to s & (POSIX path of i) & \"\\n\""; + script += "\nend repeat"; + script += "\ncopy s to stdout"; + } else { + script += "\nPOSIX path of ret"; + } + + command.push_back("-e"); + command.push_back(script); + } else if (is_zenity()) { + command.push_back("--file-selection"); + + // If the default path is a directory, make sure it ends with "/" otherwise zenity will + // open the file dialog in the parent directory. + auto filename_arg = "--filename=" + default_path; + if (in_type != type::folder && !ends_with(default_path, "/") && internal::is_directory(default_path)) + filename_arg += "/"; + command.push_back(filename_arg); + + command.push_back("--title"); + command.push_back(title); + command.push_back("--separator=\n"); + + for (size_t i = 0; i < filters.size() / 2; ++i) { + command.push_back("--file-filter"); + command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]); + } + + if (in_type == type::save) + command.push_back("--save"); + if (in_type == type::folder) + command.push_back("--directory"); + if (!(options & opt::force_overwrite)) + command.push_back("--confirm-overwrite"); + if (options & opt::multiselect) + command.push_back("--multiple"); + } else if (is_kdialog()) { + switch (in_type) { + case type::save: + command.push_back("--getsavefilename"); + break; + case type::open: + command.push_back("--getopenfilename"); + break; + case type::folder: + command.push_back("--getexistingdirectory"); + break; + } + if (options & opt::multiselect) { + command.push_back("--multiple"); + command.push_back("--separate-output"); + } + + command.push_back(default_path); + + std::string filter; + for (size_t i = 0; i < filters.size() / 2; ++i) + filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")"; + command.push_back(filter); + + command.push_back("--title"); + command.push_back(title); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline std::string internal::file_dialog::string_result() { +#if _WIN32 + return m_async->result(); +#else + auto ret = m_async->result(); + // Strip potential trailing newline (zenity). Also strip trailing slash + // added by osascript for consistency with other backends. + while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/')) + ret.pop_back(); + return ret; +#endif +} + +inline std::vector internal::file_dialog::vector_result() { +#if _WIN32 + m_async->result(); + return m_vector_result; +#else + std::vector ret; + auto result = m_async->result(); + for (;;) { + // Split result along newline characters + auto i = result.find('\n'); + if (i == 0 || i == std::string::npos) + break; + ret.push_back(result.substr(0, i)); + result = result.substr(i + 1, result.size()); + } + return ret; +#endif +} + +#if _WIN32 +// Use a static function to pass as BFFCALLBACK for legacy folder select +inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData) { + auto inst = (file_dialog*)pData; + switch (uMsg) { + case BFFM_INITIALIZED: + SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str()); + break; + } + return 0; +} + +#if PFD_HAS_IFILEDIALOG +inline std::string internal::file_dialog::select_folder_vista(IFileDialog* ifd, bool force_path) { + std::string result; + + IShellItem* folder; + + // Load library at runtime so app doesn't link it at load time (which will fail on windows XP) + dll shell32("shell32.dll"); + dll::proc create_item(shell32, "SHCreateItemFromParsingName"); + + if (!create_item) + return ""; + + auto hr = create_item(m_wdefault_path.c_str(), nullptr, IID_PPV_ARGS(&folder)); + + // Set default folder if found. This only sets the default folder. If + // Windows has any info about the most recently selected folder, it + // will display it instead. Generally, calling SetFolder() to set the + // current directory “is not a good or expected user experience and + // should therefore be avoided”: + // https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder + if (SUCCEEDED(hr)) { + if (force_path) + ifd->SetFolder(folder); + else + ifd->SetDefaultFolder(folder); + folder->Release(); + } + + // Set the dialog title and option to select folders + ifd->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM); + ifd->SetTitle(m_wtitle.c_str()); + + hr = ifd->Show(GetActiveWindow()); + if (SUCCEEDED(hr)) { + IShellItem* item; + hr = ifd->GetResult(&item); + if (SUCCEEDED(hr)) { + wchar_t* wname = nullptr; + // This is unlikely to fail because we use FOS_FORCEFILESYSTEM, but try + // to output a debug message just in case. + if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &wname))) { + result = internal::wstr2str(std::wstring(wname)); + dll::proc(ole32_dll(), "CoTaskMemFree")(wname); + } else { + if (SUCCEEDED(item->GetDisplayName(SIGDN_NORMALDISPLAY, &wname))) { + auto name = internal::wstr2str(std::wstring(wname)); + dll::proc(ole32_dll(), "CoTaskMemFree")(wname); + std::cerr << "pfd: failed to get path for " << name << std::endl; + } else + std::cerr << "pfd: item of unknown type selected" << std::endl; + } + + item->Release(); + } + } + + ifd->Release(); + + return result; +} +#endif +#endif + +// notify implementation + +inline notify::notify(std::string const& title, std::string const& message, icon _icon /* = icon::info */) { + if (_icon == icon::question) // Not supported by notifications + _icon = icon::info; + +#if _WIN32 + // Use a static shared pointer for notify_icon so that we can delete + // it whenever we need to display a new one, and we can also wait + // until the program has finished running. + struct notify_icon_data : public NOTIFYICONDATAW { + ~notify_icon_data() { + Shell_NotifyIconW(NIM_DELETE, this); + } + }; + + static std::shared_ptr nid; + + // Release the previous notification icon, if any, and allocate a new + // one. Note that std::make_shared() does value initialization, so there + // is no need to memset the structure. + nid = nullptr; + nid = std::make_shared(); + + // For XP support + nid->cbSize = NOTIFYICONDATAW_V2_SIZE; + nid->hWnd = nullptr; + nid->uID = 0; + + // Flag Description: + // - NIF_ICON The hIcon member is valid. + // - NIF_MESSAGE The uCallbackMessage member is valid. + // - NIF_TIP The szTip member is valid. + // - NIF_STATE The dwState and dwStateMask members are valid. + // - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and + // dwInfoFlags members are valid. + // - NIF_GUID Reserved. + nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO; + + // Flag Description + // - NIIF_ERROR An error icon. + // - NIIF_INFO An information icon. + // - NIIF_NONE No icon. + // - NIIF_WARNING A warning icon. + // - NIIF_ICON_MASK Version 6.0. Reserved. + // - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips + switch (_icon) { + case icon::warning: + nid->dwInfoFlags = NIIF_WARNING; + break; + case icon::error: + nid->dwInfoFlags = NIIF_ERROR; + break; + /* case icon::info: */ default: + nid->dwInfoFlags = NIIF_INFO; + break; + } + + ENUMRESNAMEPROC icon_enum_callback = [](HMODULE, LPCTSTR, LPTSTR lpName, LONG_PTR lParam) -> BOOL { + ((NOTIFYICONDATAW*)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName); + return false; + }; + + nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); + ::EnumResourceNames(nullptr, RT_GROUP_ICON, icon_enum_callback, (LONG_PTR)nid.get()); + + nid->uTimeout = 5000; + + StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str()); + StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str()); + + // Display the new icon + Shell_NotifyIconW(NIM_ADD, nid.get()); +#elif __EMSCRIPTEN__ + // FIXME: do something + (void)title; + (void)message; +#else + auto command = desktop_helper(); + + if (is_osascript()) { + command.push_back("-e"); + command.push_back("display notification " + osascript_quote(message) + " with title " + osascript_quote(title)); + } else if (is_zenity()) { + command.push_back("--notification"); + command.push_back("--window-icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--text"); + command.push_back(title + "\n" + message); + } else if (is_kdialog()) { + command.push_back("--icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--title"); + command.push_back(title); + command.push_back("--passivepopup"); + command.push_back(message); + command.push_back("5"); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +// message implementation + +inline message::message(std::string const& title, std::string const& text, choice _choice /* = choice::ok_cancel */, + icon _icon /* = icon::info */) { +#if _WIN32 + // Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought + // to front. See https://github.com/samhocevar/portable-file-dialogs/issues/52 + UINT style = MB_SYSTEMMODAL; + switch (_icon) { + case icon::warning: + style |= MB_ICONWARNING; + break; + case icon::error: + style |= MB_ICONERROR; + break; + case icon::question: + style |= MB_ICONQUESTION; + break; + /* case icon::info: */ default: + style |= MB_ICONINFORMATION; + break; + } + + switch (_choice) { + case choice::ok_cancel: + style |= MB_OKCANCEL; + break; + case choice::yes_no: + style |= MB_YESNO; + break; + case choice::yes_no_cancel: + style |= MB_YESNOCANCEL; + break; + case choice::retry_cancel: + style |= MB_RETRYCANCEL; + break; + case choice::abort_retry_ignore: + style |= MB_ABORTRETRYIGNORE; + break; + /* case choice::ok: */ default: + style |= MB_OK; + break; + } + + m_mappings[IDCANCEL] = button::cancel; + m_mappings[IDOK] = button::ok; + m_mappings[IDYES] = button::yes; + m_mappings[IDNO] = button::no; + m_mappings[IDABORT] = button::abort; + m_mappings[IDRETRY] = button::retry; + m_mappings[IDIGNORE] = button::ignore; + + m_async->start_func([text, title, style](int* exit_code) -> std::string { + auto wtext = internal::str2wstr(text); + auto wtitle = internal::str2wstr(title); + // Apply new visual style (required for all Windows versions) + new_style_context ctx; + *exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style); + return ""; + }); + +#elif __EMSCRIPTEN__ + std::string full_message; + switch (_icon) { + case icon::warning: + full_message = "⚠️"; + break; + case icon::error: + full_message = "⛔"; + break; + case icon::question: + full_message = "❓"; + break; + /* case icon::info: */ default: + full_message = "ℹ"; + break; + } + + full_message += ' ' + title + "\n\n" + text; + + // This does not really start an async task; it just passes the + // EM_ASM_INT return value to a fake start() function. + m_async->start(EM_ASM_INT( + { + if ($1) + return window.confirm(UTF8ToString($0)) ? 0 : -1; + alert(UTF8ToString($0)); + return 0; + }, + full_message.c_str(), _choice == choice::ok_cancel)); +#else + auto command = desktop_helper(); + + if (is_osascript()) { + std::string script = "display dialog " + osascript_quote(text) + " with title " + osascript_quote(title); + auto if_cancel = button::cancel; + switch (_choice) { + case choice::ok_cancel: + script += "buttons {\"OK\", \"Cancel\"}" + " default button \"OK\"" + " cancel button \"Cancel\""; + break; + case choice::yes_no: + script += "buttons {\"Yes\", \"No\"}" + " default button \"Yes\"" + " cancel button \"No\""; + if_cancel = button::no; + break; + case choice::yes_no_cancel: + script += "buttons {\"Yes\", \"No\", \"Cancel\"}" + " default button \"Yes\"" + " cancel button \"Cancel\""; + break; + case choice::retry_cancel: + script += "buttons {\"Retry\", \"Cancel\"}" + " default button \"Retry\"" + " cancel button \"Cancel\""; + break; + case choice::abort_retry_ignore: + script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}" + " default button \"Abort\"" + " cancel button \"Retry\""; + if_cancel = button::retry; + break; + case choice::ok: + default: + script += "buttons {\"OK\"}" + " default button \"OK\"" + " cancel button \"OK\""; + if_cancel = button::ok; + break; + } + m_mappings[1] = if_cancel; + m_mappings[256] = if_cancel; // XXX: I think this was never correct + script += " with icon "; + switch (_icon) { +#define PFD_OSX_ICON(n) \ + "alias ((path to library folder from system domain) as text " \ + "& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")" + case icon::info: + default: + script += PFD_OSX_ICON("ToolBarInfo"); + break; + case icon::warning: + script += "caution"; + break; + case icon::error: + script += "stop"; + break; + case icon::question: + script += PFD_OSX_ICON("GenericQuestionMarkIcon"); + break; +#undef PFD_OSX_ICON + } + + command.push_back("-e"); + command.push_back(script); + } else if (is_zenity()) { + switch (_choice) { + case choice::ok_cancel: + command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" }); + break; + case choice::yes_no: + // Do not use standard --question because it causes “No” to return -1, + // which is inconsistent with the “Yes/No/Cancel” mode below. + command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" }); + break; + case choice::yes_no_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No", + "--extra-button=Yes" }); + break; + case choice::retry_cancel: + command.insert(command.end(), + { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" }); + break; + case choice::abort_retry_ignore: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore", + "--extra-button=Abort", "--extra-button=Retry" }); + break; + case choice::ok: + default: + switch (_icon) { + case icon::error: + command.push_back("--error"); + break; + case icon::warning: + command.push_back("--warning"); + break; + default: + command.push_back("--info"); + break; + } + } + + command.insert(command.end(), { "--title", title, "--width=300", "--height=0", // sensible defaults + "--no-markup", // do not interpret text as Pango markup + "--text", text, "--icon-name=dialog-" + get_icon_name(_icon) }); + } else if (is_kdialog()) { + if (_choice == choice::ok) { + switch (_icon) { + case icon::error: + command.push_back("--error"); + break; + case icon::warning: + command.push_back("--sorry"); + break; + default: + command.push_back("--msgbox"); + break; + } + } else { + std::string flag = "--"; + if (_icon == icon::warning || _icon == icon::error) + flag += "warning"; + flag += "yesno"; + if (_choice == choice::yes_no_cancel) + flag += "cancel"; + command.push_back(flag); + if (_choice == choice::yes_no || _choice == choice::yes_no_cancel) { + m_mappings[0] = button::yes; + m_mappings[256] = button::no; + } + } + + command.push_back(text); + command.push_back("--title"); + command.push_back(title); + + // Must be after the above part + if (_choice == choice::ok_cancel) + command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" }); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline button message::result() { + int exit_code; + auto ret = m_async->result(&exit_code); + // osascript will say "button returned:Cancel\n" + // and others will just say "Cancel\n" + if (internal::ends_with(ret, "Cancel\n")) + return button::cancel; + if (internal::ends_with(ret, "OK\n")) + return button::ok; + if (internal::ends_with(ret, "Yes\n")) + return button::yes; + if (internal::ends_with(ret, "No\n")) + return button::no; + if (internal::ends_with(ret, "Abort\n")) + return button::abort; + if (internal::ends_with(ret, "Retry\n")) + return button::retry; + if (internal::ends_with(ret, "Ignore\n")) + return button::ignore; + if (m_mappings.count(exit_code) != 0) + return m_mappings[exit_code]; + return exit_code == 0 ? button::ok : button::cancel; +} + +// open_file implementation + +inline open_file::open_file(std::string const& title, std::string const& default_path /* = "" */, + std::vector const& filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::open, title, default_path, filters, options) { +} + +inline open_file::open_file(std::string const& title, std::string const& default_path, + std::vector const& filters, bool allow_multiselect) + : open_file(title, default_path, filters, (allow_multiselect ? opt::multiselect : opt::none)) { +} + +inline std::vector open_file::result() { + return vector_result(); +} + +// save_file implementation + +inline save_file::save_file(std::string const& title, std::string const& default_path /* = "" */, + std::vector const& filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::save, title, default_path, filters, options) { +} + +inline save_file::save_file(std::string const& title, std::string const& default_path, + std::vector const& filters, bool confirm_overwrite) + : save_file(title, default_path, filters, (confirm_overwrite ? opt::none : opt::force_overwrite)) { +} + +inline std::string save_file::result() { + return string_result(); +} + +// select_folder implementation + +inline select_folder::select_folder(std::string const& title, std::string const& default_path /* = "" */, + opt options /* = opt::none */) + : file_dialog(type::folder, title, default_path, {}, options) { +} + +inline std::string select_folder::result() { + return string_result(); +} + +#endif // PFD_SKIP_IMPLEMENTATION + +} // namespace pfd \ No newline at end of file diff --git a/src/port/Engine.cpp b/src/port/Engine.cpp index e514a5f3..666dfe62 100644 --- a/src/port/Engine.cpp +++ b/src/port/Engine.cpp @@ -36,6 +36,7 @@ #include #include "audio/GameAudio.h" #include "port/patches/DisplayListPatch.h" +#include "port/extractor/GameExtractor.h" // #include "sf64audio_provisional.h" #include @@ -61,32 +62,58 @@ void AudioThread_CreateNextAudioBuffer(int16_t* samples, uint32_t num_samples); GameEngine* GameEngine::Instance; static GamePool MemoryPool = { .chunk = 1024 * 512, .cursor = 0, .length = 0, .memory = nullptr }; -GameEngine::GameEngine() { - std::vector OTRFiles; - if (const std::string cube_path = Ship::Context::GetPathRelativeToAppDirectory("starship.otr"); - std::filesystem::exists(cube_path)) { - OTRFiles.push_back(cube_path); +void GameEngine::GenAssetFile() { + auto extractor = GameExtractor(); + + if (!extractor.SelectGameFromUI()) { + ShowMessage("Extractor", "No game selected."); + return; + } + if (!extractor.ValidateChecksum()) { + ShowMessage("Extractor", "Invalid checksum."); + return; + } + + extractor.DecompressGame(); + + if (!extractor.GenerateOTR()) { + ShowMessage("Extractor", "Failed to generate OTR."); + } +} + +void GameEngine::Create() { + std::vector AssetFiles; + const auto instance = Instance = new GameEngine(); + const std::string main_otr_path = Ship::Context::GetPathRelativeToAppDirectory("sf64.otr"); + const std::string assets_path = Ship::Context::GetPathRelativeToAppDirectory("starship.otr"); + const std::string patches_path = Ship::Context::GetPathRelativeToAppDirectory("mods"); + + if (std::filesystem::exists(main_otr_path)) { + AssetFiles.push_back(main_otr_path); + } else { + GenAssetFile(); + AssetFiles.push_back(main_otr_path); } - if (const std::string sm64_otr_path = Ship::Context::GetPathRelativeToAppDirectory("sf64.otr"); - std::filesystem::exists(sm64_otr_path)) { - OTRFiles.push_back(sm64_otr_path); + + if (std::filesystem::exists(assets_path)) { + AssetFiles.push_back(assets_path); } - if (const std::string patches_path = Ship::Context::GetPathRelativeToAppDirectory("mods"); - !patches_path.empty() && std::filesystem::exists(patches_path)) { + + if (!patches_path.empty() && std::filesystem::exists(patches_path)) { if (std::filesystem::is_directory(patches_path)) { for (const auto& p : std::filesystem::recursive_directory_iterator(patches_path)) { const auto ext = p.path().extension().string(); if (StringHelper::IEquals(ext, ".otr") || StringHelper::IEquals(ext, ".o2r")) { - OTRFiles.push_back(p.path().generic_string()); + AssetFiles.push_back(p.path().generic_string()); } } } } - this->context = - Ship::Context::CreateInstance("Starship", "ship", "starship.cfg.json", OTRFiles, {}, 3, { 44100, 1024*2, 2480*2 }); + instance->context = + Ship::Context::CreateInstance("Starship", "ship", "starship.cfg.json", AssetFiles, {}, 3, { 44100, 1024*2, 2480*2 }); - auto loader = context->GetResourceManager()->GetResourceLoader(); + auto loader = instance->context->GetResourceManager()->GetResourceLoader(); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Animation", static_cast(SF64::ResourceType::AnimData), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, @@ -164,11 +191,8 @@ GameEngine::GameEngine() { loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "SoundFont", static_cast(SF64::ResourceType::SoundFont), 0); -} -void GameEngine::Create() { - const auto instance = Instance = new GameEngine(); - instance->AudioInit(); + GameEngine::AudioInit(); DisplayListPatch::Run(); GameUI::SetupGuiElements(); #if defined(__SWITCH__) || defined(__WIIU__) @@ -386,6 +410,15 @@ uint32_t GameEngine::GetInterpolationFPS() { CVarGetInteger("gInterpolationFPS", 20)); } +void GameEngine::ShowMessage(const char* title, const char* message) { +#if defined(__SWITCH__) + SPDLOG_ERROR(message); +#else + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, message, nullptr); + SPDLOG_ERROR(message); +#endif +} + extern "C" uint32_t GameEngine_GetSampleRate() { auto player = Ship::Context::GetInstance()->GetAudio()->GetAudioPlayer(); if (player == nullptr) { diff --git a/src/port/Engine.h b/src/port/Engine.h index 29b8f3a5..2c9448cb 100644 --- a/src/port/Engine.h +++ b/src/port/Engine.h @@ -15,28 +15,26 @@ struct GamePool { #include #include "libultraship/src/Context.h" - - class GameEngine { public: static GameEngine* Instance; std::shared_ptr context; - GameEngine(); + GameEngine(){}; + static void GenAssetFile(); static void Create(); void StartFrame() const; static void HandleAudioThread(); - static void ProcessAudioTask(s16* audio_buffer); static void StartAudioFrame(); static void EndAudioFrame(); static void AudioInit(); static void AudioExit(); static void RunCommands(Gfx* Commands, const std::vector>& mtx_replacements); - void ProcessFrame(void (*run_one_game_iter)()) const; static void Destroy(); static void ProcessGfxCommands(Gfx* commands); static uint32_t GetInterpolationFPS(); + static void ShowMessage(const char* title, const char* message); }; extern "C" void* GameEngine_Malloc(size_t size); diff --git a/src/port/extractor/GameExtractor.cpp b/src/port/extractor/GameExtractor.cpp new file mode 100644 index 00000000..89ae79d6 --- /dev/null +++ b/src/port/extractor/GameExtractor.cpp @@ -0,0 +1,62 @@ +#include "GameExtractor.h" + +#include + +#include "Context.h" +#include "spdlog/spdlog.h" +#include "portable-file-dialogs.h" +#include + +std::unordered_map mGameList = { + { "f7475fb11e7e6830f82883412638e8390791ab87", { "Star Fox 64 (U) (V1.1)", false } }, + { "09f0d105f476b00efa5303a3ebc42e60a7753b7a", { "Star Fox 64 (U) (V1.1)", true } } +}; + +bool GameExtractor::SelectGameFromUI() { +#if !defined(__IOS__) || !defined(__ANDROID__) || !defined(__SWITCH__) + auto selection = pfd::open_file("Select a file", ".", { "N64 Roms", "*.z64" }).result(); + + if (selection.empty()) { + return false; + } + + this->mGamePath = selection[0]; +#else + this->mGamePath = Ship::Context::GetPathRelativeToAppDirectory("baserom.us.rev1.z64"); +#endif + + std::ifstream file(this->mGamePath, std::ios::binary); + this->mGameData = std::vector( std::istreambuf_iterator( file ), {} ); + file.close(); + return true; +} + +std::optional GameExtractor::ValidateChecksum() const { + const auto rom = new N64::Cartridge(this->mGameData); + rom->Initialize(); + return mGameList[rom->GetHash()]; +} + +void GameExtractor::DecompressGame() const { + auto game = this->ValidateChecksum(); + if (!game.has_value()) { + return; + } + + if (game->isCompressed) { + throw std::runtime_error("Compressed games are not supported yet."); + } +} + +bool GameExtractor::GenerateOTR() const { + Companion::Instance = new Companion(this->mGameData, true, false); + + try { + Companion::Instance->Init(ExportType::Binary); + } catch (const std::exception& e) { + GameEngine::ShowMessage("Failed to generate OTR", e.what()); + exit(1); + } + + return true; +} \ No newline at end of file diff --git a/src/port/extractor/GameExtractor.h b/src/port/extractor/GameExtractor.h new file mode 100644 index 00000000..79621d17 --- /dev/null +++ b/src/port/extractor/GameExtractor.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Companion.h" +#include +#include +#include + +struct GameEntry { + std::string name; + bool isCompressed; +}; + +class GameExtractor { + public: + std::optional ValidateChecksum() const; + bool SelectGameFromUI(); + void DecompressGame() const; + bool GenerateOTR() const; + private: + fs::path mGamePath; + std::vector mGameData; +}; \ No newline at end of file diff --git a/tools/Torch b/tools/Torch index 3bea96b1..339f9c48 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit 3bea96b12b13c64f9f2dfc76e4475ea434b996ef +Subproject commit 339f9c485a2dd1d418307ffceef1f56de5aa02ff From ebedfdbc6cb364add210cea198d11e28612479ea Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Mon, 25 Nov 2024 10:32:59 -0600 Subject: [PATCH 2/9] Bump torch and renamed external lib --- CMakeLists.txt | 8 ++++---- tools/Torch | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b054a67e..179a5d99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -536,16 +536,16 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") endif() include(ExternalProject) -ExternalProject_Add(Torch - PREFIX Torch +ExternalProject_Add(TorchExternal + PREFIX TorchExternal SOURCE_DIR ${CMAKE_SOURCE_DIR}/tools/Torch CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/Torch ) -ExternalProject_Get_Property(Torch install_dir) +ExternalProject_Get_Property(TorchExternal install_dir) set(TORCH_EXECUTABLE ${install_dir}/src/Torch-build/$/torch) add_custom_target( ExtractAssets - DEPENDS Torch + DEPENDS TorchExternal WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${TORCH_EXECUTABLE} header -o baserom.us.uncompressed.z64 COMMAND ${TORCH_EXECUTABLE} otr baserom.us.uncompressed.z64 diff --git a/tools/Torch b/tools/Torch index 339f9c48..ad2a8122 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit 339f9c485a2dd1d418307ffceef1f56de5aa02ff +Subproject commit ad2a81221bb596caeaff156848c8f5476a7a72b6 From c7bcfe8a859513cccdfae3c27ec7ef741adf14a0 Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Tue, 26 Nov 2024 14:25:26 -0600 Subject: [PATCH 3/9] Added built-in rom decompression --- src/port/extractor/CompTool.cpp | 86 ++++++++++++++++++++++++++++ src/port/extractor/CompTool.h | 18 ++++++ src/port/extractor/GameExtractor.cpp | 5 +- src/port/extractor/GameExtractor.h | 2 +- tools/Torch | 2 +- 5 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 src/port/extractor/CompTool.cpp create mode 100644 src/port/extractor/CompTool.h diff --git a/src/port/extractor/CompTool.cpp b/src/port/extractor/CompTool.cpp new file mode 100644 index 00000000..704f02b1 --- /dev/null +++ b/src/port/extractor/CompTool.cpp @@ -0,0 +1,86 @@ +#include "CompTool.h" +#include "utils/Decompressor.h" +#include "BinaryReader.h" +#include "BinaryWriter.h" +#include + +uint32_t CompTool::FindFileTable(std::vector& rom) { + uint8_t query_one[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x50, 0x00, 0x00, 0x00, 0x00 }; + uint8_t query_two[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x60, 0x00, 0x00, 0x00, 0x00 }; + + for(size_t i = 0; i < rom.size() - sizeof(query_one); i++) { + if(memcmp(rom.data() + i, query_one, sizeof(query_one)) == 0){ + return i; + } + + if(memcmp(rom.data() + i, query_two, sizeof(query_two)) == 0){ + return i; + } + } + + throw std::runtime_error("Failed to find file table"); +} + +std::vector CompTool::Decompress(std::vector rom){ + Ship::BinaryReader basefile((char*) rom.data(), rom.size()); + basefile.SetEndianness(Ship::Endianness::Big); + + Ship::BinaryWriter decompfile; + decompfile.SetEndianness(Ship::Endianness::Big); + decompfile.Write((uint8_t)0x80); + + uint32_t table = CompTool::FindFileTable(rom); + uint32_t count = 0; + while (true){ + auto entry = table + 0x10 * count; + basefile.Seek(entry, Ship::SeekOffsetType::Start); + + auto v_begin = basefile.ReadInt32(); + auto p_begin = basefile.ReadInt32(); + auto p_end = basefile.ReadInt32(); + auto comp_flag = basefile.ReadInt32(); + + auto p_size = p_end - p_begin; + auto v_size = (int32_t) 0; + DataChunk* decoded = nullptr; + + if(v_begin == 0 && p_end == 0){ + break; + } + + basefile.Seek(p_begin, Ship::SeekOffsetType::Start); + + auto bytes = new uint8_t[p_size]; + basefile.Read((char*) bytes, p_size); + + switch ((CompType) comp_flag) { + case CompType::UNCOMPRESSED: + v_size = p_size; + break; + case CompType::COMPRESSED: + decoded = Decompressor::Decode(std::vector(bytes, bytes + p_size), 0, CompressionType::MIO0, true); + bytes = decoded->data; + v_size = decoded->size; + break; + default: + throw std::runtime_error("Invalid compression flag. There may be a problem with your ROM."); + } + + decompfile.Seek(v_begin, Ship::SeekOffsetType::Start); + decompfile.Write((char*) bytes, v_size); + auto v_end = v_begin + v_size; + + decompfile.Seek(entry + 4, Ship::SeekOffsetType::Start); + decompfile.Write(v_begin); + decompfile.Write(v_end); + decompfile.Write((uint32_t) CompType::UNCOMPRESSED); + count++; + } + + decompfile.Seek(0x10, Ship::SeekOffsetType::Start); + decompfile.Write(0xA7D5F194); // CRC1 + decompfile.Write(0xFE3DF761); // CRC2 + + auto result = decompfile.ToVector(); + return { (uint8_t*) result.data(), (uint8_t*) result.data() + result.size() }; +} \ No newline at end of file diff --git a/src/port/extractor/CompTool.h b/src/port/extractor/CompTool.h new file mode 100644 index 00000000..15b7db5a --- /dev/null +++ b/src/port/extractor/CompTool.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +enum class CompType { + UNCOMPRESSED, + COMPRESSED, + UNKNOWN +}; + +class CompTool { +public: + static std::vector Decompress(std::vector rom); +private: + static uint32_t FindFileTable(std::vector& rom); +}; \ No newline at end of file diff --git a/src/port/extractor/GameExtractor.cpp b/src/port/extractor/GameExtractor.cpp index 89ae79d6..d428ca8f 100644 --- a/src/port/extractor/GameExtractor.cpp +++ b/src/port/extractor/GameExtractor.cpp @@ -3,6 +3,7 @@ #include #include "Context.h" +#include "CompTool.h" #include "spdlog/spdlog.h" #include "portable-file-dialogs.h" #include @@ -37,14 +38,14 @@ std::optional GameExtractor::ValidateChecksum() const { return mGameList[rom->GetHash()]; } -void GameExtractor::DecompressGame() const { +void GameExtractor::DecompressGame() { auto game = this->ValidateChecksum(); if (!game.has_value()) { return; } if (game->isCompressed) { - throw std::runtime_error("Compressed games are not supported yet."); + this->mGameData = CompTool::Decompress(this->mGameData); } } diff --git a/src/port/extractor/GameExtractor.h b/src/port/extractor/GameExtractor.h index 79621d17..377b7493 100644 --- a/src/port/extractor/GameExtractor.h +++ b/src/port/extractor/GameExtractor.h @@ -14,7 +14,7 @@ class GameExtractor { public: std::optional ValidateChecksum() const; bool SelectGameFromUI(); - void DecompressGame() const; + void DecompressGame(); bool GenerateOTR() const; private: fs::path mGamePath; diff --git a/tools/Torch b/tools/Torch index ad2a8122..65ed1fac 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit ad2a81221bb596caeaff156848c8f5476a7a72b6 +Subproject commit 65ed1faca66325606d1a2120d93a9814cf6d6361 From 2728f23da25df99a0b38c87b38e29a969df9375c Mon Sep 17 00:00:00 2001 From: Malkierian Date: Wed, 4 Dec 2024 15:02:29 -0700 Subject: [PATCH 4/9] Fix torchlib inclusion. --- CMakeLists.txt | 68 +++++++++++++++++++++++++++++++------------------- tools/Torch | 2 +- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 179a5d99..9a923fcf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,8 @@ set(CMAKE_C_STANDARD 11 CACHE STRING "The C standard to use") # Add a custom module path to locate additional CMake modules set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules/") +set(YAML_CPP_STATIC_DEFINE ON) +add_compile_definitions(YAML_CPP_STATIC_DEFINE) if (WIN32) include(cmake/automate-vcpkg.cmake) @@ -234,7 +236,6 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "INCLUDE_GAME_PRINTF;" "UNICODE;" "_UNICODE" - STORMLIB_NO_AUTO_LINK "_CRT_SECURE_NO_WARNINGS;" "_SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS;" ) @@ -253,10 +254,11 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "WIN32;" "UNICODE;" "_UNICODE" - STORMLIB_NO_AUTO_LINK "_SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS;" ) endif() + add_compile_definitions(STORMLIB_NO_AUTO_LINK) + set(STORMLIB_NO_AUTO_LINK ON) elseif (CMAKE_SYSTEM_NAME STREQUAL "CafeOS") target_compile_definitions(${PROJECT_NAME} PRIVATE "$<$:" @@ -321,10 +323,43 @@ endif() target_link_libraries(${PROJECT_NAME} PRIVATE "${ADDITIONAL_LIBRARY_DEPENDENCIES}") -set(USE_STANDALONE OFF) -set(BUILD_STORMLIB OFF) -set(BUILD_SM64 OFF) -set(BUILD_MK64 OFF) +include(ExternalProject) +ExternalProject_Add(TorchExternal + PREFIX TorchExternal + SOURCE_DIR ${CMAKE_SOURCE_DIR}/tools/Torch + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/TorchExternal +) +ExternalProject_Get_Property(TorchExternal install_dir) +set(TORCH_EXECUTABLE ${install_dir}/src/TorchExternal-build/$/torch) +add_custom_target( + ExtractAssetHeaders + DEPENDS TorchExternal + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND ${TORCH_EXECUTABLE} header -o baserom.us.uncompressed.z64 +) +add_custom_target( + GeneratePortOtr + DEPENDS TorchExternal + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND ${TORCH_EXECUTABLE} pack port starship.otr + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/starship.otr" "${CMAKE_BINARY_DIR}/starship.otr" +) +add_custom_target( + ExtractAssets + DEPENDS TorchExternal + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND ${TORCH_EXECUTABLE} header -o baserom.us.uncompressed.z64 + COMMAND ${TORCH_EXECUTABLE} otr baserom.us.uncompressed.z64 + COMMAND ${TORCH_EXECUTABLE} pack port starship.otr + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/sf64.otr" "${CMAKE_BINARY_DIR}/sf64.otr" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/starship.otr" "${CMAKE_BINARY_DIR}/starship.otr" +) + +option(USE_STANDALONE "Build as a standalone executable" OFF) +option(BUILD_STORMLIB "Build with StormLib support" OFF) + +option(BUILD_SM64 "Build with Super Mario 64 support" OFF) +option(BUILD_MK64 "Build with Mario Kart 64 support" OFF) add_subdirectory(tools/Torch) target_link_libraries(${PROJECT_NAME} PRIVATE torch) @@ -533,23 +568,4 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") -Wl,-export-dynamic ) endif() -endif() - -include(ExternalProject) -ExternalProject_Add(TorchExternal - PREFIX TorchExternal - SOURCE_DIR ${CMAKE_SOURCE_DIR}/tools/Torch - CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/Torch -) -ExternalProject_Get_Property(TorchExternal install_dir) -set(TORCH_EXECUTABLE ${install_dir}/src/Torch-build/$/torch) -add_custom_target( - ExtractAssets - DEPENDS TorchExternal - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMAND ${TORCH_EXECUTABLE} header -o baserom.us.uncompressed.z64 - COMMAND ${TORCH_EXECUTABLE} otr baserom.us.uncompressed.z64 - COMMAND ${TORCH_EXECUTABLE} pack port starship.otr - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/sf64.otr" "${CMAKE_BINARY_DIR}/sf64.otr" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/starship.otr" "${CMAKE_BINARY_DIR}/starship.otr" -) \ No newline at end of file +endif() \ No newline at end of file diff --git a/tools/Torch b/tools/Torch index 65ed1fac..cc8e414c 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit 65ed1faca66325606d1a2120d93a9814cf6d6361 +Subproject commit cc8e414c16dc502c62c7fec3743f6134a28b4e39 From d71ddcb39e0bfaf2d25feaabce85879a59b90c44 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Wed, 4 Dec 2024 17:57:16 -0700 Subject: [PATCH 5/9] Update current Torch ref. --- tools/Torch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Torch b/tools/Torch index cc8e414c..7ae1fed7 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit cc8e414c16dc502c62c7fec3743f6134a28b4e39 +Subproject commit 7ae1fed709989c56d63791e2f6e4713f6e0868de From 1df1ec5d66b144b419f13edb6ec9f5591cc895c0 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Wed, 4 Dec 2024 19:56:22 -0700 Subject: [PATCH 6/9] Add semicolon to _UNICODE compile option items. --- CMakeLists.txt | 4 ++-- tools/Torch | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a923fcf..7d02a904 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,7 +235,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") ">" "INCLUDE_GAME_PRINTF;" "UNICODE;" - "_UNICODE" + "_UNICODE;" "_CRT_SECURE_NO_WARNINGS;" "_SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS;" ) @@ -253,7 +253,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "INCLUDE_GAME_PRINTF;" "WIN32;" "UNICODE;" - "_UNICODE" + "_UNICODE;" "_SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS;" ) endif() diff --git a/tools/Torch b/tools/Torch index 7ae1fed7..31a164ac 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit 7ae1fed709989c56d63791e2f6e4713f6e0868de +Subproject commit 31a164acc2f755be474a597e13ab8c366fe52d5a From d3d110fecc4b5fbbc311580bd23ad0694cee7f15 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Thu, 5 Dec 2024 18:17:44 -0700 Subject: [PATCH 7/9] Add copy action for assets folder and config.yml to build dir to reference for extraction with Torchlib. --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d02a904..cf0daf0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,10 @@ set(VCPKG_TRIPLET x64-windows-static) set(VCPKG_TARGET_TRIPLET x64-windows-static) vcpkg_bootstrap() vcpkg_install_packages(zlib bzip2 libzip libpng sdl2 glew glfw3 nlohmann-json tinyxml2 spdlog) +if(NOT EXISTS ${CMAKE_BINARY_DIR}/config.yml) + execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/config.yml" "${CMAKE_BINARY_DIR}/config.yml") + execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/assets/" "${CMAKE_BINARY_DIR}/assets/") +endif() endif() if (MSVC) @@ -348,7 +352,6 @@ add_custom_target( ExtractAssets DEPENDS TorchExternal WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMAND ${TORCH_EXECUTABLE} header -o baserom.us.uncompressed.z64 COMMAND ${TORCH_EXECUTABLE} otr baserom.us.uncompressed.z64 COMMAND ${TORCH_EXECUTABLE} pack port starship.otr COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/sf64.otr" "${CMAKE_BINARY_DIR}/sf64.otr" From 07bd9b68c5a896dc7595c801ad164212c245c740 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Thu, 5 Dec 2024 18:38:33 -0700 Subject: [PATCH 8/9] Update Torch ref to latest main. --- tools/Torch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Torch b/tools/Torch index 31a164ac..42906f4d 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit 31a164acc2f755be474a597e13ab8c366fe52d5a +Subproject commit 42906f4d1504939aa7f85f08a39308db250a6b14 From a0ef79997617c2f3dbe6a75433da32abd318ff1b Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Thu, 5 Dec 2024 22:08:36 -0600 Subject: [PATCH 9/9] Updated config to support compressed rom --- config.yml | 25 +++++++++++++++++++++++-- tools/Torch | 2 +- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/config.yml b/config.yml index 562fddeb..7ec7dee2 100644 --- a/config.yml +++ b/config.yml @@ -1,12 +1,21 @@ # Supported Star Fox 64 Versions: US, JP +09f0d105f476b00efa5303a3ebc42e60a7753b7a: + name: Star Fox 64 (U) (V1.1) (Compressed) + preprocess: + decompress_mio0: + method: mio0-comptool + type: decompress + target: f7475fb11e7e6830f82883412638e8390791ab87 + restart: true + f7475fb11e7e6830f82883412638e8390791ab87: name: Star Fox 64 (U) (V1.1) path: assets/yaml/us/rev1 config: gbi: F3DEX sort: OFFSET - logging: ERROR + # logging: ERROR output: binary: sf64.otr code: src/assets @@ -37,4 +46,16 @@ d064229a32cc05ab85e2381ce07744eb3ffaf530: - include/sf64mesg.h - include/sf64audio_external.h - include/sf64event.h - - include/sf64player.h \ No newline at end of file + - include/sf64player.h + +5f658e88ffa9de23cba6986a8fd3d3a90d7b4340: + name: F-ZERO X US REV 0 (v1.0) + path: test-yml + config: + gbi: F3DEX2 + sort: OFFSET + logging: ERROR + output: + code: src/assets + headers: include/assets + modding: src/assets \ No newline at end of file diff --git a/tools/Torch b/tools/Torch index 42906f4d..01d42cfe 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit 42906f4d1504939aa7f85f08a39308db250a6b14 +Subproject commit 01d42cfed0099fad00b21b60238f8500603f17ed