From 0dc475108b134dc0edb8f1d19e274b1a27656390 Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Tue, 26 May 2026 11:39:40 -0400 Subject: [PATCH 1/5] Ensure `dMdl_c` packet is in drawlist for interp (#1818) Fixes #1010. --- src/d/d_model.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/d/d_model.cpp b/src/d/d_model.cpp index 80dfbc0953..36e77e55fa 100644 --- a/src/d/d_model.cpp +++ b/src/d/d_model.cpp @@ -45,6 +45,20 @@ void dMdl_c::create(J3DModelData* i_modelData, u16 i_materialId, dKy_tevstr_c* i } void dMdl_c::entryObj(dMdl_obj_c* i_obj) { +#ifdef TARGET_PC + // if field_0x1a is false, this dMdl_c is not in the drawlist + // if true, we need to make sure with interp enabled + if (dusk::frame_interp::is_enabled() && field_0x1a) { + auto pkt = dComIfGd_getListPacket()->mpBuffer[0]; + while (pkt && pkt != this) { + pkt = pkt->getNextPacket(); + } + if (!pkt) { + field_0x1a = false; + } + } +#endif + if (!field_0x1a) { dComIfGd_getListPacket()->entryImm(this, 0); field_0x1a = true; From 2afc52772ca793ca3547a59dbbaccf97479501dd Mon Sep 17 00:00:00 2001 From: Irastris Date: Tue, 26 May 2026 22:39:07 -0400 Subject: [PATCH 2/5] Suppress Textinput line break events on BaseStringButton (#1833) * Suppress Textinput line break events on BaseStringButton * Oops, removed a comment --- src/dusk/ui/string_button.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/dusk/ui/string_button.cpp b/src/dusk/ui/string_button.cpp index 7210e87803..9a4c3ced09 100644 --- a/src/dusk/ui/string_button.cpp +++ b/src/dusk/ui/string_button.cpp @@ -7,7 +7,7 @@ namespace dusk::ui { BaseStringButton::BaseStringButton(Rml::Element* parent, Props props) : BaseControlledSelectButton(parent, {std::move(props.key)}), mType(std::move(props.type)), mMaxLength(props.maxLength) { - mInputListeners.reserve(3); + mInputListeners.reserve(4); } void BaseStringButton::update() { @@ -54,6 +54,15 @@ void BaseStringButton::start_editing() { mRoot->DispatchEvent(Rml::EventId::Submit, {{"handled", Rml::Variant{true}}}); // Register input listeners + mInputListeners.emplace_back(std::make_unique( + mInputElem, Rml::EventId::Textinput, [this](Rml::Event& event) { + if (event.GetTargetElement() == mInputElem) { + const Rml::String text = event.GetParameter("text", Rml::String{}); + if (!text.empty() && std::ranges::all_of(text, [](const char c) { return c == '\r' || c == '\n' || c == '\t'; })) { + event.StopImmediatePropagation(); + } + } + })); mInputListeners.emplace_back(std::make_unique( mInputElem, Rml::EventId::Keydown, [this](Rml::Event& event) { const auto cmd = map_nav_event(event); From dc0c868bc1bf473d49ef22dbe8f9f4a16ffb8912 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 26 May 2026 20:42:11 -0600 Subject: [PATCH 3/5] Crash handler: Properly map stack frames from modules; fix build IDs (#1848) --- src/dusk/crash_handler.cpp | 298 +++++++++++++++++++++++++++++++------ 1 file changed, 254 insertions(+), 44 deletions(-) diff --git a/src/dusk/crash_handler.cpp b/src/dusk/crash_handler.cpp index c79e2e16c8..8562d5f14b 100644 --- a/src/dusk/crash_handler.cpp +++ b/src/dusk/crash_handler.cpp @@ -64,6 +64,15 @@ struct CrashContext { }; CrashContext g_ctx; +struct ModuleInfo { + uintptr_t base = 0; + uintptr_t size = 0; + char path[1024] = {}; + uint8_t buildId[64] = {}; + unsigned buildIdLen = 0; + unsigned pdbAge = 0; +}; + void rawWrite(int fd, const char* data, size_t len) { if (fd < 0) { return; @@ -119,9 +128,29 @@ void writeHexBytes(int fd, const uint8_t* data, unsigned len) { } } -const char* moduleName() { - const char* name = g_ctx.modulePath; - for (const char* p = g_ctx.modulePath; *p != '\0'; ++p) { +void writeHexByte(int fd, uint8_t value) { + char buf[2]; + buf[0] = kHexDigits[value >> 4]; + buf[1] = kHexDigits[value & 0xF]; + rawWrite(fd, buf, 2); +} + +void writeQuoted(int fd, const char* s) { + writeStr(fd, "\""); + if (s != nullptr) { + for (const char* p = s; *p != '\0'; ++p) { + if (*p == '"' || *p == '\\') { + rawWrite(fd, "\\", 1); + } + rawWrite(fd, p, 1); + } + } + writeStr(fd, "\""); +} + +const char* baseName(const char* path) { + const char* name = path; + for (const char* p = path; p != nullptr && *p != '\0'; ++p) { if (*p == '/' || *p == '\\') { name = p + 1; } @@ -129,6 +158,40 @@ const char* moduleName() { return name[0] != '\0' ? name : "(unknown)"; } +void writeBuildId(int fd, const uint8_t* buildId, unsigned buildIdLen, unsigned pdbAge) { + if (buildIdLen == 0) { + writeStr(fd, "(unavailable)"); + return; + } +#if defined(_WIN32) + if (buildIdLen == 16) { + writeHexByte(fd, buildId[3]); + writeHexByte(fd, buildId[2]); + writeHexByte(fd, buildId[1]); + writeHexByte(fd, buildId[0]); + writeStr(fd, "-"); + writeHexByte(fd, buildId[5]); + writeHexByte(fd, buildId[4]); + writeStr(fd, "-"); + writeHexByte(fd, buildId[7]); + writeHexByte(fd, buildId[6]); + writeStr(fd, "-"); + writeHexByte(fd, buildId[8]); + writeHexByte(fd, buildId[9]); + writeStr(fd, "-"); + writeHexBytes(fd, buildId + 10, 6); + if (pdbAge != 0) { + writeStr(fd, "-"); + writeDec(fd, pdbAge); + } + return; + } +#else + (void)pdbAge; +#endif + writeHexBytes(fd, buildId, buildIdLen); +} + const char* symbolFor(uintptr_t pc, unsigned long long* disp) { #if defined(_WIN32) && defined(DUSK_CRASH_DBGHELP) alignas(SYMBOL_INFO) static char storage[sizeof(SYMBOL_INFO) + 512]; @@ -156,7 +219,47 @@ const char* symbolFor(uintptr_t pc, unsigned long long* disp) { #endif } +void fallbackModuleInfo(ModuleInfo& info) { + info = {}; + info.base = g_ctx.moduleBase; + std::strncpy(info.path, g_ctx.modulePath, sizeof(info.path) - 1); + if (g_ctx.buildIdLen > sizeof(info.buildId)) { + info.buildIdLen = sizeof(info.buildId); + } else { + info.buildIdLen = g_ctx.buildIdLen; + } + if (info.buildIdLen != 0) { + std::memcpy(info.buildId, g_ctx.buildId, info.buildIdLen); + } + info.pdbAge = g_ctx.pdbAge; +} + +bool findModuleInfo(uintptr_t pc, ModuleInfo& info); + +void emitAddressDetail(int fd, uintptr_t pc) { + ModuleInfo info; + findModuleInfo(pc, info); + const uintptr_t rva = pc >= info.base ? pc - info.base : 0ull; + writeHex(fd, pc); + writeStr(fd, " module_base="); + writeHex(fd, info.base); + if (info.size != 0) { + writeStr(fd, " image_size="); + writeHex(fd, info.size); + } + writeStr(fd, " rva="); + writeHex(fd, rva); + writeStr(fd, " module="); + writeQuoted(fd, info.path[0] != '\0' ? info.path : baseName(g_ctx.modulePath)); + writeStr(fd, " build_id="); + writeBuildId(fd, info.buildId, info.buildIdLen, info.pdbAge); +} + void emitFrame(int fd, int index, uintptr_t pc) { + ModuleInfo info; + findModuleInfo(pc, info); + const uintptr_t rva = pc >= info.base ? pc - info.base : 0ull; + writeStr(fd, "#"); if (index < 10) { writeStr(fd, "0"); @@ -164,10 +267,18 @@ void emitFrame(int fd, int index, uintptr_t pc) { writeDec(fd, static_cast(index)); writeStr(fd, " abs="); writeHex(fd, pc); + writeStr(fd, " module_base="); + writeHex(fd, info.base); + if (info.size != 0) { + writeStr(fd, " image_size="); + writeHex(fd, info.size); + } writeStr(fd, " rva="); - writeHex(fd, pc >= g_ctx.moduleBase ? pc - g_ctx.moduleBase : 0ull); - writeStr(fd, " "); - writeStr(fd, moduleName()); + writeHex(fd, rva); + writeStr(fd, " module="); + writeQuoted(fd, info.path[0] != '\0' ? info.path : baseName(g_ctx.modulePath)); + writeStr(fd, " build_id="); + writeBuildId(fd, info.buildId, info.buildIdLen, info.pdbAge); unsigned long long disp = 0; const char* sym = symbolFor(pc, &disp); if (sym != nullptr && sym[0] != '\0') { @@ -191,18 +302,7 @@ void emitHeader(int fd, const char* reason, unsigned long long code, bool hasCod writeStr(fd, "\nModule base: "); writeHex(fd, g_ctx.moduleBase); writeStr(fd, "\nBuild-ID: "); - if (g_ctx.buildIdLen != 0) { - writeHexBytes(fd, g_ctx.buildId, g_ctx.buildIdLen); -#if defined(_WIN32) - if (g_ctx.pdbAge != 0) { - writeStr(fd, " (Age="); - writeDec(fd, g_ctx.pdbAge); - writeStr(fd, ")"); - } -#endif - } else { - writeStr(fd, "(unavailable)"); - } + writeBuildId(fd, g_ctx.buildId, g_ctx.buildIdLen, g_ctx.pdbAge); writeStr(fd, "\nReason: "); writeStr(fd, reason); if (hasCode) { @@ -214,9 +314,7 @@ void emitHeader(int fd, const char* reason, unsigned long long code, bool hasCod writeHex(fd, faultAddr); writeStr(fd, "\nCrash PC: "); if (crashPcKnown) { - writeHex(fd, crashPc); - writeStr(fd, " rva="); - writeHex(fd, crashPc >= g_ctx.moduleBase ? crashPc - g_ctx.moduleBase : 0ull); + emitAddressDetail(fd, crashPc); } else { writeStr(fd, "(unavailable on this platform)"); } @@ -233,23 +331,25 @@ void emitFooter(int fd) { LONG g_inHandler = 0; LPTOP_LEVEL_EXCEPTION_FILTER g_prevFilter = nullptr; -void captureBuildId() { - const auto* base = reinterpret_cast(g_ctx.moduleBase); +bool readPeModuleInfo(uintptr_t moduleBase, ModuleInfo& info) { + const auto* base = reinterpret_cast(moduleBase); if (base == nullptr) { - return; + return false; } const auto* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE) { - return; + return false; } const auto* nt = reinterpret_cast(base + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) { - return; + return false; } + info.base = moduleBase; + info.size = nt->OptionalHeader.SizeOfImage; const IMAGE_DATA_DIRECTORY& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG]; if (dir.VirtualAddress == 0 || dir.Size == 0) { - return; + return true; } const auto* dbg = reinterpret_cast(base + dir.VirtualAddress); const unsigned count = dir.Size / sizeof(IMAGE_DEBUG_DIRECTORY); @@ -261,11 +361,40 @@ void captureBuildId() { if (std::memcmp(cv, "RSDS", 4) != 0) { continue; } - std::memcpy(g_ctx.buildId, cv + 4, sizeof(GUID)); - g_ctx.buildIdLen = sizeof(GUID); - std::memcpy(&g_ctx.pdbAge, cv + 4 + sizeof(GUID), sizeof(g_ctx.pdbAge)); + std::memcpy(info.buildId, cv + 4, sizeof(GUID)); + info.buildIdLen = sizeof(GUID); + std::memcpy(&info.pdbAge, cv + 4 + sizeof(GUID), sizeof(info.pdbAge)); break; } + return true; +} + +void captureBuildId() { + ModuleInfo info; + if (!readPeModuleInfo(g_ctx.moduleBase, info)) { + return; + } + g_ctx.buildIdLen = info.buildIdLen; + if (g_ctx.buildIdLen != 0) { + std::memcpy(g_ctx.buildId, info.buildId, g_ctx.buildIdLen); + } + g_ctx.pdbAge = info.pdbAge; +} + +bool findModuleInfo(uintptr_t pc, ModuleInfo& info) { + fallbackModuleInfo(info); + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(reinterpret_cast(pc), &mbi, sizeof(mbi)) == 0 || + mbi.AllocationBase == nullptr) { + return false; + } + const auto moduleBase = reinterpret_cast(mbi.AllocationBase); + info = {}; + info.base = moduleBase; + GetModuleFileNameA(reinterpret_cast(moduleBase), info.path, + static_cast(sizeof(info.path) - 1)); + readPeModuleInfo(moduleBase, info); + return true; } const char* exceptionName(DWORD code) { @@ -512,23 +641,35 @@ void prewarmUnwinder() { #if defined(__APPLE__) -void captureBuildId() { - const auto* header = reinterpret_cast(g_ctx.moduleBase); +bool readMachBuildId(uintptr_t moduleBase, ModuleInfo& info) { + const auto* header = reinterpret_cast(moduleBase); if (header == nullptr || header->magic != MH_MAGIC_64) { - return; + return false; } const auto* lc = reinterpret_cast( reinterpret_cast(header) + sizeof(struct mach_header_64)); for (uint32_t i = 0; i < header->ncmds; ++i) { if (lc->cmd == LC_UUID) { const auto* uuid = reinterpret_cast(lc); - std::memcpy(g_ctx.buildId, uuid->uuid, sizeof(uuid->uuid)); - g_ctx.buildIdLen = sizeof(uuid->uuid); - return; + std::memcpy(info.buildId, uuid->uuid, sizeof(uuid->uuid)); + info.buildIdLen = sizeof(uuid->uuid); + return true; } lc = reinterpret_cast( reinterpret_cast(lc) + lc->cmdsize); } + return true; +} + +void captureBuildId() { + ModuleInfo info; + if (!readMachBuildId(g_ctx.moduleBase, info)) { + return; + } + g_ctx.buildIdLen = info.buildIdLen; + if (g_ctx.buildIdLen != 0) { + std::memcpy(g_ctx.buildId, info.buildId, g_ctx.buildIdLen); + } } #else @@ -547,7 +688,28 @@ bool segmentContains(const dl_phdr_info* info, uintptr_t addr) { return false; } -bool readGnuBuildId(const dl_phdr_info* info) { +void readElfModuleInfo(const dl_phdr_info* info, ModuleInfo& module) { + uintptr_t minAddr = ~static_cast(0); + uintptr_t maxAddr = 0; + for (int i = 0; i < info->dlpi_phnum; ++i) { + const ElfW(Phdr)& ph = info->dlpi_phdr[i]; + if (ph.p_type != PT_LOAD) { + continue; + } + const uintptr_t start = info->dlpi_addr + ph.p_vaddr; + const uintptr_t end = start + ph.p_memsz; + if (start < minAddr) { + minAddr = start; + } + if (end > maxAddr) { + maxAddr = end; + } + } + if (minAddr <= maxAddr && maxAddr != 0) { + module.base = minAddr; + module.size = maxAddr - minAddr; + } + for (int i = 0; i < info->dlpi_phnum; ++i) { const ElfW(Phdr)& ph = info->dlpi_phdr[i]; if (ph.p_type != PT_NOTE) { @@ -563,17 +725,16 @@ bool readGnuBuildId(const dl_phdr_info* info) { if (nh->n_type == NT_GNU_BUILD_ID && nh->n_namesz == 4 && std::memcmp(name, "GNU", 4) == 0) { unsigned n = nh->n_descsz; - if (n > sizeof(g_ctx.buildId)) { - n = sizeof(g_ctx.buildId); + if (n > sizeof(module.buildId)) { + n = sizeof(module.buildId); } - std::memcpy(g_ctx.buildId, desc, n); - g_ctx.buildIdLen = n; - return true; + std::memcpy(module.buildId, desc, n); + module.buildIdLen = n; + return; } p = desc + ((nh->n_descsz + 3) & ~3u); } } - return false; } int elfBuildIdCallback(dl_phdr_info* info, size_t, void* arg) { @@ -581,7 +742,12 @@ int elfBuildIdCallback(dl_phdr_info* info, size_t, void* arg) { if (!segmentContains(info, self)) { return 0; } - readGnuBuildId(info); + ModuleInfo module; + readElfModuleInfo(info, module); + g_ctx.buildIdLen = module.buildIdLen; + if (g_ctx.buildIdLen != 0) { + std::memcpy(g_ctx.buildId, module.buildId, g_ctx.buildIdLen); + } return 1; } @@ -592,6 +758,50 @@ void captureBuildId() { #endif +#if !defined(__APPLE__) +struct ElfModuleSearch { + uintptr_t pc; + ModuleInfo* module; +}; + +int elfModuleInfoCallback(dl_phdr_info* info, size_t, void* arg) { + auto* search = static_cast(arg); + if (!segmentContains(info, search->pc)) { + return 0; + } + if (info->dlpi_name != nullptr && info->dlpi_name[0] != '\0') { + std::strncpy(search->module->path, info->dlpi_name, + sizeof(search->module->path) - 1); + } + readElfModuleInfo(info, *search->module); + return 1; +} +#endif + +bool findModuleInfo(uintptr_t pc, ModuleInfo& info) { + fallbackModuleInfo(info); + Dl_info moduleInfo; + if (dladdr(reinterpret_cast(pc), &moduleInfo) == 0) { + return false; + } + if (moduleInfo.dli_fbase != nullptr) { + info.base = reinterpret_cast(moduleInfo.dli_fbase); + } + if (moduleInfo.dli_fname != nullptr && moduleInfo.dli_fname[0] != '\0') { + info.path[0] = '\0'; + std::strncpy(info.path, moduleInfo.dli_fname, sizeof(info.path) - 1); + } + info.buildIdLen = 0; + info.pdbAge = 0; +#if defined(__APPLE__) + readMachBuildId(info.base, info); +#else + ElfModuleSearch search{pc, &info}; + dl_iterate_phdr(&elfModuleInfoCallback, &search); +#endif + return true; +} + const char* signalName(int sig) { switch (sig) { case SIGSEGV: From a85f8c203d0d68de948f7e5fd79538a226551203 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 26 May 2026 20:42:26 -0600 Subject: [PATCH 4/5] Better config saving handling (#1846) --- include/dusk/io.hpp | 5 +++++ src/dusk/config.cpp | 40 +++++++++++++++++++++++++++++----- src/dusk/io.cpp | 16 +++++++++++++- src/dusk/ui/graphics_tuner.cpp | 2 +- src/dusk/ui/settings.cpp | 5 +++++ src/dusk/ui/settings.hpp | 1 + src/dusk/ui/ui.cpp | 1 - 7 files changed, 62 insertions(+), 8 deletions(-) diff --git a/include/dusk/io.hpp b/include/dusk/io.hpp index 2efc4a8d3b..aba6bf8a48 100644 --- a/include/dusk/io.hpp +++ b/include/dusk/io.hpp @@ -30,6 +30,11 @@ public: ~FileStream(); + /** + * \brief Flush buffered writes and throw if the flush fails. + */ + void Flush(); + /** * \brief Open a file for reading at the given path. */ diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index c0edd84591..aed43089c4 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -8,6 +8,8 @@ #include "dusk/settings.h" #include +#include +#include #include #include "dusk/main.h" @@ -24,8 +26,24 @@ aurora::Module DuskConfigLog("dusk::config"); static absl::flat_hash_map RegisteredConfigVars; static bool RegistrationDone = false; -static std::u8string GetConfigJsonPath() { - return (dusk::ConfigPath / ConfigFileName).u8string(); +static std::filesystem::path GetConfigJsonPath() { + return dusk::ConfigPath / ConfigFileName; +} + +static std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) { + auto tempPath = configJsonPath; + tempPath.replace_filename(fmt::format(".{}.tmp", configJsonPath.filename().string())); + return tempPath; +} + +static void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) { + std::error_code ec; + std::filesystem::rename(source, target, ec); + if (ec) { + const auto renameError = ec; + std::filesystem::remove(source, ec); + throw std::system_error(renameError); + } } ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) { @@ -211,7 +229,8 @@ void dusk::config::LoadFromUserPreferences() { if (configJsonPath.empty()) { return; } - LoadFromFileName(reinterpret_cast(configJsonPath.c_str())); + const auto configPathString = io::fs_path_to_string(configJsonPath); + LoadFromFileName(configPathString.c_str()); } static void LoadFromPath(const char* path) { @@ -254,6 +273,10 @@ void dusk::config::LoadFromFileName(const char* path) { } else { DuskConfigLog.error("Failed to load from config! {}", e.what()); } + } catch (const nlohmann::json::parse_error& e) { + DuskConfigLog.error("Failed to parse config JSON, staying with defaults: {}", e.what()); + } catch (const std::exception& e) { + DuskConfigLog.error("Failed to load from config, staying with defaults: {}", e.what()); } } @@ -262,10 +285,11 @@ void dusk::config::Save() { if (configJsonPath.empty()) { return; } + const auto configPathString = io::fs_path_to_string(configJsonPath); DuskConfigLog.info( "Saving config to '{}'", - reinterpret_cast(configJsonPath.c_str())); + configPathString); json j; @@ -276,7 +300,13 @@ void dusk::config::Save() { } } - io::FileStream::WriteAllText(reinterpret_cast(configJsonPath.c_str()), j.dump(4)); + try { + const auto tempConfigJsonPath = GetTempConfigJsonPath(configJsonPath); + io::FileStream::WriteAllText(tempConfigJsonPath, j.dump(4)); + ReplaceFile(tempConfigJsonPath, configJsonPath); + } catch (const std::exception& e) { + DuskConfigLog.error("Failed to save config to '{}': {}", configPathString, e.what()); + } } void dusk::config::ClearAllActionBindings(int port) { diff --git a/src/dusk/io.cpp b/src/dusk/io.cpp index 4f6ef16a9b..88bd04ae8f 100644 --- a/src/dusk/io.cpp +++ b/src/dusk/io.cpp @@ -1,5 +1,7 @@ +#include #include #include +#include #include "dusk/io.hpp" @@ -30,6 +32,9 @@ static FILE* ThrowIfNotOpen(const FileStream& file) { } [[noreturn]] static void ThrowForError(int code) { + if (code == 0) { + throw std::system_error(std::make_error_code(std::errc::io_error)); + } throw std::system_error(std::make_error_code(static_cast(code))); } @@ -77,6 +82,14 @@ FileStream::~FileStream() { fclose(static_cast(file)); } +void FileStream::Flush() { + FILE* fileHandle = ThrowIfNotOpen(*this); + + if (fflush(fileHandle) != 0) { + ThrowForError(errno); + } +} + FileStream FileStream::OpenRead(const char* utf8Path) { return FileStream(OpenCore(utf8Path, MODE("rb"), _SH_DENYWR)); } @@ -163,10 +176,11 @@ void FileStream::WriteAllText(const char* utf8Path, const std::string_view text) void FileStream::WriteAllText(const std::filesystem::path& path, const std::string_view text) { auto handle = Create(path); handle.Write(text.data(), text.size()); + handle.Flush(); } FILE* FileStream::ToInner() { auto handle = file; file = nullptr; return handle; -} \ No newline at end of file +} diff --git a/src/dusk/ui/graphics_tuner.cpp b/src/dusk/ui/graphics_tuner.cpp index 8cea89e984..f91c300a75 100644 --- a/src/dusk/ui/graphics_tuner.cpp +++ b/src/dusk/ui/graphics_tuner.cpp @@ -95,7 +95,6 @@ void set_value(GraphicsOption option, int value) { getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f); break; } - config::Save(); } Rml::Element* create_stepped_carousel_root(Rml::Element* parent) { @@ -292,6 +291,7 @@ void GraphicsTuner::show() { } void GraphicsTuner::hide(bool close) { + config::Save(); mRoot->RemoveAttribute("open"); if (close) { mPendingClose = true; diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index d6545f5121..0a3cf26a1f 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -1501,4 +1501,9 @@ void SettingsWindow::update() { Window::update(); } +void SettingsWindow::hide(bool close) { + config::Save(); + Window::hide(close); +} + } // namespace dusk::ui diff --git a/src/dusk/ui/settings.hpp b/src/dusk/ui/settings.hpp index 1d071e6575..1c8e9b826b 100644 --- a/src/dusk/ui/settings.hpp +++ b/src/dusk/ui/settings.hpp @@ -8,6 +8,7 @@ public: SettingsWindow(bool prelaunch = false); void update() override; + void hide(bool close) override; protected: bool mPrelaunch; diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp index 072bf70b66..af05d0f476 100644 --- a/src/dusk/ui/ui.cpp +++ b/src/dusk/ui/ui.cpp @@ -61,7 +61,6 @@ bool initialize() noexcept { } void shutdown() noexcept { - config::Save(); sDocumentStack.clear(); sPassiveDocuments.clear(); sConnectedGamepads.clear(); From 8e38f27602db37ff1153d7bf6732a225c3206e25 Mon Sep 17 00:00:00 2001 From: CraftyBoss Date: Wed, 27 May 2026 02:00:48 -0700 Subject: [PATCH 5/5] sort checks by status, add hide button, cleanup code layout --- src/dusk/imgui/ImGuiMenuRandomizer.cpp | 269 +++++++++++++++---------- src/dusk/imgui/ImGuiMenuRandomizer.hpp | 9 +- 2 files changed, 168 insertions(+), 110 deletions(-) diff --git a/src/dusk/imgui/ImGuiMenuRandomizer.cpp b/src/dusk/imgui/ImGuiMenuRandomizer.cpp index 82de66761e..033c8e44d8 100644 --- a/src/dusk/imgui/ImGuiMenuRandomizer.cpp +++ b/src/dusk/imgui/ImGuiMenuRandomizer.cpp @@ -235,114 +235,150 @@ namespace dusk { ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + ImGui::SetNextWindowBgAlpha(0.5f); - if (ImGui::Begin("Rando Tracker", nullptr, windowFlags)) { - auto trackerRando = getTrackerRando(); + if (!ImGui::Begin("Rando Tracker", nullptr, windowFlags) || !randomizer_IsActive()) { + ImGui::End(); + return; + } - // Uncomment button for manual updating - if (ImGui::Button("Update Tracker") || g_randomizerState.mUpdateTracker) { - g_randomizerState.mUpdateTracker = false; + auto trackerRando = getTrackerRando(); - // Generate tracker world if it doesn't exist - auto contextHash = randomizer_GetContext().mHash; - auto trackerHash = trackerRando->GetConfig().GetHash(false); - // If no hash, or seeds switched, try to create tracker world from currently active seed - if (trackerHash.empty() || (trackerHash != contextHash && !contextHash.empty())) { - *trackerRando = randomizer::Randomizer(ui::GetRandomizerPath()); - trackerRando->GenerateTrackerWorld(); - } + if (ImGui::Button("Update Tracker") || g_randomizerState.mUpdateTracker) { + g_randomizerState.mUpdateTracker = false; - if (randomizer_IsActive()) { - auto currentItems = getSaveItemPool(trackerRando->GetWorld()); - m_currentSearch = randomizer::logic::search::Search::AccessibleNoStartingInventory(&trackerRando->GetWorlds(), currentItems); - m_currentSearch.SearchWorlds(); - generateLocationInfo(); - } + // Generate tracker world if it doesn't exist + auto contextHash = randomizer_GetContext().mHash; + auto trackerHash = trackerRando->GetConfig().GetHash(false); + // If no hash, or seeds switched, try to create tracker world from currently active seed + if (trackerHash.empty() || (trackerHash != contextHash && !contextHash.empty())) { + *trackerRando = randomizer::Randomizer(ui::GetRandomizerPath()); + trackerRando->GenerateTrackerWorld(); } - if (trackerRando->GetConfig().GetHash(false).empty()) { - ImGui::Text("Load a seed and start a file to activate the tracker"); - } else { - ImGui::Text("Tracker world loaded from seed %s", trackerRando->GetConfig().GetHash().c_str()); - const char* curStage = dComIfGp_getStartStageName(); - const char* curStageName = lookup_map_name(curStage); - ImGui::Text("Current Stage: %s (%s)", curStageName, curStage); - - auto stageInfo = dComIfGp_getStageStagInfo(); - if (stageInfo && dStage_stagInfo_GetSaveTbl(stageInfo) != getStageSaveId(curStage)) { - ImGui::TextColored(ImVec4(1.0f, 0.0f,0.0f,1.0f), "Error: Current Stage Save Tbl mismatch!"); - } - - ImGui::Checkbox("Show Only Accessible Locations", &m_onlyAccessible); - ImGui::Checkbox("Show Location Requirements", &m_showRequirements); - ImGui::InputText("Location Filter", m_locationFilter, 100); - - // Show total number of available locations - auto locations = trackerRando->GetWorld()->GetAllLocations(); - // TODO: maybe add remaining? - ImGui::Text("Locations Available: %zu / %zu (%zu Checked)", m_numAvailableLocations, m_numProgressionLocations, m_numCollectedLocations); - - if (ImGui::BeginChild("ScrollRegion", ImVec2(500, 500), true)) - { - std::string filter = m_locationFilter; - // Show all locations. Green for accessible. Red for Unaccessible - for (const auto& [areaName, region] : m_LocationInfo) { - if (m_onlyAccessible && !region.showArea) - continue; - - int areaCount = region.locations.size(); - - bool isCurrentArea = areaName == curStageName; - - if (isCurrentArea) - ImGui::PushStyleColor(ImGuiCol_Text, TRACKER_COLOR_ACTIVE); - else if (!region.showArea) - ImGui::PushStyleColor(ImGuiCol_Text, TRACKER_COLOR_COLLECTED); - - bool isShowNode = ImGui::TreeNode(fmt::format("{} ({}/{})", - areaName, areaCount - region.collectedCount, areaCount).c_str()); - - if (!region.showArea || isCurrentArea) - ImGui::PopStyleColor(); - - if (isShowNode) { - for (const auto& info : region.locations) { - // Don't show any locations which aren't accessible if only accessible is checked - // Don't show any locations which don't meet the filter - if ((m_onlyAccessible && !info.accessible) || - !randomizer::utility::str::Contains(info.locationName, filter)) { - continue; - } - - if (info.collected) { - ImGui::TextColored(TRACKER_COLOR_COLLECTED, "%s [%s]", - info.locationName.c_str(), - info.locationItem.c_str()); - }else { - ImVec4 color; - if (info.accessible) { - // If the search found this location, change color to green - color = TRACKER_COLOR_ACCESSIBLE; - }else { - color = TRACKER_COLOR_INACCESSIBLE; - } - ImGui::TextColored(color, "%s", info.locationName.c_str()); - } - - // Show requirements for the location below it (formatting isn't pretty right now) - if (m_showRequirements) { - ImGui::SetItemTooltip("%s", info.logicStr.c_str()); - } - } - - ImGui::TreePop(); - } - } - } - ImGui::EndChild(); + if (randomizer_IsActive()) { + auto currentItems = getSaveItemPool(trackerRando->GetWorld()); + m_currentSearch = randomizer::logic::search::Search::AccessibleNoStartingInventory(&trackerRando->GetWorlds(), currentItems); + m_currentSearch.SearchWorlds(); + generateLocationInfo(); } } + if (trackerRando->GetConfig().GetHash(false).empty()) { + ImGui::Text("Load a seed and start a file to activate the tracker"); + ImGui::End(); + return; + } + + ImGui::Text("Tracker world loaded from seed %s", trackerRando->GetConfig().GetHash().c_str()); + const char* curStage = dComIfGp_getStartStageName(); + const char* curStageName = lookup_map_name(curStage); + ImGui::Text("Current Stage: %s (%s)", curStageName, curStage); + + auto stageInfo = dComIfGp_getStageStagInfo(); + if (stageInfo && dStage_stagInfo_GetSaveTbl(stageInfo) != getStageSaveId(curStage)) { + ImGui::TextColored(ImVec4(1.0f, 0.0f,0.0f,1.0f), "Error: Current Stage Save Tbl mismatch!"); + } + + ImGui::Checkbox("Show Only Accessible Locations", &m_onlyAccessible); + ImGui::Checkbox("Hide Area Header", &m_hideAreaHeader); + ImGui::Checkbox("Show Location Requirements", &m_showRequirements); + ImGui::InputText("Location Filter", m_locationFilter, 100); + + // Show total number of available locations + auto locations = trackerRando->GetWorld()->GetAllLocations(); + // TODO: maybe add remaining? + ImGui::Text("Locations Available: %zu / %zu (%zu Checked)", m_numAvailableLocations, m_numProgressionLocations, m_numCollectedLocations); + + ImGui::SetNextWindowBgAlpha(0.5f); + if (ImGui::BeginChild("ScrollRegion", ImVec2(500, 500), true)) + { + std::string filter = m_locationFilter; + auto drawLocationsFunc = [this, &filter](std::vector& locations) { + for (auto& info : locations) { + // Don't show any locations which aren't accessible if only accessible is checked + // Don't show any locations which don't meet the filter + if ((m_onlyAccessible && !info.accessible) || + !randomizer::utility::str::Contains(info.locationName, filter)) { + continue; + } + + if (info.collected) { + ImGui::TextColored(TRACKER_COLOR_COLLECTED, "%s [%s]", + info.locationName.c_str(), + info.locationItem.c_str()); + } else { + if (ImGui::SmallButton(fmt::format("{}###HideCheckBtn_{}", + info.hidden ? "+" : "-", info.locationName.c_str()).c_str())) { + if (info.hidden) { + std::erase(m_HiddenChecks, info.locationName); + } else { + m_HiddenChecks.push_back(info.locationName); + } + info.hidden = !info.hidden; + } + ImGui::SameLine(); + + if (info.hidden) { + ImGui::TextColored(TRACKER_COLOR_COLLECTED, "%s [Skipped]", + info.locationName.c_str()); + } else { + ImVec4 color; + if (info.accessible) { + // If the search found this location, change color to green + color = TRACKER_COLOR_ACCESSIBLE; + } else { + color = TRACKER_COLOR_INACCESSIBLE; + } + + ImGui::TextColored(color, "%s", info.locationName.c_str()); + } + } + + // Show requirements for the location below it (formatting isn't pretty right now) + if (m_showRequirements) { + ImGui::SetItemTooltip("%s", info.logicStr.c_str()); + } + } + }; + + if (m_hideAreaHeader) { + for (auto& region : m_LocationInfo | std::views::values) { + drawLocationsFunc(region.unobtainedLocations); + } + for (auto& region : m_LocationInfo | std::views::values) { + drawLocationsFunc(region.obtainedLocations); + } + }else { + // Show all locations. Green for accessible. Red for Unaccessible + for (auto& [areaName, region] : m_LocationInfo) { + if (m_onlyAccessible && !region.showArea) + continue; + + int areaCount = region.totalCount; + + bool isCurrentArea = areaName == curStageName; + + if (isCurrentArea) + ImGui::PushStyleColor(ImGuiCol_Text, TRACKER_COLOR_ACTIVE); + else if (!region.showArea) + ImGui::PushStyleColor(ImGuiCol_Text, TRACKER_COLOR_COLLECTED); + + bool isShowNode = ImGui::CollapsingHeader(fmt::format("{} ({}/{})###{}_{}", + areaName, areaCount - region.collectedCount, areaCount, areaName, areaCount).c_str()); + + if (!region.showArea || isCurrentArea) + ImGui::PopStyleColor(); + + if (isShowNode) { + drawLocationsFunc(region.unobtainedLocations); + drawLocationsFunc(region.obtainedLocations); + } + } + } + } + ImGui::EndChild(); + ImGui::End(); } @@ -351,6 +387,25 @@ namespace dusk { return &trackerRando; } + void ImGuiMenuRandomizer::TrackerAreaGroup::addToGroup(LocationTrackerInfo& info) { + if (!showArea && info.accessible && !info.collected) { + showArea = true; + } + + totalCount++; + + if (info.collected) + collectedCount++; + if (info.accessible) + accessibleCount++; + + if (info.collected) { + obtainedLocations.push_back(info); + }else { + unobtainedLocations.push_back(info); + } + } + void ImGuiMenuRandomizer::generateLocationInfo() { auto trackerRando = getTrackerRando(); auto world = trackerRando->GetWorld(); @@ -384,6 +439,11 @@ namespace dusk { info.collected = isLocationObtained(location); + info.hidden = std::ranges::find(m_HiddenChecks, info.locationName) != m_HiddenChecks.end(); + + if (info.collected) + m_numAvailableLocations--; + // Gather the hint regions this location is in (set avoids duplicates) std::unordered_set regions{}; for (auto access : location->GetAccessList()) { @@ -394,16 +454,7 @@ namespace dusk { for (const auto& region : regions) { auto& infoGroup = m_LocationInfo[region]; - if (!infoGroup.showArea && info.accessible && !info.collected) { - infoGroup.showArea = true; - } - - if (info.collected) - infoGroup.collectedCount++; - if (info.accessible) - infoGroup.accessibleCount++; - - infoGroup.locations.push_back(info); + infoGroup.addToGroup(info); } } diff --git a/src/dusk/imgui/ImGuiMenuRandomizer.hpp b/src/dusk/imgui/ImGuiMenuRandomizer.hpp index 32dc52e6f4..0064506e2a 100644 --- a/src/dusk/imgui/ImGuiMenuRandomizer.hpp +++ b/src/dusk/imgui/ImGuiMenuRandomizer.hpp @@ -29,13 +29,18 @@ private: std::string locationItem; bool accessible = false; bool collected = false; + bool hidden = false; }; struct TrackerAreaGroup { - std::vector locations; + std::vector obtainedLocations; + std::vector unobtainedLocations; bool showArea; + int totalCount; int collectedCount; int accessibleCount; + + void addToGroup(LocationTrackerInfo& loc); }; size_t m_numAvailableLocations; @@ -47,11 +52,13 @@ private: bool m_onlyAccessible{false}; bool m_showRequirements{false}; + bool m_hideAreaHeader{false}; char m_locationFilter[100]; randomizer::logic::search::Search m_currentSearch = randomizer::logic::search::Search(); std::map m_LocationInfo; + std::vector m_HiddenChecks; void generateLocationInfo(); };