diff --git a/README.md b/README.md index 30da5db29c..5e7412e6aa 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d - Extract the .zip file - Launch Dusk -- Press **Select Disc Image**, navigate to your game dump, and select the file -- Press **Start Game** to play! +- Press **Select Disc Image** and provide the path to your supported game dump. +- Press **Play**! # Building diff --git a/files.cmake b/files.cmake index a463ce5129..fe7947eaab 100644 --- a/files.cmake +++ b/files.cmake @@ -1447,8 +1447,6 @@ set(DUSK_FILES src/dusk/imgui/ImGuiBloomWindow.hpp src/dusk/imgui/ImGuiMenuTools.cpp src/dusk/imgui/ImGuiMenuTools.hpp - src/dusk/imgui/ImGuiPreLaunchWindow.cpp - src/dusk/imgui/ImGuiPreLaunchWindow.hpp src/dusk/imgui/ImGuiProcessOverlay.cpp src/dusk/imgui/ImGuiCameraOverlay.cpp src/dusk/imgui/ImGuiHeapOverlay.cpp @@ -1459,6 +1457,8 @@ set(DUSK_FILES src/dusk/imgui/ImGuiSaveEditor.cpp src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStateShare.cpp + src/dusk/ui/achievements.cpp + src/dusk/ui/achievements.hpp src/dusk/ui/bool_button.cpp src/dusk/ui/bool_button.hpp src/dusk/ui/button.cpp @@ -1469,16 +1469,14 @@ set(DUSK_FILES src/dusk/ui/controller_config.hpp src/dusk/ui/document.cpp src/dusk/ui/document.hpp - src/dusk/ui/achievements.cpp - src/dusk/ui/achievements.hpp - src/dusk/ui/preset.cpp - src/dusk/ui/preset.hpp src/dusk/ui/editor.cpp src/dusk/ui/editor.hpp src/dusk/ui/event.cpp src/dusk/ui/event.hpp src/dusk/ui/input.cpp src/dusk/ui/input.hpp + src/dusk/ui/modal.cpp + src/dusk/ui/modal.hpp src/dusk/ui/nav_types.hpp src/dusk/ui/number_button.cpp src/dusk/ui/number_button.hpp @@ -1492,6 +1490,8 @@ set(DUSK_FILES src/dusk/ui/prelaunch.hpp src/dusk/ui/prelaunch_options.cpp src/dusk/ui/prelaunch_options.hpp + src/dusk/ui/preset.cpp + src/dusk/ui/preset.hpp src/dusk/ui/select_button.cpp src/dusk/ui/select_button.hpp src/dusk/ui/settings.cpp diff --git a/include/dusk/main.h b/include/dusk/main.h index 065f507d36..d6b9c9927f 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -1,6 +1,10 @@ #ifndef DUSK_MAIN_H #define DUSK_MAIN_H +#if defined(__APPLE__) +#include +#endif + #include namespace dusk { @@ -8,7 +12,17 @@ namespace dusk { extern bool IsShuttingDown; extern bool IsGameLaunched; extern bool IsFocusPaused; + extern bool RestartRequested; extern std::filesystem::path ConfigPath; + +#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ + (defined(TARGET_OS_TV) && TARGET_OS_TV) + inline constexpr bool SupportsProcessRestart = false; +#else + inline constexpr bool SupportsProcessRestart = true; +#endif + + void RequestRestart() noexcept; } #endif // DUSK_MAIN_H diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss index 9556e74ef2..1c217e466c 100644 --- a/res/rml/prelaunch.rcss +++ b/res/rml/prelaunch.rcss @@ -9,17 +9,44 @@ body { font-weight: normal; font-size: 20dp; color: #FFFFFF; - background-color: #000000; - decorator: image(../prelaunch-bg.png cover left center); filter: opacity(0); transition: filter 1s 0.2s linear-in-out; z-index: -1; } +.gradient { + position: absolute; + width: 100%; + height: 100%; + /* The color gradient from the Figma bands really badly. A fully black gradient does as well, but not as badly. */ + decorator: horizontal-gradient(#000000FF #00000000); +} + +body.mirrored .gradient { + decorator: horizontal-gradient(#00000000 #000000FF); +} + +.background { + position: absolute; + width: 100%; + height: 100%; + decorator: image(../prelaunch-bg.png cover left center); + opacity: 0; + transition: opacity 1s linear-in-out; +} + body[open] { filter: opacity(1); } +body[open] .background { + opacity: 1; +} + +body.disc-ready .background { + opacity: 0; +} + content { display: block; width: 100%; @@ -35,6 +62,7 @@ content[open] { menu { position: absolute; left: 96dp; + right: auto; top: 50%; transform: translateY(-50%); /* Scale based on a reference screen width, 428/1216 */ @@ -47,6 +75,11 @@ menu { gap: 48dp; } +body.mirrored menu { + left: auto; + right: 96dp; +} + hero { display: flex; flex-direction: column; @@ -55,6 +88,10 @@ hero { gap: 8dp; } +body.mirrored hero { + align-items: flex-end; +} + hero img { width: 100%; } @@ -79,6 +116,7 @@ hero img { display: flex; flex-direction: column; gap: 12dp; + align-items: flex-start; } #menu-list button { @@ -86,6 +124,7 @@ hero img { height: 54dp; padding: 8dp 16dp; border-radius: 8dp; + text-align: left; text-transform: uppercase; font-family: "Fira Sans Condensed"; font-size: 32dp; @@ -105,25 +144,56 @@ hero img { decorator: horizontal-gradient(#FEE685FF #FEE68500); } +body.mirrored #menu-list { + align-items: flex-end; +} + +body.mirrored #menu-list button { + text-align: right; +} + +body.mirrored #menu-list button:hover, +body.mirrored #menu-list button:focus-visible { + decorator: horizontal-gradient(#FEE68500 #FEE685FF); +} + disc-info { position: absolute; left: 96dp; + right: auto; bottom: 72dp; display: flex; flex-direction: column; gap: 12dp; font-size: 24dp; + font-effect: glow(0dp 4dp 0dp 4dp black); + text-align: left; +} + +body.mirrored disc-info { + left: auto; + right: 96dp; + text-align: right; } version-info { position: absolute; right: 96dp; + left: auto; bottom: 72dp; display: flex; flex-direction: column; gap: 12dp; text-align: right; font-size: 24dp; + font-effect: glow(0dp 4dp 0dp 4dp black); + text-align: right; +} + +body.mirrored version-info { + right: auto; + left: 96dp; + text-align: left; } #disc-status { diff --git a/res/rml/window.rcss b/res/rml/window.rcss index db3558778a..c72a9aed57 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -43,11 +43,19 @@ window.preset { min-width: 650dp; } +window.modal { + max-width: 816dp; +} + window[open] { filter: opacity(1); transform: scale(1); } +window[open].blurred { + filter: blur(2dp); +} + @media (max-height: 640dp) { body { padding: 16dp; @@ -108,6 +116,12 @@ window content pane > spacer { pointer-events: none; } +window modal { + padding: 32dp; + gap: 20dp; + flex: 0 1 auto; +} + scrollbarvertical { width: 8dp; margin: 4dp 4dp 4dp 0; @@ -194,6 +208,12 @@ button:not(:disabled):active { box-shadow: #C2A42D 0 0 0 2dp; } +button.modal-btn { + font-size: 20dp; + padding: 16dp 10dp; + flex: 1 1 0; +} + select-button { display: flex; align-items: center; @@ -399,3 +419,22 @@ button.preset-btn { color: rgba(224, 219, 200, 65%); text-align: center; } + +.modal-dialog { + display: flex; + flex-flow: column; + padding: 16dp; + gap: 20dp; + flex: 0 1 auto; + min-width: 0; +} + +.modal-actions { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: stretch; + gap: 12dp; + padding-top: 12dp; + width: 100%; +} diff --git a/src/d/d_demo.cpp b/src/d/d_demo.cpp index d6425848d3..2f099f2c2a 100644 --- a/src/d/d_demo.cpp +++ b/src/d/d_demo.cpp @@ -11,6 +11,62 @@ #include "JSystem/JGadget/define.h" #include +#include "dusk/logging.h" + +#if TARGET_PC +#include "dusk/ui/ui.hpp" + +namespace { +static int sJaiSkip = -1; + +static JSUList* get_stream_list() { + return Z2GetSoundMgr()->getStreamMgr()->getStreamList(); +} + +static int get_stream_count(JSUList* list) { + int i = 0; + for (JSULink* l = list != nullptr ? list->getFirst() : nullptr; l != nullptr; + l = l->getNext()) { + i++; + } + return i; +} + +static void pause_stream(int skip_first, bool paused) { + int i = 0; + JSUList* list = get_stream_list(); + for (JSULink* l = list->getFirst(); l != nullptr; l = l->getNext(), ++i) { + if (i >= skip_first) { + l->getObject()->pause(paused); + } + } +} + +static void pause_streams(int skip_first) { + if (!dusk::ui::is_prelaunch_open()) { + return; + } + JSUList* list = get_stream_list(); + if (list == nullptr || get_stream_count(list) <= skip_first) { + return; + } + pause_stream(skip_first, true); + sJaiSkip = skip_first; +} + +static void unpause_streams(bool require_prelaunch_hidden) { + if (sJaiSkip < 0) { + return; + } + if (require_prelaunch_hidden && dusk::ui::is_prelaunch_open()) { + return; + } + pause_stream(sJaiSkip, false); + sJaiSkip = -1; +} +} // namespace +#endif + s16 dDemo_c::m_branchId = -1; namespace { @@ -1006,7 +1062,16 @@ int dDemo_c::start(u8 const* p_data, cXyz* p_translation, f32 rotationY) { m_control->setSuspend(0); } +#if TARGET_PC + const int existing_streams = get_stream_count(get_stream_list()); +#endif + m_control->forward(0); + +#if TARGET_PC + pause_streams(existing_streams); +#endif + m_translation = p_translation; if (m_translation != NULL) { @@ -1034,6 +1099,10 @@ static void dummyString2() { void dDemo_c::end() { JUT_ASSERT(1956, m_system != NULL); +#if TARGET_PC + unpause_streams(false); +#endif + m_control->destroyObject_all(); m_object->remove(); m_data = NULL; @@ -1054,6 +1123,10 @@ void dDemo_c::branch() { int dDemo_c::update() { JUT_ASSERT(2064, m_system != NULL); +#if TARGET_PC + unpause_streams(true); +#endif + if (m_data == NULL) { if (m_branchData == NULL) { return 0; diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp index f0e2330a15..f7ebf6a20d 100644 --- a/src/d/d_s_play.cpp +++ b/src/d/d_s_play.cpp @@ -40,8 +40,9 @@ #include "JSystem/JKernel/JKRAramArchive.h" #if TARGET_PC +#include "dusk/autosave.h" #include "dusk/memory.h" -#include +#include "dusk/ui/ui.hpp" #endif #if DEBUG @@ -794,7 +795,17 @@ static int dScnPly_Execute(dScnPly_c* i_this) { dJprev_c::get()->update(); #endif +#if TARGET_PC + if (!dusk::ui::is_prelaunch_open()) { + dDemo_c::update(); + } else if (dusk::getSettings().audio.menuSounds) { + s8 reverb = dComIfGp_getReverb(dComIfGp_roomControl_getStayNo()); + f32 fxMix = reverb / 127.0f; + g_mEnvSeMgr.field_0x144.startEnvSeDirLevel(JA_SE_ATM_WIND_1, fxMix, 1.0f); + } +#else dDemo_c::update(); +#endif #if DEBUG dJcame_c::get()->update(); diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 355ef14450..7530a7f613 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -274,10 +274,6 @@ namespace dusk { ImGuiMenuGame::ToggleFullscreen(); } - // if (!dusk::IsGameLaunched) { - // m_preLaunchWindow.draw(); - // } - if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) { m_isHidden = !m_isHidden; } diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 1755c02856..1aee9df373 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -9,7 +9,6 @@ #include "ImGuiMenuGame.hpp" #include "ImGuiMenuTools.hpp" -#include "ImGuiPreLaunchWindow.hpp" #include "imgui.h" union SDL_Event; @@ -45,7 +44,6 @@ private: std::deque m_toasts; ImGuiMenuGame m_menuGame; - ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last ImGuiMenuTools m_menuTools; diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp deleted file mode 100644 index a0006f0ad3..0000000000 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ /dev/null @@ -1,282 +0,0 @@ -#include "imgui.h" - -#include "ImGuiConfig.hpp" -#include "ImGuiEngine.hpp" -#include "ImGuiPreLaunchWindow.hpp" - -#include "../file_select.hpp" -#include "../iso_validate.hpp" -#include "ImGuiConsole.hpp" -#include "dusk/main.h" -#include "dusk/settings.h" - -#include -#include - -#include "aurora/lib/internal.hpp" -#include "aurora/lib/window.hpp" - -namespace dusk { - -typedef void (ImGuiPreLaunchWindow::*drawFunc)(); - -drawFunc drawTable[2] = {&ImGuiPreLaunchWindow::drawMainMenu, &ImGuiPreLaunchWindow::drawOptions}; - -static constexpr std::array skLanguageNames = { - "English", "German", "French", "Spanish", "Italian" -}; - -static constexpr std::array skGameDiscFileFilters{{ - {"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"}, - {"All Files", "*"}, -}}; - -static std::string ShowIsoInvalidError(const iso::ValidationError code) { - using namespace std::literals::string_literals; - - switch (code) { - case iso::ValidationError::IOError: - return "Unknown IO error occurred"s; - case iso::ValidationError::InvalidImage: - return "Unable to interpret selected file as a disc image"s; - case iso::ValidationError::WrongGame: - return "Selected disc image is for a different game"s; - case iso::ValidationError::WrongVersion: - return "Selected disc image is for an unsupported version of the game. Only North American GameCube (NTSC/GZ2E01) is supported at this time."s; - case iso::ValidationError::ExecutableMismatch: - return "Selected disc image contains modified executable files."s; - default: - return "Unknown error"s; - } -} - -static std::string_view card_type_name(CARDFileType type) { - switch (type) { - case CARD_GCIFOLDER: - return "GCI Folder"sv; - case CARD_RAWIMAGE: - return "Card Image"sv; - default: - return ""sv; - } -} - -void fileDialogCallback(void* userdata, const char* path, const char* error) { - auto* self = static_cast(userdata); - if (error != nullptr) { - self->m_selectedIsoPath.clear(); - self->m_errorString = fmt::format("File dialog error: {}", error); - return; - } - - if (path == nullptr) { - self->m_selectedIsoPath.clear(); - return; - } - - self->m_selectedIsoPath = path; - self->m_isPal = iso::isPal(path); - getSettings().backend.isoPath.setValue(self->m_selectedIsoPath); - config::Save(); -} - -ImGuiPreLaunchWindow::ImGuiPreLaunchWindow() = default; - -bool ImGuiPreLaunchWindow::isSelectedPathValid() const { -#if TARGET_ANDROID - return !m_selectedIsoPath.empty(); // unsure why SDL_GetPathInfo doesnt work here -#else - return !m_selectedIsoPath.empty() && SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr); -#endif -} - -void ImGuiPreLaunchWindow::draw() { - if (m_IsFirstDraw) { - m_selectedIsoPath = getSettings().backend.isoPath; - m_isPal = !m_selectedIsoPath.empty() && iso::isPal(m_selectedIsoPath.c_str()); - m_initialGraphicsBackend = getSettings().backend.graphicsBackend; - m_IsFirstDraw = false; - } - - if (isSelectedPathValid() && getSettings().backend.skipPreLaunchUI) { - dusk::IsGameLaunched = true; - return; - } - - auto& io = ImGui::GetIO(); - - ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y)); - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowBgAlpha(0.65f); - - ImGui::Begin("Pre Launch Window", nullptr, - ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | - ImGuiWindowFlags_NoBringToFrontOnFocus); - - const auto& windowSize = ImGui::GetWindowSize(); - - for (int i = 0; i < 5; i++) - ImGui::NewLine(); - - float iconSize = 150.f; - ImGui::SameLine(windowSize.x / 2 - iconSize + (iconSize / 2)); - if (ImGuiEngine::orgIcon != 0) { - ImGui::Image(ImGuiEngine::orgIcon, ImVec2{iconSize, iconSize}); - } - ImGuiTextCenter("Twilit Realm presents"); - if (ImGuiEngine::duskLogo) { - ImGui::NewLine(); - float width = iconSize * 2.5f; - ImGui::SameLine(windowSize.x / 2 - width + (width / 2)); - ImGui::Image(ImGuiEngine::duskLogo, ImVec2{width, iconSize}); - } else { - ImGui::PushFont(ImGuiEngine::fontExtraLarge); - ImGuiTextCenter("Dusk"); - ImGui::PopFont(); - } - - (this->*drawTable[m_CurMenu])(); - - ImGui::End(); -} - -void ImGuiPreLaunchWindow::drawMainMenu() { - const auto& windowSize = ImGui::GetWindowSize(); - ImGui::SetCursorPosY(windowSize.y - 200); - - ImGui::PushFont(ImGuiEngine::fontLarge); - - if (!isSelectedPathValid()) { - if (!m_errorString.empty()) { - ImGuiTextCenter(m_errorString); - } - - if (ImGuiButtonCenter("Select disc image...")) { - ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(), - skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr, - false); - } - } else { - if (ImGuiButtonCenter("Start game")) { - dusk::IsGameLaunched = true; - } - } - - if (ImGuiButtonCenter("Options")) { - m_CurMenu = 1; - } - - ImGui::PopFont(); -} - -void ImGuiPreLaunchWindow::drawOptions() { - const auto& windowSize = ImGui::GetWindowSize(); - - ImGui::NewLine(); - - ImGui::PushFont(ImGuiEngine::fontLarge); - ImGuiTextCenter("Options"); - ImGui::Separator(); - ImGui::PopFont(); - - auto cursorY = ImGui::GetCursorPosY(); - float endCursorY = windowSize.y - 100; - - float childWidth = windowSize.x - 400; - - ImGui::SetCursorPosX(windowSize.x / 2 - (childWidth / 2)); - if (ImGui::BeginChild("OptionsChild", ImVec2(childWidth, endCursorY - cursorY), - ImGuiChildFlags_None, ImGuiWindowFlags_NoBackground)) - { - if (!m_errorString.empty()) { - ImGuiTextCenter(m_errorString); - } - - ImGui::InputText("Game ISO Path", &m_selectedIsoPath, ImGuiInputTextFlags_ReadOnly); - ImGui::SameLine(); - if (ImGui::Button(m_selectedIsoPath == "" ? "Set" : "Change")) { - ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(), - skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr, - false); - } - - if (m_isPal) { - auto selectedLanguage = getSettings().game.language.getValue(); - if (ImGui::BeginCombo("Language", skLanguageNames[static_cast(selectedLanguage)])) { - for (u8 i = 0; i < skLanguageNames.size(); ++i) { - if (ImGui::Selectable(skLanguageNames[i])) { - getSettings().game.language.setValue(static_cast(i)); - config::Save(); - } - } - - ImGui::EndCombo(); - } - } - - AuroraBackend configuredBackend = BACKEND_AUTO; - const std::string& configuredBackendId = getSettings().backend.graphicsBackend; - if (!try_parse_backend(configuredBackendId, configuredBackend)) { - configuredBackend = BACKEND_AUTO; - } - - if (ImGui::BeginCombo("Graphics Backend", backend_name(configuredBackend).data())) { - if (ImGui::Selectable("Auto", configuredBackend == BACKEND_AUTO)) { - getSettings().backend.graphicsBackend.setValue("auto"); - config::Save(); - } - - size_t backendCount = 0; - const AuroraBackend* availableBackends = aurora_get_available_backends(&backendCount); - for (size_t i = 0; i < backendCount; ++i) { - const AuroraBackend backend = availableBackends[i]; - const bool isSelected = configuredBackend == backend; - if (ImGui::Selectable(backend_name(backend).data(), isSelected)) { - getSettings().backend.graphicsBackend.setValue( - std::string(backend_id(backend))); - config::Save(); - } - if (isSelected) { - ImGui::SetItemDefaultFocus(); - } - } - - ImGui::EndCombo(); - } - if (configuredBackendId != m_initialGraphicsBackend) { - ImGui::TextDisabled("Restart Required"); - } - auto curFileType = (CARDFileType)getSettings().backend.cardFileType.getValue(); - - if (ImGui::BeginCombo("Save File Type", card_type_name(curFileType).data())) { - - if (ImGui::Selectable("GCI Folder", curFileType == CARD_GCIFOLDER)) { - getSettings().backend.cardFileType.setValue(CARD_GCIFOLDER); - config::Save(); - } - - if (ImGui::Selectable("Card Image", curFileType == CARD_RAWIMAGE)) { - getSettings().backend.cardFileType.setValue(CARD_RAWIMAGE); - config::Save(); - } - - ImGui::EndCombo(); - } - - ImGui::EndChild(); - } - - ImGui::SetCursorPosY(endCursorY); - ImGui::NewLine(); - - ImGui::Separator(); - - ImGui::PushFont(ImGuiEngine::fontLarge); - if (ImGuiButtonCenter("Back")) { - m_CurMenu = 0; - } - ImGui::PopFont(); -} - -} // namespace dusk diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp b/src/dusk/imgui/ImGuiPreLaunchWindow.hpp deleted file mode 100644 index 6cb078a228..0000000000 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -namespace dusk { -class ImGuiPreLaunchWindow { -private: - int m_CurMenu = 0; - bool m_IsFirstDraw = true; - std::string m_initialGraphicsBackend; - - bool isSelectedPathValid() const; - -public: - ImGuiPreLaunchWindow(); - void draw(); - - void drawMainMenu(); - void drawOptions(); - - std::string m_selectedIsoPath; - std::string m_errorString; - bool m_isPal = false; -}; -} // namespace dusk diff --git a/src/dusk/main.cpp b/src/dusk/main.cpp index 22cd5a9fc6..e1b2fd0b6e 100644 --- a/src/dusk/main.cpp +++ b/src/dusk/main.cpp @@ -5,17 +5,110 @@ #endif #include +#include "dusk/main.h" +#include +#include +#include #include #include +#include +#include #include #include #include +#if !defined(_WIN32) +#include +#if defined(__APPLE__) +#include +#endif +#endif + int game_main(int argc, char* argv[]); namespace { +bool RestartProcess(int argc, char* argv[]) { +#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ + (defined(TARGET_OS_TV) && TARGET_OS_TV) + (void)argc; + (void)argv; + return false; +#elif _WIN32 + std::wstring commandLine = GetCommandLineW(); + STARTUPINFOW startupInfo{}; + startupInfo.cb = sizeof(startupInfo); + PROCESS_INFORMATION processInfo{}; + if (!CreateProcessW(nullptr, commandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, + &startupInfo, &processInfo)) + { + fprintf(stderr, "Failed to restart Dusk: CreateProcessW error %lu\n", GetLastError()); + return false; + } + + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + return true; +#else + std::filesystem::path executablePath; + +#if defined(__APPLE__) + uint32_t pathSize = 0; + _NSGetExecutablePath(nullptr, &pathSize); + if (pathSize > 0) { + std::string path(pathSize, '\0'); + if (_NSGetExecutablePath(path.data(), &pathSize) == 0) { + path.resize(std::strlen(path.c_str())); + std::error_code ec; + executablePath = std::filesystem::weakly_canonical(path, ec); + if (ec) { + executablePath = path; + } + } + } +#elif defined(__linux__) + std::array path{}; + const ssize_t len = readlink("/proc/self/exe", path.data(), path.size() - 1); + if (len > 0) { + path[static_cast(len)] = '\0'; + executablePath = path.data(); + } +#endif + + if (executablePath.empty() && argc > 0 && argv[0] != nullptr && argv[0][0] != '\0') { + std::error_code ec; + executablePath = std::filesystem::absolute(argv[0], ec); + if (ec) { + executablePath = argv[0]; + } + } + + if (executablePath.empty()) { + fprintf(stderr, "Failed to restart Dusk: unable to resolve executable path\n"); + return false; + } + + std::vector args; + args.reserve(static_cast(std::max(argc, 1))); + args.push_back(executablePath.string()); + for (int i = 1; i < argc; ++i) { + args.emplace_back(argv[i] != nullptr ? argv[i] : ""); + } + + std::vector execArgv; + execArgv.reserve(args.size() + 1); + for (auto& arg : args) { + execArgv.push_back(arg.data()); + } + execArgv.push_back(nullptr); + + execv(executablePath.c_str(), execArgv.data()); + fprintf(stderr, "Failed to restart Dusk: execv failed: %s\n", std::strerror(errno)); + return false; +#endif +} + #if _WIN32 bool ShouldShowWindowsConsole(int argc, char* argv[]) { if (const auto* env = std::getenv("DUSK_CONSOLE")) { @@ -53,19 +146,25 @@ void WindowsSetupConsole(bool showConsole) { SetConsoleOutputCP(CP_UTF8); if (const HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); - stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr) { + stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr) + { DWORD consoleMode = 0; if (GetConsoleMode(stdoutHandle, &consoleMode)) { SetConsoleMode(stdoutHandle, - consoleMode | ENABLE_PROCESSED_OUTPUT - | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + consoleMode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); } } } int DuskMain(int argc, char* argv[]) { WindowsSetupConsole(ShouldShowWindowsConsole(argc, argv)); - return game_main(argc, argv); + const int result = game_main(argc, argv); + if constexpr (dusk::SupportsProcessRestart) { + if (dusk::RestartRequested) { + return RestartProcess(argc, argv) ? 0 : result; + } + } + return result; } std::vector WideArgsToUtf8(int argc, wchar_t** argv) { @@ -81,8 +180,8 @@ std::vector WideArgsToUtf8(int argc, wchar_t** argv) { } std::vector utf8Buffer(static_cast(requiredSize)); - WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr, - nullptr); + WideCharToMultiByte( + CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr, nullptr); utf8Args.emplace_back(utf8Buffer.data()); } @@ -109,7 +208,11 @@ int RunWindowsGuiEntryPoint() { } #else int DuskMain(int argc, char* argv[]) { - return game_main(argc, argv); + const int result = game_main(argc, argv); + if (dusk::RestartRequested && RestartProcess(argc, argv)) { + return 0; + } + return result; } #endif diff --git a/src/dusk/ui/component.cpp b/src/dusk/ui/component.cpp index 466420eb1e..748df848be 100644 --- a/src/dusk/ui/component.cpp +++ b/src/dusk/ui/component.cpp @@ -59,16 +59,6 @@ void Component::set_disabled(bool value) { } } -Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) { - if (parent == nullptr) { - return nullptr; - } - auto* doc = parent->GetOwnerDocument(); - if (doc == nullptr) { - return nullptr; - } - return parent->AppendChild(doc->CreateElement(tag)); -} void Component::listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback, bool capture) { if (element == nullptr) { diff --git a/src/dusk/ui/component.hpp b/src/dusk/ui/component.hpp index 0c49ce3254..e0a602e7fd 100644 --- a/src/dusk/ui/component.hpp +++ b/src/dusk/ui/component.hpp @@ -47,7 +47,6 @@ public: Rml::Element* root() const { return mRoot; } protected: - static Rml::Element* append(Rml::Element* parent, const Rml::String& tag); void clear_children(); Rml::Element* mRoot = nullptr; diff --git a/src/dusk/ui/modal.cpp b/src/dusk/ui/modal.cpp new file mode 100644 index 0000000000..93ea060b51 --- /dev/null +++ b/src/dusk/ui/modal.cpp @@ -0,0 +1,75 @@ +#include "modal.hpp" + +namespace dusk::ui { + +Modal::Modal(Props props) + : WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) { + auto* title = append(mDialog, "div"); + title->SetClass("preset-title", true); + title->SetInnerRML(mProps.title); + + auto* body = append(mDialog, "div"); + body->SetClass("preset-intro", true); + body->SetInnerRML(mProps.bodyRml); + + auto* actions = append(mDialog, "div"); + actions->SetClass("modal-actions", true); + + for (auto& action : mProps.actions) { + auto btn = std::make_unique