#include "imgui.h" #include "ImGuiConsole.hpp" #include "ImGuiMenuRandomizer.hpp" #include "dusk/logging.h" #include "dusk/data.hpp" #include "dusk/map_loader_definitions.h" #include "dusk/ui/rando_config.hpp" #include "dusk/randomizer/generator/logic/search.hpp" #include "dusk/randomizer/generator/utility/string.hpp" #include "dusk/randomizer/game/randomizer_context.hpp" #include "dusk/randomizer/game/tools.h" #include #include #include #include #include namespace dusk { static bool generatingSeed = false; static std::string generationStatusMsg{}; static std::mutex generationStatusMsgMutex{}; static constexpr ImVec4 TRACKER_COLOR_INACCESSIBLE = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); static constexpr ImVec4 TRACKER_COLOR_ACCESSIBLE = ImVec4(0.f, 1.f, 0.f, 1.f); static constexpr ImVec4 TRACKER_COLOR_COLLECTED = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); static constexpr ImVec4 TRACKER_COLOR_ACTIVE = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); static void StartSeedGeneration() { if (generatingSeed) { return; } generatingSeed = true; std::lock_guard lock(generationStatusMsgMutex); GenerateAndWriteSeed(generationStatusMsg); generatingSeed = false; DuskLog.debug("{}", generationStatusMsg); } static const char* lookup_map_name(const char* mapFile) { if (!mapFile || mapFile[0] == '\0') return nullptr; for (const auto& region : gameRegions) { for (const auto& map : region.maps) { if (map.mapFile && strcmp(mapFile, map.mapFile) == 0) { return map.mapName; } } } return nullptr; } ImGuiMenuRandomizer::ImGuiMenuRandomizer() {} void ImGuiMenuRandomizer::draw() { if (ImGui::BeginMenu("Randomizer")) { if (ImGui::BeginMenu("Generate")) { if (ImGui::MenuItem("Generate Seed", nullptr, false, !generatingSeed)) { // Put seed generation on a separate thread so it doesn't freeze the game std::thread randoGenerationThread(StartSeedGeneration); randoGenerationThread.detach(); // StartSeedGeneration(); m_showRandoGeneration = true; } ImGui::EndMenu(); } std::string loadSeedText = "Load Seed"; if (!playerIsOnTitleScreen()) { loadSeedText += " (Must be on title screen)"; } if (ImGui::BeginMenu(loadSeedText.c_str(), playerIsOnTitleScreen())) { std::filesystem::path seedDirectory = ui::GetRandomizerSeedsPath(); std::list hashes{}; if (std::filesystem::exists(seedDirectory)) { for (const auto& entry : std::filesystem::directory_iterator(seedDirectory)) { if (entry.is_directory()) { hashes.push_back(entry.path().filename().string()); } } }else { std::filesystem::create_directories(seedDirectory); } if (hashes.empty()) { if (ImGui::MenuItem("No seeds generated")) {} } for (const auto& hash : hashes) { std::string name = hash; if (randomizer_GetContext().mHash == hash) { name += " (Current Seed)"; } if (ImGui::MenuItem(name.c_str())) { if (!randomizer_IsActive()) { randomizer_GetContext() = RandomizerContext(); randomizer_GetContext().LoadFromHash(hash); } } } ImGui::EndMenu(); } if (ImGui::BeginMenu("Delete Seed")) { std::filesystem::path seedDirectory = ui::GetRandomizerSeedsPath(); std::list hashes{}; for (const auto& entry : std::filesystem::directory_iterator(seedDirectory)) { if (entry.is_directory()) { hashes.push_back(entry.path().filename().string()); } } if (hashes.empty()) { if (ImGui::MenuItem("No seeds generated")) {} } for (const auto& hash : hashes) { std::string name = hash; if (randomizer_GetContext().mHash == hash) { name += " (Current Seed)"; } if (ImGui::MenuItem(name.c_str(), nullptr, false, playerIsOnTitleScreen() || randomizer_GetContext().mHash != hash)) { std::filesystem::path hashDirectory = seedDirectory / hash; if (randomizer_GetContext().mHash != hash) { std::filesystem::remove_all(hashDirectory); } else if (!randomizer_IsActive()){ // If the user selected the currently seed, but it's not active, we'll allow the delete std::filesystem::remove_all(hashDirectory); randomizer_GetContext() = RandomizerContext(); } } } ImGui::EndMenu(); } std::string name = "Deactivate Randomizer"; if (!playerIsOnTitleScreen()) { name += " (Must be on title screen)"; } if (ImGui::MenuItem(name.c_str(), nullptr, false, playerIsOnTitleScreen())) { // Reset the main randomizer context only if we're not active if (!randomizer_IsActive()) { randomizer_GetContext() = RandomizerContext(); } } ImGui::Checkbox("Show Rando Stats", &m_showRandoStats); ImGui::Checkbox("Show Rando Tracker", &g_randomizerState.mShowTracker); ImGui::EndMenu(); } } void ImGuiMenuRandomizer::windowRandoGeneration() { if (!m_showRandoGeneration) { return; } ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize; ImGui::SetNextWindowBgAlpha(0.65f); if (!ImGui::Begin("Randomizer Generation", &m_showRandoGeneration, windowFlags)) { ImGui::End(); return; } // Print "Generating..." until the seed finishes generating (at which point it will release // the lock on the mutex) std::string generationText = "Generating Randomizer Seed..."; if (generationStatusMsgMutex.try_lock()) { generationText = generationStatusMsg; generationStatusMsgMutex.unlock(); } ImGui::Text("%s", generationText.c_str()); ImGui::End(); } void ImGuiMenuRandomizer::windowRandoStats() { if (!m_showRandoStats) { return; } ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; ImGui::SetNextWindowBgAlpha(0.65f); if (ImGui::Begin("Rando Stats", nullptr, windowFlags)) { const char* seed = randomizer_GetContext().mHash.empty() ? "None" : randomizer_GetContext().mHash.c_str(); ImGui::Text("Current Seed: %s", seed); ImGui::Text("Status:"); ImGui::SameLine(); if (randomizer_IsActive()) { ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "ACTIVE"); } else { std::string reason{"Unknown"}; if (randomizer_GetContext().mHash.empty()) { reason = "No Seed Selected"; } else if (playerIsOnTitleScreen()) { reason = "On Title Screen"; } ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "NOT ACTIVE (Reason: %s)", reason.c_str()); } } ImGui::End(); } void ImGuiMenuRandomizer::windowRandoTracker() { if (!g_randomizerState.mShowTracker) { return; } ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; if (ImGui::Begin("Rando Tracker", nullptr, windowFlags)) { auto trackerRando = getTrackerRando(); // Uncomment button for manual updating if (ImGui::Button("Update Tracker") || g_randomizerState.mUpdateTracker) { g_randomizerState.mUpdateTracker = false; // 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 (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"); } 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(); } } ImGui::End(); } randomizer::Randomizer* ImGuiMenuRandomizer::getTrackerRando() { static randomizer::Randomizer trackerRando{ui::GetRandomizerPath()}; return &trackerRando; } void ImGuiMenuRandomizer::generateLocationInfo() { auto trackerRando = getTrackerRando(); auto world = trackerRando->GetWorld(); auto locations = world->GetAllLocations(); m_LocationInfo.clear(); m_numProgressionLocations = std::ranges::count_if(locations, [](auto* location) {return location->IsProgression();}); m_numAvailableLocations = std::ranges::count_if(m_currentSearch._visitedLocations, [](auto* location) {return location->IsProgression();}); for (auto location : locations) { // Don't show locations which aren't progression // Don't show any locations which aren't accessible if only accessible is checked if (!location->IsProgression()) { continue; } // Don't show warp portals for now either if (location->HasCategories("Warp Portal")) { continue; } int itemId = getLocationItem(location); LocationTrackerInfo info { .locationName = location->GetName(), .logicStr = location->GetComputedRequirement().to_string(), .locationItem = itemId >= 0 ? world->GetItem(itemId)->GetName() : "Unknown", .accessible = m_currentSearch._visitedLocations.contains(location) }; info.collected = isLocationObtained(location); // Gather the hint regions this location is in (set avoids duplicates) std::unordered_set regions{}; for (auto access : location->GetAccessList()) { for (const auto& region : access->GetArea()->GetHintRegions()) { regions.insert(region); } } 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); } } auto counts = m_LocationInfo | std::views::transform([](const std::pair& location) { return location.second.collectedCount; }); m_numCollectedLocations = std::accumulate(counts.begin(), counts.end(), 0); } } // namespace dusk