diff --git a/CMakeLists.txt b/CMakeLists.txt index 26f9a64f2..a1b617cad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,7 +111,7 @@ add_custom_target( # CMake versions prior to 3.17 do not have the rm command, use remove instead for older versions COMMAND ${CMAKE_COMMAND} -E $,remove,rm> -f oot.otr oot-mq.otr soh.otr COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/OTRExporter/extract_assets.py -z "$" --non-interactive - COMMAND ${CMAKE_COMMAND} -DSYSTEM_NAME=${CMAKE_SYSTEM_NAME} -DTARGET_DIR="$" -DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} -DBINARY_DIR=${CMAKE_BINARY_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/copy-existing-otrs.cmake + COMMAND ${CMAKE_COMMAND} -DSYSTEM_NAME=${CMAKE_SYSTEM_NAME} -DTARGET_DIR="$" -DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} -DBINARY_DIR=${CMAKE_BINARY_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/copy-existing-otrs.cmake WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/OTRExporter COMMENT "Running asset extraction..." DEPENDS ZAPD diff --git a/OTRExporter/OTRExporter/Main.cpp b/OTRExporter/OTRExporter/Main.cpp index ac8c60d4a..85300e6f8 100644 --- a/OTRExporter/OTRExporter/Main.cpp +++ b/OTRExporter/OTRExporter/Main.cpp @@ -107,8 +107,13 @@ static void ExporterProgramEnd() } } otrArchive = nullptr; + delete fileWriter; + files.clear(); // Add any additional files that need to be manually copied... + if (File::Exists("soh.otr")) { + return; + } const auto& lst = Directory::ListFiles("Extract"); std::shared_ptr sohOtr = Ship::Archive::CreateArchive("soh.otr", 4096); //sohOtr->AddFile("version", (uintptr_t)versionStream->ToVector().data(), versionStream->GetLength()); @@ -273,7 +278,7 @@ void AddFile(std::string fName, std::vector data) } } -static void ImportExporters() +void ImportExporters() { // In this example we set up a new exporter called "EXAMPLE". // By running ZAPD with the argument -se EXAMPLE, we tell it that we want to use this exporter for our resources. @@ -312,6 +317,3 @@ static void ImportExporters() InitVersionInfo(); } - -// When ZAPD starts up, it will automatically call the below function, which in turn sets up our exporters. -REGISTER_EXPORTER(ImportExporters); diff --git a/ZAPDTR/ZAPD/CMakeLists.txt b/ZAPDTR/ZAPD/CMakeLists.txt index 4b9110c32..0b0412c40 100644 --- a/ZAPDTR/ZAPD/CMakeLists.txt +++ b/ZAPDTR/ZAPD/CMakeLists.txt @@ -1,4 +1,4 @@ -set(PROJECT_NAME ZAPD) +set(PROJECT_NAME ZAPDLib) set(CMAKE_CXX_STANDARD 20 CACHE STRING "The C++ standard to use") #set(CMAKE_C_STANDARD 11 CACHE STRING "The C standard to use") @@ -260,10 +260,17 @@ set(ALL_FILES ################################################################################ # Target ################################################################################ -add_executable(${PROJECT_NAME} ${ALL_FILES}) + +add_library(${PROJECT_NAME} STATIC ${ALL_FILES}) + +add_executable(ZAPD ExecutableMain.cpp) +target_link_libraries(ZAPD ${PROJECT_NAME}) + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") use_props(${PROJECT_NAME} "${CMAKE_CONFIGURATION_TYPES}" "${DEFAULT_CXX_PROPS}") +use_props(ZAPD "${CMAKE_CONFIGURATION_TYPES}" "${DEFAULT_CXX_PROPS}") endif() ################################################################################ # Includes for CMake from *.props @@ -282,7 +289,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") ) endif() elseif (CMAKE_SYSTEM_NAME MATCHES "Linux|Darwin") - set_target_properties(${PROJECT_NAME} PROPERTIES + set_target_properties(ZAPD PROPERTIES OUTPUT_NAME "ZAPD.out" ) endif() @@ -290,7 +297,8 @@ endif() # MSVC runtime library ################################################################################ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") - get_property(MSVC_RUNTIME_LIBRARY_DEFAULT TARGET ${PROJECT_NAME} PROPERTY MSVC_RUNTIME_LIBRARY) +foreach(ZTarget ${PROJECT_NAME} ZAPD) + get_property(MSVC_RUNTIME_LIBRARY_DEFAULT TARGET ${ZTarget} PROPERTY MSVC_RUNTIME_LIBRARY) if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64") string(CONCAT "MSVC_RUNTIME_LIBRARY_STR" $<$: @@ -302,7 +310,8 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") $<$,$>>:${MSVC_RUNTIME_LIBRARY_DEFAULT}> ) endif() - set_target_properties(${PROJECT_NAME} PROPERTIES MSVC_RUNTIME_LIBRARY ${MSVC_RUNTIME_LIBRARY_STR}) + set_target_properties(${ZTarget} PROPERTIES MSVC_RUNTIME_LIBRARY ${MSVC_RUNTIME_LIBRARY_STR}) +endforeach() endif() ################################################################################ # Compile definitions @@ -404,7 +413,7 @@ if(MSVC) endif() if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang|AppleClang") - target_compile_options(${PROJECT_NAME} PRIVATE + target_compile_options(${PROJECT_NAME} PUBLIC -Wall -Wextra -Wno-error -Wno-unused-parameter -Wno-unused-function @@ -417,11 +426,11 @@ if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang|AppleClang") ) if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - target_link_options(${PROJECT_NAME} PRIVATE + target_link_options(${PROJECT_NAME} PUBLIC -pthread ) else() - target_link_options(${PROJECT_NAME} PRIVATE + target_link_options(${PROJECT_NAME} PUBLIC -pthread -Wl,-export-dynamic ) @@ -491,7 +500,7 @@ endif() if(CMAKE_SYSTEM_NAME MATCHES "NintendoSwitch|CafeOS") add_library(pathconf OBJECT pathconf.c) -target_link_libraries(${PROJECT_NAME} PRIVATE "${ADDITIONAL_LIBRARY_DEPENDENCIES}" $ ) +target_link_libraries(${PROJECT_NAME} PUBLIC "${ADDITIONAL_LIBRARY_DEPENDENCIES}" $ ) else() -target_link_libraries(${PROJECT_NAME} PRIVATE "${ADDITIONAL_LIBRARY_DEPENDENCIES}") +target_link_libraries(${PROJECT_NAME} PUBLIC "${ADDITIONAL_LIBRARY_DEPENDENCIES}") endif() diff --git a/ZAPDTR/ZAPD/ExecutableMain.cpp b/ZAPDTR/ZAPD/ExecutableMain.cpp new file mode 100644 index 000000000..303ea24ad --- /dev/null +++ b/ZAPDTR/ZAPD/ExecutableMain.cpp @@ -0,0 +1,5 @@ +extern "C" int zapd_main(int argc, char* argv[]); + +int main(int argc, char* argv[]) { + return zapd_main(argc, argv); +} diff --git a/ZAPDTR/ZAPD/Globals.cpp b/ZAPDTR/ZAPD/Globals.cpp index c02c8df4b..8443e4d07 100644 --- a/ZAPDTR/ZAPD/Globals.cpp +++ b/ZAPDTR/ZAPD/Globals.cpp @@ -27,11 +27,14 @@ Globals::Globals() Globals::~Globals() { - auto& exporters = GetExporterMap(); - - for (auto& it : exporters) + for (const auto& w : workerData) { - delete it.second; + delete w.second; + } + + if (rom != nullptr) + { + delete rom; } } diff --git a/ZAPDTR/ZAPD/Globals.h b/ZAPDTR/ZAPD/Globals.h index 629a5d353..c599266ef 100644 --- a/ZAPDTR/ZAPD/Globals.h +++ b/ZAPDTR/ZAPD/Globals.h @@ -67,7 +67,7 @@ public: bool buildRawTexture = false; bool onlyGenSohOtr = false; - ZRom* rom; + ZRom* rom = nullptr; std::vector files; std::vector externalFiles; std::vector segments; diff --git a/ZAPDTR/ZAPD/Main.cpp b/ZAPDTR/ZAPD/Main.cpp index 38b0f1597..486813f1e 100644 --- a/ZAPDTR/ZAPD/Main.cpp +++ b/ZAPDTR/ZAPD/Main.cpp @@ -4,9 +4,71 @@ #include "Utils/File.h" #include "Utils/Path.h" #include "WarningHandler.h" + #include "ZAnimation.h" +ZNormalAnimation nAnim(nullptr); +ZCurveAnimation cAnim(nullptr); +ZLinkAnimation lAnim(nullptr); +ZLegacyAnimation lAnim2(nullptr); + +#include "ZArray.h" +ZArray arr(nullptr); + +#include "ZAudio.h" +ZAudio audio(nullptr); + #include "ZBackground.h" +ZBackground back(nullptr); + #include "ZBlob.h" +ZBlob blob(nullptr); + +#include "ZCollision.h" +ZCollisionHeader colHeader(nullptr); + +#include "ZCutscene.h" +ZCutscene cs(nullptr); + +#include "ZLimb.h" +ZLimb limb(nullptr); + +#include "ZMtx.h" +ZMtx mtx(nullptr); + +#include "ZPath.h" +ZPath path(nullptr); + +#include "ZPlayerAnimationData.h" +ZPlayerAnimationData pAnimData(nullptr); + +#include "ZScalar.h" +ZScalar scalar(nullptr); + +#include "ZSkeleton.h" +ZLimbTable limbTbl(nullptr); +ZSkeleton skel(nullptr); + +#include "ZString.h" +ZString str(nullptr); + +#include "ZSymbol.h" +ZSymbol sym(nullptr); + +#include "ZText.h" +ZText txt(nullptr); + +#include "ZTexture.h" +ZTexture tex(nullptr); + +#include "ZVector.h" +ZVector vec(nullptr); + +#include "ZVtx.h" +ZVtx vtx(nullptr); + +#include "ZRoom/ZRoom.h" +ZRoom room(nullptr); + #include "ZFile.h" #include "ZTexture.h" @@ -29,29 +91,6 @@ const char gBuildHash[] = ""; // LINUX_TODO: remove, those are because of soh <-> lus dependency problems -float divisor_num = 0.0f; - -extern "C" void Audio_SetGameVolume(int player_id, float volume) -{ - -} - - -extern "C" int ResourceMgr_OTRSigCheck(char* imgData) -{ - return 0; -} - -void DebugConsole_SaveCVars() -{ - -} - -void DebugConsole_LoadCVars() -{ - -} - bool Parse(const fs::path& xmlFilePath, const fs::path& basePath, const fs::path& outPath, ZFileMode fileMode, int workerID); @@ -119,7 +158,9 @@ void ErrorHandler(int sig) } #endif -int main(int argc, char* argv[]) +extern void ImportExporters(); + +extern "C" int zapd_main(int argc, char* argv[]) { // Syntax: ZAPD.out [mode (btex/bovl/e)] (Arbritrary Number of Arguments) @@ -242,6 +283,7 @@ int main(int argc, char* argv[]) } else if (arg == "-se" || arg == "--set-exporter") // Set Current Exporter { + ImportExporters(); Globals::Instance->currentExporter = argv[++i]; } else if (arg == "--gcc-compat") // GCC compatibility @@ -434,6 +476,9 @@ int main(int argc, char* argv[]) exporterSet->endProgramFunc(); end: + delete exporterSet; + + //Globals::Instance->GetExporterSet() = nullptr; //TODO NULL this out. Compiler complains about lvalue assignment. delete g; return 0; diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index 1a873fbf8..5ad431978 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -96,6 +96,10 @@ if (NOT TARGET ZAPDUtils) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../ZAPDTR/ZAPDUtils ${CMAKE_BINARY_DIR}/ZAPDUtils) endif() +if (NOT TARGET ZAPDLib) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../ZAPDTR/ZAPD ${CMAKE_BINARY_DIR}/ZAPD) +endif() + set(PROJECT_NAME soh) ################################################################################ @@ -176,6 +180,23 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") endif() # }}} +if(NOT CMAKE_SYSTEM_NAME MATCHES "NintendoSwitch|CafeOS") + # soh/Extractor {{{ + file(GLOB_RECURSE soh__Extractor RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + "soh/Extractor/*.c" + "soh/Extractor/*.cpp" + "soh/Extractor/*.h" + "soh/Extractor/*.hpp" + ) + # }}} +else() + file(GLOB_RECURSE soh__Extractor RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + "soh/Extractor/*.h" + "soh/Extractor/*.hpp" + ) +# }}} +endif() + # soh/resource {{{ file(GLOB_RECURSE soh__Resource RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "soh/resource/*.cpp" "soh/resource/*.h") @@ -224,6 +245,7 @@ set(ALL_FILES ${Header_Files__include} ${soh__} ${soh__Enhancements} + ${soh__Extractor} ${soh__Resource} ${src__} ) @@ -477,7 +499,7 @@ if(MSVC) /FORCE:MULTIPLE > /DEBUG; - /SUBSYSTEM:WINDOWS + /SUBSYSTEM:WINDOWS ) elseif("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "Win32") target_link_options(${PROJECT_NAME} PRIVATE @@ -599,6 +621,17 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") COMMAND $ copy /b $build.c +,, ) endif() + +if(NOT CMAKE_SYSTEM_NAME MATCHES "NintendoSwitch|CafeOS") + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMENT "Copying asset xmls..." + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/OTRGui/assets $/assets + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/soh/assets/xml $/assets/extractor/xmls + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/OTRExporter/assets $/Extract/assets + ) +endif() ################################################################################ # Dependencies ################################################################################ @@ -606,12 +639,18 @@ add_dependencies(${PROJECT_NAME} ZAPDUtils libultraship ) +if(NOT CMAKE_SYSTEM_NAME MATCHES "NintendoSwitch|CafeOS") +add_dependencies(${PROJECT_NAME} + ZAPDLib +) +endif() if (CMAKE_SYSTEM_NAME STREQUAL "Windows") if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64") set(ADDITIONAL_LIBRARY_DEPENDENCIES "libultraship;" "ZAPDUtils;" + "ZAPDLib;" "glu32;" "SDL2::SDL2;" "SDL2::SDL2main;" @@ -626,6 +665,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") set(ADDITIONAL_LIBRARY_DEPENDENCIES "libultraship;" "ZAPDUtils;" + "ZAPDLib;" "glu32;" "SDL2::SDL2;" "SDL2::SDL2main;" @@ -651,7 +691,6 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "CafeOS") find_package(SDL2 REQUIRED) set(ADDITIONAL_LIBRARY_DEPENDENCIES "libultraship;" - "ZAPDUtils;" SDL2::SDL2-static "$<$:-Wl,--wrap=abort>" @@ -666,6 +705,7 @@ else() set(ADDITIONAL_LIBRARY_DEPENDENCIES "libultraship;" "ZAPDUtils;" + "ZAPDLib;" SDL2::SDL2 "$<$:SDL2_net::SDL2_net>" ${CMAKE_DL_LIBS} diff --git a/soh/soh/Extractor/EndianCvt.c b/soh/soh/Extractor/EndianCvt.c new file mode 100644 index 000000000..5503287d5 --- /dev/null +++ b/soh/soh/Extractor/EndianCvt.c @@ -0,0 +1,42 @@ +#include +#include + +#ifdef _MSC_VER +#define BSWAP32 _byteswap_ulong +#define BSWAP16 _byteswap_ushort +#elif __has_include() +#include +#define BSWAP32 bswap_32 +#define BSWAP16 bswap_16 +#else +#define BSWAP16(value) ((((value)&0xff) << 8) | ((value) >> 8)) + +#define BSWAP32(value) \ + (((uint32_t)BSWAP16((uint16_t)((value)&0xffff)) << 16) | (uint32_t)BSWAP16((uint16_t)((value) >> 16))) +#endif + + +#ifdef __cplusplus +extern "C" { +#endif +void RomToBigEndian(void* rom, size_t romSize) { + uint8_t firstbyte = ((uint8_t*)rom)[0]; + + switch (firstbyte) { + case 0x80: // BE + return; // Already BE, no need to swap + case 0x37: + for (size_t pos = 0; pos < (romSize / 2); pos++) { + ((uint16_t*)rom)[pos] = BSWAP16(((uint16_t*)rom)[pos]); + } + return; + case 0x40: + for (size_t pos = 0; pos < (romSize / 4); pos++) { + ((uint32_t*)rom)[pos] = BSWAP32(((uint32_t*)rom)[pos]); + } + } +} + +#ifdef __cplusplus +} +#endif diff --git a/soh/soh/Extractor/Extract.cpp b/soh/soh/Extractor/Extract.cpp new file mode 100644 index 000000000..b8031f017 --- /dev/null +++ b/soh/soh/Extractor/Extract.cpp @@ -0,0 +1,476 @@ +#ifdef _WIN32 +#include +#include +#include +#pragma comment(lib, "Shlwapi.lib") +#endif +#include "Extract.h" +#include "portable-file-dialogs.h" + +#ifdef unix +#include +#include +#include +#include +#endif + +#ifdef _MSC_VER +#define BSWAP32 _byteswap_ulong +#define BSWAP16 _byteswap_ushort +#elif __has_include() +#include +#define BSWAP32 bswap_32 +#define BSWAP16 bswap_16 +#else +#define BSWAP16(value) ((((value)&0xff) << 8) | ((value) >> 8)) + +#define BSWAP32(value) \ + (((uint32_t)BSWAP16((uint16_t)((value)&0xffff)) << 16) | (uint32_t)BSWAP16((uint16_t)((value) >> 16))) +#endif + +#if defined(_MSC_VER) +#define UNREACHABLE __assume(0) +#elif __llvm__ +#define UNREACHABLE __builtin_assume(0) +#else +#define UNREACHABLE __builtin_unreachable(); +#endif + +#include + +#include + +#include +#include +#include +#include + +extern "C" uint32_t CRC32C(unsigned char* data, size_t dataSize); +extern "C" void RomToBigEndian(void* rom, size_t romSize); + +static constexpr uint32_t OOT_PAL_GC = 0x09465AC3; +static constexpr uint32_t OOT_PAL_GC_DBG1 = 0x871E1C92; // 03-21-2002 build +static constexpr uint32_t OOT_PAL_GC_DBG2 = 0x87121EFE; // 03-13-2002 build +static constexpr uint32_t OOT_PAL_GC_MQ_DBG = 0x917D18F6; + +static const std::unordered_map verMap = { + { OOT_PAL_GC, "Pal Gamecube" }, + { OOT_PAL_GC_DBG1, "PAL Debug 1" }, + { OOT_PAL_GC_DBG2, "PAL Debug 2" }, + { OOT_PAL_GC_MQ_DBG, "PAL MQ Debug" }, +}; + +// TODO only check the first 54MB of the rom. +static constexpr std::array goodCrcs = { + 0xfa8c0555, // MQ DBG 64MB (Original overdump) + 0x8652ac4c, // MQ DBG 64MB + 0x5B8A1EB7, // MQ DBG 64MB (Empty overdump) + 0x1f731ffe, // MQ DBG 54MB + 0x044b3982, // NMQ DBG 54MB + 0xEB15D7B9, // NMQ DBG 64MB + 0xDA8E61BF, // GC PAL +}; + +enum class ButtonId : int { + YES, + NO, + FIND, +}; + + +void Extractor::ShowErrorBox(const char* title, const char* text) { +#ifdef _WIN32 + MessageBoxA(nullptr, text, title, MB_OK | MB_ICONERROR); +#else + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, text, nullptr); +#endif +} + +void Extractor::ShowSizeErrorBox() const { + std::unique_ptr boxBuffer = std::make_unique(mCurrentRomPath.size() + 100); + snprintf(boxBuffer.get(), mCurrentRomPath.size() + 100, + "The rom file %s was not a valid size. Was %zu MB, expecting 32, 54, or 64MB.", mCurrentRomPath.c_str(), + mCurRomSize / MB_BASE); + ShowErrorBox("Invalid Rom Size", boxBuffer.get()); +} + +void Extractor::ShowCrcErrorBox() const { + ShowErrorBox("Rom CRC invalid", "Rom CRC did not match the list of known good roms. Please find another."); +} + +int Extractor::ShowRomPickBox(uint32_t verCrc) const { + std::unique_ptr boxBuffer = std::make_unique(mCurrentRomPath.size() + 100); + SDL_MessageBoxData boxData = { 0 }; + SDL_MessageBoxButtonData buttons[3] = { { 0 } }; + int ret; + + buttons[0].buttonid = 0; + buttons[0].text = "Yes"; + buttons[0].flags = SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT; + buttons[1].buttonid = 1; + buttons[1].text = "No"; + buttons[1].flags = SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT; + buttons[2].buttonid = 2; + buttons[2].text = "Find ROM"; + boxData.numbuttons = 3; + boxData.flags = SDL_MESSAGEBOX_INFORMATION; + boxData.message = boxBuffer.get(); + boxData.title = "Rom Detected"; + boxData.window = nullptr; + + boxData.buttons = buttons; + snprintf(boxBuffer.get(), mCurrentRomPath.size() + 100, + "Rom detected: %s, Header CRC32: %8X. It appears to be: %s. Use this rom?", mCurrentRomPath.c_str(), + verCrc, verMap.at(verCrc)); + + SDL_ShowMessageBox(&boxData, &ret); + return ret; +} + +int Extractor::ShowYesNoBox(const char* title, const char* box) { + int ret; +#ifdef _WIN32 + ret = MessageBoxA(nullptr, box, title, MB_YESNO | MB_ICONQUESTION); +#else + SDL_MessageBoxData boxData = { 0 }; + SDL_MessageBoxButtonData buttons[2] = { { 0 } }; + + buttons[0].buttonid = IDYES; + buttons[0].text = "Yes"; + buttons[0].flags = SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT; + buttons[1].buttonid = IDNO; + buttons[1].text = "No"; + buttons[1].flags = SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT; + boxData.numbuttons = 2; + boxData.flags = SDL_MESSAGEBOX_INFORMATION; + boxData.message = box; + boxData.title = title; + boxData.buttons = buttons; + SDL_ShowMessageBox(&boxData, &ret); +#endif + return ret; +} + +void Extractor::SetRomInfo(const std::string& path) { + mCurrentRomPath = path; + mCurRomSize = GetCurRomSize(); +} + +void Extractor::GetRoms(std::vector& roms) { +#ifdef _WIN32 + WIN32_FIND_DATAA ffd; + HANDLE h = FindFirstFileA(".\\*", &ffd); + + do { + if (!(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + char* ext = PathFindExtensionA(ffd.cFileName); + + // Check for any standard N64 rom file extensions. + if ((strcmp(ext, ".z64") == 0) || (strcmp(ext, ".n64") == 0) || (strcmp(ext, ".v64") == 0)) + roms.push_back(ffd.cFileName); + } + } while (FindNextFileA(h, &ffd) != 0); + // if (h != nullptr) { + // CloseHandle(h); + //} +#elif unix + // Open the directory of the app. + DIR* d = opendir("."); + struct dirent* dir; + + if (d != NULL) { + // Go through each file in the directory + while ((dir = readdir(d)) != NULL) { + struct stat path; + + // Check if current entry is not folder + stat(dir->d_name, &path); + if (S_ISREG(path.st_mode)) { + + // Get the position of the extension character. + char* ext = strchr(dir->d_name, '.'); + if (ext != NULL && (strcmp(ext, ".z64") == 0) && (strcmp(ext, ".n64") == 0) && + (strcmp(ext, ".v64") == 0)) { + roms.push_back(dir->d_name); + } + } + } + } + closedir(d); +#else + for (const auto& file : std::filesystem::directory_iterator("./")) { + if (file.is_directory()) + continue; + if ((file.path().extension() == ".n64") || (file.path().extension() == ".z64") || + (file.path().extension() == ".v64")) { + roms.push_back((file.path())); + } + } +#endif +} + +bool Extractor::GetRomPathFromBox() { +#ifdef _WIN32 + OPENFILENAMEA box = { 0 }; + char nameBuffer[512]; + nameBuffer[0] = 0; + + box.lStructSize = sizeof(box); + box.lpstrFile = nameBuffer; + box.nMaxFile = sizeof(nameBuffer) / sizeof(nameBuffer[0]); + box.lpstrTitle = "Open Rom"; + box.Flags = OFN_NOCHANGEDIR | OFN_ENABLESIZING | OFN_FILEMUSTEXIST | OFN_LONGNAMES | OFN_PATHMUSTEXIST | OFN_HIDEREADONLY; + box.lpstrFilter = "N64 Roms\0*.z64;*.v64;*.n64\0\0"; + if (!GetOpenFileNameA(&box)) { + DWORD err = CommDlgExtendedError(); + // GetOpenFileName will return 0 but no error is set if the user just closes the box. + if (err != 0) { + const char* errStr = nullptr; + switch (err) { + case FNERR_BUFFERTOOSMALL: + errStr = "Path buffer too small. Move file closer to root of your drive"; + break; + case FNERR_INVALIDFILENAME: + errStr = "File name for rom provided is invalid."; + break; + case FNERR_SUBCLASSFAILURE: + errStr = "Failed to open a filebox because there is not enough RAM to do so."; + break; + } + MessageBoxA(nullptr, "Box Error", errStr, MB_OK | MB_ICONERROR); + return false; + } + } + // The box was closed without something being selected. + if (nameBuffer[0] == 0) { + return false; + } + mCurrentRomPath = nameBuffer; + #else + auto selection = pfd::open_file("Select a file", ".", { "N64 Roms", "*.z64 *.n64 *.v64" }).result(); + + if (selection.empty()) { + return false; + } + + mCurrentRomPath = selection[0]; + #endif + mCurRomSize = GetCurRomSize(); + return true; +} +uint32_t Extractor::GetRomVerCrc() const { + return BSWAP32(((uint32_t*)mRomData.get())[4]); +} + +size_t Extractor::GetCurRomSize() const { + return std::filesystem::file_size(mCurrentRomPath); +} + +bool Extractor::ValidateAndFixRom() { + // The MQ debug rom sometimes has the header patched to look like a US rom. Change it back + if (GetRomVerCrc() == OOT_PAL_GC_MQ_DBG) { + mRomData[0x3E] = 'P'; + } + + const uint32_t actualCrc = CRC32C(mRomData.get(), mCurRomSize); + + for (const uint32_t crc : goodCrcs) { + if (actualCrc == crc) { + return true; + } + } + return false; +} + +bool Extractor::ValidateRomSize() const { + if (mCurRomSize != MB32 && mCurRomSize != MB54 && mCurRomSize != MB64) { + return false; + } + return true; +} + +bool Extractor::ValidateRom(bool skipCrcTextBox) { + if (!ValidateRomSize()) { + ShowSizeErrorBox(); + return false; + } + if (!ValidateAndFixRom()) { + if (!skipCrcTextBox) { + ShowCrcErrorBox(); + } + return false; + } + return true; +} + +bool Extractor::Run() { + std::vector roms; + std::ifstream inFile; + uint32_t verCrc; + + GetRoms(roms); + + if (roms.empty()) { + int ret = ShowYesNoBox("No roms found", "No roms found. Look for one?"); + + switch (ret) { + case IDYES: + if (!GetRomPathFromBox()) { + ShowErrorBox("No rom selected", "No rom selected. Exiting"); + return false; + } + inFile.open(mCurrentRomPath, std::ios::in | std::ios::binary); + if (!inFile.is_open()) { + return false; // TODO Handle error + } + inFile.read((char*)mRomData.get(), mCurRomSize); + if (!ValidateRom()) { + return false; + } + break; + case IDNO: + ShowErrorBox("No rom selected", "No rom selected. Exiting"); + return false; + default: + UNREACHABLE; + break; + } + } + + for (const auto& rom : roms) { + int option; + + SetRomInfo(rom); + if (inFile.is_open()) { + inFile.close(); + } + inFile.open(rom, std::ios::in | std::ios::binary); + if (!ValidateRomSize()) { + ShowSizeErrorBox(); + continue; + } + inFile.read((char*)mRomData.get(), mCurRomSize); + RomToBigEndian(mRomData.get(), mCurRomSize); + verCrc = GetRomVerCrc(); + + // Rom doesn't claim to be valid + if (!verMap.contains(verCrc)) { + continue; + } + + option = ShowRomPickBox(verCrc); + if (option == (int)ButtonId::YES) { + if (!ValidateRom(true)) { + if (rom == roms.back()) { + ShowCrcErrorBox(); + } else { + ShowErrorBox("Rom CRC invalid", + "Rom CRC did not match the list of known good roms. Trying the next one..."); + } + continue; + } + break; + } else if (option == (int)ButtonId::FIND) { + if (!GetRomPathFromBox()) { + ShowErrorBox("No rom selected", "No Rom selected. Exiting"); + return false; + } + inFile.open(mCurrentRomPath, std::ios::in | std::ios::binary); + if (!inFile.is_open()) { + return false; // TODO Handle error + } + inFile.read((char*)mRomData.get(), mCurRomSize); + if (!ValidateRom()) { + return false; + } + break; + } else if (option == (int)ButtonId::NO) { + inFile.close(); + if (rom == roms.back()) { + ShowErrorBox("No rom provided", "No rom provided. Exiting"); + return false; + } + continue; + } + break; + } + return true; +} + +bool Extractor::IsMasterQuest() const { + switch (GetRomVerCrc()) { + case OOT_PAL_GC_MQ_DBG: + return true; + case OOT_PAL_GC: + case OOT_PAL_GC_DBG1: + return false; + default: + UNREACHABLE; + } +} + +const char* Extractor::GetZapdVerStr() const { + switch (GetRomVerCrc()) { + case OOT_PAL_GC: + return "GC_NMQ_PAL_F"; + case OOT_PAL_GC_DBG1: + return "GC_NMQ_D"; + case OOT_PAL_GC_MQ_DBG: + return "GC_MQ_D"; + default: + // We should never be in a state where this path happens. + UNREACHABLE; + break; + } +} + +extern "C" int zapd_main(int argc, char** argv); + +bool Extractor::CallZapd() { + constexpr int argc = 16; + char xmlPath[100]; + char baseromPath[100]; + char confPath[100]; + std::array argv; + const char* version = GetZapdVerStr(); + + snprintf(xmlPath, 100, "assets/extractor/xmls/%s", version); + snprintf(baseromPath, 100, "%s", mCurrentRomPath.c_str()); + snprintf(confPath, 100, "assets/extractor/Config_%s.xml", version); + + argv[0] = "ZAPD"; + argv[1] = "ed"; + argv[2] = "-i"; + argv[3] = xmlPath; + argv[4] = "-b"; + argv[5] = baseromPath; + argv[6] = "-fl"; + argv[7] = "assets/extractor/filelists"; + argv[8] = "-gsf"; + argv[9] = "1"; + argv[10] = "-rconf"; + argv[11] = confPath; + argv[12] = "-se"; + argv[13] = "OTR"; + argv[14] = "--otrfile"; + argv[15] = IsMasterQuest() ? "oot-mq.otr" : "oot.otr"; + +#ifdef _WIN32 + // Grab a handle to the command window. + HWND cmdWindow = GetConsoleWindow(); + + // Normally the command window is hidden. We want the window to be shown here so the user can see the progess of the extraction. + ShowWindow(cmdWindow, SW_SHOW); + SetWindowPos(cmdWindow, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); +#endif + + zapd_main(argc, (char**)argv.data()); + +#ifdef _WIN32 + // Hide the command window again. + ShowWindow(cmdWindow, SW_HIDE); +#endif + + return 0; +} + diff --git a/soh/soh/Extractor/Extract.h b/soh/soh/Extractor/Extract.h new file mode 100644 index 000000000..3ca23aaf3 --- /dev/null +++ b/soh/soh/Extractor/Extract.h @@ -0,0 +1,54 @@ +#ifndef EXTRACT_H +#define EXTRACT_H + +#include +#include +#include +#include + +// Values come from windows.h +#ifndef IDYES +#define IDYES 6 +#endif +#ifndef IDNO +#define IDNO 7 +#endif + +static constexpr size_t MB_BASE = 1024 * 1024; +static constexpr size_t MB32 = 32 * MB_BASE; +static constexpr size_t MB54 = 54 * MB_BASE; +static constexpr size_t MB64 = 64 * MB_BASE; + +class Extractor { + std::unique_ptr mRomData = std::make_unique(MB64); + std::string mCurrentRomPath; + size_t mCurRomSize = 0; + + bool GetRomPathFromBox(); + + uint32_t GetRomVerCrc() const; + size_t GetCurRomSize() const; + bool ValidateAndFixRom(); + bool ValidateRomSize() const; + + bool ValidateRom(bool skipCrcBox = false); + const char* GetZapdVerStr() const; + bool IsMasterQuest() const; + + void SetRomInfo(const std::string& path); + + void GetRoms(std::vector& roms); + void ShowSizeErrorBox() const; + void ShowCrcErrorBox() const; + int ShowRomPickBox(uint32_t verCrc) const; + + public: + //TODO create some kind of abstraction for message boxes. + static int ShowYesNoBox(const char* title, const char* text); + static void ShowErrorBox(const char* title, const char* text); + + bool Run(); + bool CallZapd(); + const char* GetZapdStr(); +}; +#endif diff --git a/soh/soh/Extractor/FastCrc32C.c b/soh/soh/Extractor/FastCrc32C.c new file mode 100644 index 000000000..5ec097193 --- /dev/null +++ b/soh/soh/Extractor/FastCrc32C.c @@ -0,0 +1,110 @@ +#include +#include + +#ifdef _WIN32 +#include +#elif ((defined(__GNUC__) && defined(__x86_64__) || defined(__i386__)) && defined(__SSE4_2__)) +#include +#elif defined(__aarch64__) && defined(__ARM_FEATURE_CRC32) +// Nothing cause its a compiler builtin +#else +#define USE_CRC_TABLE +#endif + +#if defined(__aarch64__) && defined(__ARM_FEATURE_CRC32) +#define INTRIN_CRC32_64(crc, value) __asm__("crc32cx %w[c], %w[c], %x[v]" : [c] "+r"(crc) : [v] "r"(value)) +#define INTRIN_CRC32_32(crc, value) __asm__("crc32cw %w[c], %w[c], %w[v]" : [c] "+r"(crc) : [v] "r"(value)) +#define INTRIN_CRC32_16(crc, value) __asm__("crc32ch %w[c], %w[c], %w[v]" : [c] "+r"(crc) : [v] "r"(value)) +#define INTRIN_CRC32_8(crc, value) __asm__("crc32cb %w[c], %w[c], %w[v]" : [c] "+r"(crc) : [v] "r"(value)) +#elif defined(__SSE4_2__) || defined(_MSC_VER) +#define INTRIN_CRC32_64(crc, data) crc = _mm_crc32_u64(crc, data) +#define INTRIN_CRC32_32(crc, data) crc = _mm_crc32_u32(crc, data) +#define INTRIN_CRC32_16(crc, data) crc = _mm_crc32_u16(crc, data) +#define INTRIN_CRC32_8(crc, data) crc = _mm_crc32_u8(crc, data) +#endif + +#ifdef USE_CRC_TABLE +static const uint32_t crc32Table[256] = { + 0x00000000L, 0xF26B8303L, 0xE13B70F7L, 0x1350F3F4L, 0xC79A971FL, 0x35F1141CL, 0x26A1E7E8L, 0xD4CA64EBL, 0x8AD958CFL, + 0x78B2DBCCL, 0x6BE22838L, 0x9989AB3BL, 0x4D43CFD0L, 0xBF284CD3L, 0xAC78BF27L, 0x5E133C24L, 0x105EC76FL, 0xE235446CL, + 0xF165B798L, 0x030E349BL, 0xD7C45070L, 0x25AFD373L, 0x36FF2087L, 0xC494A384L, 0x9A879FA0L, 0x68EC1CA3L, 0x7BBCEF57L, + 0x89D76C54L, 0x5D1D08BFL, 0xAF768BBCL, 0xBC267848L, 0x4E4DFB4BL, 0x20BD8EDEL, 0xD2D60DDDL, 0xC186FE29L, 0x33ED7D2AL, + 0xE72719C1L, 0x154C9AC2L, 0x061C6936L, 0xF477EA35L, 0xAA64D611L, 0x580F5512L, 0x4B5FA6E6L, 0xB93425E5L, 0x6DFE410EL, + 0x9F95C20DL, 0x8CC531F9L, 0x7EAEB2FAL, 0x30E349B1L, 0xC288CAB2L, 0xD1D83946L, 0x23B3BA45L, 0xF779DEAEL, 0x05125DADL, + 0x1642AE59L, 0xE4292D5AL, 0xBA3A117EL, 0x4851927DL, 0x5B016189L, 0xA96AE28AL, 0x7DA08661L, 0x8FCB0562L, 0x9C9BF696L, + 0x6EF07595L, 0x417B1DBCL, 0xB3109EBFL, 0xA0406D4BL, 0x522BEE48L, 0x86E18AA3L, 0x748A09A0L, 0x67DAFA54L, 0x95B17957L, + 0xCBA24573L, 0x39C9C670L, 0x2A993584L, 0xD8F2B687L, 0x0C38D26CL, 0xFE53516FL, 0xED03A29BL, 0x1F682198L, 0x5125DAD3L, + 0xA34E59D0L, 0xB01EAA24L, 0x42752927L, 0x96BF4DCCL, 0x64D4CECFL, 0x77843D3BL, 0x85EFBE38L, 0xDBFC821CL, 0x2997011FL, + 0x3AC7F2EBL, 0xC8AC71E8L, 0x1C661503L, 0xEE0D9600L, 0xFD5D65F4L, 0x0F36E6F7L, 0x61C69362L, 0x93AD1061L, 0x80FDE395L, + 0x72966096L, 0xA65C047DL, 0x5437877EL, 0x4767748AL, 0xB50CF789L, 0xEB1FCBADL, 0x197448AEL, 0x0A24BB5AL, 0xF84F3859L, + 0x2C855CB2L, 0xDEEEDFB1L, 0xCDBE2C45L, 0x3FD5AF46L, 0x7198540DL, 0x83F3D70EL, 0x90A324FAL, 0x62C8A7F9L, 0xB602C312L, + 0x44694011L, 0x5739B3E5L, 0xA55230E6L, 0xFB410CC2L, 0x092A8FC1L, 0x1A7A7C35L, 0xE811FF36L, 0x3CDB9BDDL, 0xCEB018DEL, + 0xDDE0EB2AL, 0x2F8B6829L, 0x82F63B78L, 0x709DB87BL, 0x63CD4B8FL, 0x91A6C88CL, 0x456CAC67L, 0xB7072F64L, 0xA457DC90L, + 0x563C5F93L, 0x082F63B7L, 0xFA44E0B4L, 0xE9141340L, 0x1B7F9043L, 0xCFB5F4A8L, 0x3DDE77ABL, 0x2E8E845FL, 0xDCE5075CL, + 0x92A8FC17L, 0x60C37F14L, 0x73938CE0L, 0x81F80FE3L, 0x55326B08L, 0xA759E80BL, 0xB4091BFFL, 0x466298FCL, 0x1871A4D8L, + 0xEA1A27DBL, 0xF94AD42FL, 0x0B21572CL, 0xDFEB33C7L, 0x2D80B0C4L, 0x3ED04330L, 0xCCBBC033L, 0xA24BB5A6L, 0x502036A5L, + 0x4370C551L, 0xB11B4652L, 0x65D122B9L, 0x97BAA1BAL, 0x84EA524EL, 0x7681D14DL, 0x2892ED69L, 0xDAF96E6AL, 0xC9A99D9EL, + 0x3BC21E9DL, 0xEF087A76L, 0x1D63F975L, 0x0E330A81L, 0xFC588982L, 0xB21572C9L, 0x407EF1CAL, 0x532E023EL, 0xA145813DL, + 0x758FE5D6L, 0x87E466D5L, 0x94B49521L, 0x66DF1622L, 0x38CC2A06L, 0xCAA7A905L, 0xD9F75AF1L, 0x2B9CD9F2L, 0xFF56BD19L, + 0x0D3D3E1AL, 0x1E6DCDEEL, 0xEC064EEDL, 0xC38D26C4L, 0x31E6A5C7L, 0x22B65633L, 0xD0DDD530L, 0x0417B1DBL, 0xF67C32D8L, + 0xE52CC12CL, 0x1747422FL, 0x49547E0BL, 0xBB3FFD08L, 0xA86F0EFCL, 0x5A048DFFL, 0x8ECEE914L, 0x7CA56A17L, 0x6FF599E3L, + 0x9D9E1AE0L, 0xD3D3E1ABL, 0x21B862A8L, 0x32E8915CL, 0xC083125FL, 0x144976B4L, 0xE622F5B7L, 0xF5720643L, 0x07198540L, + 0x590AB964L, 0xAB613A67L, 0xB831C993L, 0x4A5A4A90L, 0x9E902E7BL, 0x6CFBAD78L, 0x7FAB5E8CL, 0x8DC0DD8FL, 0xE330A81AL, + 0x115B2B19L, 0x020BD8EDL, 0xF0605BEEL, 0x24AA3F05L, 0xD6C1BC06L, 0xC5914FF2L, 0x37FACCF1L, 0x69E9F0D5L, 0x9B8273D6L, + 0x88D28022L, 0x7AB90321L, 0xAE7367CAL, 0x5C18E4C9L, 0x4F48173DL, 0xBD23943EL, 0xF36E6F75L, 0x0105EC76L, 0x12551F82L, + 0xE03E9C81L, 0x34F4F86AL, 0xC69F7B69L, 0xD5CF889DL, 0x27A40B9EL, 0x79B737BAL, 0x8BDCB4B9L, 0x988C474DL, 0x6AE7C44EL, + 0xBE2DA0A5L, 0x4C4623A6L, 0x5F16D052L, 0xAD7D5351L +}; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef USE_CRC_TABLE +uint32_t CRC32C(unsigned char* data, size_t dataSize) { + uint32_t ret = 0xFFFFFFFF; + int64_t sizeSigned = dataSize; + +#if defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__) + while ((sizeSigned -= sizeof(uint64_t)) >= 0) { + INTRIN_CRC32_64(ret, *(uint64_t*)data); + data += sizeof(uint64_t); + } + + if (sizeSigned & sizeof(uint32_t)) { + INTRIN_CRC32_32(ret, *(uint32_t*)data); + + data += sizeof(uint32_t); + } +#elif defined(_M_IX86) || defined(__i386__) + while ((sizeSigned -= sizeof(uint32_t)) >= 0) { + INTRIN_CRC32_32(ret, *(uint32_t*)data); + data += sizeof(uint32_t); + } +#endif + if (sizeSigned & sizeof(uint16_t)) { + INTRIN_CRC32_16(ret, *(uint16_t*)data); + data += sizeof(uint16_t); + } + + if (sizeSigned & sizeof(uint8_t)) { + INTRIN_CRC32_8(ret, *data); + } + + return ~ret; +} +#else +uint32_t CRC32C(const void* buf, size_t size) { + const uint8_t* p = buf; + uint32_t crc = 0xFFFFFFFF; + + while (size--) + crc = crc32Table[(crc ^ *p++) & 0xff] ^ (crc >> 8); + + return ~crc; +} +#endif +#ifdef __cplusplus +} +#endif diff --git a/soh/soh/Extractor/portable-file-dialogs.h b/soh/soh/Extractor/portable-file-dialogs.h new file mode 100644 index 000000000..1fc79a291 --- /dev/null +++ b/soh/soh/Extractor/portable-file-dialogs.h @@ -0,0 +1,1887 @@ +// +// 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 diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index bf35664f6..988810659 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -49,6 +49,10 @@ #include #include "Enhancements/custom-message/CustomMessageManager.h" +#if not defined (__SWITCH__) && not defined(__WIIU__) +#include "Extractor/Extract.h" +#endif + #include #include @@ -703,6 +707,28 @@ extern "C" void OTRExtScanner() { } extern "C" void InitOTR() { +#if not defined (__SWITCH__) && not defined(__WIIU__) + if (!std::filesystem::exists(Ship::Window::GetPathRelativeToAppDirectory("oot-mq.otr")) && + !std::filesystem::exists(Ship::Window::GetPathRelativeToAppDirectory("oot.otr"))){ + if (Extractor::ShowYesNoBox("No OTR Files", "No OTR files found. Generate one now?") == IDYES) { + Extractor extract; + if (!extract.Run()) { + Extractor::ShowErrorBox("Error", "An error occured, no OTR file was generated. Exiting..."); + exit(1); + } + extract.CallZapd(); + } + if (Extractor::ShowYesNoBox("Extraction Complete", "ROM Extracted. Extract another?") == IDYES) { + Extractor extract; + if (!extract.Run()) { + Extractor::ShowErrorBox("Error", "An error occured, an OTR file may have been generated by a different step. Continuing..."); + } else { + extract.CallZapd(); + } + } + } +#endif + #ifdef __SWITCH__ Ship::Switch::Init(Ship::PreInitPhase); #elif defined(__WIIU__) diff --git a/soh/src/code/main.c b/soh/src/code/main.c index 08e9ecdc1..7ecff0b63 100644 --- a/soh/src/code/main.c +++ b/soh/src/code/main.c @@ -10,7 +10,6 @@ #include #include "soh/CrashHandlerExp.h" - s32 gScreenWidth = SCREEN_WIDTH; s32 gScreenHeight = SCREEN_HEIGHT; size_t gSystemHeapSize = 0; @@ -44,11 +43,21 @@ void Main_LogSystemHeap(void) { } #ifdef _WIN32 -int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow) -#else -int main(int argc, char** argv) -#endif +int SDL_main(int argc, char** argv) { + AllocConsole(); + (void)freopen("CONIN$", "r", stdin); + (void)freopen("CONOUT$", "w", stdout); + (void)freopen("CONOUT$", "w", stderr); +#ifndef _DEBUG + ShowWindow(GetConsoleWindow(), SW_HIDE); +#endif + +#else //_WIN32 +int main(int argc, char** argv) +{ +#endif + GameConsole_Init(); InitOTR(); // TODO: Was moved to below InitOTR because it requires window to be setup. But will be late to catch crashes.