diff --git a/files.cmake b/files.cmake index e9149a1777..2f14dd49f0 100644 --- a/files.cmake +++ b/files.cmake @@ -1360,6 +1360,8 @@ set(DUSK_FILES src/dusk/imgui/ImGuiMenuTools.hpp src/dusk/imgui/ImGuiMenuEnhancements.cpp src/dusk/imgui/ImGuiMenuEnhancements.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 diff --git a/include/dusk/main.h b/include/dusk/main.h index b357de8ea5..8c1335d5c8 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -3,6 +3,7 @@ namespace dusk { extern bool IsShuttingDown; + extern bool IsGameLaunched; } #endif // DUSK_MAIN_H diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 8b818801a7..3e085b411e 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -70,6 +70,11 @@ struct UserSettings { // Controls ConfigVar enableTurboKeybind; } game; + + struct { + ConfigVar isoPath; + ConfigVar skipPreLaunchUI; + } backend; }; UserSettings& getSettings(); diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index a238bb7c7a..7f12b1a570 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -14,6 +14,7 @@ #include "JSystem/JUtility/JUTGamePad.h" #include "SDL3/SDL_mouse.h" #include "dusk/config.hpp" +#include "dusk/main.h" #include "dusk/settings.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/dusk.h" @@ -35,6 +36,21 @@ namespace dusk { ImGui::TextUnformatted(text.data(), text.data() + text.size()); } + void ImGuiTextCenter(std::string_view text) { + ImGui::NewLine(); + float fontSize = ImGui::CalcTextSize(text.data(), text.data() + text.size()).x; + ImGui::SameLine(ImGui::GetWindowSize().x / 2 - fontSize + fontSize / 2); + ImGuiStringViewText(text); + } + + bool ImGuiButtonCenter(std::string_view text) { + ImGui::NewLine(); + float fontSize = ImGui::CalcTextSize(text.data(), text.data() + text.size()).x; + fontSize += ImGui::GetStyle().FramePadding.x; + ImGui::SameLine(ImGui::GetWindowSize().x / 2 - fontSize + fontSize / 2); + return ImGui::Button(text.data()); + } + std::string BytesToString(size_t bytes) { constexpr std::array suffixes{ "B"sv, "KB"sv, "MB"sv, "GB"sv, "TB"sv, "PB"sv, "EB"sv }; uint32_t s = 0; @@ -221,7 +237,10 @@ namespace dusk { ImGuiMenuGame::ToggleFullscreen(); } - if (CheckMenuViewToggle(ImGuiKey_F1, m_isHidden)) { + if (!dusk::IsGameLaunched) { + m_preLaunchWindow.draw(); + } + else if (CheckMenuViewToggle(ImGuiKey_F1, m_isHidden)) { ShowToasts(); ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; SDL_HideCursor(); diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 3d13e2ed6d..bb420e6d1f 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -8,6 +8,7 @@ #include "ImGuiMenuEnhancements.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiMenuTools.hpp" +#include "ImGuiPreLaunchWindow.hpp" #include "imgui.h" namespace dusk { @@ -36,6 +37,7 @@ private: ImGuiMenuGame m_menuGame; ImGuiMenuEnhancements m_menuEnhancements; + ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last ImGuiMenuTools m_menuTools; @@ -53,6 +55,8 @@ bool ShowCornerContextMenu(int& corner, int avoidCorner); void ImGuiStringViewText(std::string_view text); void ImGuiBeginGroupPanel(const char* name, const ImVec2& size); void ImGuiEndGroupPanel(); +void ImGuiTextCenter(std::string_view text); +bool ImGuiButtonCenter(std::string_view text); float ImGuiScale(); } // namespace dusk diff --git a/src/dusk/imgui/ImGuiEngine.cpp b/src/dusk/imgui/ImGuiEngine.cpp index a6abc5b0b9..15879d49db 100644 --- a/src/dusk/imgui/ImGuiEngine.cpp +++ b/src/dusk/imgui/ImGuiEngine.cpp @@ -32,7 +32,28 @@ bool AssetExists(const std::string& path) { ImFont* ImGuiEngine::fontNormal; ImFont* ImGuiEngine::fontLarge; -ImTextureID ImGuiEngine::duskIcon; +ImFont* ImGuiEngine::fontExtraLarge; +ImTextureID ImGuiEngine::duskIcon = 0; + +inline ImFont* CreateFont(float size, bool hasFontFile, const std::string& fontPath) { + ImGuiIO& io = ImGui::GetIO(); + + ImFontConfig fontConfig{}; + fontConfig.SizePixels = size; + snprintf(static_cast(fontConfig.Name), sizeof(fontConfig.Name), + "Noto Mono Regular, %dpx", static_cast(fontConfig.SizePixels)); + ImFont* outFont = + hasFontFile ? + io.Fonts->AddFontFromFileTTF(fontPath.c_str(), fontConfig.SizePixels, &fontConfig) : + nullptr; + if (outFont == nullptr) { + if (hasFontFile) { + DuskLog.warn("Failed to load font '{}': {}", fontPath, SDL_GetError()); + } + outFont = io.Fonts->AddFontDefault(&fontConfig); + } + return outFont; +} void ImGuiEngine_Initialize(float scale) { ImGui::GetCurrentContext(); @@ -78,6 +99,8 @@ void ImGuiEngine_Initialize(float scale) { ImGuiEngine::fontLarge = io.Fonts->AddFontDefault(&fontConfig); } + ImGuiEngine::fontExtraLarge = CreateFont(std::floor(40.f * scale), hasFontFile, fontPath); + auto& style = ImGui::GetStyle(); style = {}; // Reset sizes style.WindowPadding = ImVec2(15, 15); @@ -190,6 +213,9 @@ Icon GetIcon() { } void ImGuiEngine_AddTextures() { + if (ImGuiEngine::duskIcon != 0) + return; + auto icon = GetIcon(); if (icon.data == nullptr || icon.width == 0 || icon.height == 0) { ImGuiEngine::duskIcon = 0; diff --git a/src/dusk/imgui/ImGuiEngine.hpp b/src/dusk/imgui/ImGuiEngine.hpp index 18e49c2a47..8d55c5b3d5 100644 --- a/src/dusk/imgui/ImGuiEngine.hpp +++ b/src/dusk/imgui/ImGuiEngine.hpp @@ -9,6 +9,7 @@ class ImGuiEngine { public: static ImFont* fontNormal; static ImFont* fontLarge; + static ImFont* fontExtraLarge; static ImTextureID duskIcon; }; diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 8125583c8b..56f8b3b46e 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -107,6 +107,12 @@ namespace dusk { ImGui::EndMenu(); } + if (ImGui::BeginMenu("Misc")) { + config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); + + ImGui::EndMenu(); + } + ImGui::EndMenu(); } diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 44e92a4082..c367b1a7f8 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -10,6 +10,7 @@ #include "d/actor/d_a_alink.h" #include "d/actor/d_a_horse.h" #include "d/d_com_inf_game.h" +#include "dusk/main.h" #include "dusk/dusk.h" #include "m_Do/m_Do_main.h" @@ -40,15 +41,18 @@ namespace dusk { ImGui::EndMenu(); } - ImGui::MenuItem("Process Management", hotkeys::SHOW_PROCESS_MANAGEMENT, &m_showProcessManagement); - ImGui::MenuItem("Debug Overlay", hotkeys::SHOW_DEBUG_OVERLAY, &m_showDebugOverlay); - ImGui::MenuItem("Heap Viewer", hotkeys::SHOW_HEAP_VIEWER, &m_showHeapOverlay); - ImGui::MenuItem("Stub Log", hotkeys::SHOW_STUB_LOG, &m_showStubLog); - ImGui::MenuItem("Debug Camera", hotkeys::SHOW_CAMERA_DEBUG, &m_showCameraOverlay); - ImGui::MenuItem("Map Loader", nullptr, &m_showMapLoader); - ImGui::MenuItem("Player Info", nullptr, &m_showPlayerInfo); - ImGui::MenuItem("Save Editor", nullptr, &m_showSaveEditor); - ImGui::MenuItem("Audio Debug", hotkeys::SHOW_AUDIO_DEBUG, &m_showAudioDebug); + if (dusk::IsGameLaunched) { + ImGui::MenuItem("Process Management", hotkeys::SHOW_PROCESS_MANAGEMENT, &m_showProcessManagement); + ImGui::MenuItem("Debug Overlay", hotkeys::SHOW_DEBUG_OVERLAY, &m_showDebugOverlay); + ImGui::MenuItem("Heap Viewer", hotkeys::SHOW_HEAP_VIEWER, &m_showHeapOverlay); + ImGui::MenuItem("Stub Log", hotkeys::SHOW_STUB_LOG, &m_showStubLog); + ImGui::MenuItem("Debug Camera", hotkeys::SHOW_CAMERA_DEBUG, &m_showCameraOverlay); + ImGui::MenuItem("Map Loader", nullptr, &m_showMapLoader); + ImGui::MenuItem("Player Info", nullptr, &m_showPlayerInfo); + ImGui::MenuItem("Save Editor", nullptr, &m_showSaveEditor); + ImGui::MenuItem("Audio Debug", hotkeys::SHOW_AUDIO_DEBUG, &m_showAudioDebug); + } + ImGui::MenuItem("OSReport Force", nullptr, &OSReportReallyForceEnable); ImGui::EndMenu(); } diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp new file mode 100644 index 0000000000..965ae2c29d --- /dev/null +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp @@ -0,0 +1,185 @@ +#include "imgui.h" + +#include "ImGuiConfig.hpp" +#include "ImGuiEngine.hpp" +#include "ImGuiPreLaunchWindow.hpp" + +#include "ImGuiConsole.hpp" +#include "dusk/main.h" +#include "dusk/settings.h" + +#include +#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 skGameDiscFileFilters{{ + {"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"}, + {"All Files", "*"}, +}}; + +void fileDialogCallback(void* userdata, const char* const* filelist, [[maybe_unused]] int filter) { + auto* self = static_cast(userdata); + if (filelist != nullptr) { + if (filelist[0] == nullptr) { + // Cancelled + self->m_selectedIsoPath.clear(); + } else { + self->m_selectedIsoPath = filelist[0]; + getSettings().backend.isoPath.setValue(self->m_selectedIsoPath); + config::Save(); + } + } else { + // Error occurred + self->m_selectedIsoPath.clear(); + self->m_errorString = fmt::format("File dialog error: {}", SDL_GetError()); + } +} + + +ImGuiPreLaunchWindow::ImGuiPreLaunchWindow() { + +} + +bool ImGuiPreLaunchWindow::isSelectedPathValid() const { + return !m_selectedIsoPath.empty() && SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr); +} + +void ImGuiPreLaunchWindow::draw() { + if (m_IsFirstDraw) { + m_selectedIsoPath = getSettings().backend.isoPath; + 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::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 = 128.f; + ImGui::SameLine(windowSize.x / 2 - iconSize + (iconSize / 2)); + if (ImGuiEngine::duskIcon != 0) + ImGui::Image(ImGuiEngine::duskIcon, ImVec2{iconSize, iconSize}); + ImGui::PushFont(ImGuiEngine::fontExtraLarge); + ImGuiTextCenter("Dusk"); + ImGui::PopFont(); + + (this->*drawTable[m_CurMenu])(); + + ImGui::End(); +} + +void ImGuiPreLaunchWindow::drawMainMenu() { + const auto& windowSize = ImGui::GetWindowSize(); + static const char* popopModalLabel = "Skip Pre-Launch UI?"; + ImGui::SetCursorPosY(windowSize.y - 200); + + ImGui::PushFont(ImGuiEngine::fontLarge); + + bool disabled = false; + if (m_selectedIsoPath.empty() || !SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr)) { + ImGui::BeginDisabled(); + disabled = true; + } + + if (ImGuiButtonCenter("Start Game")) { + if (!getSettings().backend.skipPreLaunchUI) { + ImGui::OpenPopup(popopModalLabel); + }else { + dusk::IsGameLaunched = true; + } + } + + if (disabled) { + ImGui::EndDisabled(); + } + + if (ImGuiButtonCenter("Options")) { + m_CurMenu = 1; + } + + ImGui::PopFont(); + + if (ImGui::BeginPopupModal(popopModalLabel, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Would you like to skip this menu on future launches?"); + ImGui::Separator(); + + if (ImGui::Button("Yes", ImVec2(120, 0))) { + getSettings().backend.skipPreLaunchUI.setValue(true); + config::Save(); + ImGui::CloseCurrentPopup(); + dusk::IsGameLaunched = true; + } + ImGui::SameLine(); + if (ImGui::Button("No", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + dusk::IsGameLaunched = true; + } + ImGui::EndPopup(); + } +} + +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)) { + ImGui::InputText("Game ISO Path", &m_selectedIsoPath, ImGuiInputTextFlags_ReadOnly); + ImGui::SameLine(); + if (ImGui::Button("Set")) { + SDL_ShowOpenFileDialog(&fileDialogCallback, this, aurora::window::get_sdl_window(), skGameDiscFileFilters.data(), + int(skGameDiscFileFilters.size()), nullptr, false); + } + ImGui::EndChild(); + } + + ImGui::SetCursorPosY(endCursorY); + ImGui::NewLine(); + + ImGui::Separator(); + + ImGui::PushFont(ImGuiEngine::fontLarge); + if (ImGuiButtonCenter("Back")) { + m_CurMenu = 0; + } + ImGui::PopFont(); +} + +} diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp b/src/dusk/imgui/ImGuiPreLaunchWindow.hpp new file mode 100644 index 0000000000..d70bcda342 --- /dev/null +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace dusk { +class ImGuiPreLaunchWindow { +private: + int m_CurMenu = 0; + bool m_IsFirstDraw = true; + + bool isSelectedPathValid() const; + +public: + ImGuiPreLaunchWindow(); + void draw(); + + void drawMainMenu(); + void drawOptions(); + + std::string m_selectedIsoPath; + std::string m_errorString; +}; +} // namespace dusk \ No newline at end of file diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index cf76ed9025..bf492ed3dc 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -58,6 +58,11 @@ UserSettings g_userSettings = { // Controls .enableTurboKeybind {"game.enableTurboKeybind", true}, }, + + .backend = { + .isoPath {"backend.isoPath", ""}, + .skipPreLaunchUI {"backend.skipPreLaunchUI", false} + } }; UserSettings& getSettings() { @@ -103,6 +108,9 @@ void registerSettings() { Register(g_userSettings.game.midnasLamentNonStop); Register(g_userSettings.game.enableTurboKeybind); Register(g_userSettings.game.fastSpinner); + + Register(g_userSettings.backend.isoPath); + Register(g_userSettings.backend.skipPreLaunchUI); } // Transient settings diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index edceafd01c..c8addc1140 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -62,6 +62,7 @@ #include "SDL3/SDL_filesystem.h" #include "cxxopts.hpp" #include "dusk/config.hpp" +#include "dusk/imgui/ImGuiConsole.hpp" #include "tracy/Tracy.hpp" // --- GLOBALS --- @@ -83,6 +84,7 @@ const int audioHeapSize = 0x14D800; #if TARGET_PC bool dusk::IsShuttingDown = false; +bool dusk::IsGameLaunched = false; #endif s32 LOAD_COPYDATE(void*) { @@ -117,6 +119,43 @@ AuroraStats dusk::lastFrameAuroraStats; float dusk::frameUsagePct = 0.0f; const char* configPath; +AuroraWindowSize preLaunchUIWindowSize; + +bool launchUILoop() { + while (!dusk::IsGameLaunched) { + const AuroraEvent* event = aurora_update(); + while (event != nullptr && event->type != AURORA_NONE) { + switch (event->type) { + case AURORA_WINDOW_RESIZED: + preLaunchUIWindowSize = event->windowSize; + break; + case AURORA_DISPLAY_SCALE_CHANGED: + dusk::ImGuiEngine_Initialize(event->windowSize.scale); + break; + case AURORA_EXIT: + return false; + } + + event++; + } + + if (!aurora_begin_frame()) { + DuskLog.debug("aurora_begin_frame returned false, skipping draw this frame"); + continue; + } + + dusk::g_imguiConsole.PreDraw(); + + dusk::g_imguiConsole.PostDraw(); + + aurora_end_frame(); + } + + DuskLog.info("Game Launched!"); + + return true; +} + void main01(void) { OS_REPORT("\x1b[m"); @@ -155,6 +194,8 @@ void main01(void) { OSReport("Entering Main Loop (main01)...\n"); + if (preLaunchUIWindowSize.width != 0) + mDoGph_gInf_c::setWindowSize(preLaunchUIWindowSize); do { // 1. Update Window Events @@ -243,6 +284,7 @@ static AuroraBackend ParseAuroraBackend(const std::string& value) { static void aurora_imgui_init_callback(const AuroraWindowSize* size) { dusk::ImGuiEngine_Initialize(size->scale); + dusk::ImGuiEngine_AddTextures(); } static void ApplyCVarOverrides(const cxxopts::OptionValue& option) { @@ -380,7 +422,22 @@ int game_main(int argc, char* argv[]) { fmt::format("Dusk {} [{}]", DUSK_WC_DESCRIBE, dusk::backend_name(auroraInfo.backend)) .c_str()); - const auto& dvd_path = parsed_arg_options["dvd"].as(); + // pre game launch ui main loop + if (!launchUILoop()) { + aurora_shutdown(); + return 0; + } + + std::string dvd_path; + if (parsed_arg_options.count("dvd")) { + dvd_path = parsed_arg_options["dvd"].as(); + }else { + dvd_path = dusk::getSettings().backend.isoPath; + + if (dvd_path.empty()) { + DuskLog.fatal("No DVD image specified, unable to boot!"); + } + } DuskLog.info("Loading DVD image: {}", dvd_path); if (!aurora_dvd_open(dvd_path.c_str())) { DuskLog.fatal("Failed to open DVD image: {}", dvd_path);