implement pre-launch ui for configuring iso directory (#301)

UI will automatically open on boot and will not launch the game if there is no valid iso path specified in the config. Can optionally be disabled on future game launches.
This commit is contained in:
CraftyBoss
2026-04-09 22:06:37 -07:00
committed by GitHub
parent 8b82ea818b
commit ed7fa8f2c5
13 changed files with 351 additions and 12 deletions
+2
View File
@@ -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
+1
View File
@@ -3,6 +3,7 @@
namespace dusk {
extern bool IsShuttingDown;
extern bool IsGameLaunched;
}
#endif // DUSK_MAIN_H
+5
View File
@@ -70,6 +70,11 @@ struct UserSettings {
// Controls
ConfigVar<bool> enableTurboKeybind;
} game;
struct {
ConfigVar<std::string> isoPath;
ConfigVar<bool> skipPreLaunchUI;
} backend;
};
UserSettings& getSettings();
+20 -1
View File
@@ -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();
+4
View File
@@ -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
+27 -1
View File
@@ -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<char*>(fontConfig.Name), sizeof(fontConfig.Name),
"Noto Mono Regular, %dpx", static_cast<int>(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;
+1
View File
@@ -9,6 +9,7 @@ class ImGuiEngine {
public:
static ImFont* fontNormal;
static ImFont* fontLarge;
static ImFont* fontExtraLarge;
static ImTextureID duskIcon;
};
+6
View File
@@ -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();
}
+13 -9
View File
@@ -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();
}
+185
View File
@@ -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 <SDL3/SDL_dialog.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_filesystem.h>
#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<SDL_DialogFileFilter, 2> 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<ImGuiPreLaunchWindow*>(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();
}
}
+21
View File
@@ -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
+8
View File
@@ -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
+58 -1
View File
@@ -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<std::string>();
// 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<std::string>();
}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);