diff --git a/extern/CommonLibSSE-NG b/extern/CommonLibSSE-NG index e2a29de9ff..a9bc9fe07e 160000 --- a/extern/CommonLibSSE-NG +++ b/extern/CommonLibSSE-NG @@ -1 +1 @@ -Subproject commit e2a29de9ff59d998b75fbad4d7135cfef4b45ded +Subproject commit a9bc9fe07ed912aeb0319a6c1992392d720e5592 diff --git a/features/Weather Picker/CORE b/features/Weather Picker/CORE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/features/Weather Picker/Shaders/Features/WeatherPicker.ini b/features/Weather Picker/Shaders/Features/WeatherPicker.ini new file mode 100644 index 0000000000..19f01444dc --- /dev/null +++ b/features/Weather Picker/Shaders/Features/WeatherPicker.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 \ No newline at end of file diff --git a/src/Feature.cpp b/src/Feature.cpp index 4725afff4f..0cf18922a7 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -26,6 +26,7 @@ #include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "Features/WaterEffects.h" +#include "Features/WeatherPicker.h" #include "Features/WetnessEffects.h" #include "State.h" @@ -209,6 +210,7 @@ const std::vector& Feature::GetFeatureList() globals::features::dynamicCubemaps, globals::features::cloudShadows, globals::features::waterEffects, + globals::features::weatherPicker, globals::features::subsurfaceScattering, globals::features::terrainShadows, globals::features::screenSpaceGI, diff --git a/src/Feature.h b/src/Feature.h index fda2f5cb51..73b47db1bc 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -66,10 +66,8 @@ struct Feature * \return Pair containing feature summary description and vector of key feature bullet points */ virtual std::pair> GetFeatureSummary() { return {}; } - virtual void SetupResources() {} virtual void Reset() {} - virtual void DrawSettings() {} virtual void DrawUnloadedUI() { @@ -110,6 +108,29 @@ struct Feature virtual void RestoreDefaultSettings() {} virtual bool ToggleAtBootSetting(); + /** + * Weather analysis configuration for features that want to provide weather analysis. + * If sectionName is empty, the feature will not appear in weather analysis UI. + * Features should populate this struct to opt-in to weather analysis display. + */ + struct WeatherAnalysisConfig + { + std::string sectionName; // Display name for the collapsible section (empty = no weather analysis) + std::function drawFunction; // Custom draw function for weather analysis content + + // Constructor for easy initialization + WeatherAnalysisConfig() = default; + WeatherAnalysisConfig(const std::string& name, std::function drawFunc) : + sectionName(name), drawFunction(std::move(drawFunc)) {} + }; + + /** + * Get weather analysis configuration for this feature. + * Returns empty sectionName by default (no weather analysis). + * Features should override this to provide their weather analysis section name and draw function. + */ + virtual WeatherAnalysisConfig GetWeatherAnalysisConfig() const { return {}; } + virtual bool ValidateCache(CSimpleIniA& a_ini); virtual void WriteDiskCacheInfo(CSimpleIniA& a_ini); virtual void ClearShaderCache() {} diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp new file mode 100644 index 0000000000..de1d4581be --- /dev/null +++ b/src/Features/WeatherPicker.cpp @@ -0,0 +1,856 @@ +#include "WeatherPicker.h" +#include "Feature.h" +#include "Menu.h" +#include "Utils/Game.h" + +void WeatherPicker::DataLoaded() +{ + LoadAllWeathers(); +} + +std::pair> WeatherPicker::GetFeatureSummary() +{ + std::string description = "Interactive weather control system that lets you instantly change and analyze weather conditions in-game."; + + std::vector keyFeatures = { + "Instantly switch between any weather with immediate or gradual transitions", + "Filter weather by type (Pleasant, Cloudy, Rainy, Snow, Aurora) for easy browsing", + "View detailed weather information including wind, precipitation, and lightning data", + "Color-coded weather names show all weather properties at a glance", + "Persistent overlay window for continuous weather monitoring while playing" + }; + + return { description, keyFeatures }; +} + +void WeatherPicker::DrawSettings() +{ + if (ImGui::TreeNodeEx("Weather Details", ImGuiTreeNodeFlags_DefaultOpen)) { + auto menu = Menu::GetSingleton(); + const auto& themeSettings = menu->GetTheme(); + const auto& menuSettings = menu->GetSettings(); + + // Show as Overlay checkbox + bool windowEnabled = menuSettings.WeatherDetailsWindow.Enabled; + if (ImGui::Checkbox("Show as Overlay", &windowEnabled)) { + menu->GetSettings().WeatherDetailsWindow.Enabled = windowEnabled; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Opens weather details in a separate window that stays open\neven when the main menu is closed. "); + ImGui::Text("Toggle with "); + ImGui::SameLine(); + ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", Menu::KeyIdToString(menuSettings.PerfOverlay.OverlayToggleKey)); + } + ImGui::Spacing(); + + // Render core weather details + RenderCoreWeatherDetails(true); // true = show interactive elements in main settings panel + + // Render weather analysis from features with collapsible headers + RenderFeatureWeatherAnalysis(); + + ImGui::TreePop(); + } +} + +void WeatherPicker::RenderWeatherDetailsWindow(bool* open) +{ + if (!*open) + return; + + auto menu = Menu::GetSingleton(); + auto& settings = menu->GetSettings(); + + // Set initial position if not already set + if (!settings.WeatherDetailsWindow.PositionSet) { + ImGui::SetNextWindowPos(ImVec2(50.0f, 50.0f)); + settings.WeatherDetailsWindow.Position = ImVec2(50.0f, 50.0f); + settings.WeatherDetailsWindow.PositionSet = true; + } else { + ImGui::SetNextWindowPos(settings.WeatherDetailsWindow.Position, ImGuiCond_FirstUseEver); + } + + ImGui::SetNextWindowSize(ImVec2(600, 800), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Weather Details##Popup", open, ImGuiWindowFlags_None)) { + // Remember window position for next frame + ImVec2 currentPos = ImGui::GetWindowPos(); + if (currentPos.x != settings.WeatherDetailsWindow.Position.x || currentPos.y != settings.WeatherDetailsWindow.Position.y) { + settings.WeatherDetailsWindow.Position = currentPos; + } + + // Enable interactive elements when a menu is open + auto shouldEnableInteractiveElements = []() -> bool { + return (Menu::GetSingleton()->ShouldSwallowInput() || + (globals::game::ui && globals::game::ui->IsMenuOpen(RE::CursorMenu::MENU_NAME))); + }; + + RenderCoreWeatherDetails(shouldEnableInteractiveElements()); + + // Render weather analysis from features with collapsible headers + RenderFeatureWeatherAnalysis(); + } + ImGui::End(); +} + +ImVec4 WeatherPicker::GetWeatherTypeColor(RE::TESWeather* weather) +{ + if (!weather) { + return Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor; + } + + const auto& theme = Menu::GetSingleton()->GetTheme(); + + // Priority order for weather classification colors (highest priority first) + static const std::vector> priorityColors = { + { RE::TESWeather::WeatherDataFlag::kPleasant, ImVec4(0.0f, 1.0f, 0.0f, 1.0f) }, // Placeholder, will use theme + { RE::TESWeather::WeatherDataFlag::kCloudy, ImVec4(0.7f, 0.7f, 0.7f, 1.0f) }, // Gray for cloudy + { RE::TESWeather::WeatherDataFlag::kRainy, ImVec4(0.4f, 0.7f, 1.0f, 1.0f) }, // Light blue for rain + { RE::TESWeather::WeatherDataFlag::kSnow, ImVec4(0.9f, 0.9f, 1.0f, 1.0f) }, // Light blue-white for snow + { RE::TESWeather::WeatherDataFlag::kPermAurora, ImVec4(0.8f, 0.4f, 1.0f, 1.0f) }, // Purple for aurora + { RE::TESWeather::WeatherDataFlag::kAuroraFollowsSun, ImVec4(0.9f, 0.6f, 1.0f, 1.0f) } // Light purple for aurora follows sun + }; + + // Check flags in priority order + for (const auto& [flag, color] : priorityColors) { + if (weather->data.flags.any(flag)) { + // Handle theme-dependent colors + if (flag == RE::TESWeather::WeatherDataFlag::kPleasant) { + return theme.StatusPalette.SuccessColor; + } + return color; + } + } + + // Check for unclassified/unflagged weather + if (weather->data.flags.underlying() == 0) { + return ImVec4(0.9f, 0.85f, 0.7f, 1.0f); // Light tan/beige for unclassified/unflagged + } + + return theme.StatusPalette.InfoColor; // Default blue +} + +// --- Helper: Display basic weather info (name, flags, percentage) --- +void WeatherPicker::DisplayWeatherBasicInfo(RE::TESWeather* weather, float weatherPct) +{ + if (!weather) { + ImGui::BulletText("No Weather Found"); + return; + } + std::string weatherText = Util::FormatWeather(weather); + ImGui::Bullet(); + ImGui::SameLine(); + bool showTooltip = WeatherPicker::RenderMultiColorWeatherName(weather, weatherText); + if (showTooltip) { + ImGui::BeginTooltip(); + ImGui::Text("Name: %s", weather->GetName() ? weather->GetName() : "Unnamed"); + ImGui::Text("Editor ID: %s", weather->GetFormEditorID() ? weather->GetFormEditorID() : "None"); + ImGui::Text("Form ID: 0x%08X", weather->GetFormID()); + auto flagNames = WeatherPicker::GetWeatherFlagNames(weather); + if (!flagNames.empty()) { + std::string joinedFlags = flagNames[0]; + for (size_t j = 1; j < flagNames.size(); ++j) { + joinedFlags += ", " + flagNames[j]; + } + ImGui::Text("Flags: %s", joinedFlags.c_str()); + } else { + ImGui::Text("Flags: None"); + } + ImGui::EndTooltip(); + } + if (weatherPct >= 0.0f) { + ImGui::BulletText("Weather Percentage: %.1f%%", weatherPct * 100.0f); + } +} + +void WeatherPicker::DisplayPrecipitationInfo(RE::TESWeather* weather) +{ + if (!weather || !weather->precipitationData) { + ImGui::BulletText("Particle Density: No precipitation data"); + return; + } + auto particleDensity = weather->precipitationData->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; + ImGui::BulletText("Particle Density: %.3f", particleDensity); + GET_INSTANCE_MEMBER(particleTexture, weather->precipitationData) + if (particleTexture.textureName.c_str()) { + ImGui::BulletText("Particle Texture: %s", particleTexture.textureName.c_str()); + } else { + ImGui::BulletText("Particle Texture: None"); + } + uint8_t precipBeginFadeIn = weather->data.precipitationBeginFadeIn; + uint8_t precipEndFadeOut = weather->data.precipitationEndFadeOut; + float precipBeginNormalized = precipBeginFadeIn / 255.0f; + float precipEndNormalized = precipEndFadeOut / 255.0f; + ImGui::BulletText("Precip Begin Fade-In: %.3f (raw %u)", precipBeginNormalized, precipBeginFadeIn); + ImGui::BulletText("Precip End Fade-Out: %.3f (raw %u)", precipEndNormalized, precipEndFadeOut); + if (auto _tt = Util::HoverTooltipWrapper()) { + Util::DrawMultiLineTooltip({ "Precipitation fade transition parameters:", + "Begin Fade-In: Point where precipitation starts appearing", + "End Fade-Out: Point where precipitation fully disappears", + "Raw values: 0-255 (uint8), Normalized: 0.0-1.0" }); + } +} + +void WeatherPicker::DisplayLightningInfo(RE::TESWeather* weather, bool showInteractiveElements) +{ + if (!weather || weather->data.thunderLightningFrequency <= 0) + return; + auto menu = Menu::GetSingleton(); + const auto& theme = menu->GetTheme(); + uint8_t lightningR = weather->data.lightningColor.red; + uint8_t lightningG = weather->data.lightningColor.green; + uint8_t lightningB = weather->data.lightningColor.blue; + ImGui::Text("Lightning Color:"); + ImGui::SameLine(); + float lightningColor[3] = { lightningR / 255.0f, lightningG / 255.0f, lightningB / 255.0f }; + ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel; + if (!showInteractiveElements) { + flags |= ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip; + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, theme.StatusPalette.Disable.w); + } + bool colorChanged = ImGui::ColorEdit3("##LightningColor", lightningColor, flags); + if (!showInteractiveElements) { + ImGui::PopStyleVar(); + } + if (colorChanged && showInteractiveElements) { + weather->data.lightningColor.red = static_cast(lightningColor[0] * 255.0f); + weather->data.lightningColor.green = static_cast(lightningColor[1] * 255.0f); + weather->data.lightningColor.blue = static_cast(lightningColor[2] * 255.0f); + } + int8_t thunderFreqRaw = weather->data.thunderLightningFrequency; + ImGui::BulletText("Thunder Frequency: %d (signed 8-bit)", static_cast(thunderFreqRaw)); + ImGui::Indent(); + if (thunderFreqRaw >= 76) { + if (thunderFreqRaw == 76) { + ImGui::BulletText("This matches ~75%% frequency in Creation Kit"); + } else if (thunderFreqRaw > 76) { + ImGui::BulletText("High frequency range: Above 75%% (raw > 76)"); + } + } else if (thunderFreqRaw >= 15) { + if (thunderFreqRaw == 15) { + ImGui::BulletText("This matches maximum observed frequency in Creation Kit"); + } else { + ImGui::BulletText("High-medium frequency range: 75-100%% (raw 15-76)"); + } + } else if (thunderFreqRaw >= 0) { + ImGui::BulletText("Medium frequency range: 25-75%% (raw 0-15)"); + } else if (thunderFreqRaw >= -10) { + if (thunderFreqRaw == -1) { + ImGui::BulletText("This matches minimum frequency in Creation Kit (255 unsigned)"); + } else if (thunderFreqRaw == -10) { + ImGui::BulletText("This matches ~5%% frequency in Creation Kit (246 unsigned)"); + } else { + ImGui::BulletText("Low frequency range: 0-25%% (raw -10 to 0)"); + } + } else if (thunderFreqRaw >= -53) { + if (thunderFreqRaw == -53) { + ImGui::BulletText("This matches ~20%% frequency in Creation Kit (203 unsigned)"); + } else { + ImGui::BulletText("Low-medium frequency range: 5-20%% (raw -53 to -10)"); + } + } else if (thunderFreqRaw >= -100) { + ImGui::BulletText("Very low frequency range: Near 0%% (raw -100 to -53)"); + } else { + ImGui::BulletText("Extreme low frequency: Likely no thunder (raw < -100)"); + } + ImGui::Unindent(); + if (auto _tt = Util::HoverTooltipWrapper()) { + Util::DrawMultiLineTooltip({ "Thunder frequency raw value with observed Creation Kit behavior:", + "", + "Known data points from Creation Kit slider:", + "- Raw 15 = ~100% frequency (highest thunder)", + "- Raw 76 = ~75% frequency", + "- Raw -10 (246 unsigned) = ~5% frequency", + "- Raw -53 (203 unsigned) = ~20% frequency", + "- Raw -1 (255 unsigned) = ~0% frequency (lowest thunder)", + "", + "Pattern: Higher positive values = more frequent thunder", + "Lower/negative values = less frequent thunder", + "", + "Range: -128 to +127 (signed 8-bit integer)", + "Note: Creation Kit interprets this value non-linearly" }); + } + uint8_t lightningBeginFadeIn = weather->data.thunderLightningBeginFadeIn; + uint8_t lightningEndFadeOut = weather->data.thunderLightningEndFadeOut; + float lightningBeginNormalized = lightningBeginFadeIn / 255.0f; + float lightningEndNormalized = lightningEndFadeOut / 255.0f; + ImGui::BulletText("Lightning Begin Fade-In: %.3f (raw %u)", lightningBeginNormalized, lightningBeginFadeIn); + ImGui::BulletText("Lightning End Fade-Out: %.3f (raw %u)", lightningEndNormalized, lightningEndFadeOut); + if (auto _tt = Util::HoverTooltipWrapper()) { + Util::DrawMultiLineTooltip({ "Lightning fade transition parameters:", + "Begin Fade-In: Point where lightning starts appearing", + "End Fade-Out: Point where lightning fully disappears", + "Raw values: 0-255 (uint8), Normalized: 0.0-1.0" }); + } +} + +void WeatherPicker::DisplayWindInfo(RE::TESWeather* weather) +{ + auto sky = globals::game::sky; + if (!weather || (weather->data.windSpeed <= 0 && (!sky || sky->windSpeed <= 0.0f))) + return; + auto menu = Menu::GetSingleton(); + const auto& theme = menu->GetTheme(); + float windSpeedDisplay = weather->data.windSpeed / 255.0f; + ImGui::BulletText("Weather Wind Speed: %.2f (raw %d)", windSpeedDisplay, weather->data.windSpeed); + if (auto _tt = Util::HoverTooltipWrapper()) { + std::string windStr = Util::Units::FormatWindSpeed(weather->data.windSpeed); + Util::DrawMultiLineTooltip({ "Wind speed from weather definition", + windStr.c_str() }); + } + if (sky) { + ImGui::BulletText("Sky Wind Speed: %.2f", sky->windSpeed); + if (auto _tt = Util::HoverTooltipWrapper()) { + Util::DrawMultiLineTooltip({ "Current active wind speed from the sky system", + "This affects particle behavior and wind-based effects" }); + } + } + float weatherWindDirDegrees = Util::Units::DirectionRawToDegrees(weather->data.windDirection); + ImGui::BulletText("Wind Direction: %.1f° (raw %d)", weatherWindDirDegrees, weather->data.windDirection); + if (auto _tt = Util::HoverTooltipWrapper()) { + std::string dirStr = Util::Units::FormatDirection(weather->data.windDirection); + Util::DrawMultiLineTooltip({ "Wind direction from weather definition", + dirStr.c_str() }); + } + float weatherWindRangeDegrees = Util::Units::DirectionRangeToDegrees(weather->data.windDirectionRange); + ImGui::BulletText("Wind Direction Range: %.1f° (raw %d)", weatherWindRangeDegrees, weather->data.windDirectionRange); + + if (auto player = RE::PlayerCharacter::GetSingleton()) { + float playerAngleZ = player->GetAngleZ(); + float playerAngleDegrees = Util::Units::NormalizeDegrees0To360(Util::Units::RadiansToDegrees(playerAngleZ)); + ImGui::BulletText("Player Direction: %.1f°", playerAngleDegrees); + float effectiveWindDirection = Util::Units::NormalizeDegrees0To360(weatherWindDirDegrees - WIND_DIRECTION_OFFSET); + float rawDifference = Util::Units::NormalizeDegreesToSignedRange(effectiveWindDirection - playerAngleDegrees); + ImGui::BulletText("Effective Wind Dir: %.1f° (raw - %.1f°)", effectiveWindDirection, WIND_DIRECTION_OFFSET); + ImGui::BulletText("Wind vs Player: %.1f°", rawDifference); + const char* windRelation; + if (std::abs(rawDifference) < 30.0f) { + windRelation = "Tailwind (wind behind player)"; + } else if (std::abs(rawDifference) > 150.0f) { + windRelation = "Headwind (wind coming toward player)"; + } else if (rawDifference > 0) { + windRelation = "Right crosswind"; + } else { + windRelation = "Left crosswind"; + } + ImGui::SameLine(); + ImGui::TextColored(theme.StatusPalette.RestartNeeded, "(%s)", windRelation); + if (auto _tt = Util::HoverTooltipWrapper()) { + Util::DrawMultiLineTooltip({ + "Wind relative to player direction:", + "- ~0° = Tailwind (wind behind player)", + "- ~±90° = Crosswind (left/right)", + "- ~±180° = Headwind (wind coming toward player)", + }); + } + } +} +// --- Main function: now just delegates to helpers --- +void WeatherPicker::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct, bool showInteractiveElements) +{ + WeatherPicker::DisplayWeatherBasicInfo(weather, weatherPct); + WeatherPicker::DisplayPrecipitationInfo(weather); + WeatherPicker::DisplayLightningInfo(weather, showInteractiveElements); + WeatherPicker::DisplayWindInfo(weather); +} + +void WeatherPicker::RenderWeatherControls(RE::Sky* sky) +{ + // Weather Selection Section (only show interactive elements in inline mode) + static bool weatherControlsExpanded = true; + Util::DrawSectionHeader("Weather Controls", false, true, &weatherControlsExpanded); + + if (!weatherControlsExpanded) + return; + + ImGui::Text("Filter by Weather Type:"); + if (ImGui::Button("Select All")) { + s_weatherFlagFilter = ALL_WEATHER_FLAGS; // All weather flags (bits 0-6, including unclassified) + } + ImGui::SameLine(); + if (ImGui::Button("Clear All")) { + s_weatherFlagFilter = 0x00; // No flags + } + // Dynamic checkbox layout - calculate how many fit per row + float availableWidth = ImGui::GetContentRegionAvail().x; + float checkboxWidth = 80.0f; // Adjusted for "None" + int checkboxesPerRow = std::max(1, static_cast(availableWidth / checkboxWidth)); + + // Colored checkboxes with dynamic layout + struct WeatherFilter + { + const char* label; + RE::TESWeather::WeatherDataFlag flag; + bool isUnclassified; + }; + + std::vector filters = { + { "Pleasant", RE::TESWeather::WeatherDataFlag::kPleasant, false }, + { "Cloudy", RE::TESWeather::WeatherDataFlag::kCloudy, false }, + { "Rainy", RE::TESWeather::WeatherDataFlag::kRainy, false }, + { "Snow", RE::TESWeather::WeatherDataFlag::kSnow, false }, + { "Aurora", RE::TESWeather::WeatherDataFlag::kPermAurora, false }, + { "Aurora Sun", RE::TESWeather::WeatherDataFlag::kAuroraFollowsSun, false }, + { "None", RE::TESWeather::WeatherDataFlag::kNone, true } // Special case for unclassified + }; + for (size_t i = 0; i < filters.size(); ++i) { + if (i > 0 && i % checkboxesPerRow != 0) { + ImGui::SameLine(); + } + // Get color - use the helper function for consistency + ImVec4 filterColor; + if (filters[i].isUnclassified) { + filterColor = ImVec4(0.9f, 0.85f, 0.7f, 1.0f); // Light tan/beige for none/unclassified + } else { + filterColor = GetWeatherFlagColor(filters[i].flag); + } + + ImGui::PushStyleColor(ImGuiCol_Text, filterColor); + if (filters[i].isUnclassified) { + // Special handling for None filter - use CheckboxFlags for consistency + ImGui::CheckboxFlags(filters[i].label, &s_weatherFlagFilter, UNCLASSIFIED_FLAG); + if (auto _tt = Util::HoverTooltipWrapper()) { + Util::DrawMultiLineTooltip({ "Shows weathers that are not classified under any specific category.", + "Includes weathers with no flags or only untracked flags.", + "Categories tracked: Pleasant, Cloudy, Rainy, Snow, Aurora, Aurora Sun" }); + } + } else { + ImGui::CheckboxFlags(filters[i].label, &s_weatherFlagFilter, static_cast(filters[i].flag)); + } + ImGui::PopStyleColor(); + } + + // Update filtered weathers when filter changes + if (s_lastWeatherFlagFilter != s_weatherFlagFilter) { + UpdateFilteredWeathers(); + s_selectedWeatherIdx = -1; + s_lastWeatherFlagFilter = s_weatherFlagFilter; + } + + // Accelerate checkbox + ImGui::Checkbox("Accelerate Weather Change", &s_accelerateWeatherChange); + if (auto _tt = Util::HoverTooltipWrapper()) { + Util::DrawMultiLineTooltip({ "When enabled, weather changes are immediate.", + "When disabled, uses normal transition speed." }); + } // Reset Weather button + std::string resetButtonLabel = "Reset Weather"; + if (sky->defaultWeather) { + resetButtonLabel += " to " + Util::FormatWeather(sky->defaultWeather); + } + + // Color the reset button to match the default weather + if (sky->defaultWeather) { + ImVec4 weatherColor = GetWeatherTypeColor(sky->defaultWeather); + ImGui::PushStyleColor(ImGuiCol_Text, weatherColor); + } + + if (ImGui::Button(resetButtonLabel.c_str())) { + sky->ResetWeather(); + // Update the selection box to reflect the reset weather without double-applying + s_selectedWeatherIdx = FindWeatherIndex(sky->defaultWeather); + logger::info("[WeatherPicker] Reset weather to default"); + } + + if (sky->defaultWeather) { + ImGui::PopStyleColor(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + if (sky->defaultWeather) { + Util::DrawMultiLineTooltip({ "Resets to default weather:", + Util::FormatWeather(sky->defaultWeather).c_str() }); + } else { + ImGui::Text("Resets weather to default (no default weather set)"); + } + } // Weather Selection - now with colored text + std::vector weatherLabels; + weatherLabels.reserve(s_filteredWeathers.size()); + for (const auto& weather : s_filteredWeathers) { + weatherLabels.push_back(Util::FormatWeather(weather)); + } + + // Custom combo with colored text + const char* comboPreview = (s_selectedWeatherIdx >= 0 && s_selectedWeatherIdx < weatherLabels.size()) ? + weatherLabels[s_selectedWeatherIdx].c_str() : + "Select Weather"; + + if (ImGui::BeginCombo("Weather", comboPreview)) { + for (int i = 0; i < s_filteredWeathers.size(); ++i) { + const bool isSelected = (s_selectedWeatherIdx == i); + auto weather = s_filteredWeathers[i]; + ImVec4 textColor = GetWeatherTypeColor(weather); + + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + if (ImGui::Selectable(weatherLabels[i].c_str(), isSelected)) { + s_selectedWeatherIdx = i; + // Weather changed, apply it + auto selectedWeather = s_filteredWeathers[s_selectedWeatherIdx]; + sky->SetWeather(selectedWeather, true, s_accelerateWeatherChange); + logger::info("[WeatherPicker] Changed weather to: {}", Util::FormatWeather(selectedWeather)); + } + ImGui::PopStyleColor(); + // Add hover tooltip to show full weather information + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Weather: %s", weather->GetName() ? weather->GetName() : "Unnamed"); + ImGui::Text("Editor ID: %s", weather->GetFormEditorID() ? weather->GetFormEditorID() : "None"); + ImGui::Text("Form ID: 0x%08X", weather->GetFormID()); + ImGui::EndTooltip(); + } + + // Set the initial focus when opening the combo (scrolls to it) + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + ImGui::Spacing(); +} + +void WeatherPicker::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements) +{ + static bool weatherInfoExpanded = true; + Util::DrawSectionHeader("Weather Information", false, true, &weatherInfoExpanded); + + if (!weatherInfoExpanded) + return; + + // Update cache: store current lastWeather if it exists, otherwise keep the cached one + if (sky->lastWeather) { + s_cachedLastWeather = sky->lastWeather; + } + + // Use cached last weather for display if sky->lastWeather is null + RE::TESWeather* displayLastWeather = sky->lastWeather ? sky->lastWeather : s_cachedLastWeather; + + // Create resizable 2-column table for current and last weather + if (ImGui::BeginTable("WeatherComparison", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersV)) { + // Set up columns + ImGui::TableSetupColumn("Current Weather", ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableSetupColumn("Last Weather", ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableHeadersRow(); + + ImGui::TableNextRow(); + + // Current Weather Column + ImGui::TableNextColumn(); + DisplayWeatherInfo(sky->currentWeather, sky->currentWeatherPct, showInteractiveElements); + + // Last Weather Column + ImGui::TableNextColumn(); + DisplayWeatherInfo(displayLastWeather, std::abs(sky->currentWeatherPct - 1.0f), showInteractiveElements); + + ImGui::EndTable(); + } +} + +void WeatherPicker::RenderCoreWeatherDetails(bool showInteractiveElements) +{ + const auto showError = [](const char* msg) { + auto menu = Menu::GetSingleton(); + const auto& theme = menu->GetTheme(); + ImGui::TextColored(theme.StatusPalette.Error, "%s", msg); + }; + + if (auto sky = globals::game::sky) { + if (sky->mode.get() == RE::Sky::Mode::kFull) { + if (showInteractiveElements) { + RenderWeatherControls(sky); + } + RenderWeatherInformationDisplay(sky, showInteractiveElements); + ImGui::Spacing(); + } else { + showError("Sky not in full mode"); + } + } else { + showError("Sky not available"); + } +} + +void WeatherPicker::LoadAllWeathers() +{ + if (s_weathersLoaded) + return; + + auto dataHandler = RE::TESDataHandler::GetSingleton(); + if (dataHandler) { + auto& weatherArray = dataHandler->GetFormArray(); + s_allWeathers.clear(); + s_allWeathers.reserve(weatherArray.size()); + for (auto weather : weatherArray) { + if (weather) { + s_allWeathers.push_back(weather); + } + } + + // Sort by name, then editorID, then formID for consistent ordering + std::sort(s_allWeathers.begin(), s_allWeathers.end(), WeatherNameComparator{}); + s_weathersLoaded = true; + // Initial population of filtered weathers + UpdateFilteredWeathers(); + } +} + +void WeatherPicker::UpdateFilteredWeathers() +{ + s_filteredWeathers.clear(); + for (auto weather : s_allWeathers) { + bool shouldInclude = false; + + // Check if all filters are selected (0x7F = all 7 bits) + if (s_weatherFlagFilter == ALL_WEATHER_FLAGS) { + shouldInclude = true; + } else { + // Check regular weather flags + uint32_t weatherFlags = weather->data.flags.underlying(); + if ((weatherFlags & (s_weatherFlagFilter & 0x3F)) != 0) { + shouldInclude = true; + } + + // Check for None filter (bit 6) - includes weathers that don't match any of our tracked flags + if (s_weatherFlagFilter & UNCLASSIFIED_FLAG) { + // Define the mask for all the specific weather flags we track + uint32_t trackedFlags = static_cast(RE::TESWeather::WeatherDataFlag::kPleasant) | + static_cast(RE::TESWeather::WeatherDataFlag::kCloudy) | + static_cast(RE::TESWeather::WeatherDataFlag::kRainy) | + static_cast(RE::TESWeather::WeatherDataFlag::kSnow) | + static_cast(RE::TESWeather::WeatherDataFlag::kPermAurora) | + static_cast(RE::TESWeather::WeatherDataFlag::kAuroraFollowsSun); + + // Include if weather has no flags or only has flags we don't track + if ((weatherFlags & trackedFlags) == 0) { + shouldInclude = true; + } + } + } + + if (shouldInclude) { + s_filteredWeathers.push_back(weather); + } + } + + // Sort filtered weathers using the same comparator + std::sort(s_filteredWeathers.begin(), s_filteredWeathers.end(), WeatherNameComparator{}); +} + +int WeatherPicker::FindWeatherIndex(RE::TESWeather* targetWeather) +{ + if (!targetWeather) + return -1; + for (size_t i = 0; i < s_filteredWeathers.size(); ++i) { + if (s_filteredWeathers[i] == targetWeather) { + return static_cast(i); + } + } + return -1; +} + +void WeatherPicker::RenderFeatureWeatherAnalysis() +{ + // Iterate through all loaded features to show their weather analysis + for (auto* feature : Feature::GetFeatureList()) { + if (feature->loaded) { + // Skip the WeatherPicker itself to avoid recursion + if (feature == WeatherPicker::GetSingleton()) { + continue; + } + + // Check if this feature provides weather analysis + auto weatherConfig = feature->GetWeatherAnalysisConfig(); + if (weatherConfig.sectionName.empty()) { + continue; // Skip features that don't provide weather analysis + } + + auto featureName = feature->GetShortName(); + ImGui::PushID(featureName.c_str()); + + // Create collapsible header for feature weather analysis + bool isExpanded = ImGui::CollapsingHeader(weatherConfig.sectionName.c_str(), ImGuiTreeNodeFlags_DefaultOpen); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Weather analysis provided by: %s", feature->GetName().c_str()); + ImGui::Text("Feature category: %s", std::string(feature->GetCategory()).c_str()); + ImGui::Text("Click to %s this feature's weather data", isExpanded ? "collapse" : "expand"); + } + + if (isExpanded && weatherConfig.drawFunction) { + // Call the feature's weather analysis draw function + weatherConfig.drawFunction(); + } + + ImGui::PopID(); + } + } +} + +std::vector WeatherPicker::GetWeatherFlagNames(RE::TESWeather* weather) +{ + std::vector flagNames; + if (!weather) { + return flagNames; + } + + uint32_t flags = weather->data.flags.underlying(); + if (flags == 0) { + flagNames.push_back("None"); + return flagNames; + } + + // Use magic_enum to iterate through all weather flags + for (auto flagValue : magic_enum::enum_values()) { + if (flagValue != RE::TESWeather::WeatherDataFlag::kNone && + weather->data.flags.any(flagValue)) { + // Convert enum name to human-readable format + std::string flagName = std::string(magic_enum::enum_name(flagValue)); + + // Remove 'k' prefix and convert to readable format + if (flagName.starts_with("k")) { + flagName = flagName.substr(1); + } + + // Convert specific cases to more readable names + if (flagName == "PermAurora") { + flagName = "Aurora"; + } else if (flagName == "AuroraFollowsSun") { + flagName = "Aurora Sun"; + } + + flagNames.push_back(flagName); + } + } + + // Check for any unknown flags (flags not covered by the enum) + uint32_t knownFlags = 0; + for (auto flagValue : magic_enum::enum_values()) { + if (flagValue != RE::TESWeather::WeatherDataFlag::kNone) { + knownFlags |= static_cast(flagValue); + } + } + + uint32_t unknownFlags = flags & ~knownFlags; + if (unknownFlags != 0) { + flagNames.push_back("Unknown(" + std::to_string(unknownFlags) + ")"); + } + + return flagNames; +} + +bool WeatherPicker::RenderMultiColorWeatherName(RE::TESWeather* weather, const std::string& weatherName) +{ + if (!weather) { + ImGui::Text("%s", weatherName.c_str()); + return false; + } + + // Get all flags present in this weather + std::vector flagNames = GetWeatherFlagNames(weather); + + // If no flags or only one flag, use simple single-color display + if (flagNames.empty() || flagNames.size() == 1 || (flagNames.size() == 1 && flagNames[0] == "None")) { + ImVec4 weatherColor = GetWeatherTypeColor(weather); + ImGui::PushStyleColor(ImGuiCol_Text, weatherColor); + ImGui::Text("%s", weatherName.c_str()); + ImGui::PopStyleColor(); + return ImGui::IsItemHovered(); + } + // For multiple flags, create a color-coded display + // We'll show the weather name in segments, each with its own color + + // Create a visual representation with colored segments + // Format: "WeatherName [Flag1][Flag2][Flag3]" + + // Display the main weather name in the primary color (highest priority flag) + ImVec4 primaryColor = GetWeatherTypeColor(weather); + ImGui::PushStyleColor(ImGuiCol_Text, primaryColor); + + // Extract base weather name (without the flag suffix) + std::string baseName = weatherName; + size_t bracketPos = baseName.find(" ["); + if (bracketPos != std::string::npos) { + baseName = baseName.substr(0, bracketPos); + } + + ImGui::Text("%s", baseName.c_str()); + ImGui::PopStyleColor(); + + // Check if the main weather name (the most important part) was hovered + bool baseNameHovered = ImGui::IsItemHovered(); + + // Display flags as colored chips on the same line + ImGui::SameLine(); + ImGui::Text(" "); + + for (size_t i = 0; i < flagNames.size(); ++i) { + if (flagNames[i] == "None" || flagNames[i].find("Unknown") == 0) { + continue; // Skip "None" and "Unknown" flags for cleaner display + } + + ImGui::SameLine(); + ImVec4 flagColor = GetWeatherFlagColorByName(flagNames[i]); + ImGui::PushStyleColor(ImGuiCol_Text, flagColor); + ImGui::Text("[%s]", flagNames[i].c_str()); + ImGui::PopStyleColor(); + } + + // Return true if the base name (largest, most visible part) was hovered + return baseNameHovered; +} + +// Helper function to get color for a specific weather flag +ImVec4 WeatherPicker::GetWeatherFlagColor(RE::TESWeather::WeatherDataFlag flag) +{ + const auto& theme = Menu::GetSingleton()->GetTheme(); + + switch (flag) { + case RE::TESWeather::WeatherDataFlag::kRainy: + return ImVec4(0.4f, 0.7f, 1.0f, 1.0f); // Light blue for rain + case RE::TESWeather::WeatherDataFlag::kSnow: + return ImVec4(0.9f, 0.9f, 1.0f, 1.0f); // Light blue-white for snow + case RE::TESWeather::WeatherDataFlag::kPermAurora: + return ImVec4(0.8f, 0.4f, 1.0f, 1.0f); // Purple for aurora + case RE::TESWeather::WeatherDataFlag::kAuroraFollowsSun: + return ImVec4(0.9f, 0.6f, 1.0f, 1.0f); // Light purple for aurora follows sun + case RE::TESWeather::WeatherDataFlag::kCloudy: + return ImVec4(0.7f, 0.7f, 0.7f, 1.0f); // Gray for cloudy + case RE::TESWeather::WeatherDataFlag::kPleasant: + return theme.StatusPalette.SuccessColor; // Green for pleasant + default: + return theme.StatusPalette.InfoColor; // Default blue + } +} + +// Helper function to get color for a specific flag name +ImVec4 WeatherPicker::GetWeatherFlagColorByName(const std::string& flagName) +{ + // Map display flag names back to enum values + // Note: We use manual mapping here because the display names (from GetWeatherFlagNames) + // are transformed from the original enum names (e.g., "kRainy" -> "Rainy") + static const std::unordered_map flagNameMap = { + { "Rainy", RE::TESWeather::WeatherDataFlag::kRainy }, + { "Snow", RE::TESWeather::WeatherDataFlag::kSnow }, + { "Aurora", RE::TESWeather::WeatherDataFlag::kPermAurora }, + { "Aurora Sun", RE::TESWeather::WeatherDataFlag::kAuroraFollowsSun }, + { "Cloudy", RE::TESWeather::WeatherDataFlag::kCloudy }, + { "Pleasant", RE::TESWeather::WeatherDataFlag::kPleasant } + }; + + auto it = flagNameMap.find(flagName); + if (it != flagNameMap.end()) { + return GetWeatherFlagColor(it->second); + } + + // Default for unclassified or unknown flags + return ImVec4(0.9f, 0.85f, 0.7f, 1.0f); // Light tan/beige for none/unclassified +} + +std::string WeatherPicker::GetDisplayName(const RE::TESWeather* weather) +{ + const char* name = weather->GetName(); + if (name && strlen(name) > 0) { + return std::string(name); + } + const char* editorID = weather->GetFormEditorID(); + if (editorID && strlen(editorID) > 0) { + return std::string(editorID); + } + return std::to_string(weather->GetFormID()); +} \ No newline at end of file diff --git a/src/Features/WeatherPicker.h b/src/Features/WeatherPicker.h new file mode 100644 index 0000000000..0eabcf8c0c --- /dev/null +++ b/src/Features/WeatherPicker.h @@ -0,0 +1,111 @@ +#pragma once + +#include "Feature.h" + +struct WeatherPicker : Feature +{ + static WeatherPicker* GetSingleton() + { + static WeatherPicker singleton; + return &singleton; + } + + // Virtual overrides in Feature.h order + std::string GetName() override { return "Weather Picker"; } + std::string GetShortName() override { return "WeatherPicker"; } + + virtual bool SupportsVR() override { return true; } + virtual bool IsCore() const override { return true; } + virtual std::string_view GetCategory() const override { return "Debug"; } + virtual bool IsInMenu() const override { return true; } // Show in main menu to provide weather debugging UI + + virtual std::pair> GetFeatureSummary() override; + virtual void DrawSettings() override; + + virtual void DataLoaded() override; + + // WeatherPicker-specific methods + void RenderWeatherDetailsWindow(bool* open); + + // Core weather display functions that other features can use + static void DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct = -1.0f, bool showInteractiveElements = true); + static void RenderCoreWeatherDetails(bool showInteractiveElements = true); + static void RenderFeatureWeatherAnalysis(); + + // --- Refactor helpers for RenderCoreWeatherDetails --- + static void RenderWeatherControls(RE::Sky* sky); + static void RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements = true); + +public: + /** + * Gets the appropriate color for a weather type based on its flags. + * Uses a priority system: Rain > Snow > Aurora > Aurora Follows Sun > Cloudy > Pleasant > Unclassified > Default + * @param weather Pointer to the weather object + * @return ImVec4 color appropriate for the weather type + */ + static ImVec4 GetWeatherTypeColor(RE::TESWeather* weather); + /** + * Renders a weather name with multiple colors if the weather has multiple flags. + * Each flag gets its own color segment in the weather name display. + * @param weather Pointer to the weather object + * @param weatherName The formatted weather name to display + * @return true if the main weather name (base name) was hovered, false otherwise + */ + static bool RenderMultiColorWeatherName(RE::TESWeather* weather, const std::string& weatherName); + + /** + * Get the color associated with a specific weather flag. + * @param flag The weather flag to get the color for + * @return ImVec4 color for the flag + */ + static ImVec4 GetWeatherFlagColor(RE::TESWeather::WeatherDataFlag flag); + + /** + * Get the color associated with a specific weather flag by name. + * @param flagName The name of the flag to get the color for + * @return ImVec4 color for the flag + */ + static ImVec4 GetWeatherFlagColorByName(const std::string& flagName); + +private: + // Wind direction offset to align with game's coordinate system + static constexpr float WIND_DIRECTION_OFFSET = 30.5f; + + // Weather flag filter bits (for 7 weather types) + static constexpr uint32_t ALL_WEATHER_FLAGS = 0x7F; // Bits 0-6 all enabled + static constexpr uint32_t UNCLASSIFIED_FLAG = 0x40; // Bit 6 only + + // Static state for weather picker and data + static inline bool s_weathersLoaded = false; + static inline std::vector s_allWeathers; + static inline std::vector s_filteredWeathers; + static inline int s_selectedWeatherIdx = -1; + static inline uint32_t s_weatherFlagFilter = ALL_WEATHER_FLAGS; // Start with all filters enabled by default (bits 0-6) + static inline uint32_t s_lastWeatherFlagFilter = UNCLASSIFIED_FLAG; + static inline bool s_accelerateWeatherChange = true; + static inline RE::TESWeather* s_cachedLastWeather = nullptr; + + // Static helper for display name extraction + static std::string GetDisplayName(const RE::TESWeather* weather); + + // Weather comparator for consistent sorting + struct WeatherNameComparator + { + bool operator()(const RE::TESWeather* a, const RE::TESWeather* b) const + { + return WeatherPicker::GetDisplayName(a) < WeatherPicker::GetDisplayName(b); + } + }; + + // --- Refactor helpers for DisplayWeatherInfo --- + static void DisplayWeatherBasicInfo(RE::TESWeather* weather, float weatherPct); + static void DisplayPrecipitationInfo(RE::TESWeather* weather); + static void DisplayLightningInfo(RE::TESWeather* weather, bool showInteractiveElements); + static void DisplayWindInfo(RE::TESWeather* weather); + + // Helper functions + static void LoadAllWeathers(); + static void UpdateFilteredWeathers(); + static int FindWeatherIndex(RE::TESWeather* targetWeather); + static std::vector GetWeatherFlagNames(RE::TESWeather* weather); +}; diff --git a/src/Globals.cpp b/src/Globals.cpp index e0b135d5bf..5f9b0d4607 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -35,6 +35,7 @@ #include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "Features/WaterEffects.h" +#include "Features/WeatherPicker.h" #include "Features/WetnessEffects.h" #include "Features/LightLimitFix/ParticleLights.h" @@ -75,6 +76,7 @@ namespace globals VolumetricLighting* volumetricLighting = nullptr; VR* vr = nullptr; WaterEffects* waterEffects = nullptr; + WeatherPicker* weatherPicker = nullptr; WetnessEffects* wetnessEffects = nullptr; ExtendedTranslucency* extendedTranslucency = nullptr; @@ -159,6 +161,7 @@ namespace globals features::volumetricLighting = VolumetricLighting::GetSingleton(); features::vr = VR::GetSingleton(); features::waterEffects = WaterEffects::GetSingleton(); + features::weatherPicker = WeatherPicker::GetSingleton(); features::wetnessEffects = WetnessEffects::GetSingleton(); features::extendedTranslucency = ExtendedTranslucency::GetSingleton(); diff --git a/src/Globals.h b/src/Globals.h index b6a397bd8f..75a6cb0a19 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -23,6 +23,7 @@ struct TerrainShadows; struct VolumetricLighting; struct VR; struct WaterEffects; +struct WeatherPicker; struct WetnessEffects; struct ExtendedTranslucency; @@ -76,6 +77,7 @@ namespace globals extern VolumetricLighting* volumetricLighting; extern VR* vr; extern WaterEffects* waterEffects; + extern WeatherPicker* weatherPicker; extern WetnessEffects* wetnessEffects; extern ExtendedTranslucency* extendedTranslucency; diff --git a/src/Menu.cpp b/src/Menu.cpp index d0860784f9..a1ff9d6420 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -22,6 +22,7 @@ #include "Utils/UI.h" #include "Features/LightLimitFix/ParticleLights.h" +#include "Features/WeatherPicker.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, @@ -66,6 +67,12 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( PositionSet, OverlayToggleKey) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Menu::Settings::WeatherDetailsWindowSettings, + Enabled, + Position, + PositionSet) + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ImGuiStyle, WindowPadding, @@ -1625,6 +1632,9 @@ void Menu::DrawOverlay() if (settings.PerfOverlay.Enabled) DrawPerfOverlay(); + // Draw weather details window independently of main menu + DrawWeatherDetailsWindow(); + if (inTestMode) { // In test mode float seconds = (float)duration_cast(high_resolution_clock::now() - lastTestSwitch).count() / 1000.0f; auto remaining = (float)testInterval - seconds; @@ -2716,6 +2726,20 @@ void Menu::SelectFeatureMenu(const std::string& featureName) logger::info("Queued navigation to {} feature menu", featureName); } +void Menu::DrawWeatherDetailsWindow() +{ + if (!settings.WeatherDetailsWindow.Enabled) { + return; + } + + // Use Weather core feature for all window management and rendering + auto weather = globals::features::weatherPicker; + if (weather) { + bool* p_open = &settings.WeatherDetailsWindow.Enabled; + weather->RenderWeatherDetailsWindow(p_open); + } +} + void Menu::BuildCategoryCounts() { const std::vector& features = Feature::GetFeatureList(); @@ -2726,4 +2750,4 @@ void Menu::BuildCategoryCounts() categoryCounts[std::string(category)]++; } } -} \ No newline at end of file +} diff --git a/src/Menu.h b/src/Menu.h index 5d5d47c6dd..e6020e510e 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -43,6 +43,7 @@ class Menu void DrawSettings(); void DrawOverlay(); void DrawPerfOverlay(); + void DrawWeatherDetailsWindow(); void ProcessInputEvents(RE::InputEvent* const* a_events); bool ShouldSwallowInput(); @@ -210,13 +211,23 @@ class Menu bool PositionSet = false; uint32_t OverlayToggleKey = VK_F10; } PerfOverlay; - }; + struct WeatherDetailsWindowSettings + { + bool Enabled = false; + ImVec2 Position = ImVec2(50.f, 50.f); + bool PositionSet = false; + } WeatherDetailsWindow; + }; const ThemeSettings& GetTheme() const { return settings.Theme; } // Provide read-only access to the Theme. + Settings& GetSettings() { return settings; } // Provide access to settings for other components void SelectFeatureMenu(const std::string& featureName); static std::unordered_map categoryCounts; // Number of features in each feature category + // Static utility functions + static const char* KeyIdToString(uint32_t key); + private: Settings settings; @@ -279,7 +290,6 @@ class Menu Menu() = default; void SetupImGuiStyle() const; - const char* KeyIdToString(uint32_t key); const ImGuiKey VirtualKeyToImGuiKey(WPARAM vkKey); void DrawGeneralSettings(); diff --git a/src/Utils/Game.cpp b/src/Utils/Game.cpp index 7ad5ce6c13..a4dd810672 100644 --- a/src/Utils/Game.cpp +++ b/src/Utils/Game.cpp @@ -178,6 +178,104 @@ namespace Util return *bDynamicResolution; } + std::string FormatTESForm(const RE::TESForm* form) + { + if (!form) { + return "nullptr"; + } + + // Get name and editor ID + const char* rawName = form->GetName(); + const char* rawEditorID = form->GetFormEditorID(); + + std::string name; + std::string editorID = rawEditorID ? rawEditorID : "Unknown"; + + // Check if name exists and is not just whitespace + if (rawName && strlen(rawName) > 0) { + std::string tempName(rawName); + // Check if name is only whitespace + bool isOnlyWhitespace = std::all_of(tempName.begin(), tempName.end(), + [](unsigned char c) { return std::isspace(c); }); + + if (!isOnlyWhitespace) { + name = tempName; + } + } + // Format the FormID part once + std::string formIDStr = " - 0x" + std::format("{:08X}", form->GetFormID()); + + // If no valid name, use editor ID as name and don't show it twice + if (name.empty()) { + return editorID + formIDStr; + } else { + return name + " " + editorID + formIDStr; + } + } + std::string FormatWeather(const RE::TESWeather* weather) + { + if (!weather) { + return "nullptr"; + } + + std::string baseFormat = FormatTESForm(weather); + + // Get all flag names for this weather using magic_enum + std::vector flagNames; + uint32_t flags = weather->data.flags.underlying(); + + if (flags == 0) { + flagNames.push_back("None"); + } else { + // Use magic_enum to iterate through all weather flags + for (auto flagValue : magic_enum::enum_values()) { + if (flagValue != RE::TESWeather::WeatherDataFlag::kNone && + weather->data.flags.any(flagValue)) { + // Convert enum name to human-readable format + std::string flagName = std::string(magic_enum::enum_name(flagValue)); + + // Remove 'k' prefix and convert to readable format + if (flagName.starts_with("k")) { + flagName = flagName.substr(1); + } + + // Convert specific cases to more readable names + if (flagName == "PermAurora") { + flagName = "Aurora"; + } else if (flagName == "AuroraFollowsSun") { + flagName = "Aurora Sun"; + } + + flagNames.push_back(flagName); + } + } + + // Check for any unknown flags (flags not covered by the enum) + uint32_t knownFlags = 0; + for (auto flagValue : magic_enum::enum_values()) { + if (flagValue != RE::TESWeather::WeatherDataFlag::kNone) { + knownFlags |= static_cast(flagValue); + } + } + + uint32_t unknownFlags = flags & ~knownFlags; + if (unknownFlags != 0) { + flagNames.push_back("Unknown(" + std::to_string(unknownFlags) + ")"); + } + } + + // Join flag names with commas + std::string flagsStr; + for (size_t i = 0; i < flagNames.size(); ++i) { + if (i > 0) { + flagsStr += ", "; + } + flagsStr += flagNames[i]; + } + + return baseFormat + " [" + flagsStr + "]"; + } + bool FrameChecker::IsNewFrame() { return IsNewFrame(globals::state->frameCount); diff --git a/src/Utils/Game.h b/src/Utils/Game.h index 6b5bf09fba..474555362c 100644 --- a/src/Utils/Game.h +++ b/src/Utils/Game.h @@ -43,6 +43,77 @@ namespace Util float2 ConvertToDynamic(float2 a_size); + // Game unit conversions + namespace Units + { + // Conversion constants + constexpr float GAME_UNIT_TO_CM = 1.428f; + constexpr float GAME_UNIT_TO_M = GAME_UNIT_TO_CM / 100.0f; + constexpr float GAME_UNIT_TO_FEET = GAME_UNIT_TO_CM / 30.48f; + constexpr float GAME_UNIT_TO_INCHES = GAME_UNIT_TO_CM / 2.54f; + + // Wind speed conversions + constexpr float WIND_RAW_TO_NORMALIZED = 1.0f / 255.0f; // Raw to 0-1 scale + constexpr float WIND_RAW_TO_PERCENT = 100.0f / 255.0f; // Raw to percentage + + // Direction conversions + constexpr float DIR_RAW_TO_DEGREES = 360.0f / 256.0f; // Raw 0-256 to 0-360 degrees + constexpr float DIR_RANGE_TO_DEGREES = 180.0f / 256.0f; // Range 0-256 to 0-180 degrees + constexpr float RADIANS_TO_DEGREES = 180.0f / DirectX::XM_PI; + + // Distance conversions + inline float GameUnitsToMeters(float gameUnits) { return gameUnits * GAME_UNIT_TO_M; } + inline float GameUnitsToCm(float gameUnits) { return gameUnits * GAME_UNIT_TO_CM; } + inline float GameUnitsToFeet(float gameUnits) { return gameUnits * GAME_UNIT_TO_FEET; } + inline float GameUnitsToInches(float gameUnits) { return gameUnits * GAME_UNIT_TO_INCHES; } + + // Wind speed conversions + inline float WindRawToNormalized(uint8_t rawWind) { return rawWind * WIND_RAW_TO_NORMALIZED; } + inline float WindRawToPercent(uint8_t rawWind) { return rawWind * WIND_RAW_TO_PERCENT; } + + // Direction conversions + inline float DirectionRawToDegrees(uint8_t rawDirection) { return rawDirection * DIR_RAW_TO_DEGREES; } + inline float DirectionRangeToDegrees(uint8_t rawRange) { return rawRange * DIR_RANGE_TO_DEGREES; } + inline float RadiansToDegrees(float radians) { return radians * RADIANS_TO_DEGREES; } + + // Angle normalization helpers + inline float NormalizeDegrees0To360(float degrees) + { + if (!std::isfinite(degrees)) + return 0.0f; + while (degrees < 0.0f) degrees += 360.0f; + while (degrees >= 360.0f) degrees -= 360.0f; + return degrees; + } + + inline float NormalizeDegreesToSignedRange(float degrees) + { + if (!std::isfinite(degrees)) + return 0.0f; + while (degrees > 180.0f) degrees -= 360.0f; + while (degrees < -180.0f) degrees += 360.0f; + return degrees; + } + + // Formatted string helpers for tooltips + inline std::string FormatDistance(float gameUnits) + { + return std::format("{:.1f} units ({:.2f} m, {:.1f} ft)", + gameUnits, GameUnitsToMeters(gameUnits), GameUnitsToFeet(gameUnits)); + } + inline std::string FormatWindSpeed(uint8_t rawWind) + { + return std::format("{:.1f}% (raw {}, {:.2f} normalized)", + WindRawToPercent(rawWind), rawWind, WindRawToNormalized(rawWind)); + } + + inline std::string FormatDirection(uint8_t rawDirection) + { + return std::format("{:.1f}° (raw {})", + DirectionRawToDegrees(rawDirection), rawDirection); + } + } + struct DispatchCount { uint x; @@ -92,4 +163,9 @@ namespace Util * textureSet is nullptr. */ [[nodiscard]] RE::BGSTextureSet* GetSeasonalSwap(RE::BGSTextureSet* textureSet); + + // TESForm formatting helpers + std::string FormatTESForm(const RE::TESForm* form); + std::string FormatWeather(const RE::TESWeather* weather); + } // namespace Util diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 75df17b48f..e0b925f557 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -407,41 +407,145 @@ namespace Util return clicked; } - void DrawSectionHeader(const char* sectionName, bool useWhiteText) + bool DrawSectionHeader(const char* sectionName, bool useWhiteText, bool isCollapsible, bool* isExpanded) { - // Draw custom styled header similar to CategoryHeader but non-collapsible - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 pos = ImGui::GetCursorScreenPos(); - float availableWidth = ImGui::GetContentRegionAvail().x; - ImVec2 textSize = ImGui::CalcTextSize(sectionName); + bool stateChanged = false; - // Calculate line positions - float lineY = pos.y + textSize.y * 0.5f; - float lineLength = (availableWidth - textSize.x - 20.0f) * 0.5f; // 20px for padding // Use Menu theme colors for consistent styling auto& theme = Menu::GetSingleton()->GetTheme().FeatureHeading; - ImVec4 color = theme.ColorDefault; - if (useWhiteText) { - color.w = color.w; - } + ImVec4 color = useWhiteText ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : theme.ColorDefault; + ImU32 headerColor = ImGui::GetColorU32(color); - // Left line - if (lineLength > 0) { - drawList->AddLine(ImVec2(pos.x, lineY), ImVec2(pos.x + lineLength, lineY), headerColor, 1.0f); + if (isCollapsible && isExpanded) { + // Use collapsible header similar to DrawCategoryHeader + ImGui::PushID(sectionName); + + ImGui::PushStyleColor(ImGuiCol_Text, headerColor); + + if (ImGui::CollapsingHeader(sectionName, ImGuiTreeNodeFlags_DefaultOpen)) { + if (!*isExpanded) { + stateChanged = true; + } + *isExpanded = true; + } else { + if (*isExpanded) { + stateChanged = true; + } + *isExpanded = false; + } + + ImGui::PopStyleColor(); + ImGui::PopID(); + } else { + // Non-collapsible header - use custom styled header similar to CategoryHeader + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 pos = ImGui::GetCursorScreenPos(); + float availableWidth = ImGui::GetContentRegionAvail().x; + ImVec2 textSize = ImGui::CalcTextSize(sectionName); + + // Calculate line positions + float lineY = pos.y + textSize.y * 0.5f; + float lineLength = (availableWidth - textSize.x - 20.0f) * 0.5f; // 20px for padding + + // Left line + if (lineLength > 0) { + drawList->AddLine(ImVec2(pos.x, lineY), ImVec2(pos.x + lineLength, lineY), headerColor, 1.0f); + } + + // Right line + float rightLineStart = pos.x + lineLength + 10.0f + textSize.x + 10.0f; + if (rightLineStart < pos.x + availableWidth) { + drawList->AddLine(ImVec2(rightLineStart, lineY), ImVec2(pos.x + availableWidth, lineY), headerColor, 1.0f); + } + + // Center text + ImVec2 textPos = ImVec2(pos.x + lineLength + 10.0f, pos.y + 2.0f); + drawList->AddText(textPos, headerColor, sectionName); + + // Move cursor to next line + ImGui::SetCursorScreenPos(ImVec2(pos.x, pos.y + textSize.y + 8.0f)); } - // Right line - float rightLineStart = pos.x + lineLength + 10.0f + textSize.x + 10.0f; - if (rightLineStart < pos.x + availableWidth) { - drawList->AddLine(ImVec2(rightLineStart, lineY), ImVec2(pos.x + availableWidth, lineY), headerColor, 1.0f); + return stateChanged; + } + + // ColorCodedValueConfig static helper implementations + ColorCodedValueConfig ColorCodedValueConfig::HighIsBad(float low, float med, float high) + { + ColorCodedValueConfig config; + const auto& theme = Menu::GetSingleton()->GetTheme().StatusPalette; + config.thresholds = { + { low, theme.Disable }, // Very low - gray + { med, theme.InfoColor }, // Low - blue + { high, theme.Warning }, // Medium - orange + { FLT_MAX, theme.Error } // High - red (bad) + }; + return config; + } + + ColorCodedValueConfig ColorCodedValueConfig::HighIsGood(float low, float med, float high) + { + ColorCodedValueConfig config; + const auto& theme = Menu::GetSingleton()->GetTheme().StatusPalette; + config.thresholds = { + { low, theme.Disable }, // Very low - gray + { med, theme.InfoColor }, // Low - blue + { high, theme.Warning }, // Medium - orange + { FLT_MAX, theme.SuccessColor } // High - green (good) + }; + return config; + } + + void DrawColorCodedValue( + const std::string& label, + float valueToCheck, + const std::string& valueStr, + const ColorCodedValueConfig& config, + bool useBullet) + { + // Display label + if (useBullet) { + ImGui::BulletText("%s", label.c_str()); + } else { + ImGui::Text("%s", label.c_str()); + } + if (config.sameLine) { + ImGui::SameLine(); } - // Center text - ImVec2 textPos = ImVec2(pos.x + lineLength + 10.0f, pos.y + 2.0f); - drawList->AddText(textPos, headerColor, sectionName); + // Determine color based on thresholds + ImVec4 valueColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // Default white + for (const auto& tc : config.thresholds) { + if (valueToCheck < tc.threshold) { + valueColor = tc.color; + break; + } + } - // Move cursor to next line - ImGui::SetCursorScreenPos(ImVec2(pos.x, pos.y + textSize.y + 8.0f)); + // Display colored value (arbitrary string) + ImGui::TextColored(valueColor, "%s", valueStr.c_str()); + + // Add tooltip if provided + if (config.tooltipText) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", config.tooltipText); + } + } } -} // namespace Util + + void DrawMultiLineTooltip(const std::vector& lines, const std::vector& colors) + { + for (size_t i = 0; i < lines.size(); ++i) { + const char* lineCStr = lines[i].c_str(); + if (!colors.empty() && i < colors.size()) { + // Use provided color for this line + ImGui::TextColored(colors[i], "%s", lineCStr); + } else { + // Use default color + ImGui::Text("%s", lineCStr); + } + } + } + +} // namespace Util \ No newline at end of file diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 397a5e5c6e..4fc41990a9 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -140,11 +140,57 @@ namespace Util bool DrawCategoryHeader(const char* categoryName, bool& isExpanded, int categoryCount); /** - * Draws a custom styled section header (non-collapsible) with lines extending from both sides + * Draws a custom styled section header with lines extending from both sides * @param sectionName The name of the section to display * @param useWhiteText Whether to use white text (for differentiation) + * @param isCollapsible Whether the header should be collapsible + * @param isExpanded Reference to the expansion state (only used if collapsible) + * @return true if the expansion state was toggled (only relevant if collapsible) */ - void DrawSectionHeader(const char* sectionName, bool useWhiteText = false); + bool DrawSectionHeader(const char* sectionName, bool useWhiteText = false, bool isCollapsible = true, bool* isExpanded = nullptr); + + /** + * Configuration for color-coded value display with flexible thresholds and colors. + * Supports variable number of thresholds and corresponding colors. + */ + struct ColorCodedValueConfig + { + struct ThresholdColor + { + float threshold; + ImVec4 color; + + ThresholdColor(float t, const ImVec4& c) : + threshold(t), color(c) {} + }; + + std::vector thresholds; // Thresholds in ascending order with their colors + const char* format = "%.1f%%"; // Printf-style format string for the value + const char* tooltipText = nullptr; // Optional tooltip text + bool sameLine = true; // Whether to put value on same line as label + + // Helper methods for common patterns (implemented in UI.cpp to avoid header dependencies) + // Use when higher values indicate problems/danger (intensity, errors, warnings) + static ColorCodedValueConfig HighIsBad(float low, float med, float high); + // Use when higher values indicate good things (performance, quality, progress) + static ColorCodedValueConfig HighIsGood(float low, float med, float high); + }; + /** + * Color-codes a value based on flexible thresholds and displays it with optional tooltip. + * Common pattern for showing status values (percentages, intensities, etc.) with color feedback. + * + * @param label The label to display next to the value. + * @param valueToCheck The numeric value to use for color-coding (compared to thresholds). + * @param valueStr The string to display (can be formatted, units, or descriptive text). + * @param config The configuration for thresholds, colors, formatting, and tooltip. + * @param useBullet If true (default), use ImGui::BulletText for the label; if false, use ImGui::Text. + */ + void DrawColorCodedValue( + const std::string& label, + float valueToCheck, + const std::string& valueStr, + const ColorCodedValueConfig& config, + bool useBullet = true); class PerformanceOverlay { @@ -160,4 +206,11 @@ namespace Util } }; extern PerformanceOverlay performanceOverlay; + + /** + * @brief Draws a multi-line tooltip with optional per-line coloring. + * @param lines The lines of text to display in the tooltip (as std::vector). + * @param colors Optional per-line colors (if empty, default color is used for all lines). + */ + void DrawMultiLineTooltip(const std::vector& lines, const std::vector& colors = {}); } // namespace Util