From 9a5ca260b7764d1eb193989315495752a9fd5349 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 20 Jun 2025 21:16:54 -0700 Subject: [PATCH 01/16] feat: Add Weather Picker core feature with comprehensive weather analysis - Add interactive weather control system with instant weather switching - Implement weather filtering by type (Pleasant, Cloudy, Rainy, Snow, Aurora) - Add detailed weather information display with wind, precipitation, and lightning data - Provide color-coded weather names showing all properties at a glance - Support persistent overlay window for continuous weather monitoring - Add weather analysis infrastructure for other features to extend - Enhance weather and form formatting utilities with magic_enum integration - Include robust tooltip system for detailed weather information display Core feature marked for inclusion in main Community Shaders distribution. Supports both SE and VR platforms. --- extern/CommonLibSSE-NG | 2 +- features/Weather Picker/CORE | 0 .../Shaders/Features/WeatherPicker.ini | 2 + src/Feature.cpp | 2 + src/Feature.h | 25 +- src/Features/WeatherPicker.cpp | 868 ++++++++++++++++++ src/Features/WeatherPicker.h | 102 ++ src/Globals.cpp | 3 + src/Globals.h | 2 + src/Menu.cpp | 44 +- src/Menu.h | 15 +- src/Utils/Game.cpp | 98 ++ src/Utils/Game.h | 75 ++ src/Utils/UI.cpp | 150 ++- src/Utils/UI.h | 49 +- 15 files changed, 1394 insertions(+), 43 deletions(-) create mode 100644 features/Weather Picker/CORE create mode 100644 features/Weather Picker/Shaders/Features/WeatherPicker.ini create mode 100644 src/Features/WeatherPicker.cpp create mode 100644 src/Features/WeatherPicker.h 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 d139a0f28e..34a246c095 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -25,6 +25,7 @@ #include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "Features/WaterEffects.h" +#include "Features/WeatherPicker.h" #include "Features/WetnessEffects.h" #include "State.h" @@ -208,6 +209,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..163e7d42f8 --- /dev/null +++ b/src/Features/WeatherPicker.cpp @@ -0,0 +1,868 @@ +#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(false); // false = not popup window + + // 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; + } + + // Render core weather details (popup mode - no interactive elements) + RenderCoreWeatherDetails(true); // true = popup window + + // 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 +} + +void WeatherPicker::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct, bool showInteractiveElements) +{ + if (!weather) { + ImGui::BulletText("No Weather Found"); + return; + } + auto menu = Menu::GetSingleton(); + const auto& theme = menu->GetTheme(); + // Display weather name with multi-color support and hover tooltip + std::string weatherText = Util::FormatWeather(weather); + ImGui::Bullet(); + ImGui::SameLine(); + bool showTooltip = RenderMultiColorWeatherName(weather, weatherText); + // Add hover tooltip for weather name (attached to the main weather name element) + 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()); + + // Show weather flags using magic_enum + auto flagNames = GetWeatherFlagNames(weather); + if (!flagNames.empty()) { + // Use string joining algorithm for better performance + 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(); + } + + // Weather transition data (only show if percentage is provided) + if (weatherPct >= 0.0f) { + ImGui::BulletText("Weather Percentage: %.1f%%", weatherPct * 100.0f); + } + + // Precipitation data + if (weather->precipitationData) { + auto particleDensity = weather->precipitationData->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; + ImGui::BulletText("Particle Density: %.3f", particleDensity); + + // Precipitation texture name + 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"); + } + + // Precipitation transition data + 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" }); + } + } else { + ImGui::BulletText("Particle Density: No precipitation data"); + } + + // Lightning color as color picker (only show if thunder frequency > 0) + if (weather->data.thunderLightningFrequency > 0) { + // Treat color values as unsigned 8-bit (0-255 range) + unsigned int lightningR = static_cast(static_cast(weather->data.lightningColor.red)); + unsigned int lightningG = static_cast(static_cast(weather->data.lightningColor.green)); + unsigned int lightningB = static_cast(static_cast(weather->data.lightningColor.blue)); + ImGui::Text("Lightning Color:"); + + // Always show color picker, but disable interaction when not in interactive mode + ImGui::SameLine(); + // Convert to 0-1 range for color picker + float lightningColor[3] = { + lightningR / 255.0f, + lightningG / 255.0f, + lightningB / 255.0f + }; + + // Configure color picker flags based on whether interaction is allowed + ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel; + if (!showInteractiveElements) { + flags |= ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip; // Disable picker interaction but show color + // Style the disabled color picker with theme-based reduced alpha + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, theme.StatusPalette.Disable.w); + } + + // Always show the color picker, but conditionally handle interaction + bool colorChanged = ImGui::ColorEdit3("##LightningColor", lightningColor, flags); + + if (!showInteractiveElements) { + ImGui::PopStyleVar(); // Restore normal alpha + } + + if (colorChanged && showInteractiveElements) { + // Only update the weather's lightning color if interactive elements are enabled + weather->data.lightningColor.red = static_cast(static_cast(lightningColor[0] * 255.0f)); + weather->data.lightningColor.green = static_cast(static_cast(lightningColor[1] * 255.0f)); + weather->data.lightningColor.blue = static_cast(static_cast(lightningColor[2] * 255.0f)); + } // Thunder frequency as signed 8-bit with contextual information + int8_t thunderFreqRaw = weather->data.thunderLightningFrequency; + + // Display the raw value with context + ImGui::BulletText("Thunder Frequency: %d (signed 8-bit)", static_cast(thunderFreqRaw)); + + // Show both signed and unsigned interpretations for debugging + 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" }); + } + + // Lightning transition data + 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" }); + } + } + + // Wind data with player comparison (only show if wind speed > 0) + auto sky = globals::game::sky; + if (weather->data.windSpeed > 0 || (sky && sky->windSpeed > 0.0f)) { + float windSpeedDisplay = weather->data.windSpeed / 255.0f; + if (windSpeedDisplay < 0) + windSpeedDisplay = 0; // Clamp to prevent negative values + ImGui::BulletText("Weather Wind Speed: %.2f (raw %d)", windSpeedDisplay, weather->data.windSpeed); + if (auto _tt = Util::HoverTooltipWrapper()) { + char buffer[128]; + Util::Units::FormatWindSpeed(weather->data.windSpeed, buffer, sizeof(buffer)); + Util::DrawMultiLineTooltip({ "Wind speed from weather definition", + buffer }); + } + 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" }); + } + } + // Convert weather wind direction from 0-256 scale to 0-360 degrees + float weatherWindDirDegrees = Util::Units::DirectionRawToDegrees(weather->data.windDirection); + ImGui::BulletText("Wind Direction: %.1f° (raw %d)", weatherWindDirDegrees, weather->data.windDirection); + if (auto _tt = Util::HoverTooltipWrapper()) { + char buffer[128]; + Util::Units::FormatDirection(weather->data.windDirection, buffer, sizeof(buffer)); + Util::DrawMultiLineTooltip({ "Wind direction from weather definition", + buffer }); + } + float weatherWindRangeDegrees = Util::Units::DirectionRangeToDegrees(weather->data.windDirectionRange); + ImGui::BulletText("Wind Direction Range: %.1f° (raw %d)", weatherWindRangeDegrees, weather->data.windDirectionRange); + + // Player direction for comparison + 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); + // Calculate raw difference between wind and player direction + float effectiveWindDirection = Util::Units::NormalizeDegrees0To360(weatherWindDirDegrees - 30.5f); + + float rawDifference = Util::Units::NormalizeDegreesToSignedRange(effectiveWindDirection - playerAngleDegrees); + + ImGui::BulletText("Effective Wind Dir: %.1f° (raw - 30.5°)", effectiveWindDirection); + 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)", + }); + } + } + } +} + +void WeatherPicker::RenderCoreWeatherDetails(bool isPopupWindow) +{ + // Helper function to find weather index in filtered list + auto findWeatherIndex = [&](RE::TESWeather* targetWeather) -> int { + 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; + }; + + if (auto sky = globals::game::sky) { + if (sky->mode.get() == RE::Sky::Mode::kFull) { + // Weather Selection Section (only show interactive elements in inline mode) + if (!isPopupWindow) { + static bool weatherControlsExpanded = true; + Util::DrawSectionHeader("Weather Controls", false, true, &weatherControlsExpanded); + + if (weatherControlsExpanded) { + ImGui::Text("Filter by Weather Type:"); + if (ImGui::Button("Select All")) { + s_weatherFlagFilter = 0x7F; // 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 (int 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, 0x40); + 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(); + } + } + + // Weather Information Display (always show) + static bool weatherInfoExpanded = true; + Util::DrawSectionHeader("Weather Information", false, true, &weatherInfoExpanded); + + if (weatherInfoExpanded) { + // 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, !isPopupWindow); + + // Last Weather Column + ImGui::TableNextColumn(); + DisplayWeatherInfo(displayLastWeather, abs(sky->currentWeatherPct - 1.0f), !isPopupWindow); + + ImGui::EndTable(); + } + } + + ImGui::Spacing(); + } else { + auto menu = Menu::GetSingleton(); + const auto& theme = menu->GetTheme(); + ImGui::TextColored(theme.StatusPalette.Error, "Sky not in full mode"); + } + } else { + auto menu = Menu::GetSingleton(); + const auto& theme = menu->GetTheme(); + ImGui::TextColored(theme.StatusPalette.Error, "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 == 0x7F) { + 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 & 0x40) { + // 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 +} \ No newline at end of file diff --git a/src/Features/WeatherPicker.h b/src/Features/WeatherPicker.h new file mode 100644 index 0000000000..0a5458ace8 --- /dev/null +++ b/src/Features/WeatherPicker.h @@ -0,0 +1,102 @@ +#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 isPopupWindow = false); + static void RenderFeatureWeatherAnalysis(); + +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: + // 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 = 0x7F; // Start with all filters enabled by default (bits 0-6) + static inline uint32_t s_lastWeatherFlagFilter = 0x40; + static inline bool s_accelerateWeatherChange = true; + static inline RE::TESWeather* s_cachedLastWeather = nullptr; + + // Weather comparator for consistent sorting + struct WeatherNameComparator + { + bool operator()(const RE::TESWeather* a, const RE::TESWeather* b) const + { + auto getDisplayName = [](const RE::TESWeather* weather) -> std::string { + 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()); + }; + return getDisplayName(a) < getDisplayName(b); + } + }; + + // 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 9c93ebed4a..0226108f0a 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -34,6 +34,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" @@ -74,6 +75,7 @@ namespace globals VolumetricLighting* volumetricLighting = nullptr; VR* vr = nullptr; WaterEffects* waterEffects = nullptr; + WeatherPicker* weatherPicker = nullptr; WetnessEffects* wetnessEffects = nullptr; namespace llf @@ -157,6 +159,7 @@ namespace globals features::volumetricLighting = VolumetricLighting::GetSingleton(); features::vr = VR::GetSingleton(); features::waterEffects = WaterEffects::GetSingleton(); + features::weatherPicker = WeatherPicker::GetSingleton(); features::wetnessEffects = WetnessEffects::GetSingleton(); features::llf::particleLights = ParticleLights::GetSingleton(); diff --git a/src/Globals.h b/src/Globals.h index 2de8ec65f6..99c60648c8 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; class ParticleLights; @@ -75,6 +76,7 @@ namespace globals extern VolumetricLighting* volumetricLighting; extern VR* vr; extern WaterEffects* waterEffects; + extern WeatherPicker* weatherPicker; extern WetnessEffects* wetnessEffects; namespace llf diff --git a/src/Menu.cpp b/src/Menu.cpp index 25e09978a9..949f9dde04 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" #include "Utils/UI.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( @@ -69,6 +70,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, @@ -315,15 +322,15 @@ void Menu::DrawSettings() uiIcons.clearCache.texture || uiIcons.clearDiskCache.texture); - // Debug logging for icon availability - if (settings.Theme.ShowActionIcons) { - logger::debug("Icon status - Save: {}, Load: {}, Cache: {}, Disk: {}, Logo: {}", - uiIcons.saveSettings.texture ? "OK" : "NULL", - uiIcons.loadSettings.texture ? "OK" : "NULL", - uiIcons.clearCache.texture ? "OK" : "NULL", - uiIcons.clearDiskCache.texture ? "OK" : "NULL", - uiIcons.logo.texture ? "OK" : "NULL"); - } + // // Debug logging for icon availability + // if (settings.Theme.ShowActionIcons) { + // logger::debug("Icon status - Save: {}, Load: {}, Cache: {}, Disk: {}, Logo: {}", + // uiIcons.saveSettings.texture ? "OK" : "NULL", + // uiIcons.loadSettings.texture ? "OK" : "NULL", + // uiIcons.clearCache.texture ? "OK" : "NULL", + // uiIcons.clearDiskCache.texture ? "OK" : "NULL", + // uiIcons.logo.texture ? "OK" : "NULL"); + // } // Always show logo if available, regardless of action icons setting bool showLogo = uiIcons.logo.texture != nullptr; @@ -1467,6 +1474,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; @@ -2556,4 +2566,18 @@ void Menu::SelectFeatureMenu(const std::string& featureName) { pendingFeatureSelection = featureName; logger::info("Queued navigation to {} feature menu", featureName); -} \ No newline at end of file +} + +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); + } +} diff --git a/src/Menu.h b/src/Menu.h index 3325092cc9..b79f3fc544 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -42,6 +42,8 @@ class Menu void DrawSettings(); void DrawOverlay(); void DrawPerfOverlay(); + void DrawWeatherDetailsWindow(); + void DrawCoreWeatherDetails(bool isPopupWindow); // Core weather functionality that features can extend void ProcessInputEvents(RE::InputEvent* const* a_events); bool ShouldSwallowInput(); @@ -211,12 +213,22 @@ 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 utility functions + static const char* KeyIdToString(uint32_t key); + private: Settings settings; @@ -279,7 +291,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..3c5bd37312 100644 --- a/src/Utils/Game.h +++ b/src/Utils/Game.h @@ -43,6 +43,76 @@ 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 / 3.14159f; + + // 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) + { + while (degrees < 0.0f) degrees += 360.0f; + while (degrees >= 360.0f) degrees -= 360.0f; + return degrees; + } + + inline float NormalizeDegreesToSignedRange(float degrees) + { + while (degrees > 180.0f) degrees -= 360.0f; + while (degrees < -180.0f) degrees += 360.0f; + return degrees; + } + + // Formatted string helpers for tooltips + inline const char* FormatDistance(float gameUnits, char* buffer, size_t bufferSize) + { + snprintf(buffer, bufferSize, "%.1f units (%.2f m, %.1f ft)", + gameUnits, GameUnitsToMeters(gameUnits), GameUnitsToFeet(gameUnits)); + return buffer; + } + inline const char* FormatWindSpeed(uint8_t rawWind, char* buffer, size_t bufferSize) + { + snprintf(buffer, bufferSize, "%.1f%% (raw %d, %.2f normalized)", + WindRawToPercent(rawWind), rawWind, WindRawToNormalized(rawWind)); + return buffer; + } + + inline const char* FormatDirection(uint8_t rawDirection, char* buffer, size_t bufferSize) + { + snprintf(buffer, bufferSize, "%.1f° (raw %d)", + DirectionRawToDegrees(rawDirection), rawDirection); + return buffer; + } + } + struct DispatchCount { uint x; @@ -92,4 +162,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 8cdf0023fd..625e1cdd04 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -398,38 +398,136 @@ 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; + + if (isCollapsible && isExpanded) { + // Use collapsible header similar to DrawCategoryHeader + ImGui::PushID(sectionName); + + const ImVec4 headerColor = useWhiteText ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0.8f, 0.8f, 0.8f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Text, headerColor); + + if (ImGui::CollapsingHeader(sectionName, ImGuiTreeNodeFlags_DefaultOpen)) { + if (!*isExpanded) { + stateChanged = true; + } + *isExpanded = true; + } else { + if (*isExpanded) { + stateChanged = true; + } + *isExpanded = 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; - ImU32 lineColor = theme.LineColorDefault; - ImU32 textColor = useWhiteText ? theme.TextColorWhite : theme.TextColorDefault; + 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 + + // Use Menu theme colors for consistent styling + auto& theme = Menu::GetSingleton()->GetTheme().FeatureHeading; + ImU32 lineColor = theme.LineColorDefault; + ImU32 textColor = useWhiteText ? theme.TextColorWhite : theme.TextColorDefault; + + // Left line + if (lineLength > 0) { + drawList->AddLine(ImVec2(pos.x, lineY), ImVec2(pos.x + lineLength, lineY), lineColor, 1.0f); + } - // Left line - if (lineLength > 0) { - drawList->AddLine(ImVec2(pos.x, lineY), ImVec2(pos.x + lineLength, lineY), lineColor, 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), lineColor, 1.0f); + } + + // Center text + ImVec2 textPos = ImVec2(pos.x + lineLength + 10.0f, pos.y + 2.0f); + drawList->AddText(textPos, textColor, 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), lineColor, 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 char* label, float value, const ColorCodedValueConfig& config) + { + // Display label + ImGui::BulletText("%s", label); + if (config.sameLine) { + ImGui::SameLine(); } - // Center text - ImVec2 textPos = ImVec2(pos.x + lineLength + 10.0f, pos.y + 2.0f); - drawList->AddText(textPos, textColor, sectionName); + // Determine color based on thresholds + ImVec4 valueColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // Default white - // Move cursor to next line - ImGui::SetCursorScreenPos(ImVec2(pos.x, pos.y + textSize.y + 8.0f)); + for (const auto& tc : config.thresholds) { + if (value < tc.threshold) { + valueColor = tc.color; + break; + } + } + + // Display colored value + ImGui::TextColored(valueColor, config.format, value); + + // 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) { + if (!colors.empty() && i < colors.size()) { + // Use provided color for this line + ImGui::TextColored(colors[i], "%s", lines[i]); + } else { + // Use default color + ImGui::Text("%s", lines[i]); + } + } + } + +} // namespace Util \ No newline at end of file diff --git a/src/Utils/UI.h b/src/Utils/UI.h index e75088e05a..039099cfc5 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -139,11 +139,49 @@ namespace Util bool DrawCategoryHeader(const char* categoryName, bool& isExpanded); /** - * 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 text to display before the value + * @param value The numeric value to display and color-code + * @param config Configuration struct containing thresholds, colors, format, and tooltip + */ + void DrawColorCodedValue(const char* label, float value, const ColorCodedValueConfig& config); class PerformanceOverlay { @@ -159,4 +197,11 @@ namespace Util } }; extern PerformanceOverlay performanceOverlay; + + /** + * Helper function for drawing multi-line tooltips with better code readability. + * @param lines Vector of strings, each will be displayed on its own line + * @param colors Optional vector of colors for each line (if empty, uses default color) + */ + void DrawMultiLineTooltip(const std::vector& lines, const std::vector& colors = {}); } // namespace Util From b6f5a56888af13b991b42548d635eee75510e386 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 11:06:36 -0700 Subject: [PATCH 02/16] refactor: use builtin pi --- src/Utils/Game.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utils/Game.h b/src/Utils/Game.h index 3c5bd37312..3221202b3d 100644 --- a/src/Utils/Game.h +++ b/src/Utils/Game.h @@ -59,7 +59,7 @@ namespace Util // 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 / 3.14159f; + constexpr float RADIANS_TO_DEGREES = 180.0f / DirectX::XM_PI; // Distance conversions inline float GameUnitsToMeters(float gameUnits) { return gameUnits * GAME_UNIT_TO_M; } From 897d823542000e04cf67d42c57d74c7c10661207 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 17:07:35 -0700 Subject: [PATCH 03/16] refactor: simplify logic --- src/Features/WeatherPicker.cpp | 20 ++++++++------------ src/Utils/Game.h | 15 ++++++--------- src/Utils/UI.cpp | 27 ++++++++++++++++++--------- src/Utils/UI.h | 26 +++++++++++++++++--------- 4 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 163e7d42f8..4a9c25f73b 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -229,9 +229,9 @@ void WeatherPicker::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct if (colorChanged && showInteractiveElements) { // Only update the weather's lightning color if interactive elements are enabled - weather->data.lightningColor.red = static_cast(static_cast(lightningColor[0] * 255.0f)); - weather->data.lightningColor.green = static_cast(static_cast(lightningColor[1] * 255.0f)); - weather->data.lightningColor.blue = static_cast(static_cast(lightningColor[2] * 255.0f)); + 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); } // Thunder frequency as signed 8-bit with contextual information int8_t thunderFreqRaw = weather->data.thunderLightningFrequency; @@ -311,14 +311,11 @@ void WeatherPicker::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct auto sky = globals::game::sky; if (weather->data.windSpeed > 0 || (sky && sky->windSpeed > 0.0f)) { float windSpeedDisplay = weather->data.windSpeed / 255.0f; - if (windSpeedDisplay < 0) - windSpeedDisplay = 0; // Clamp to prevent negative values ImGui::BulletText("Weather Wind Speed: %.2f (raw %d)", windSpeedDisplay, weather->data.windSpeed); if (auto _tt = Util::HoverTooltipWrapper()) { - char buffer[128]; - Util::Units::FormatWindSpeed(weather->data.windSpeed, buffer, sizeof(buffer)); + std::string windStr = Util::Units::FormatWindSpeed(weather->data.windSpeed); Util::DrawMultiLineTooltip({ "Wind speed from weather definition", - buffer }); + windStr.c_str() }); } if (sky) { ImGui::BulletText("Sky Wind Speed: %.2f", sky->windSpeed); @@ -331,10 +328,9 @@ void WeatherPicker::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct float weatherWindDirDegrees = Util::Units::DirectionRawToDegrees(weather->data.windDirection); ImGui::BulletText("Wind Direction: %.1f° (raw %d)", weatherWindDirDegrees, weather->data.windDirection); if (auto _tt = Util::HoverTooltipWrapper()) { - char buffer[128]; - Util::Units::FormatDirection(weather->data.windDirection, buffer, sizeof(buffer)); + std::string dirStr = Util::Units::FormatDirection(weather->data.windDirection); Util::DrawMultiLineTooltip({ "Wind direction from weather definition", - buffer }); + dirStr.c_str() }); } float weatherWindRangeDegrees = Util::Units::DirectionRangeToDegrees(weather->data.windDirectionRange); ImGui::BulletText("Wind Direction Range: %.1f° (raw %d)", weatherWindRangeDegrees, weather->data.windDirectionRange); @@ -429,7 +425,7 @@ void WeatherPicker::RenderCoreWeatherDetails(bool isPopupWindow) { "Aurora Sun", RE::TESWeather::WeatherDataFlag::kAuroraFollowsSun, false }, { "None", RE::TESWeather::WeatherDataFlag::kNone, true } // Special case for unclassified }; - for (int i = 0; i < filters.size(); ++i) { + for (size_t i = 0; i < filters.size(); ++i) { if (i > 0 && i % checkboxesPerRow != 0) { ImGui::SameLine(); } diff --git a/src/Utils/Game.h b/src/Utils/Game.h index 3221202b3d..0f79b38374 100644 --- a/src/Utils/Game.h +++ b/src/Utils/Game.h @@ -92,24 +92,21 @@ namespace Util } // Formatted string helpers for tooltips - inline const char* FormatDistance(float gameUnits, char* buffer, size_t bufferSize) + inline std::string FormatDistance(float gameUnits) { - snprintf(buffer, bufferSize, "%.1f units (%.2f m, %.1f ft)", + return std::format("{:.1f} units ({:.2f} m, {:.1f} ft)", gameUnits, GameUnitsToMeters(gameUnits), GameUnitsToFeet(gameUnits)); - return buffer; } - inline const char* FormatWindSpeed(uint8_t rawWind, char* buffer, size_t bufferSize) + inline std::string FormatWindSpeed(uint8_t rawWind) { - snprintf(buffer, bufferSize, "%.1f%% (raw %d, %.2f normalized)", + return std::format("{:.1f}% (raw {}, {:.2f} normalized)", WindRawToPercent(rawWind), rawWind, WindRawToNormalized(rawWind)); - return buffer; } - inline const char* FormatDirection(uint8_t rawDirection, char* buffer, size_t bufferSize) + inline std::string FormatDirection(uint8_t rawDirection) { - snprintf(buffer, bufferSize, "%.1f° (raw %d)", + return std::format("{:.1f}° (raw {})", DirectionRawToDegrees(rawDirection), rawDirection); - return buffer; } } diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 625e1cdd04..89e4f192df 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -488,26 +488,34 @@ namespace Util return config; } - void DrawColorCodedValue(const char* label, float value, const ColorCodedValueConfig& config) + void DrawColorCodedValue( + const std::string& label, + float valueToCheck, + const std::string& valueStr, + const ColorCodedValueConfig& config, + bool useBullet) { // Display label - ImGui::BulletText("%s", label); + if (useBullet) { + ImGui::BulletText("%s", label.c_str()); + } else { + ImGui::Text("%s", label.c_str()); + } if (config.sameLine) { ImGui::SameLine(); } // 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 (value < tc.threshold) { + if (valueToCheck < tc.threshold) { valueColor = tc.color; break; } } - // Display colored value - ImGui::TextColored(valueColor, config.format, value); + // Display colored value (arbitrary string) + ImGui::TextColored(valueColor, "%s", valueStr.c_str()); // Add tooltip if provided if (config.tooltipText) { @@ -517,15 +525,16 @@ namespace Util } } - void DrawMultiLineTooltip(const std::vector& lines, const std::vector& colors) + 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", lines[i]); + ImGui::TextColored(colors[i], "%s", lineCStr); } else { // Use default color - ImGui::Text("%s", lines[i]); + ImGui::Text("%s", lineCStr); } } } diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 039099cfc5..241596582f 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -167,7 +167,8 @@ namespace Util 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) + + // 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) @@ -177,11 +178,18 @@ namespace Util * 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 text to display before the value - * @param value The numeric value to display and color-code - * @param config Configuration struct containing thresholds, colors, format, and tooltip + * @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 char* label, float value, const ColorCodedValueConfig& config); + void DrawColorCodedValue( + const std::string& label, + float valueToCheck, + const std::string& valueStr, + const ColorCodedValueConfig& config, + bool useBullet = true); class PerformanceOverlay { @@ -199,9 +207,9 @@ namespace Util extern PerformanceOverlay performanceOverlay; /** - * Helper function for drawing multi-line tooltips with better code readability. - * @param lines Vector of strings, each will be displayed on its own line - * @param colors Optional vector of colors for each line (if empty, uses default color) + * @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 = {}); + void DrawMultiLineTooltip(const std::vector& lines, const std::vector& colors = {}); } // namespace Util From 5aaa97d46a77adeb3095d2189784718e9541416d Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 19:36:44 -0700 Subject: [PATCH 04/16] feat: enable ui elements when game menu open --- src/Features/WeatherPicker.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 4a9c25f73b..08baa67a1c 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -78,8 +78,20 @@ void WeatherPicker::RenderWeatherDetailsWindow(bool* open) settings.WeatherDetailsWindow.Position = currentPos; } - // Render core weather details (popup mode - no interactive elements) - RenderCoreWeatherDetails(true); // true = popup window + // Render core weather details (popup mode) + + // Determine if interactive elements should be enabled + bool menuOpen = false; + if (auto ui = globals::game::ui) { + static const std::array menuNames = { + RE::StatsMenu::MENU_NAME, + RE::JournalMenu::MENU_NAME, + RE::CursorMenu::MENU_NAME + }; + menuOpen = std::any_of(menuNames.begin(), menuNames.end(), + [&](auto name) { return ui->IsMenuOpen(name); }); + } + RenderCoreWeatherDetails(!menuOpen); // Render weather analysis from features with collapsible headers RenderFeatureWeatherAnalysis(); From 35660cf92532a950ab8fad28ac507661e80b72ee Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 19:59:18 -0700 Subject: [PATCH 05/16] refactor: use named constants for flags --- src/Features/WeatherPicker.cpp | 8 ++++---- src/Features/WeatherPicker.h | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 08baa67a1c..da846eef6c 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -409,7 +409,7 @@ void WeatherPicker::RenderCoreWeatherDetails(bool isPopupWindow) if (weatherControlsExpanded) { ImGui::Text("Filter by Weather Type:"); if (ImGui::Button("Select All")) { - s_weatherFlagFilter = 0x7F; // All weather flags (bits 0-6, including unclassified) + s_weatherFlagFilter = ALL_WEATHER_FLAGS; // All weather flags (bits 0-6, including unclassified) } ImGui::SameLine(); if (ImGui::Button("Clear All")) { @@ -452,7 +452,7 @@ void WeatherPicker::RenderCoreWeatherDetails(bool isPopupWindow) 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, 0x40); + 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.", @@ -631,7 +631,7 @@ void WeatherPicker::UpdateFilteredWeathers() bool shouldInclude = false; // Check if all filters are selected (0x7F = all 7 bits) - if (s_weatherFlagFilter == 0x7F) { + if (s_weatherFlagFilter == ALL_WEATHER_FLAGS) { shouldInclude = true; } else { // Check regular weather flags @@ -641,7 +641,7 @@ void WeatherPicker::UpdateFilteredWeathers() } // Check for None filter (bit 6) - includes weathers that don't match any of our tracked flags - if (s_weatherFlagFilter & 0x40) { + 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) | diff --git a/src/Features/WeatherPicker.h b/src/Features/WeatherPicker.h index 0a5458ace8..895b153a81 100644 --- a/src/Features/WeatherPicker.h +++ b/src/Features/WeatherPicker.h @@ -64,13 +64,17 @@ struct WeatherPicker : Feature static ImVec4 GetWeatherFlagColorByName(const std::string& flagName); private: + // 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 = 0x7F; // Start with all filters enabled by default (bits 0-6) - static inline uint32_t s_lastWeatherFlagFilter = 0x40; + 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; From c3afc30f8da0c685d7b943126b3f87b5d6f0ad8d Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 21:17:05 -0700 Subject: [PATCH 06/16] feat: enable interaction when cs menu open --- src/Features/WeatherPicker.cpp | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index da846eef6c..5c8e166d67 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -81,16 +81,7 @@ void WeatherPicker::RenderWeatherDetailsWindow(bool* open) // Render core weather details (popup mode) // Determine if interactive elements should be enabled - bool menuOpen = false; - if (auto ui = globals::game::ui) { - static const std::array menuNames = { - RE::StatsMenu::MENU_NAME, - RE::JournalMenu::MENU_NAME, - RE::CursorMenu::MENU_NAME - }; - menuOpen = std::any_of(menuNames.begin(), menuNames.end(), - [&](auto name) { return ui->IsMenuOpen(name); }); - } + bool menuOpen = Menu::GetSingleton()->ShouldSwallowInput() || (globals::game::ui && globals::game::ui->IsMenuOpen(RE::CursorMenu::MENU_NAME)); RenderCoreWeatherDetails(!menuOpen); // Render weather analysis from features with collapsible headers From eede412de3046cadabbbd1fd6ecdf33d89251147 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 21:21:59 -0700 Subject: [PATCH 07/16] fix: use std::abs --- src/Features/WeatherPicker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 5c8e166d67..0bc64c708c 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -572,7 +572,7 @@ void WeatherPicker::RenderCoreWeatherDetails(bool isPopupWindow) // Last Weather Column ImGui::TableNextColumn(); - DisplayWeatherInfo(displayLastWeather, abs(sky->currentWeatherPct - 1.0f), !isPopupWindow); + DisplayWeatherInfo(displayLastWeather, std::abs(sky->currentWeatherPct - 1.0f), !isPopupWindow); ImGui::EndTable(); } From 13b8ab62b4af877473f12560868621cb6c9c3b33 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 21:35:57 -0700 Subject: [PATCH 08/16] refactor: simplify code --- src/Features/WeatherPicker.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 0bc64c708c..3f4198f996 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -80,9 +80,13 @@ void WeatherPicker::RenderWeatherDetailsWindow(bool* open) // Render core weather details (popup mode) - // Determine if interactive elements should be enabled - bool menuOpen = Menu::GetSingleton()->ShouldSwallowInput() || (globals::game::ui && globals::game::ui->IsMenuOpen(RE::CursorMenu::MENU_NAME)); - RenderCoreWeatherDetails(!menuOpen); + // Helper function to determine if interactive elements should be enabled + 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(); @@ -201,9 +205,9 @@ void WeatherPicker::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct // Lightning color as color picker (only show if thunder frequency > 0) if (weather->data.thunderLightningFrequency > 0) { // Treat color values as unsigned 8-bit (0-255 range) - unsigned int lightningR = static_cast(static_cast(weather->data.lightningColor.red)); - unsigned int lightningG = static_cast(static_cast(weather->data.lightningColor.green)); - unsigned int lightningB = static_cast(static_cast(weather->data.lightningColor.blue)); + 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:"); // Always show color picker, but disable interaction when not in interactive mode From baf68ef0e3940d274de338c5c77f3c4cac53f9ad Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 22:14:23 -0700 Subject: [PATCH 09/16] fix: make safe normalize functions --- src/Utils/Game.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Utils/Game.h b/src/Utils/Game.h index 0f79b38374..474555362c 100644 --- a/src/Utils/Game.h +++ b/src/Utils/Game.h @@ -79,6 +79,8 @@ namespace Util // 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; @@ -86,6 +88,8 @@ namespace Util 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; From 191243c040c3cc8e7cd5b98dea9fb60f45256ff6 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 22:24:37 -0700 Subject: [PATCH 10/16] refactor: break apart DisplayWeatherInfo --- src/Features/WeatherPicker.cpp | 379 +++++++++++++++------------------ src/Features/WeatherPicker.h | 9 + 2 files changed, 186 insertions(+), 202 deletions(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 3f4198f996..a368c15bc9 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -131,30 +131,24 @@ ImVec4 WeatherPicker::GetWeatherTypeColor(RE::TESWeather* weather) return theme.StatusPalette.InfoColor; // Default blue } -void WeatherPicker::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct, bool showInteractiveElements) +// --- Helper: Display basic weather info (name, flags, percentage) --- +void WeatherPicker::DisplayWeatherBasicInfo(RE::TESWeather* weather, float weatherPct) { if (!weather) { ImGui::BulletText("No Weather Found"); return; } - auto menu = Menu::GetSingleton(); - const auto& theme = menu->GetTheme(); - // Display weather name with multi-color support and hover tooltip std::string weatherText = Util::FormatWeather(weather); ImGui::Bullet(); ImGui::SameLine(); - bool showTooltip = RenderMultiColorWeatherName(weather, weatherText); - // Add hover tooltip for weather name (attached to the main weather name element) + 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()); - - // Show weather flags using magic_enum - auto flagNames = GetWeatherFlagNames(weather); + auto flagNames = WeatherPicker::GetWeatherFlagNames(weather); if (!flagNames.empty()) { - // Use string joining algorithm for better performance std::string joinedFlags = flagNames[0]; for (size_t j = 1; j < flagNames.size(); ++j) { joinedFlags += ", " + flagNames[j]; @@ -165,220 +159,201 @@ void WeatherPicker::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct } ImGui::EndTooltip(); } - - // Weather transition data (only show if percentage is provided) if (weatherPct >= 0.0f) { ImGui::BulletText("Weather Percentage: %.1f%%", weatherPct * 100.0f); } +} - // Precipitation data - if (weather->precipitationData) { - auto particleDensity = weather->precipitationData->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; - ImGui::BulletText("Particle Density: %.3f", particleDensity); - - // Precipitation texture name - 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"); - } - - // Precipitation transition data - 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" }); - } - } else { +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" }); + } +} - // Lightning color as color picker (only show if thunder frequency > 0) - if (weather->data.thunderLightningFrequency > 0) { - // Treat color values as unsigned 8-bit (0-255 range) - 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:"); - - // Always show color picker, but disable interaction when not in interactive mode - ImGui::SameLine(); - // Convert to 0-1 range for color picker - float lightningColor[3] = { - lightningR / 255.0f, - lightningG / 255.0f, - lightningB / 255.0f - }; - - // Configure color picker flags based on whether interaction is allowed - ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel; - if (!showInteractiveElements) { - flags |= ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip; // Disable picker interaction but show color - // Style the disabled color picker with theme-based reduced alpha - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, theme.StatusPalette.Disable.w); - } - - // Always show the color picker, but conditionally handle interaction - bool colorChanged = ImGui::ColorEdit3("##LightningColor", lightningColor, flags); - - if (!showInteractiveElements) { - ImGui::PopStyleVar(); // Restore normal alpha +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)"); } - - if (colorChanged && showInteractiveElements) { - // Only update the weather's lightning color if interactive elements are enabled - 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); - } // Thunder frequency as signed 8-bit with contextual information - int8_t thunderFreqRaw = weather->data.thunderLightningFrequency; - - // Display the raw value with context - ImGui::BulletText("Thunder Frequency: %d (signed 8-bit)", static_cast(thunderFreqRaw)); - - // Show both signed and unsigned interpretations for debugging - 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 if (thunderFreqRaw >= 15) { + if (thunderFreqRaw == 15) { + ImGui::BulletText("This matches maximum observed frequency in Creation Kit"); } else { - ImGui::BulletText("Extreme low frequency: Likely no thunder (raw < -100)"); + ImGui::BulletText("High-medium frequency range: 75-100%% (raw 15-76)"); } - 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" }); + } 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)"); } - - // Lightning transition data - 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" }); + } 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" }); } +} - // Wind data with player comparison (only show if wind speed > 0) +void WeatherPicker::DisplayWindInfo(RE::TESWeather* weather) +{ auto sky = globals::game::sky; - if (weather->data.windSpeed > 0 || (sky && sky->windSpeed > 0.0f)) { - float windSpeedDisplay = weather->data.windSpeed / 255.0f; - ImGui::BulletText("Weather Wind Speed: %.2f (raw %d)", windSpeedDisplay, weather->data.windSpeed); + 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()) { - std::string windStr = Util::Units::FormatWindSpeed(weather->data.windSpeed); - Util::DrawMultiLineTooltip({ "Wind speed from weather definition", - windStr.c_str() }); + Util::DrawMultiLineTooltip({ "Current active wind speed from the sky system", + "This affects particle behavior and wind-based effects" }); } - 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"; } - // Convert weather wind direction from 0-256 scale to 0-360 degrees - float weatherWindDirDegrees = Util::Units::DirectionRawToDegrees(weather->data.windDirection); - ImGui::BulletText("Wind Direction: %.1f° (raw %d)", weatherWindDirDegrees, weather->data.windDirection); + ImGui::SameLine(); + ImGui::TextColored(theme.StatusPalette.RestartNeeded, "(%s)", windRelation); 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); - - // Player direction for comparison - 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); - // Calculate raw difference between wind and player direction - float effectiveWindDirection = Util::Units::NormalizeDegrees0To360(weatherWindDirDegrees - 30.5f); - - float rawDifference = Util::Units::NormalizeDegreesToSignedRange(effectiveWindDirection - playerAngleDegrees); - - ImGui::BulletText("Effective Wind Dir: %.1f° (raw - 30.5°)", effectiveWindDirection); - 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)", - }); - } + 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::RenderCoreWeatherDetails(bool isPopupWindow) { diff --git a/src/Features/WeatherPicker.h b/src/Features/WeatherPicker.h index 895b153a81..b9ed1f2a79 100644 --- a/src/Features/WeatherPicker.h +++ b/src/Features/WeatherPicker.h @@ -64,6 +64,9 @@ struct WeatherPicker : Feature 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 @@ -98,6 +101,12 @@ struct WeatherPicker : Feature } }; + // --- 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(); From 53b930da879da7307a7b66619481c7fb8394fca5 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 23:21:24 -0700 Subject: [PATCH 11/16] refactor: break apart RenderCoreWeatherDetails --- src/Features/WeatherPicker.cpp | 397 ++++++++++++++++----------------- src/Features/WeatherPicker.h | 6 +- 2 files changed, 202 insertions(+), 201 deletions(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index a368c15bc9..e89e430195 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -44,7 +44,7 @@ void WeatherPicker::DrawSettings() ImGui::Spacing(); // Render core weather details - RenderCoreWeatherDetails(false); // false = not popup window + RenderCoreWeatherDetails(true); // true = show interactive elements in main settings panel // Render weather analysis from features with collapsible headers RenderFeatureWeatherAnalysis(); @@ -78,12 +78,10 @@ void WeatherPicker::RenderWeatherDetailsWindow(bool* open) settings.WeatherDetailsWindow.Position = currentPos; } - // Render core weather details (popup mode) - - // Helper function to determine if interactive elements should be enabled + // 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))); + return (Menu::GetSingleton()->ShouldSwallowInput() || + (globals::game::ui && globals::game::ui->IsMenuOpen(RE::CursorMenu::MENU_NAME))); }; RenderCoreWeatherDetails(shouldEnableInteractiveElements()); @@ -355,218 +353,217 @@ void WeatherPicker::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct WeatherPicker::DisplayWindInfo(weather); } -void WeatherPicker::RenderCoreWeatherDetails(bool isPopupWindow) +void WeatherPicker::RenderWeatherControls(RE::Sky* sky) { - // Helper function to find weather index in filtered list - auto findWeatherIndex = [&](RE::TESWeather* targetWeather) -> int { - if (!targetWeather) - return -1; - for (size_t i = 0; i < s_filteredWeathers.size(); ++i) { - if (s_filteredWeathers[i] == targetWeather) { - return static_cast(i); + // 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)); } - return -1; - }; + ImGui::PopStyleColor(); + } - if (auto sky = globals::game::sky) { - if (sky->mode.get() == RE::Sky::Mode::kFull) { - // Weather Selection Section (only show interactive elements in inline mode) - if (!isPopupWindow) { - static bool weatherControlsExpanded = true; - Util::DrawSectionHeader("Weather Controls", false, true, &weatherControlsExpanded); - - if (weatherControlsExpanded) { - 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(); - } + // 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(); } - // Weather Information Display (always show) - static bool weatherInfoExpanded = true; - Util::DrawSectionHeader("Weather Information", false, true, &weatherInfoExpanded); + // Set the initial focus when opening the combo (scrolls to it) + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } - if (weatherInfoExpanded) { - // Update cache: store current lastWeather if it exists, otherwise keep the cached one - if (sky->lastWeather) { - s_cachedLastWeather = sky->lastWeather; - } + ImGui::Spacing(); +} - // Use cached last weather for display if sky->lastWeather is null - RE::TESWeather* displayLastWeather = sky->lastWeather ? sky->lastWeather : s_cachedLastWeather; +void WeatherPicker::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements) +{ + static bool weatherInfoExpanded = true; + Util::DrawSectionHeader("Weather Information", false, true, &weatherInfoExpanded); - // 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(); + if (!weatherInfoExpanded) + return; - ImGui::TableNextRow(); + // Update cache: store current lastWeather if it exists, otherwise keep the cached one + if (sky->lastWeather) { + s_cachedLastWeather = sky->lastWeather; + } - // Current Weather Column - ImGui::TableNextColumn(); - DisplayWeatherInfo(sky->currentWeather, sky->currentWeatherPct, !isPopupWindow); + // Use cached last weather for display if sky->lastWeather is null + RE::TESWeather* displayLastWeather = sky->lastWeather ? sky->lastWeather : s_cachedLastWeather; - // Last Weather Column - ImGui::TableNextColumn(); - DisplayWeatherInfo(displayLastWeather, std::abs(sky->currentWeatherPct - 1.0f), !isPopupWindow); + // 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::EndTable(); - } - } + 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 { - auto menu = Menu::GetSingleton(); - const auto& theme = menu->GetTheme(); - ImGui::TextColored(theme.StatusPalette.Error, "Sky not in full mode"); + showError("Sky not in full mode"); } } else { - auto menu = Menu::GetSingleton(); - const auto& theme = menu->GetTheme(); - ImGui::TextColored(theme.StatusPalette.Error, "Sky not available"); + showError("Sky not available"); } } diff --git a/src/Features/WeatherPicker.h b/src/Features/WeatherPicker.h index b9ed1f2a79..0eacaa881c 100644 --- a/src/Features/WeatherPicker.h +++ b/src/Features/WeatherPicker.h @@ -29,9 +29,13 @@ struct WeatherPicker : Feature // 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 isPopupWindow = false); + 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. From c445c2c56fe112ce4bebe6c73d47e1a0fdd31247 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 23:31:32 -0700 Subject: [PATCH 12/16] refactor: make GetDisplayName static --- src/Features/WeatherPicker.h | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Features/WeatherPicker.h b/src/Features/WeatherPicker.h index 0eacaa881c..f36128bccb 100644 --- a/src/Features/WeatherPicker.h +++ b/src/Features/WeatherPicker.h @@ -85,23 +85,26 @@ struct WeatherPicker : Feature 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) + { + 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()); + } + // Weather comparator for consistent sorting struct WeatherNameComparator { bool operator()(const RE::TESWeather* a, const RE::TESWeather* b) const { - auto getDisplayName = [](const RE::TESWeather* weather) -> std::string { - 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()); - }; - return getDisplayName(a) < getDisplayName(b); + return WeatherPicker::GetDisplayName(a) < WeatherPicker::GetDisplayName(b); } }; From edeae64350a692c603f19b7083430e0a53a4c2f1 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 23:35:24 -0700 Subject: [PATCH 13/16] refactor: move GetDisplayName to cpp --- src/Features/WeatherPicker.cpp | 13 +++++++++++++ src/Features/WeatherPicker.h | 13 +------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index e89e430195..2f4bc7cf8d 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -840,4 +840,17 @@ ImVec4 WeatherPicker::GetWeatherFlagColorByName(const std::string& flagName) // 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 index f36128bccb..0eabcf8c0c 100644 --- a/src/Features/WeatherPicker.h +++ b/src/Features/WeatherPicker.h @@ -86,18 +86,7 @@ struct WeatherPicker : Feature static inline RE::TESWeather* s_cachedLastWeather = nullptr; // Static helper for display name extraction - static std::string 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()); - } + static std::string GetDisplayName(const RE::TESWeather* weather); // Weather comparator for consistent sorting struct WeatherNameComparator From c9da8963f541a92a8d9818302b13a6bbc47be41d Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 23:38:44 -0700 Subject: [PATCH 14/16] refactor: remove unused function --- src/Menu.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Menu.h b/src/Menu.h index b79f3fc544..153f36edf5 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -43,7 +43,6 @@ class Menu void DrawOverlay(); void DrawPerfOverlay(); void DrawWeatherDetailsWindow(); - void DrawCoreWeatherDetails(bool isPopupWindow); // Core weather functionality that features can extend void ProcessInputEvents(RE::InputEvent* const* a_events); bool ShouldSwallowInput(); From 0240bea5c30a9d19fb7bf8c0f8e0ef6c6ecfd7fa Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Jun 2025 23:54:17 -0700 Subject: [PATCH 15/16] fix: casting of lightning color --- src/Features/WeatherPicker.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 2f4bc7cf8d..de1d4581be 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -212,9 +212,9 @@ void WeatherPicker::DisplayLightningInfo(RE::TESWeather* weather, bool showInter 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); + 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)); From bf101a461b990e71b67f9c28eb557b81517cae7a Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 23 Jun 2025 17:33:13 -0700 Subject: [PATCH 16/16] style: sync colors for DrawSectionHeader --- src/Utils/UI.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 907caf8109..e0b925f557 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -411,11 +411,16 @@ namespace Util { bool stateChanged = false; + // Use Menu theme colors for consistent styling + auto& theme = Menu::GetSingleton()->GetTheme().FeatureHeading; + ImVec4 color = useWhiteText ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : theme.ColorDefault; + + ImU32 headerColor = ImGui::GetColorU32(color); + if (isCollapsible && isExpanded) { // Use collapsible header similar to DrawCategoryHeader ImGui::PushID(sectionName); - const ImVec4 headerColor = useWhiteText ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0.8f, 0.8f, 0.8f, 1.0f); ImGui::PushStyleColor(ImGuiCol_Text, headerColor); if (ImGui::CollapsingHeader(sectionName, ImGuiTreeNodeFlags_DefaultOpen)) { @@ -442,13 +447,6 @@ namespace Util // 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; - } - ImU32 headerColor = ImGui::GetColorU32(color); // Left line if (lineLength > 0) {