Merge branch 'randomizer' of https://github.com/TwilitRealm/dusk into randomizer

This commit is contained in:
gymnast86
2026-05-27 10:54:52 -07:00
12 changed files with 508 additions and 163 deletions
+5
View File
@@ -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.
*/
+14
View File
@@ -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;
+35 -5
View File
@@ -8,6 +8,8 @@
#include "dusk/settings.h"
#include <limits>
#include <filesystem>
#include <system_error>
#include <string>
#include "dusk/main.h"
@@ -24,8 +26,24 @@ aurora::Module DuskConfigLog("dusk::config");
static absl::flat_hash_map<std::string_view, ConfigVarBase*> 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<const char*>(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<const char*>(configJsonPath.c_str()));
configPathString);
json j;
@@ -276,7 +300,13 @@ void dusk::config::Save() {
}
}
io::FileStream::WriteAllText(reinterpret_cast<const char*>(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) {
+254 -44
View File
@@ -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<unsigned int>(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<const uint8_t*>(g_ctx.moduleBase);
bool readPeModuleInfo(uintptr_t moduleBase, ModuleInfo& info) {
const auto* base = reinterpret_cast<const uint8_t*>(moduleBase);
if (base == nullptr) {
return;
return false;
}
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE) {
return;
return false;
}
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(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<const IMAGE_DEBUG_DIRECTORY*>(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<LPCVOID>(pc), &mbi, sizeof(mbi)) == 0 ||
mbi.AllocationBase == nullptr) {
return false;
}
const auto moduleBase = reinterpret_cast<uintptr_t>(mbi.AllocationBase);
info = {};
info.base = moduleBase;
GetModuleFileNameA(reinterpret_cast<HMODULE>(moduleBase), info.path,
static_cast<DWORD>(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<const struct mach_header_64*>(g_ctx.moduleBase);
bool readMachBuildId(uintptr_t moduleBase, ModuleInfo& info) {
const auto* header = reinterpret_cast<const struct mach_header_64*>(moduleBase);
if (header == nullptr || header->magic != MH_MAGIC_64) {
return;
return false;
}
const auto* lc = reinterpret_cast<const struct load_command*>(
reinterpret_cast<const char*>(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<const struct uuid_command*>(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<const struct load_command*>(
reinterpret_cast<const char*>(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<uintptr_t>(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<ElfModuleSearch*>(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<void*>(pc), &moduleInfo) == 0) {
return false;
}
if (moduleInfo.dli_fbase != nullptr) {
info.base = reinterpret_cast<uintptr_t>(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:
+160 -109
View File
@@ -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<LocationTrackerInfo>& 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<std::string> 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);
}
}
+8 -1
View File
@@ -29,13 +29,18 @@ private:
std::string locationItem;
bool accessible = false;
bool collected = false;
bool hidden = false;
};
struct TrackerAreaGroup {
std::vector<LocationTrackerInfo> locations;
std::vector<LocationTrackerInfo> obtainedLocations;
std::vector<LocationTrackerInfo> 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<std::string, TrackerAreaGroup> m_LocationInfo;
std::vector<std::string> m_HiddenChecks;
void generateLocationInfo();
};
+15 -1
View File
@@ -1,5 +1,7 @@
#include <cerrno>
#include <cstdio>
#include <filesystem>
#include <system_error>
#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<std::errc>(code)));
}
@@ -77,6 +82,14 @@ FileStream::~FileStream() {
fclose(static_cast<FILE*>(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;
}
}
+1 -1
View File
@@ -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;
+5
View File
@@ -1501,4 +1501,9 @@ void SettingsWindow::update() {
Window::update();
}
void SettingsWindow::hide(bool close) {
config::Save();
Window::hide(close);
}
} // namespace dusk::ui
+1
View File
@@ -8,6 +8,7 @@ public:
SettingsWindow(bool prelaunch = false);
void update() override;
void hide(bool close) override;
protected:
bool mPrelaunch;
+10 -1
View File
@@ -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<ScopedEventListener>(
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<ScopedEventListener>(
mInputElem, Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto cmd = map_nav_event(event);
-1
View File
@@ -61,7 +61,6 @@ bool initialize() noexcept {
}
void shutdown() noexcept {
config::Save();
sDocumentStack.clear();
sPassiveDocuments.clear();
sConnectedGamepads.clear();