From 02c2c96e9389e311b29f0ced525a35a6967155bd Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 13 Jan 2026 17:36:29 +1000 Subject: [PATCH 1/4] per weather fixes --- src/WeatherEditor/Weather/WeatherWidget.cpp | 190 +++++++++++++++++--- 1 file changed, 164 insertions(+), 26 deletions(-) diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 645b4f166b..3c0c315417 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -1476,7 +1476,8 @@ void WeatherWidget::RevertChanges() void WeatherWidget::DrawFeatureSettings() { - ImGui::TextWrapped("Configure feature-specific settings that will be applied when this weather is active."); + ImGui::TextWrapped("Configure feature-specific settings that will be applied when this weather is active. " + "These override the feature's global settings for this weather only."); ImGui::Spacing(); auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); @@ -1487,48 +1488,185 @@ void WeatherWidget::DrawFeatureSettings() } std::string featureName = feature->GetShortName(); + auto* featureRegistry = globalRegistry->GetFeatureRegistry(featureName); // Check if feature has registered weather variables - if (!globalRegistry->HasWeatherSupport(featureName)) { + if (!featureRegistry) { continue; } std::string displayName = feature->GetName(); + // Get or initialize feature settings for this weather + if (settings.featureSettings.find(featureName) == settings.featureSettings.end()) { + settings.featureSettings[featureName] = json::object(); + } + auto& featureJson = settings.featureSettings[featureName]; + if (ImGui::TreeNode(displayName.c_str())) { - ImGui::Text("Feature: %s", featureName.c_str()); + bool hasAnySettings = !featureJson.empty(); + + // Header buttons + if (hasAnySettings) { + if (Util::ButtonWithFlash("Reset to Global")) { + featureJson = json::object(); + EditorWindow::GetSingleton()->PushUndoState(this); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Remove all weather-specific overrides and use global feature settings"); + } + } + + ImGui::Spacing(); + ImGui::Separator(); ImGui::Spacing(); - // Show if settings exist for this feature - bool hasSettings = settings.featureSettings.find(featureName) != settings.featureSettings.end() && - !settings.featureSettings[featureName].empty(); + // Draw UI for each registered variable + const auto& variables = featureRegistry->GetVariables(); + bool modified = false; - if (hasSettings) { - ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has weather-specific settings"); + for (const auto& var : variables) { + std::string varName = var->GetName(); + std::string varDisplayName = var->GetDisplayName(); + std::string tooltip = var->GetTooltip(); - if (Util::ButtonWithFlash("Clear Settings")) { - settings.featureSettings[featureName] = json::object(); - } - ImGui::SameLine(); - if (Util::ButtonWithFlash("View JSON")) { - ImGui::OpenPopup("FeatureJSON"); + ImGui::PushID(varName.c_str()); + + // Check if this variable has a weather-specific value + bool hasOverride = featureJson.contains(varName); + + // Get the current value (from weather JSON if exists, otherwise from feature's live value) + json currentValue; + if (hasOverride) { + currentValue = featureJson[varName]; + } else { + // Save current feature value to temporary JSON and extract the value + json tempJson; + var->SaveToJson(tempJson); + currentValue = tempJson[varName]; } - if (ImGui::BeginPopup("FeatureJSON")) { - ImGui::Text("Settings JSON:"); - ImGui::Separator(); - std::string jsonStr = settings.featureSettings[featureName].dump(2); - ImGui::TextWrapped("%s", jsonStr.c_str()); - ImGui::EndPopup(); + // Try to detect variable type and render appropriate control + // Check if it's a FloatVariable (most common case) + if (auto* floatVar = dynamic_cast(var.get())) { + float value = currentValue.get(); + float minVal = floatVar->GetMin(); + float maxVal = floatVar->GetMax(); + + if (!hasOverride) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + } + + if (ImGui::SliderFloat(varDisplayName.c_str(), &value, minVal, maxVal, "%.3f")) { + featureJson[varName] = value; + modified = true; + } + + if (!hasOverride) { + ImGui::PopStyleColor(); + } + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", tooltip.c_str()); + if (!hasOverride) { + ImGui::Separator(); + ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Using global default"); + ImGui::Text("Click and drag to set weather-specific value"); + } + } + + // Right-click context menu to reset individual values + if (hasOverride && ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Reset to Global")) { + featureJson.erase(varName); + modified = true; + } + ImGui::EndPopup(); + } + + } else if (auto* float3Var = dynamic_cast(var.get())) { + // Handle float3 (color) variables + float3 value = currentValue.get(); + float colorArray[3] = { value.x, value.y, value.z }; + + if (!hasOverride) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + } + + if (ImGui::ColorEdit3(varDisplayName.c_str(), colorArray)) { + featureJson[varName] = json{ colorArray[0], colorArray[1], colorArray[2] }; + modified = true; + } + + if (!hasOverride) { + ImGui::PopStyleColor(); + } + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", tooltip.c_str()); + if (!hasOverride) { + ImGui::Separator(); + ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Using global default"); + } + } + + if (hasOverride && ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Reset to Global")) { + featureJson.erase(varName); + modified = true; + } + ImGui::EndPopup(); + } + + } else if (auto* float4Var = dynamic_cast(var.get())) { + // Handle float4 (color with alpha) variables + float4 value = currentValue.get(); + float colorArray[4] = { value.x, value.y, value.z, value.w }; + + if (!hasOverride) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + } + + if (ImGui::ColorEdit4(varDisplayName.c_str(), colorArray)) { + featureJson[varName] = json{ colorArray[0], colorArray[1], colorArray[2], colorArray[3] }; + modified = true; + } + + if (!hasOverride) { + ImGui::PopStyleColor(); + } + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", tooltip.c_str()); + if (!hasOverride) { + ImGui::Separator(); + ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Using global default"); + } + } + + if (hasOverride && ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Reset to Global")) { + featureJson.erase(varName); + modified = true; + } + ImGui::EndPopup(); + } + + } else { + // Generic handling for other types + ImGui::Text("%s: %s", varDisplayName.c_str(), currentValue.dump().c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", tooltip.c_str()); + ImGui::Text("(Generic display - type-specific UI not implemented)"); + } } - } else { - ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "No weather-specific settings"); + + ImGui::PopID(); } - ImGui::Spacing(); - ImGui::TextWrapped( - "Note: Feature settings should be configured through the feature's own settings panel. " - "This section shows which features have per-weather overrides."); + if (modified) { + EditorWindow::GetSingleton()->PushUndoState(this); + } ImGui::TreePop(); } From 7f640979c5ba44872bc515f0e6bfa8f7d48771b1 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 13 Jan 2026 19:46:32 +1000 Subject: [PATCH 2/4] idk --- .../InteriorExteriorSettings.md | 336 ++++++++++++++++++ .../WeatherVariableRegistration.md | 39 +- src/Feature.cpp | 4 +- src/Utils/UI.cpp | 253 +++++++++++++ src/Utils/UI.h | 40 +++ src/WeatherEditor/EditorWindow.cpp | 31 ++ src/WeatherEditor/EditorWindow.h | 3 + src/WeatherEditor/Weather/WeatherWidget.cpp | 216 +++++++---- src/WeatherEditor/Weather/WeatherWidget.h | 5 + src/WeatherManager.cpp | 17 +- 10 files changed, 877 insertions(+), 67 deletions(-) create mode 100644 docs/weather-system-docs/InteriorExteriorSettings.md diff --git a/docs/weather-system-docs/InteriorExteriorSettings.md b/docs/weather-system-docs/InteriorExteriorSettings.md new file mode 100644 index 0000000000..725d478aa1 --- /dev/null +++ b/docs/weather-system-docs/InteriorExteriorSettings.md @@ -0,0 +1,336 @@ +# Interior vs Exterior Settings Management + +## Overview + +This document explores different approaches for managing per-weather feature settings that differ between interior and exterior environments. This is important for features like IBL, lighting, and effects that need different intensities or behaviors indoors vs outdoors. + +## Use Cases + +- **Image-Based Lighting (IBL)**: Strong exterior lighting during sunny weather should be reduced indoors +- **Screen Space Effects**: Full-strength exterior effects may be too intense in small interior spaces +- **Weather-Dependent Effects**: Rain/storm settings for exterior shouldn't fully apply to covered interiors +- **Performance**: Different quality settings for interior vs exterior environments + +--- + +## Option 1: Nested Interior Overrides (Recommended) + +### UI Structure + +``` +[Feature Name (e.g., IBL)] + [Button: Using Weather-Specific Settings ✓] + + Exterior Settings (always shown when weather overrides enabled) + ├─ DiffuseIBLScale: [slider: 5.0] + ├─ IBLSaturation: [slider: 1.2] + └─ ... + + Interior Overrides + ├─ [Button: Override for Interiors ✓/✗] + └─ (If enabled:) + ├─ DiffuseIBLScale: [slider: 0.3] + "Reset to Exterior" + ├─ IBLSaturation: [grayed: 1.2] (inherited from exterior) + └─ ... +``` + +### JSON Structure + +```json +{ + "IBL": { + "__enabled": true, + "DiffuseIBLScale": 5.0, + "IBLSaturation": 1.2, + "__interior_overrides": { + "__enabled": true, + "DiffuseIBLScale": 0.3 + // Other values inherit from exterior + } + } +} +``` + +### Implementation Overview + +**1. UI Changes (WeatherWidget.cpp):** +- After exterior sliders, add collapsible "Interior Overrides" section +- Second toggle button for `__interior_overrides.__enabled` +- Show sliders with exterior value in tooltip when not overridden +- "Use Exterior Value" context menu option for individual settings + +**2. WeatherManager Changes:** +- Add `IsPlayerInInterior()` check using Skyrim's player location data +- When loading settings, detect if player is indoors +- If interior and overrides enabled, merge `__interior_overrides` over base values +- Priority: Interior override > Exterior weather > Global feature settings + +**3. Variable Registration (no changes needed):** +- Same variables work for both contexts +- Registry doesn't need to know about interior/exterior distinction + +### Pros + +- ✅ Intuitive hierarchy (global → weather → exterior → interior) +- ✅ Only override what differs (efficient storage) +- ✅ Easy to see what's different between interior/exterior +- ✅ Follows existing `__enabled` pattern +- ✅ Supports per-variable granularity +- ✅ Authors can disable all interior overrides with one toggle +- ✅ Values inherit naturally (set once, override selectively) + +### Cons + +- ❌ Adds UI complexity with nested sections +- ❌ Requires interior/exterior detection in WeatherManager +- ❌ More clicks to set up initially + +--- + +## Option 2: Side-by-Side Columns + +### UI Structure + +``` +[Feature Name] + [Button: Using Weather-Specific Settings] + [Button: Enable Interior Differences] + + Setting Name | Exterior | Interior + ───────────────────────────────────────── + DiffuseIBLScale | [5.0] | [0.3] + IBLSaturation | [1.2] | [1.2] (grayed if same) + DALCAmount | [0.5] | [0.5] +``` + +### JSON Structure + +```json +{ + "IBL": { + "__enabled": true, + "exterior": { + "DiffuseIBLScale": 5.0, + "IBLSaturation": 1.2 + }, + "interior": { + "DiffuseIBLScale": 0.3, + "IBLSaturation": 1.2 + } + } +} +``` + +### Pros + +- ✅ Easy to compare values at a glance +- ✅ Compact vertical space +- ✅ Clear what differs between interior/exterior +- ✅ No nested sections + +### Cons + +- ❌ Wide UI (may not fit in editor panel) +- ❌ Redundant storage (must set all values for both) +- ❌ Less intuitive for "most settings are the same" use case +- ❌ Harder to implement with ImGui's layout system + +--- + +## Option 3: Interior Multipliers + +### UI Structure + +``` +[Feature Name] + [Button: Using Weather-Specific Settings] + + DiffuseIBLScale: [5.0] [✓ Interior Mult: 0.1x] + IBLSaturation: [1.2] [✗ Interior Mult: 1.0x] + DALCAmount: [0.5] [✓ Interior Mult: 0.8x] +``` + +### JSON Structure + +```json +{ + "IBL": { + "__enabled": true, + "DiffuseIBLScale": 5.0, + "DiffuseIBLScale_InteriorMult": 0.1, // Effective value: 0.5 indoors + "IBLSaturation": 1.2, + "IBLSaturation_InteriorMult": 1.0, // No change indoors + "DALCAmount": 0.5, + "DALCAmount_InteriorMult": 0.8 // Effective value: 0.4 indoors + } +} +``` + +### Pros + +- ✅ Compact UI (one line per setting) +- ✅ Easy to understand relationship (0.5x = half strength) +- ✅ Simple implementation +- ✅ No nested structures + +### Cons + +- ❌ Less flexible (can't set completely independent values) +- ❌ Multipliers may not make sense for all value types (colors, toggles) +- ❌ Can't disable specific settings for interiors +- ❌ Awkward for settings where interior should be higher than exterior + +--- + +## Option 4: Separate Interior Toggle at Top Level + +### UI Structure + +``` +Weather Editor Tabs: +┌─────────────────────────────────┐ +│ [Exterior] | [Interior] │ +└─────────────────────────────────┘ + +Exterior Tab: + [IBL Feature] + ├─ [Using Weather-Specific Settings ✓] + ├─ DiffuseIBLScale: [5.0] + └─ ... + + [ScreenSpaceGI Feature] + └─ ... + +Interior Tab: + [IBL Feature] + ├─ [○ Use Exterior Settings | ● Override] + ├─ DiffuseIBLScale: [0.3] + └─ ... + + [ScreenSpaceGI Feature] + └─ [● Use Exterior Settings] +``` + +### JSON Structure + +```json +{ + "exterior": { + "IBL": { + "__enabled": true, + "DiffuseIBLScale": 5.0 + } + }, + "interior": { + "IBL": { + "__enabled": true, + "DiffuseIBLScale": 0.3 + }, + "ScreenSpaceGI": { + "__use_exterior": true + } + } +} +``` + +### Pros + +- ✅ Clean separation of concerns +- ✅ Entire set of interior settings in one place +- ✅ Can bulk "use exterior for all features" +- ✅ Tab-based approach is familiar + +### Cons + +- ❌ Must switch tabs to compare values +- ❌ More clicking to set up +- ❌ Harder to see what's overridden at a glance +- ❌ Duplicates feature tree structure + +--- + +## Recommendation: Option 1 (Nested Interior Overrides) + +### Why This Approach? + +1. **Natural Hierarchy**: Follows the existing pattern of global → weather-specific → interior-specific +2. **Efficient**: Only store what differs from exterior settings +3. **Discoverable**: UI clearly shows when interior overrides are active +4. **Flexible**: Can override individual settings or all of them +5. **Extensible**: Could add "dungeon overrides" or "underwater overrides" using same pattern + +### Implementation Plan + +#### Phase 1: Core Infrastructure +1. Add interior detection utility function +2. Extend WeatherManager to handle `__interior_overrides` JSON key +3. Update LoadSettingsFromWeather to merge interior overrides when appropriate + +#### Phase 2: UI +1. Add collapsible "Interior Overrides" section in DrawFeatureSettings +2. Add toggle button for `__interior_overrides.__enabled` +3. Implement control rendering with inheritance indicators +4. Add "Copy from Exterior" and "Reset to Exterior" context menu options + +#### Phase 3: Polish +1. Add visual indicators showing which values differ from exterior +2. Add bulk operations (copy all, clear all interior overrides) +3. Update documentation with interior override examples + +### Key Design Decisions + +**Q: Should interior overrides be per-weather or global across all weathers?** +A: Per-weather. Different weathers may need different interior adjustments (e.g., storm vs clear sky). + +**Q: Should there be a "copy all exterior to interior" bulk action?** +A: Yes, as a context menu option on the "Interior Overrides" header. + +**Q: How to handle weather transitions when entering/exiting buildings?** +A: Apply interior/exterior settings immediately on cell change, respecting ongoing weather transitions. The lerp factor remains the same, but the "to" values switch between interior/exterior variants. + +**Q: What about features that shouldn't have interior/exterior differences?** +A: The system is opt-in. If a feature doesn't need interior overrides, authors simply don't enable them. The toggle button is always available but defaults to off. + +### Example Workflow + +1. Author opens weather editor for "Clear" weather +2. Navigates to Features tab → IBL +3. Enables "Using Weather-Specific Settings" +4. Sets DiffuseIBLScale to 5.0 (strong outdoor lighting) +5. Clicks "Interior Overrides" section to expand +6. Clicks "Override for Interiors" toggle button +7. Sets DiffuseIBLScale to 0.8 (reduced indoor lighting) +8. Other IBL settings (saturation, etc.) inherit from exterior automatically +9. Saves weather → JSON contains both exterior and interior values +10. Player enters an interior during clear weather → IBL smoothly adjusts to 0.8 + +--- + +## Future Enhancements + +### Conditional Overrides +- **Dungeon Interiors**: Different settings for caves/dungeons vs buildings +- **Underwater**: Special handling for underwater environments +- **Location-Specific**: Override for specific interior cells (Dragonsreach vs Bannered Mare) + +### Visual Indicators +- Color-code sliders: Green = exterior only, Blue = has interior override +- Show diff values in tooltips: "Exterior: 5.0 → Interior: 0.8" +- Icons indicating inheritance status + +### Bulk Operations +- "Copy All Exterior to Interior" +- "Clear All Interior Overrides" +- "Invert Interior/Exterior" (swap values) +- "Apply Interior Multiplier to All" (global 0.5x reduction) + +### Testing Tools +- "Preview Interior" toggle in editor (shows what values would be with interior overrides) +- "Toggle Interior/Exterior" hotkey for quick testing +- Log window showing which override is active + +--- + +## Conclusion + +Nested interior overrides (Option 1) provides the best balance of flexibility, usability, and efficiency for managing interior vs exterior feature settings in the weather system. It follows existing patterns, minimizes UI complexity, and supports gradual adoption by feature authors. diff --git a/docs/weather-system-docs/WeatherVariableRegistration.md b/docs/weather-system-docs/WeatherVariableRegistration.md index 04671712d6..7b7b9bdbad 100644 --- a/docs/weather-system-docs/WeatherVariableRegistration.md +++ b/docs/weather-system-docs/WeatherVariableRegistration.md @@ -95,14 +95,47 @@ void MyFeature::RegisterWeatherVariables() override } ``` -#### Step 3: Implementation Complete +#### Step 3: Update DrawSettings() to Use Weather-Aware UI Controls + +For weather-controlled settings, use the `Util::WeatherUI` helpers instead of direct ImGui calls. This automatically greys out controls when a per-weather override is active and displays tooltips: + +```cpp +void MyFeature::DrawSettings() +{ + // Weather-aware slider (will be disabled if current weather overrides it) + Util::WeatherUI::SliderFloat("Effect Intensity", this, "Intensity", + &settings.intensity, 0.0f, 2.0f, "%.2f"); + + // Weather-aware color picker + Util::WeatherUI::ColorEdit3("Effect Color", this, "Color", + (float*)&settings.color); + + // Regular checkbox (not weather-controlled in this example) + ImGui::Checkbox("Enable Effect", (bool*)&settings.enabled); +} +``` + +**Available Weather-Aware Helpers:** +- `Util::WeatherUI::SliderFloat()` - Float slider with min/max +- `Util::WeatherUI::Checkbox()` - Boolean checkbox +- `Util::WeatherUI::ColorEdit3()` - RGB color picker +- `Util::WeatherUI::ColorEdit4()` - RGBA color picker with alpha + +**Why Use These?** +- Automatically detects if the current weather has overridden the setting +- Disables and greys out the control to show it's weather-controlled +- Shows tooltip: "Weather Override Active - This setting is controlled by the current weather (WeatherName)" +- Prevents confusion when editing global settings that are overridden by weather + +#### Step 4: Implementation Complete The system now automatically: - Saves/loads weather-specific settings to JSON - Interpolates variables during weather transitions -- Appears in the weather editor UI -- Handles default values and missing dataanced Usage +- Appears in the weather editor UI with per-weather toggle buttons +- Handles default values and missing data +- Shows weather-controlled status in feature settings UI ### Custom Variable Types diff --git a/src/Feature.cpp b/src/Feature.cpp index c5a2681276..89176efc9d 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -36,6 +36,8 @@ #include "Menu.h" #include "SettingsOverrideManager.h" #include "Utils/Format.h" +#include "WeatherManager.h" +#include "WeatherVariableRegistry.h" #include "State.h" @@ -362,4 +364,4 @@ bool Feature::IsFeatureKnown(const std::string& shortName, REL::Version* outVers } return false; -} \ No newline at end of file +} diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index f2524b9f5f..77915e13b9 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -3,6 +3,9 @@ #include "FileSystem.h" #include "Menu.h" #include "Menu/IconLoader.h" +#include "WeatherManager.h" +#include "WeatherVariableRegistry.h" +#include "../WeatherEditor/EditorWindow.h" #ifndef DIRECTINPUT_VERSION # define DIRECTINPUT_VERSION 0x0800 @@ -1329,4 +1332,254 @@ namespace Util return clicked; } + namespace WeatherUI + { + bool IsWeatherControlled(Feature* feature, const char* settingName) + { + if (!feature || !settingName) { + return false; + } + + auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); + auto* weatherManager = WeatherManager::GetSingleton(); + + // Check if this feature has registered weather variables + std::string featureName = feature->GetShortName(); + if (!globalRegistry->HasWeatherSupport(featureName)) { + return false; + } + + // Check if current weather exists + auto currentWeathers = weatherManager->GetCurrentWeathers(); + if (!currentWeathers.currentWeather) { + return false; + } + + // Load weather settings for this feature + json weatherSettings; + if (!weatherManager->LoadSettingsFromWeather(currentWeathers.currentWeather, featureName, weatherSettings)) { + return false; + } + + // Check if this specific setting has an override + return weatherSettings.contains(settingName) && !weatherSettings[settingName].is_null(); + } + + bool SliderFloat(const char* label, Feature* feature, const char* settingName, float* value, float min, float max, const char* format) + { + bool isControlled = IsWeatherControlled(feature, settingName); + + if (isControlled) { + auto* weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + // Make it look like a clickable button when weather-controlled + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.3f, 0.3f, 0.4f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.4f, 0.4f, 0.5f, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.5f, 0.5f, 0.6f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.7f); + } + + ImGuiSliderFlags flags = isControlled ? (static_cast(ImGuiSliderFlags_NoInput) | static_cast(ImGuiSliderFlags_ReadOnly)) : ImGuiSliderFlags_None; + bool changed = ImGui::SliderFloat(label, value, min, max, format, flags); + + if (isControlled) { + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); + + // Check if clicked + if (ImGui::IsItemClicked()) { + auto* weatherManager = WeatherManager::GetSingleton(); + auto* editorWindow = EditorWindow::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + if (currentWeathers.currentWeather && editorWindow) { + editorWindow->OpenWeatherFeatureSetting( + currentWeathers.currentWeather, + feature->GetShortName(), + settingName + ); + } + } + + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + ImGui::BeginTooltip(); + auto* weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Weather Override Active"); + ImGui::TextWrapped("This setting is controlled by the current weather (%s).", + currentWeathers.currentWeather ? currentWeathers.currentWeather->GetFormEditorID() : "Unknown"); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open Weather Editor"); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + return false; // Prevent changes when weather-controlled + } + + return changed; + } + + bool Checkbox(const char* label, Feature* feature, const char* settingName, bool* value) + { + bool isControlled = IsWeatherControlled(feature, settingName); + + if (isControlled) { + auto* weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.3f, 0.3f, 0.4f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.4f, 0.4f, 0.5f, 0.9f)); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.7f); + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + } + + bool changed = ImGui::Checkbox(label, value); + + if (isControlled) { + ImGui::PopItemFlag(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + + if (ImGui::IsItemClicked()) { + auto* weatherManager = WeatherManager::GetSingleton(); + auto* editorWindow = EditorWindow::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + if (currentWeathers.currentWeather && editorWindow) { + editorWindow->OpenWeatherFeatureSetting( + currentWeathers.currentWeather, + feature->GetShortName(), + settingName + ); + } + } + + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + ImGui::BeginTooltip(); + auto* weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Weather Override Active"); + ImGui::TextWrapped("This setting is controlled by the current weather (%s).", + currentWeathers.currentWeather ? currentWeathers.currentWeather->GetFormEditorID() : "Unknown"); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open Weather Editor"); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + return false; + } + + return changed; + } + + bool ColorEdit3(const char* label, Feature* feature, const char* settingName, float col[3]) + { + bool isControlled = IsWeatherControlled(feature, settingName); + + if (isControlled) { + auto* weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.7f); + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + } + + bool changed = ImGui::ColorEdit3(label, col); + + if (isControlled) { + ImGui::PopItemFlag(); + ImGui::PopStyleVar(); + + if (ImGui::IsItemClicked()) { + auto* weatherManager = WeatherManager::GetSingleton(); + auto* editorWindow = EditorWindow::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + if (currentWeathers.currentWeather && editorWindow) { + editorWindow->OpenWeatherFeatureSetting( + currentWeathers.currentWeather, + feature->GetShortName(), + settingName + ); + } + } + + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + ImGui::BeginTooltip(); + auto* weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Weather Override Active"); + ImGui::TextWrapped("This setting is controlled by the current weather (%s).", + currentWeathers.currentWeather ? currentWeathers.currentWeather->GetFormEditorID() : "Unknown"); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open Weather Editor"); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + return false; + } + + return changed; + } + + bool ColorEdit4(const char* label, Feature* feature, const char* settingName, float col[4]) + { + bool isControlled = IsWeatherControlled(feature, settingName); + + if (isControlled) { + auto* weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.7f); + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + } + + bool changed = ImGui::ColorEdit4(label, col); + + if (isControlled) { + ImGui::PopItemFlag(); + ImGui::PopStyleVar(); + + if (ImGui::IsItemClicked()) { + auto* weatherManager = WeatherManager::GetSingleton(); + auto* editorWindow = EditorWindow::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + if (currentWeathers.currentWeather && editorWindow) { + editorWindow->OpenWeatherFeatureSetting( + currentWeathers.currentWeather, + feature->GetShortName(), + settingName + ); + } + } + + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + ImGui::BeginTooltip(); + auto* weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Weather Override Active"); + ImGui::TextWrapped("This setting is controlled by the current weather (%s).", + currentWeathers.currentWeather ? currentWeathers.currentWeather->GetFormEditorID() : "Unknown"); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open Weather Editor"); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + return false; + } + + return changed; + } + } + } // namespace Util diff --git a/src/Utils/UI.h b/src/Utils/UI.h index dbe609df13..d2cf42bfaf 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -197,6 +197,46 @@ namespace Util */ float GetCenterOffsetForContent(float contentWidth); + /** + * Weather-controlled UI helpers + * These functions automatically check if a setting has a weather-specific override + * and disable the control if it's being controlled by the current weather + */ + namespace WeatherUI + { + /** + * Check if a specific setting is currently controlled by weather + * @param feature The feature to check + * @param settingName The name of the setting (must match registered weather variable name) + * @return True if weather is overriding this setting + */ + bool IsWeatherControlled(Feature* feature, const char* settingName); + + /** + * Weather-aware slider float that greys out when controlled by weather + * @param label The label for the slider + * @param feature The feature this setting belongs to + * @param settingName The name of the setting (must match registered weather variable name) + * @param value Pointer to the value + * @param min Minimum value + * @param max Maximum value + * @param format Display format + * @return True if value was changed (only possible when not weather-controlled) + */ + bool SliderFloat(const char* label, Feature* feature, const char* settingName, float* value, float min, float max, const char* format = "%.3f"); + + /** + * Weather-aware checkbox that greys out when controlled by weather + */ + bool Checkbox(const char* label, Feature* feature, const char* settingName, bool* value); + + /** + * Weather-aware color edit that greys out when controlled by weather + */ + bool ColorEdit3(const char* label, Feature* feature, const char* settingName, float col[3]); + bool ColorEdit4(const char* label, Feature* feature, const char* settingName, float col[4]); + } + /** * Draws a custom styled collapsible category header with lines extending from both sides * @param categoryName The name of the category to display diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index e3218876bb..15c5df6cb0 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -1093,6 +1093,37 @@ void EditorWindow::RenderUI() io.FontGlobalScale = previousScale; } +void EditorWindow::OpenWeatherFeatureSetting(RE::TESWeather* weather, const std::string& featureName, const std::string& settingName) +{ + if (!weather) { + return; + } + + // Open the editor if it's not already open + if (!open) { + open = true; + } + + // Find the weather widget + for (auto& widget : weatherWidgets) { + auto* weatherWidget = dynamic_cast(widget.get()); + if (weatherWidget && weatherWidget->weather == weather) { + // Open the widget if it's not already open + if (!weatherWidget->open) { + weatherWidget->open = true; + } + + // Set up navigation to the specific feature/setting + weatherWidget->NavigateToFeatureSetting(featureName, settingName); + + // Focus the widget window + std::string windowName = std::format("{}###widget_{}", weatherWidget->GetEditorID(), (void*)weatherWidget); + ImGui::SetWindowFocus(windowName.c_str()); + break; + } + } +} + EditorWindow::~EditorWindow() { delete tempTexture; diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index 17c73df807..98959811b8 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -161,6 +161,9 @@ class EditorWindow void SaveSessionWidgets(); void RestoreSessionWidgets(); + // Navigation helpers for weather-controlled settings + void OpenWeatherFeatureSetting(RE::TESWeather* weather, const std::string& featureName, const std::string& settingName); + ~EditorWindow(); private: diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 3c0c315417..508c1a5d8a 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -597,6 +597,35 @@ void WeatherWidget::SetWeatherValues() } } weather->cloudLayerDisabledBits = disabledBits; + + // Save feature settings + auto* weatherManager = WeatherManager::GetSingleton(); + for (const auto& [featureName, featureSettings] : settings.featureSettings) { + weatherManager->SaveSettingsToWeather(weather, featureName, featureSettings); + } + + // If this weather is currently active, immediately apply feature settings + auto currentWeathers = weatherManager->GetCurrentWeathers(); + if (currentWeathers.currentWeather == weather) { + auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); + for (const auto& [featureName, featureSettings] : settings.featureSettings) { + // Check if overrides are enabled for this feature + bool enabled = featureSettings.value("__enabled", false); + if (enabled && globalRegistry->HasWeatherSupport(featureName)) { + // Filter out the __enabled flag before applying + json filteredSettings = json::object(); + for (auto it = featureSettings.begin(); it != featureSettings.end(); ++it) { + if (it.key() != "__enabled") { + filteredSettings[it.key()] = it.value(); + } + } + + // Apply the weather-specific settings immediately + json emptyWeather; // No previous weather during instant update + globalRegistry->UpdateFeatureFromWeathers(featureName, emptyWeather, filteredSettings, 1.0f); + } + } + } } void WeatherWidget::LoadWeatherValues() @@ -1503,17 +1532,66 @@ void WeatherWidget::DrawFeatureSettings() } auto& featureJson = settings.featureSettings[featureName]; + // Handle pending navigation - auto-expand this feature if it matches + bool shouldAutoExpand = (pendingFeatureNavigation == featureName); + if (shouldAutoExpand) { + ImGui::SetNextItemOpen(true); + } + if (ImGui::TreeNode(displayName.c_str())) { - bool hasAnySettings = !featureJson.empty(); + // Check if weather-specific overrides are enabled (using special key) + bool overridesEnabled = featureJson.value("__enabled", false); + + // Weather-specific override toggle + ImGui::PushStyleColor(ImGuiCol_Button, overridesEnabled ? ImVec4(0.2f, 0.7f, 0.2f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, overridesEnabled ? ImVec4(0.3f, 0.8f, 0.3f, 1.0f) : ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, overridesEnabled ? ImVec4(0.1f, 0.6f, 0.1f, 1.0f) : ImVec4(0.4f, 0.4f, 0.4f, 1.0f)); + + bool toggleClicked = ImGui::Button(overridesEnabled ? "Using Weather-Specific Settings" : "Using Global Settings", ImVec2(-1, 0)); + + ImGui::PopStyleColor(3); + + if (auto _tt = Util::HoverTooltipWrapper()) { + if (overridesEnabled) { + ImGui::Text("This weather has custom overrides for this feature."); + ImGui::Text("Click to disable overrides and use global settings instead."); + ImGui::Text("(Settings will be preserved but not applied)"); + } else { + ImGui::Text("This weather uses global feature settings."); + ImGui::Text("Click to enable weather-specific overrides."); + } + } - // Header buttons - if (hasAnySettings) { - if (Util::ButtonWithFlash("Reset to Global")) { - featureJson = json::object(); - EditorWindow::GetSingleton()->PushUndoState(this); + if (toggleClicked) { + if (overridesEnabled) { + // Disable overrides - mark as disabled but keep the settings + featureJson["__enabled"] = false; + } else { + // Enable overrides - mark as enabled + featureJson["__enabled"] = true; + // If no settings exist yet, copy current global values as starting point + bool hasActualSettings = false; + for (auto it = featureJson.begin(); it != featureJson.end(); ++it) { + if (it.key() != "__enabled") { + hasActualSettings = true; + break; + } + } + if (!hasActualSettings) { + const auto& variables = featureRegistry->GetVariables(); + for (const auto& var : variables) { + json tempJson; + var->SaveToJson(tempJson); + std::string varName = var->GetName(); + if (tempJson.contains(varName)) { + featureJson[varName] = tempJson[varName]; + } + } + } } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Remove all weather-specific overrides and use global feature settings"); + EditorWindow::GetSingleton()->PushUndoState(this); + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } } @@ -1521,9 +1599,11 @@ void WeatherWidget::DrawFeatureSettings() ImGui::Separator(); ImGui::Spacing(); - // Draw UI for each registered variable - const auto& variables = featureRegistry->GetVariables(); - bool modified = false; + // Only show controls if weather-specific overrides are enabled + if (overridesEnabled) { + // Draw UI for each registered variable + const auto& variables = featureRegistry->GetVariables(); + bool modified = false; for (const auto& var : variables) { std::string varName = var->GetName(); @@ -1535,48 +1615,59 @@ void WeatherWidget::DrawFeatureSettings() // Check if this variable has a weather-specific value bool hasOverride = featureJson.contains(varName); - // Get the current value (from weather JSON if exists, otherwise from feature's live value) - json currentValue; - if (hasOverride) { - currentValue = featureJson[varName]; - } else { - // Save current feature value to temporary JSON and extract the value + // Get the current value + // If we have an override, use it; otherwise get from feature's live value + if (!hasOverride) { + // Initialize from feature's current value json tempJson; var->SaveToJson(tempJson); - currentValue = tempJson[varName]; + if (tempJson.contains(varName)) { + featureJson[varName] = tempJson[varName]; + hasOverride = true; // Now we have a value to work with + } } + json currentValue = featureJson[varName]; + // Try to detect variable type and render appropriate control - // Check if it's a FloatVariable (most common case) - if (auto* floatVar = dynamic_cast(var.get())) { + // Check if it's a bool variable first + if (auto* boolVar = dynamic_cast*>(var.get())) { + bool value = currentValue.get(); + + if (ImGui::Checkbox(varDisplayName.c_str(), &value)) { + featureJson[varName] = value; + modified = true; + } + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", tooltip.c_str()); + } + + // Right-click context menu to reset individual values + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Reset to Global")) { + featureJson.erase(varName); + modified = true; + } + ImGui::EndPopup(); + } + + } else if (auto* floatVar = dynamic_cast(var.get())) { float value = currentValue.get(); float minVal = floatVar->GetMin(); float maxVal = floatVar->GetMax(); - if (!hasOverride) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); - } - if (ImGui::SliderFloat(varDisplayName.c_str(), &value, minVal, maxVal, "%.3f")) { featureJson[varName] = value; modified = true; } - if (!hasOverride) { - ImGui::PopStyleColor(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("%s", tooltip.c_str()); - if (!hasOverride) { - ImGui::Separator(); - ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Using global default"); - ImGui::Text("Click and drag to set weather-specific value"); - } } // Right-click context menu to reset individual values - if (hasOverride && ImGui::BeginPopupContextItem()) { + if (ImGui::BeginPopupContextItem()) { if (ImGui::MenuItem("Reset to Global")) { featureJson.erase(varName); modified = true; @@ -1589,28 +1680,16 @@ void WeatherWidget::DrawFeatureSettings() float3 value = currentValue.get(); float colorArray[3] = { value.x, value.y, value.z }; - if (!hasOverride) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); - } - if (ImGui::ColorEdit3(varDisplayName.c_str(), colorArray)) { featureJson[varName] = json{ colorArray[0], colorArray[1], colorArray[2] }; modified = true; } - if (!hasOverride) { - ImGui::PopStyleColor(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("%s", tooltip.c_str()); - if (!hasOverride) { - ImGui::Separator(); - ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Using global default"); - } } - if (hasOverride && ImGui::BeginPopupContextItem()) { + if (ImGui::BeginPopupContextItem()) { if (ImGui::MenuItem("Reset to Global")) { featureJson.erase(varName); modified = true; @@ -1623,28 +1702,16 @@ void WeatherWidget::DrawFeatureSettings() float4 value = currentValue.get(); float colorArray[4] = { value.x, value.y, value.z, value.w }; - if (!hasOverride) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); - } - if (ImGui::ColorEdit4(varDisplayName.c_str(), colorArray)) { featureJson[varName] = json{ colorArray[0], colorArray[1], colorArray[2], colorArray[3] }; modified = true; } - if (!hasOverride) { - ImGui::PopStyleColor(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("%s", tooltip.c_str()); - if (!hasOverride) { - ImGui::Separator(); - ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Using global default"); - } } - if (hasOverride && ImGui::BeginPopupContextItem()) { + if (ImGui::BeginPopupContextItem()) { if (ImGui::MenuItem("Reset to Global")) { featureJson.erase(varName); modified = true; @@ -1654,10 +1721,12 @@ void WeatherWidget::DrawFeatureSettings() } else { // Generic handling for other types - ImGui::Text("%s: %s", varDisplayName.c_str(), currentValue.dump().c_str()); + ImGui::TextDisabled("%s: %s", varDisplayName.c_str(), currentValue.dump().c_str()); if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "Unsupported Variable Type"); ImGui::Text("%s", tooltip.c_str()); - ImGui::Text("(Generic display - type-specific UI not implemented)"); + ImGui::Separator(); + ImGui::TextWrapped("This variable type doesn't have a custom UI implementation yet. The raw JSON value is shown above."); } } @@ -1666,11 +1735,34 @@ void WeatherWidget::DrawFeatureSettings() if (modified) { EditorWindow::GetSingleton()->PushUndoState(this); + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } + + } else { + ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Enable weather-specific overrides above to customize settings for this weather."); } ImGui::TreePop(); } } + + // Clear navigation state after processing + if (!pendingFeatureNavigation.empty()) { + pendingFeatureNavigation.clear(); + pendingSettingHighlight.clear(); + } +} + +void WeatherWidget::NavigateToFeatureSetting(const std::string& featureName, const std::string& settingName) +{ + // Store the navigation request + pendingFeatureNavigation = featureName; + pendingSettingHighlight = settingName; + + // Switch to Features tab + activeTabOverride = "Features"; } void WeatherWidget::UpdateSearchResults() diff --git a/src/WeatherEditor/Weather/WeatherWidget.h b/src/WeatherEditor/Weather/WeatherWidget.h index cf3f0f5403..84c696cbd4 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.h +++ b/src/WeatherEditor/Weather/WeatherWidget.h @@ -123,6 +123,11 @@ class WeatherWidget : public Widget void SaveFeatureSettings(); void LoadFeatureSettings(); + // Navigation state for opening specific features + std::string pendingFeatureNavigation = ""; + std::string pendingSettingHighlight = ""; + void NavigateToFeatureSetting(const std::string& featureName, const std::string& settingName); + private: void DrawDALCSettings(); void DrawWeatherColorSettings(); diff --git a/src/WeatherManager.cpp b/src/WeatherManager.cpp index 2dd2d14e8f..99a0707b0e 100644 --- a/src/WeatherManager.cpp +++ b/src/WeatherManager.cpp @@ -209,7 +209,22 @@ bool WeatherManager::LoadSettingsFromWeather(RE::TESWeather* weather, const std: if (weatherIt != perWeatherSettingsCache.end()) { auto featureIt = weatherIt->second.find(featureName); if (featureIt != weatherIt->second.end()) { - o_json = featureIt->second; + const json& featureJson = featureIt->second; + + // Check if weather-specific overrides are enabled + bool enabled = featureJson.value("__enabled", false); + if (!enabled) { + // Settings exist but are disabled, return empty + return false; + } + + // Copy all settings except the __enabled flag + o_json = json::object(); + for (auto it = featureJson.begin(); it != featureJson.end(); ++it) { + if (it.key() != "__enabled") { + o_json[it.key()] = it.value(); + } + } return true; } } From 0d6c8f11bda5fac5b4d9ff11b14c41d1016fecf8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:06:34 +0000 Subject: [PATCH 3/4] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commit.?= =?UTF-8?q?ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../InteriorExteriorSettings.md | 226 +++++++++--------- .../WeatherVariableRegistration.md | 26 +- src/Utils/UI.cpp | 30 +-- src/WeatherEditor/Weather/WeatherWidget.cpp | 221 ++++++++--------- src/WeatherManager.cpp | 4 +- 5 files changed, 258 insertions(+), 249 deletions(-) diff --git a/docs/weather-system-docs/InteriorExteriorSettings.md b/docs/weather-system-docs/InteriorExteriorSettings.md index 725d478aa1..58d68b0d2a 100644 --- a/docs/weather-system-docs/InteriorExteriorSettings.md +++ b/docs/weather-system-docs/InteriorExteriorSettings.md @@ -6,10 +6,10 @@ This document explores different approaches for managing per-weather feature set ## Use Cases -- **Image-Based Lighting (IBL)**: Strong exterior lighting during sunny weather should be reduced indoors -- **Screen Space Effects**: Full-strength exterior effects may be too intense in small interior spaces -- **Weather-Dependent Effects**: Rain/storm settings for exterior shouldn't fully apply to covered interiors -- **Performance**: Different quality settings for interior vs exterior environments +- **Image-Based Lighting (IBL)**: Strong exterior lighting during sunny weather should be reduced indoors +- **Screen Space Effects**: Full-strength exterior effects may be too intense in small interior spaces +- **Weather-Dependent Effects**: Rain/storm settings for exterior shouldn't fully apply to covered interiors +- **Performance**: Different quality settings for interior vs exterior environments --- @@ -20,12 +20,12 @@ This document explores different approaches for managing per-weather feature set ``` [Feature Name (e.g., IBL)] [Button: Using Weather-Specific Settings ✓] - + Exterior Settings (always shown when weather overrides enabled) ├─ DiffuseIBLScale: [slider: 5.0] ├─ IBLSaturation: [slider: 1.2] └─ ... - + Interior Overrides ├─ [Button: Override for Interiors ✓/✗] └─ (If enabled:) @@ -38,52 +38,55 @@ This document explores different approaches for managing per-weather feature set ```json { - "IBL": { - "__enabled": true, - "DiffuseIBLScale": 5.0, - "IBLSaturation": 1.2, - "__interior_overrides": { - "__enabled": true, - "DiffuseIBLScale": 0.3 - // Other values inherit from exterior + "IBL": { + "__enabled": true, + "DiffuseIBLScale": 5.0, + "IBLSaturation": 1.2, + "__interior_overrides": { + "__enabled": true, + "DiffuseIBLScale": 0.3 + // Other values inherit from exterior + } } - } } ``` ### Implementation Overview **1. UI Changes (WeatherWidget.cpp):** -- After exterior sliders, add collapsible "Interior Overrides" section -- Second toggle button for `__interior_overrides.__enabled` -- Show sliders with exterior value in tooltip when not overridden -- "Use Exterior Value" context menu option for individual settings + +- After exterior sliders, add collapsible "Interior Overrides" section +- Second toggle button for `__interior_overrides.__enabled` +- Show sliders with exterior value in tooltip when not overridden +- "Use Exterior Value" context menu option for individual settings **2. WeatherManager Changes:** -- Add `IsPlayerInInterior()` check using Skyrim's player location data -- When loading settings, detect if player is indoors -- If interior and overrides enabled, merge `__interior_overrides` over base values -- Priority: Interior override > Exterior weather > Global feature settings + +- Add `IsPlayerInInterior()` check using Skyrim's player location data +- When loading settings, detect if player is indoors +- If interior and overrides enabled, merge `__interior_overrides` over base values +- Priority: Interior override > Exterior weather > Global feature settings **3. Variable Registration (no changes needed):** -- Same variables work for both contexts -- Registry doesn't need to know about interior/exterior distinction + +- Same variables work for both contexts +- Registry doesn't need to know about interior/exterior distinction ### Pros -- ✅ Intuitive hierarchy (global → weather → exterior → interior) -- ✅ Only override what differs (efficient storage) -- ✅ Easy to see what's different between interior/exterior -- ✅ Follows existing `__enabled` pattern -- ✅ Supports per-variable granularity -- ✅ Authors can disable all interior overrides with one toggle -- ✅ Values inherit naturally (set once, override selectively) +- ✅ Intuitive hierarchy (global → weather → exterior → interior) +- ✅ Only override what differs (efficient storage) +- ✅ Easy to see what's different between interior/exterior +- ✅ Follows existing `__enabled` pattern +- ✅ Supports per-variable granularity +- ✅ Authors can disable all interior overrides with one toggle +- ✅ Values inherit naturally (set once, override selectively) ### Cons -- ❌ Adds UI complexity with nested sections -- ❌ Requires interior/exterior detection in WeatherManager -- ❌ More clicks to set up initially +- ❌ Adds UI complexity with nested sections +- ❌ Requires interior/exterior detection in WeatherManager +- ❌ More clicks to set up initially --- @@ -95,7 +98,7 @@ This document explores different approaches for managing per-weather feature set [Feature Name] [Button: Using Weather-Specific Settings] [Button: Enable Interior Differences] - + Setting Name | Exterior | Interior ───────────────────────────────────────── DiffuseIBLScale | [5.0] | [0.3] @@ -107,33 +110,33 @@ This document explores different approaches for managing per-weather feature set ```json { - "IBL": { - "__enabled": true, - "exterior": { - "DiffuseIBLScale": 5.0, - "IBLSaturation": 1.2 - }, - "interior": { - "DiffuseIBLScale": 0.3, - "IBLSaturation": 1.2 + "IBL": { + "__enabled": true, + "exterior": { + "DiffuseIBLScale": 5.0, + "IBLSaturation": 1.2 + }, + "interior": { + "DiffuseIBLScale": 0.3, + "IBLSaturation": 1.2 + } } - } } ``` ### Pros -- ✅ Easy to compare values at a glance -- ✅ Compact vertical space -- ✅ Clear what differs between interior/exterior -- ✅ No nested sections +- ✅ Easy to compare values at a glance +- ✅ Compact vertical space +- ✅ Clear what differs between interior/exterior +- ✅ No nested sections ### Cons -- ❌ Wide UI (may not fit in editor panel) -- ❌ Redundant storage (must set all values for both) -- ❌ Less intuitive for "most settings are the same" use case -- ❌ Harder to implement with ImGui's layout system +- ❌ Wide UI (may not fit in editor panel) +- ❌ Redundant storage (must set all values for both) +- ❌ Less intuitive for "most settings are the same" use case +- ❌ Harder to implement with ImGui's layout system --- @@ -144,7 +147,7 @@ This document explores different approaches for managing per-weather feature set ``` [Feature Name] [Button: Using Weather-Specific Settings] - + DiffuseIBLScale: [5.0] [✓ Interior Mult: 0.1x] IBLSaturation: [1.2] [✗ Interior Mult: 1.0x] DALCAmount: [0.5] [✓ Interior Mult: 0.8x] @@ -154,31 +157,31 @@ This document explores different approaches for managing per-weather feature set ```json { - "IBL": { - "__enabled": true, - "DiffuseIBLScale": 5.0, - "DiffuseIBLScale_InteriorMult": 0.1, // Effective value: 0.5 indoors - "IBLSaturation": 1.2, - "IBLSaturation_InteriorMult": 1.0, // No change indoors - "DALCAmount": 0.5, - "DALCAmount_InteriorMult": 0.8 // Effective value: 0.4 indoors - } + "IBL": { + "__enabled": true, + "DiffuseIBLScale": 5.0, + "DiffuseIBLScale_InteriorMult": 0.1, // Effective value: 0.5 indoors + "IBLSaturation": 1.2, + "IBLSaturation_InteriorMult": 1.0, // No change indoors + "DALCAmount": 0.5, + "DALCAmount_InteriorMult": 0.8 // Effective value: 0.4 indoors + } } ``` ### Pros -- ✅ Compact UI (one line per setting) -- ✅ Easy to understand relationship (0.5x = half strength) -- ✅ Simple implementation -- ✅ No nested structures +- ✅ Compact UI (one line per setting) +- ✅ Easy to understand relationship (0.5x = half strength) +- ✅ Simple implementation +- ✅ No nested structures ### Cons -- ❌ Less flexible (can't set completely independent values) -- ❌ Multipliers may not make sense for all value types (colors, toggles) -- ❌ Can't disable specific settings for interiors -- ❌ Awkward for settings where interior should be higher than exterior +- ❌ Less flexible (can't set completely independent values) +- ❌ Multipliers may not make sense for all value types (colors, toggles) +- ❌ Can't disable specific settings for interiors +- ❌ Awkward for settings where interior should be higher than exterior --- @@ -197,7 +200,7 @@ Exterior Tab: ├─ [Using Weather-Specific Settings ✓] ├─ DiffuseIBLScale: [5.0] └─ ... - + [ScreenSpaceGI Feature] └─ ... @@ -206,7 +209,7 @@ Interior Tab: ├─ [○ Use Exterior Settings | ● Override] ├─ DiffuseIBLScale: [0.3] └─ ... - + [ScreenSpaceGI Feature] └─ [● Use Exterior Settings] ``` @@ -215,37 +218,37 @@ Interior Tab: ```json { - "exterior": { - "IBL": { - "__enabled": true, - "DiffuseIBLScale": 5.0 - } - }, - "interior": { - "IBL": { - "__enabled": true, - "DiffuseIBLScale": 0.3 + "exterior": { + "IBL": { + "__enabled": true, + "DiffuseIBLScale": 5.0 + } }, - "ScreenSpaceGI": { - "__use_exterior": true + "interior": { + "IBL": { + "__enabled": true, + "DiffuseIBLScale": 0.3 + }, + "ScreenSpaceGI": { + "__use_exterior": true + } } - } } ``` ### Pros -- ✅ Clean separation of concerns -- ✅ Entire set of interior settings in one place -- ✅ Can bulk "use exterior for all features" -- ✅ Tab-based approach is familiar +- ✅ Clean separation of concerns +- ✅ Entire set of interior settings in one place +- ✅ Can bulk "use exterior for all features" +- ✅ Tab-based approach is familiar ### Cons -- ❌ Must switch tabs to compare values -- ❌ More clicking to set up -- ❌ Harder to see what's overridden at a glance -- ❌ Duplicates feature tree structure +- ❌ Must switch tabs to compare values +- ❌ More clicking to set up +- ❌ Harder to see what's overridden at a glance +- ❌ Duplicates feature tree structure --- @@ -262,17 +265,20 @@ Interior Tab: ### Implementation Plan #### Phase 1: Core Infrastructure + 1. Add interior detection utility function 2. Extend WeatherManager to handle `__interior_overrides` JSON key 3. Update LoadSettingsFromWeather to merge interior overrides when appropriate #### Phase 2: UI + 1. Add collapsible "Interior Overrides" section in DrawFeatureSettings 2. Add toggle button for `__interior_overrides.__enabled` 3. Implement control rendering with inheritance indicators 4. Add "Copy from Exterior" and "Reset to Exterior" context menu options #### Phase 3: Polish + 1. Add visual indicators showing which values differ from exterior 2. Add bulk operations (copy all, clear all interior overrides) 3. Update documentation with interior override examples @@ -309,25 +315,29 @@ A: The system is opt-in. If a feature doesn't need interior overrides, authors s ## Future Enhancements ### Conditional Overrides -- **Dungeon Interiors**: Different settings for caves/dungeons vs buildings -- **Underwater**: Special handling for underwater environments -- **Location-Specific**: Override for specific interior cells (Dragonsreach vs Bannered Mare) + +- **Dungeon Interiors**: Different settings for caves/dungeons vs buildings +- **Underwater**: Special handling for underwater environments +- **Location-Specific**: Override for specific interior cells (Dragonsreach vs Bannered Mare) ### Visual Indicators -- Color-code sliders: Green = exterior only, Blue = has interior override -- Show diff values in tooltips: "Exterior: 5.0 → Interior: 0.8" -- Icons indicating inheritance status + +- Color-code sliders: Green = exterior only, Blue = has interior override +- Show diff values in tooltips: "Exterior: 5.0 → Interior: 0.8" +- Icons indicating inheritance status ### Bulk Operations -- "Copy All Exterior to Interior" -- "Clear All Interior Overrides" -- "Invert Interior/Exterior" (swap values) -- "Apply Interior Multiplier to All" (global 0.5x reduction) + +- "Copy All Exterior to Interior" +- "Clear All Interior Overrides" +- "Invert Interior/Exterior" (swap values) +- "Apply Interior Multiplier to All" (global 0.5x reduction) ### Testing Tools -- "Preview Interior" toggle in editor (shows what values would be with interior overrides) -- "Toggle Interior/Exterior" hotkey for quick testing -- Log window showing which override is active + +- "Preview Interior" toggle in editor (shows what values would be with interior overrides) +- "Toggle Interior/Exterior" hotkey for quick testing +- Log window showing which override is active --- diff --git a/docs/weather-system-docs/WeatherVariableRegistration.md b/docs/weather-system-docs/WeatherVariableRegistration.md index 7b7b9bdbad..09c9c2aa12 100644 --- a/docs/weather-system-docs/WeatherVariableRegistration.md +++ b/docs/weather-system-docs/WeatherVariableRegistration.md @@ -103,29 +103,31 @@ For weather-controlled settings, use the `Util::WeatherUI` helpers instead of di void MyFeature::DrawSettings() { // Weather-aware slider (will be disabled if current weather overrides it) - Util::WeatherUI::SliderFloat("Effect Intensity", this, "Intensity", + Util::WeatherUI::SliderFloat("Effect Intensity", this, "Intensity", &settings.intensity, 0.0f, 2.0f, "%.2f"); - + // Weather-aware color picker - Util::WeatherUI::ColorEdit3("Effect Color", this, "Color", + Util::WeatherUI::ColorEdit3("Effect Color", this, "Color", (float*)&settings.color); - + // Regular checkbox (not weather-controlled in this example) ImGui::Checkbox("Enable Effect", (bool*)&settings.enabled); } ``` **Available Weather-Aware Helpers:** -- `Util::WeatherUI::SliderFloat()` - Float slider with min/max -- `Util::WeatherUI::Checkbox()` - Boolean checkbox -- `Util::WeatherUI::ColorEdit3()` - RGB color picker -- `Util::WeatherUI::ColorEdit4()` - RGBA color picker with alpha + +- `Util::WeatherUI::SliderFloat()` - Float slider with min/max +- `Util::WeatherUI::Checkbox()` - Boolean checkbox +- `Util::WeatherUI::ColorEdit3()` - RGB color picker +- `Util::WeatherUI::ColorEdit4()` - RGBA color picker with alpha **Why Use These?** -- Automatically detects if the current weather has overridden the setting -- Disables and greys out the control to show it's weather-controlled -- Shows tooltip: "Weather Override Active - This setting is controlled by the current weather (WeatherName)" -- Prevents confusion when editing global settings that are overridden by weather + +- Automatically detects if the current weather has overridden the setting +- Disables and greys out the control to show it's weather-controlled +- Shows tooltip: "Weather Override Active - This setting is controlled by the current weather (WeatherName)" +- Prevents confusion when editing global settings that are overridden by weather #### Step 4: Implementation Complete diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 77915e13b9..4fc410c9b1 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1,11 +1,11 @@ #include "UI.h" +#include "../WeatherEditor/EditorWindow.h" #include "FileSystem.h" #include "Menu.h" #include "Menu/IconLoader.h" #include "WeatherManager.h" #include "WeatherVariableRegistry.h" -#include "../WeatherEditor/EditorWindow.h" #ifndef DIRECTINPUT_VERSION # define DIRECTINPUT_VERSION 0x0800 @@ -1372,7 +1372,7 @@ namespace Util if (isControlled) { auto* weatherManager = WeatherManager::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); - + // Make it look like a clickable button when weather-controlled ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.3f, 0.3f, 0.4f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.4f, 0.4f, 0.5f, 0.9f)); @@ -1392,13 +1392,12 @@ namespace Util auto* weatherManager = WeatherManager::GetSingleton(); auto* editorWindow = EditorWindow::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); - + if (currentWeathers.currentWeather && editorWindow) { editorWindow->OpenWeatherFeatureSetting( currentWeathers.currentWeather, feature->GetShortName(), - settingName - ); + settingName); } } @@ -1429,7 +1428,7 @@ namespace Util if (isControlled) { auto* weatherManager = WeatherManager::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); - + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.3f, 0.3f, 0.4f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.4f, 0.4f, 0.5f, 0.9f)); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.7f); @@ -1447,13 +1446,12 @@ namespace Util auto* weatherManager = WeatherManager::GetSingleton(); auto* editorWindow = EditorWindow::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); - + if (currentWeathers.currentWeather && editorWindow) { editorWindow->OpenWeatherFeatureSetting( currentWeathers.currentWeather, feature->GetShortName(), - settingName - ); + settingName); } } @@ -1484,7 +1482,7 @@ namespace Util if (isControlled) { auto* weatherManager = WeatherManager::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); - + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.7f); ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); } @@ -1499,13 +1497,12 @@ namespace Util auto* weatherManager = WeatherManager::GetSingleton(); auto* editorWindow = EditorWindow::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); - + if (currentWeathers.currentWeather && editorWindow) { editorWindow->OpenWeatherFeatureSetting( currentWeathers.currentWeather, feature->GetShortName(), - settingName - ); + settingName); } } @@ -1536,7 +1533,7 @@ namespace Util if (isControlled) { auto* weatherManager = WeatherManager::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); - + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.7f); ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); } @@ -1551,13 +1548,12 @@ namespace Util auto* weatherManager = WeatherManager::GetSingleton(); auto* editorWindow = EditorWindow::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); - + if (currentWeathers.currentWeather && editorWindow) { editorWindow->OpenWeatherFeatureSetting( currentWeathers.currentWeather, feature->GetShortName(), - settingName - ); + settingName); } } diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 508c1a5d8a..9350a88169 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -619,7 +619,7 @@ void WeatherWidget::SetWeatherValues() filteredSettings[it.key()] = it.value(); } } - + // Apply the weather-specific settings immediately json emptyWeather; // No previous weather during instant update globalRegistry->UpdateFeatureFromWeathers(featureName, emptyWeather, filteredSettings, 1.0f); @@ -1505,8 +1505,9 @@ void WeatherWidget::RevertChanges() void WeatherWidget::DrawFeatureSettings() { - ImGui::TextWrapped("Configure feature-specific settings that will be applied when this weather is active. " - "These override the feature's global settings for this weather only."); + ImGui::TextWrapped( + "Configure feature-specific settings that will be applied when this weather is active. " + "These override the feature's global settings for this weather only."); ImGui::Spacing(); auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); @@ -1546,9 +1547,9 @@ void WeatherWidget::DrawFeatureSettings() ImGui::PushStyleColor(ImGuiCol_Button, overridesEnabled ? ImVec4(0.2f, 0.7f, 0.2f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, overridesEnabled ? ImVec4(0.3f, 0.8f, 0.3f, 1.0f) : ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, overridesEnabled ? ImVec4(0.1f, 0.6f, 0.1f, 1.0f) : ImVec4(0.4f, 0.4f, 0.4f, 1.0f)); - + bool toggleClicked = ImGui::Button(overridesEnabled ? "Using Weather-Specific Settings" : "Using Global Settings", ImVec2(-1, 0)); - + ImGui::PopStyleColor(3); if (auto _tt = Util::HoverTooltipWrapper()) { @@ -1605,140 +1606,140 @@ void WeatherWidget::DrawFeatureSettings() const auto& variables = featureRegistry->GetVariables(); bool modified = false; - for (const auto& var : variables) { - std::string varName = var->GetName(); - std::string varDisplayName = var->GetDisplayName(); - std::string tooltip = var->GetTooltip(); - - ImGui::PushID(varName.c_str()); - - // Check if this variable has a weather-specific value - bool hasOverride = featureJson.contains(varName); - - // Get the current value - // If we have an override, use it; otherwise get from feature's live value - if (!hasOverride) { - // Initialize from feature's current value - json tempJson; - var->SaveToJson(tempJson); - if (tempJson.contains(varName)) { - featureJson[varName] = tempJson[varName]; - hasOverride = true; // Now we have a value to work with + for (const auto& var : variables) { + std::string varName = var->GetName(); + std::string varDisplayName = var->GetDisplayName(); + std::string tooltip = var->GetTooltip(); + + ImGui::PushID(varName.c_str()); + + // Check if this variable has a weather-specific value + bool hasOverride = featureJson.contains(varName); + + // Get the current value + // If we have an override, use it; otherwise get from feature's live value + if (!hasOverride) { + // Initialize from feature's current value + json tempJson; + var->SaveToJson(tempJson); + if (tempJson.contains(varName)) { + featureJson[varName] = tempJson[varName]; + hasOverride = true; // Now we have a value to work with + } } - } - - json currentValue = featureJson[varName]; - // Try to detect variable type and render appropriate control - // Check if it's a bool variable first - if (auto* boolVar = dynamic_cast*>(var.get())) { - bool value = currentValue.get(); + json currentValue = featureJson[varName]; - if (ImGui::Checkbox(varDisplayName.c_str(), &value)) { - featureJson[varName] = value; - modified = true; - } - - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", tooltip.c_str()); - } + // Try to detect variable type and render appropriate control + // Check if it's a bool variable first + if (auto* boolVar = dynamic_cast*>(var.get())) { + bool value = currentValue.get(); - // Right-click context menu to reset individual values - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to Global")) { - featureJson.erase(varName); + if (ImGui::Checkbox(varDisplayName.c_str(), &value)) { + featureJson[varName] = value; modified = true; } - ImGui::EndPopup(); - } - } else if (auto* floatVar = dynamic_cast(var.get())) { - float value = currentValue.get(); - float minVal = floatVar->GetMin(); - float maxVal = floatVar->GetMax(); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", tooltip.c_str()); + } - if (ImGui::SliderFloat(varDisplayName.c_str(), &value, minVal, maxVal, "%.3f")) { - featureJson[varName] = value; - modified = true; - } + // Right-click context menu to reset individual values + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Reset to Global")) { + featureJson.erase(varName); + modified = true; + } + ImGui::EndPopup(); + } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", tooltip.c_str()); - } + } else if (auto* floatVar = dynamic_cast(var.get())) { + float value = currentValue.get(); + float minVal = floatVar->GetMin(); + float maxVal = floatVar->GetMax(); - // Right-click context menu to reset individual values - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to Global")) { - featureJson.erase(varName); + if (ImGui::SliderFloat(varDisplayName.c_str(), &value, minVal, maxVal, "%.3f")) { + featureJson[varName] = value; modified = true; } - ImGui::EndPopup(); - } - } else if (auto* float3Var = dynamic_cast(var.get())) { - // Handle float3 (color) variables - float3 value = currentValue.get(); - float colorArray[3] = { value.x, value.y, value.z }; + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", tooltip.c_str()); + } - if (ImGui::ColorEdit3(varDisplayName.c_str(), colorArray)) { - featureJson[varName] = json{ colorArray[0], colorArray[1], colorArray[2] }; - modified = true; - } + // Right-click context menu to reset individual values + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Reset to Global")) { + featureJson.erase(varName); + modified = true; + } + ImGui::EndPopup(); + } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", tooltip.c_str()); - } + } else if (auto* float3Var = dynamic_cast(var.get())) { + // Handle float3 (color) variables + float3 value = currentValue.get(); + float colorArray[3] = { value.x, value.y, value.z }; - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to Global")) { - featureJson.erase(varName); + if (ImGui::ColorEdit3(varDisplayName.c_str(), colorArray)) { + featureJson[varName] = json{ colorArray[0], colorArray[1], colorArray[2] }; modified = true; } - ImGui::EndPopup(); - } - } else if (auto* float4Var = dynamic_cast(var.get())) { - // Handle float4 (color with alpha) variables - float4 value = currentValue.get(); - float colorArray[4] = { value.x, value.y, value.z, value.w }; + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", tooltip.c_str()); + } - if (ImGui::ColorEdit4(varDisplayName.c_str(), colorArray)) { - featureJson[varName] = json{ colorArray[0], colorArray[1], colorArray[2], colorArray[3] }; - modified = true; - } + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Reset to Global")) { + featureJson.erase(varName); + modified = true; + } + ImGui::EndPopup(); + } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", tooltip.c_str()); - } + } else if (auto* float4Var = dynamic_cast(var.get())) { + // Handle float4 (color with alpha) variables + float4 value = currentValue.get(); + float colorArray[4] = { value.x, value.y, value.z, value.w }; - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to Global")) { - featureJson.erase(varName); + if (ImGui::ColorEdit4(varDisplayName.c_str(), colorArray)) { + featureJson[varName] = json{ colorArray[0], colorArray[1], colorArray[2], colorArray[3] }; modified = true; } - ImGui::EndPopup(); - } - } else { - // Generic handling for other types - ImGui::TextDisabled("%s: %s", varDisplayName.c_str(), currentValue.dump().c_str()); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "Unsupported Variable Type"); - ImGui::Text("%s", tooltip.c_str()); - ImGui::Separator(); - ImGui::TextWrapped("This variable type doesn't have a custom UI implementation yet. The raw JSON value is shown above."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", tooltip.c_str()); + } + + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Reset to Global")) { + featureJson.erase(varName); + modified = true; + } + ImGui::EndPopup(); + } + + } else { + // Generic handling for other types + ImGui::TextDisabled("%s: %s", varDisplayName.c_str(), currentValue.dump().c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "Unsupported Variable Type"); + ImGui::Text("%s", tooltip.c_str()); + ImGui::Separator(); + ImGui::TextWrapped("This variable type doesn't have a custom UI implementation yet. The raw JSON value is shown above."); + } } - } - ImGui::PopID(); - } + ImGui::PopID(); + } - if (modified) { - EditorWindow::GetSingleton()->PushUndoState(this); - if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); + if (modified) { + EditorWindow::GetSingleton()->PushUndoState(this); + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } } - } } else { ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Enable weather-specific overrides above to customize settings for this weather."); diff --git a/src/WeatherManager.cpp b/src/WeatherManager.cpp index 99a0707b0e..41f3ec39b1 100644 --- a/src/WeatherManager.cpp +++ b/src/WeatherManager.cpp @@ -210,14 +210,14 @@ bool WeatherManager::LoadSettingsFromWeather(RE::TESWeather* weather, const std: auto featureIt = weatherIt->second.find(featureName); if (featureIt != weatherIt->second.end()) { const json& featureJson = featureIt->second; - + // Check if weather-specific overrides are enabled bool enabled = featureJson.value("__enabled", false); if (!enabled) { // Settings exist but are disabled, return empty return false; } - + // Copy all settings except the __enabled flag o_json = json::object(); for (auto it = featureJson.begin(); it != featureJson.end(); ++it) { From 71a50e646f9ce6b42b0b92278135b67653f4d999 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 13 Jan 2026 20:07:51 +1000 Subject: [PATCH 4/4] remove ai doc --- .../InteriorExteriorSettings.md | 336 ------------------ 1 file changed, 336 deletions(-) delete mode 100644 docs/weather-system-docs/InteriorExteriorSettings.md diff --git a/docs/weather-system-docs/InteriorExteriorSettings.md b/docs/weather-system-docs/InteriorExteriorSettings.md deleted file mode 100644 index 725d478aa1..0000000000 --- a/docs/weather-system-docs/InteriorExteriorSettings.md +++ /dev/null @@ -1,336 +0,0 @@ -# Interior vs Exterior Settings Management - -## Overview - -This document explores different approaches for managing per-weather feature settings that differ between interior and exterior environments. This is important for features like IBL, lighting, and effects that need different intensities or behaviors indoors vs outdoors. - -## Use Cases - -- **Image-Based Lighting (IBL)**: Strong exterior lighting during sunny weather should be reduced indoors -- **Screen Space Effects**: Full-strength exterior effects may be too intense in small interior spaces -- **Weather-Dependent Effects**: Rain/storm settings for exterior shouldn't fully apply to covered interiors -- **Performance**: Different quality settings for interior vs exterior environments - ---- - -## Option 1: Nested Interior Overrides (Recommended) - -### UI Structure - -``` -[Feature Name (e.g., IBL)] - [Button: Using Weather-Specific Settings ✓] - - Exterior Settings (always shown when weather overrides enabled) - ├─ DiffuseIBLScale: [slider: 5.0] - ├─ IBLSaturation: [slider: 1.2] - └─ ... - - Interior Overrides - ├─ [Button: Override for Interiors ✓/✗] - └─ (If enabled:) - ├─ DiffuseIBLScale: [slider: 0.3] + "Reset to Exterior" - ├─ IBLSaturation: [grayed: 1.2] (inherited from exterior) - └─ ... -``` - -### JSON Structure - -```json -{ - "IBL": { - "__enabled": true, - "DiffuseIBLScale": 5.0, - "IBLSaturation": 1.2, - "__interior_overrides": { - "__enabled": true, - "DiffuseIBLScale": 0.3 - // Other values inherit from exterior - } - } -} -``` - -### Implementation Overview - -**1. UI Changes (WeatherWidget.cpp):** -- After exterior sliders, add collapsible "Interior Overrides" section -- Second toggle button for `__interior_overrides.__enabled` -- Show sliders with exterior value in tooltip when not overridden -- "Use Exterior Value" context menu option for individual settings - -**2. WeatherManager Changes:** -- Add `IsPlayerInInterior()` check using Skyrim's player location data -- When loading settings, detect if player is indoors -- If interior and overrides enabled, merge `__interior_overrides` over base values -- Priority: Interior override > Exterior weather > Global feature settings - -**3. Variable Registration (no changes needed):** -- Same variables work for both contexts -- Registry doesn't need to know about interior/exterior distinction - -### Pros - -- ✅ Intuitive hierarchy (global → weather → exterior → interior) -- ✅ Only override what differs (efficient storage) -- ✅ Easy to see what's different between interior/exterior -- ✅ Follows existing `__enabled` pattern -- ✅ Supports per-variable granularity -- ✅ Authors can disable all interior overrides with one toggle -- ✅ Values inherit naturally (set once, override selectively) - -### Cons - -- ❌ Adds UI complexity with nested sections -- ❌ Requires interior/exterior detection in WeatherManager -- ❌ More clicks to set up initially - ---- - -## Option 2: Side-by-Side Columns - -### UI Structure - -``` -[Feature Name] - [Button: Using Weather-Specific Settings] - [Button: Enable Interior Differences] - - Setting Name | Exterior | Interior - ───────────────────────────────────────── - DiffuseIBLScale | [5.0] | [0.3] - IBLSaturation | [1.2] | [1.2] (grayed if same) - DALCAmount | [0.5] | [0.5] -``` - -### JSON Structure - -```json -{ - "IBL": { - "__enabled": true, - "exterior": { - "DiffuseIBLScale": 5.0, - "IBLSaturation": 1.2 - }, - "interior": { - "DiffuseIBLScale": 0.3, - "IBLSaturation": 1.2 - } - } -} -``` - -### Pros - -- ✅ Easy to compare values at a glance -- ✅ Compact vertical space -- ✅ Clear what differs between interior/exterior -- ✅ No nested sections - -### Cons - -- ❌ Wide UI (may not fit in editor panel) -- ❌ Redundant storage (must set all values for both) -- ❌ Less intuitive for "most settings are the same" use case -- ❌ Harder to implement with ImGui's layout system - ---- - -## Option 3: Interior Multipliers - -### UI Structure - -``` -[Feature Name] - [Button: Using Weather-Specific Settings] - - DiffuseIBLScale: [5.0] [✓ Interior Mult: 0.1x] - IBLSaturation: [1.2] [✗ Interior Mult: 1.0x] - DALCAmount: [0.5] [✓ Interior Mult: 0.8x] -``` - -### JSON Structure - -```json -{ - "IBL": { - "__enabled": true, - "DiffuseIBLScale": 5.0, - "DiffuseIBLScale_InteriorMult": 0.1, // Effective value: 0.5 indoors - "IBLSaturation": 1.2, - "IBLSaturation_InteriorMult": 1.0, // No change indoors - "DALCAmount": 0.5, - "DALCAmount_InteriorMult": 0.8 // Effective value: 0.4 indoors - } -} -``` - -### Pros - -- ✅ Compact UI (one line per setting) -- ✅ Easy to understand relationship (0.5x = half strength) -- ✅ Simple implementation -- ✅ No nested structures - -### Cons - -- ❌ Less flexible (can't set completely independent values) -- ❌ Multipliers may not make sense for all value types (colors, toggles) -- ❌ Can't disable specific settings for interiors -- ❌ Awkward for settings where interior should be higher than exterior - ---- - -## Option 4: Separate Interior Toggle at Top Level - -### UI Structure - -``` -Weather Editor Tabs: -┌─────────────────────────────────┐ -│ [Exterior] | [Interior] │ -└─────────────────────────────────┘ - -Exterior Tab: - [IBL Feature] - ├─ [Using Weather-Specific Settings ✓] - ├─ DiffuseIBLScale: [5.0] - └─ ... - - [ScreenSpaceGI Feature] - └─ ... - -Interior Tab: - [IBL Feature] - ├─ [○ Use Exterior Settings | ● Override] - ├─ DiffuseIBLScale: [0.3] - └─ ... - - [ScreenSpaceGI Feature] - └─ [● Use Exterior Settings] -``` - -### JSON Structure - -```json -{ - "exterior": { - "IBL": { - "__enabled": true, - "DiffuseIBLScale": 5.0 - } - }, - "interior": { - "IBL": { - "__enabled": true, - "DiffuseIBLScale": 0.3 - }, - "ScreenSpaceGI": { - "__use_exterior": true - } - } -} -``` - -### Pros - -- ✅ Clean separation of concerns -- ✅ Entire set of interior settings in one place -- ✅ Can bulk "use exterior for all features" -- ✅ Tab-based approach is familiar - -### Cons - -- ❌ Must switch tabs to compare values -- ❌ More clicking to set up -- ❌ Harder to see what's overridden at a glance -- ❌ Duplicates feature tree structure - ---- - -## Recommendation: Option 1 (Nested Interior Overrides) - -### Why This Approach? - -1. **Natural Hierarchy**: Follows the existing pattern of global → weather-specific → interior-specific -2. **Efficient**: Only store what differs from exterior settings -3. **Discoverable**: UI clearly shows when interior overrides are active -4. **Flexible**: Can override individual settings or all of them -5. **Extensible**: Could add "dungeon overrides" or "underwater overrides" using same pattern - -### Implementation Plan - -#### Phase 1: Core Infrastructure -1. Add interior detection utility function -2. Extend WeatherManager to handle `__interior_overrides` JSON key -3. Update LoadSettingsFromWeather to merge interior overrides when appropriate - -#### Phase 2: UI -1. Add collapsible "Interior Overrides" section in DrawFeatureSettings -2. Add toggle button for `__interior_overrides.__enabled` -3. Implement control rendering with inheritance indicators -4. Add "Copy from Exterior" and "Reset to Exterior" context menu options - -#### Phase 3: Polish -1. Add visual indicators showing which values differ from exterior -2. Add bulk operations (copy all, clear all interior overrides) -3. Update documentation with interior override examples - -### Key Design Decisions - -**Q: Should interior overrides be per-weather or global across all weathers?** -A: Per-weather. Different weathers may need different interior adjustments (e.g., storm vs clear sky). - -**Q: Should there be a "copy all exterior to interior" bulk action?** -A: Yes, as a context menu option on the "Interior Overrides" header. - -**Q: How to handle weather transitions when entering/exiting buildings?** -A: Apply interior/exterior settings immediately on cell change, respecting ongoing weather transitions. The lerp factor remains the same, but the "to" values switch between interior/exterior variants. - -**Q: What about features that shouldn't have interior/exterior differences?** -A: The system is opt-in. If a feature doesn't need interior overrides, authors simply don't enable them. The toggle button is always available but defaults to off. - -### Example Workflow - -1. Author opens weather editor for "Clear" weather -2. Navigates to Features tab → IBL -3. Enables "Using Weather-Specific Settings" -4. Sets DiffuseIBLScale to 5.0 (strong outdoor lighting) -5. Clicks "Interior Overrides" section to expand -6. Clicks "Override for Interiors" toggle button -7. Sets DiffuseIBLScale to 0.8 (reduced indoor lighting) -8. Other IBL settings (saturation, etc.) inherit from exterior automatically -9. Saves weather → JSON contains both exterior and interior values -10. Player enters an interior during clear weather → IBL smoothly adjusts to 0.8 - ---- - -## Future Enhancements - -### Conditional Overrides -- **Dungeon Interiors**: Different settings for caves/dungeons vs buildings -- **Underwater**: Special handling for underwater environments -- **Location-Specific**: Override for specific interior cells (Dragonsreach vs Bannered Mare) - -### Visual Indicators -- Color-code sliders: Green = exterior only, Blue = has interior override -- Show diff values in tooltips: "Exterior: 5.0 → Interior: 0.8" -- Icons indicating inheritance status - -### Bulk Operations -- "Copy All Exterior to Interior" -- "Clear All Interior Overrides" -- "Invert Interior/Exterior" (swap values) -- "Apply Interior Multiplier to All" (global 0.5x reduction) - -### Testing Tools -- "Preview Interior" toggle in editor (shows what values would be with interior overrides) -- "Toggle Interior/Exterior" hotkey for quick testing -- Log window showing which override is active - ---- - -## Conclusion - -Nested interior overrides (Option 1) provides the best balance of flexibility, usability, and efficiency for managing interior vs exterior feature settings in the weather system. It follows existing patterns, minimizes UI complexity, and supports gradual adoption by feature authors.