Skip to content

refactor(weather-editor): cleanup and consolidate widget json serialization#1954

Closed
SkrubbySkrubInAShrub wants to merge 14 commits into
community-shaders:devfrom
SkrubbySkrubInAShrub:widget-refactor
Closed

refactor(weather-editor): cleanup and consolidate widget json serialization#1954
SkrubbySkrubInAShrub wants to merge 14 commits into
community-shaders:devfrom
SkrubbySkrubInAShrub:widget-refactor

Conversation

@SkrubbySkrubInAShrub
Copy link
Copy Markdown
Collaborator

@SkrubbySkrubInAShrub SkrubbySkrubInAShrub commented Mar 3, 2026

The aim of this PR is not to change any functionality, but to clean up the code and reduce the amount of lines used, as well as removing dead code from the weather widgets. Compiles fine and no obvious bugs.

Summary by CodeRabbit

  • Refactor

    • Reorganized Weather Editor widgets for consistent Begin/End flows, tabs, headers, and unified change-tracking; many widgets now support loading/saving via consolidated Settings with sensible defaults.
    • Added safer load/apply/revert flows and explicit "load from game" handling across multiple weather panels.
  • Bug Fixes

    • Early-exit guards prevent empty windows; values are clamped/sanitized on apply; undo support and clearer inline tooltips/UI polish added.
    • Improved stability for search/dropdown and parent/inherit workflows.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Encapsulates widget internals (moves previously-public members to private and adds accessors), introduces nested Settings types with JSON (de)serialization, consolidates load/save/apply flows, normalizes ImGui Begin/End and change-tracking across many WeatherEditor widgets, and updates codepaths to use accessors (GetCell/GetLightingTemplate).

Changes

Cohort / File(s) Summary
Editor window updates
src/WeatherEditor/EditorWindow.cpp
Replaced direct member access (->cell, ->lightingTemplate) with accessor calls (->GetCell(), ->GetLightingTemplate()) across selection, filtering, double-click/open, Enter-to-open, and render paths.
CellLightingWidget
src/WeatherEditor/Weather/CellLightingWidget.h, src/WeatherEditor/Weather/CellLightingWidget.cpp
Moved cell to private with GetCell(); added nested DALC and Inherit structs and consolidated Settings.dalc/Settings.inherit; introduced LoadFromGameSettings, merged JSON handling, and migrated all IO to nested groupings.
LightingTemplateWidget
src/WeatherEditor/Weather/LightingTemplateWidget.h, src/WeatherEditor/Weather/LightingTemplateWidget.cpp
Made lightingTemplate private with GetLightingTemplate(); changed per-section Draw* to return bool change flags; added LoadFromGameSettings and RevertChanges; centralized change-tracking and auto-apply; replaced legacy per-field loaders.
ImageSpaceWidget
src/WeatherEditor/Weather/ImageSpaceWidget.h, src/WeatherEditor/Weather/ImageSpaceWidget.cpp
Moved imageSpace private; removed old Set/Load helpers in favor of LoadFromGameSettings and in-place ApplyChanges with clamping; inverted ImGui Begin flow and moved SetCurrentWidget after successful Begin; public lifecycle methods declared as overrides.
LensFlareWidget
src/WeatherEditor/Weather/LensFlareWidget.h, src/WeatherEditor/Weather/LensFlareWidget.cpp
Moved lensFlare private; added public Settings struct and NLOHMANN macro; swapped raw ImGui sliders for WeatherUtils helpers; Load/Save switched to whole-settings assignment with clamping.
PrecipitationWidget
src/WeatherEditor/Weather/PrecipitationWidget.h, src/WeatherEditor/Weather/PrecipitationWidget.cpp
Made precipitation member private; added public Settings and NLOHMANN macro; unified tab UI flow with single change flag; Load/Save use merged JSON -> settings; ApplyChanges uses DataID-based updates with validation/clamping.
VolumetricLightingWidget
src/WeatherEditor/Weather/VolumetricLightingWidget.h, src/WeatherEditor/Weather/VolumetricLightingWidget.cpp
Expanded Settings with new fields and NLOHMANN macro; LoadSettings merges JSON into vanillaSettings with try/catch fallback and clamps fields; SaveSettings emits full settings; DrawWidget Begin guarded and SetCurrentWidget moved after Begin.
ReferenceEffectWidget
src/WeatherEditor/Weather/ReferenceEffectWidget.h, src/WeatherEditor/Weather/ReferenceEffectWidget.cpp
Removed stored artObjectArray/effectShaderArray; inverted Begin/End and centralized scrollable content and change-tracking; unified ApplyChanges path.
Widget UI patterns (multiple files)
src/WeatherEditor/Weather/...
Consistent changes across many widgets: early-return when ImGui::Begin fails, move WeatherUtils::SetCurrentWidget and header rendering after successful Begin, centralized change flags and auto-apply behavior, added NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT for several Settings, and moved several formerly-public members to private with accessors.

Sequence Diagram(s)

sequenceDiagram
    participant EditorWindow
    participant Widget
    participant GameData
    participant JSONStorage

    EditorWindow->>Widget: Open / double-click / Enter
    Widget->>Widget: GetCell() / GetLightingTemplate()
    Widget->>GameData: LoadFromGameSettings() (read runtime data)
    GameData-->>Widget: runtime settings/data
    Widget->>JSONStorage: LoadSettings() (merge_patch/deserialize)
    JSONStorage-->>Widget: merged settings
    Widget->>Widget: apply clamping/validate
    Widget->>GameData: ApplyChanges() (in-place update or DataID write)
    GameData-->>Widget: confirm
    Widget->>JSONStorage: SaveSettings() (js = settings)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • doodlum
  • alandtse

Poem

🐇
I hop through code with tiny paws,
I tuck the fields behind the walls,
Nested settings, JSON bright,
Widgets hum and UI's right,
Hooray — the editor leaps in light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring effort: consolidating and cleaning up widget JSON serialization across multiple weather editor components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 3, 2026

No actionable suggestions for changed features.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/WeatherEditor/Weather/PrecipitationWidget.cpp (1)

160-181: ApplyChanges uses consistent DataID-based indexing.

The refactored approach using the DataID alias with static_cast<uint32_t> is clearer than hardcoded numeric indices. Note that LoadFromGameSettings() uses GetSettingValue() while ApplyChanges() uses direct array access—this asymmetry works but could be unified in a future cleanup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp` around lines 160 - 181, No
code change required; the ApplyChanges implementation in
PrecipitationWidget::ApplyChanges correctly uses the DataID alias with
static_cast<uint32_t> to index runtime.data and set fields (e.g.,
DataID::kGravityVelocity, DataID::kParticleSizeX,
runtime.particleTexture.textureName), but if you want consistency with
LoadFromGameSettings() consider refactoring later to use a shared
GetSettingValue-based accessor for reading/writing these runtime.data entries to
keep load and apply paths uniform.
src/WeatherEditor/Weather/LensFlareWidget.cpp (1)

41-43: Consider using explicit JSON extraction for consistency with the explicit deserialization pattern.

While settings = js; works correctly here (implicit conversions are enabled and the macro NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT explicitly supports this), the pattern js.get_to(settings) provides more explicit intent. Note: This same pattern is used consistently throughout the codebase in LightingTemplateWidget.cpp, WeatherWidget.cpp, VolumetricLightingWidget.cpp, PrecipitationWidget.cpp, and CellLightingWidget.cpp. If refactoring, consider making the change uniformly across all widget classes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/LensFlareWidget.cpp` around lines 41 - 43, Replace
the implicit assignment "settings = js" with an explicit JSON extraction using
nlohmann::json's get_to to match the project's deserialization pattern; locate
the code in LensFlareWidget.cpp where the local json variable "js" is checked
and replace the assignment to "settings" with "js.get_to(settings)" (keeping the
surrounding if/else logic intact) so it aligns with other widgets like
LightingTemplateWidget, WeatherWidget, VolumetricLightingWidget,
PrecipitationWidget, and CellLightingWidget.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/WeatherEditor/Weather/LightingTemplateWidget.cpp`:
- Around line 141-158: The code reads both TOD::Sunrise and TOD::Day from
settings.dalc.directional[0] but when writing back it only writes Sunrise, so
edits to the Day row are lost; update the write-back to assign
settings.dalc.directional[0].max = maxColors[TOD::Day] and
settings.dalc.directional[0].min = minColors[TOD::Day] (instead of using
TOD::Sunrise) in the block after TOD::DrawTODColorRow("Positive Direction (+)",
...) and make the analogous change in the second block (the other
DrawTODColorRow call) so the directional[0] entries reflect the Day row edits
rather than always using Sunrise.
- Around line 76-81: The directional slider values (settings.directionalXY,
settings.directionalZ, settings.directionalFade) must be validated and clamped
before any uint32_t casts; locate where those floats are later cast (around the
uint32_t assignments) and replace direct casts with a safe sequence: clamp each
float to the valid range (e.g., for angles clamp to 0.0f..360.0f; for fades
clamp to 0.0f..1.0f or the intended range), round to the nearest integer (e.g.,
std::round) and then static_cast<uint32_t>, and ensure
WeatherUtils::DrawSliderFloat return paths preserve clamped values so
malformed/negative config values cannot wrap or truncate at runtime.

In `@src/WeatherEditor/Weather/LightingTemplateWidget.h`:
- Around line 8-19: The constructor
LightingTemplateWidget(RE::BGSLightingTemplate* a_lightingTemplate) returns
early on a null a_lightingTemplate without initializing snapshot members,
leading to later undefined reads; fix by initializing form to nullptr (or
appropriate default), set settings to a safe default value, and assign
vanillaSettings and originalSettings from that default before returning—ensure
these members (form, settings, vanillaSettings, originalSettings) are all set on
the null-template path so LoadFromGameSettings() is only called for a real
template and snapshots are always initialized.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp`:
- Around line 25-107: The ImGui::Begin(...) check currently exits immediately,
so the tab bar and all widgets (BeginTabBar, BeginTabItem,
BeginScrollableContent, WeatherUtils::DrawSliderFloat, ImGui::InputInt,
ImGui::InputText, ApplyChanges, etc.) are drawn even when ImGui::Begin returns
false; move the entire block that creates the UI (everything from the first
ImGui::BeginTabBar call down to ImGui::EndTabBar and the changed/apply logic)
inside the scope guarded by if (ImGui::Begin(...)) so those calls only run when
Begin() returned true, and keep a single ImGui::End() after that guarded block
(still call ImGui::End() unconditionally) to properly close the window.

In `@src/WeatherEditor/Weather/ReferenceEffectWidget.cpp`:
- Around line 41-43: The auto-apply branch calls ApplyChanges() without
recording an undo snapshot; before calling ApplyChanges() when changed &&
editorWindow->settings.autoApplyChanges is true, call
editorWindow->PushUndoState(this) so the change is capturable by undo
history—mirror the pattern used in WeatherWidget.cpp where PushUndoState(...) is
invoked prior to ApplyChanges().
- Around line 9-13: The ImGui::Begin() result is not checked before rendering
the rest of the window, so UI (BeginScrollableContent, other widgets) runs even
when the window is collapsed/closed; change the flow in ReferenceEffectWidget
(and apply the same pattern to LensFlareWidget, ImageSpaceWidget,
VolumetricLightingWidget, PrecipitationWidget, etc.): after calling
ImGui::Begin(GetEditorID().c_str(), &open, ...), immediately test its return
value and if it returns false call ImGui::End() and return/skip the rest of the
function; otherwise continue to call DrawWidgetHeader, BeginScrollableContent,
render UI, and finally call ImGui::End() as usual.

---

Nitpick comments:
In `@src/WeatherEditor/Weather/LensFlareWidget.cpp`:
- Around line 41-43: Replace the implicit assignment "settings = js" with an
explicit JSON extraction using nlohmann::json's get_to to match the project's
deserialization pattern; locate the code in LensFlareWidget.cpp where the local
json variable "js" is checked and replace the assignment to "settings" with
"js.get_to(settings)" (keeping the surrounding if/else logic intact) so it
aligns with other widgets like LightingTemplateWidget, WeatherWidget,
VolumetricLightingWidget, PrecipitationWidget, and CellLightingWidget.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp`:
- Around line 160-181: No code change required; the ApplyChanges implementation
in PrecipitationWidget::ApplyChanges correctly uses the DataID alias with
static_cast<uint32_t> to index runtime.data and set fields (e.g.,
DataID::kGravityVelocity, DataID::kParticleSizeX,
runtime.particleTexture.textureName), but if you want consistency with
LoadFromGameSettings() consider refactoring later to use a shared
GetSettingValue-based accessor for reading/writing these runtime.data entries to
keep load and apply paths uniform.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae924de and 1f0551e.

📒 Files selected for processing (15)
  • src/WeatherEditor/EditorWindow.cpp
  • src/WeatherEditor/Weather/CellLightingWidget.cpp
  • src/WeatherEditor/Weather/CellLightingWidget.h
  • src/WeatherEditor/Weather/ImageSpaceWidget.cpp
  • src/WeatherEditor/Weather/ImageSpaceWidget.h
  • src/WeatherEditor/Weather/LensFlareWidget.cpp
  • src/WeatherEditor/Weather/LensFlareWidget.h
  • src/WeatherEditor/Weather/LightingTemplateWidget.cpp
  • src/WeatherEditor/Weather/LightingTemplateWidget.h
  • src/WeatherEditor/Weather/PrecipitationWidget.cpp
  • src/WeatherEditor/Weather/PrecipitationWidget.h
  • src/WeatherEditor/Weather/ReferenceEffectWidget.cpp
  • src/WeatherEditor/Weather/ReferenceEffectWidget.h
  • src/WeatherEditor/Weather/VolumetricLightingWidget.cpp
  • src/WeatherEditor/Weather/VolumetricLightingWidget.h
💤 Files with no reviewable changes (1)
  • src/WeatherEditor/Weather/ReferenceEffectWidget.h

Comment thread src/WeatherEditor/Weather/LightingTemplateWidget.cpp
Comment thread src/WeatherEditor/Weather/LightingTemplateWidget.cpp
Comment thread src/WeatherEditor/Weather/LightingTemplateWidget.h
Comment thread src/WeatherEditor/Weather/PrecipitationWidget.cpp Outdated
Comment thread src/WeatherEditor/Weather/ReferenceEffectWidget.cpp Outdated
Comment thread src/WeatherEditor/Weather/ReferenceEffectWidget.cpp
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/WeatherEditor/Weather/ReferenceEffectWidget.cpp (1)

9-13: ⚠️ Potential issue | 🟠 Major

Guard window rendering when ImGui::Begin() returns false.

At Line 9, ImGui::Begin(...) only gates DrawWidgetHeader, but Line 12+ and Line 45-46 still execute even when Begin is false. This can break ImGui window stack expectations. Early-return after ImGui::End() when Begin is false.

Proposed fix
-	if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings | kStickyHeaderFlags)) {
-		DrawWidgetHeader("##ReferenceEffectSearch", true, true);
-	}
-	BeginScrollableContent("##REScroll");
+	if (!ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings | kStickyHeaderFlags)) {
+		ImGui::End();
+		return;
+	}
+	DrawWidgetHeader("##ReferenceEffectSearch", true, true);
+	BeginScrollableContent("##REScroll");
#!/bin/bash
# Verify whether DrawWidget continues rendering outside the ImGui::Begin guard.
rg -n -C4 --type=cpp 'if \(ImGui::Begin\(GetEditorID\(\)\.c_str\(\), &open, ImGuiWindowFlags_NoSavedSettings \| kStickyHeaderFlags\)\)' src/WeatherEditor/Weather/ReferenceEffectWidget.cpp
rg -n -C3 --type=cpp 'BeginScrollableContent\("##REScroll"\)|EndScrollableContent\(\)|ImGui::End\(\)' src/WeatherEditor/Weather/ReferenceEffectWidget.cpp

Expected result: BeginScrollableContent("##REScroll") should appear only after a successful Begin path (or after an early-return guard pattern).

Also applies to: 45-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/ReferenceEffectWidget.cpp` around lines 9 - 13, The
ImGui::Begin(...) call only gates DrawWidgetHeader but subsequent UI calls like
BeginScrollableContent("##REScroll") and ImGui::End() run even when Begin
returned false; fix by checking the boolean result of
ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings |
kStickyHeaderFlags) and if it returns false call ImGui::End() and return early
from the function so BeginScrollableContent, EndScrollableContent and further
rendering (including the DrawWidgetHeader path for the non-begin case) only
execute when Begin succeeded; update the control flow around ImGui::Begin /
DrawWidgetHeader / BeginScrollableContent / EndScrollableContent / ImGui::End
accordingly.
src/WeatherEditor/Weather/LightingTemplateWidget.cpp (1)

192-193: ⚠️ Potential issue | 🟠 Major

Clamp to uint32_t upper bound before casting.

Current logic only clamps negative values. Malformed large config values can still exceed uint32_t range at cast time. Please clamp to [0, uint32_max] before the cast.

🛠️ Proposed fix
-	data.directionalXY = static_cast<std::uint32_t>(std::max(0.0f, std::round(settings.directionalXY)));
-	data.directionalZ = static_cast<std::uint32_t>(std::max(0.0f, std::round(settings.directionalZ)));
+	const float u32Max = static_cast<float>(std::numeric_limits<std::uint32_t>::max());
+	data.directionalXY =
+		static_cast<std::uint32_t>(std::clamp(std::round(settings.directionalXY), 0.0f, u32Max));
+	data.directionalZ =
+		static_cast<std::uint32_t>(std::clamp(std::round(settings.directionalZ), 0.0f, u32Max));
#!/bin/bash
# Verify current casts use lower-bound-only clamping.
rg -nP 'directional(XY|Z)\s*=\s*static_cast<std::uint32_t>\(std::max\(0\.0f,\s*std::round\(settings\.directional(XY|Z)\)\)\)' src/WeatherEditor/Weather/LightingTemplateWidget.cpp

As per coding guidelines "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/LightingTemplateWidget.cpp` around lines 192 - 193,
Current assignments to data.directionalXY and data.directionalZ only clamp the
lower bound, risking overflow when casting to std::uint32_t; change the logic in
LightingTemplateWidget.cpp where data.directionalXY/Z are set (using
settings.directionalXY and settings.directionalZ) to first compute the rounded
value, then clamp it to the full uint32_t range (0 to
std::numeric_limits<std::uint32_t>::max())—for example using std::clamp or
std::min/std::max with the rounded float—and only then static_cast to
std::uint32_t so malformed large config values cannot overflow the cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/WeatherEditor/Weather/LightingTemplateWidget.cpp`:
- Around line 212-216: RevertChanges currently assigns vanillaSettings to
settings, which is wrong for presets loaded from JSON; change
LightingTemplateWidget::RevertChanges to restore originalSettings (the saved
pre-edit snapshot) into settings and then call ApplyChanges() so the widget
truly returns to the original state; update any related comments or tests that
referenced vanillaSettings in RevertChanges if present.

---

Duplicate comments:
In `@src/WeatherEditor/Weather/LightingTemplateWidget.cpp`:
- Around line 192-193: Current assignments to data.directionalXY and
data.directionalZ only clamp the lower bound, risking overflow when casting to
std::uint32_t; change the logic in LightingTemplateWidget.cpp where
data.directionalXY/Z are set (using settings.directionalXY and
settings.directionalZ) to first compute the rounded value, then clamp it to the
full uint32_t range (0 to std::numeric_limits<std::uint32_t>::max())—for example
using std::clamp or std::min/std::max with the rounded float—and only then
static_cast to std::uint32_t so malformed large config values cannot overflow
the cast.

In `@src/WeatherEditor/Weather/ReferenceEffectWidget.cpp`:
- Around line 9-13: The ImGui::Begin(...) call only gates DrawWidgetHeader but
subsequent UI calls like BeginScrollableContent("##REScroll") and ImGui::End()
run even when Begin returned false; fix by checking the boolean result of
ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings |
kStickyHeaderFlags) and if it returns false call ImGui::End() and return early
from the function so BeginScrollableContent, EndScrollableContent and further
rendering (including the DrawWidgetHeader path for the non-begin case) only
execute when Begin succeeded; update the control flow around ImGui::Begin /
DrawWidgetHeader / BeginScrollableContent / EndScrollableContent / ImGui::End
accordingly.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f0551e and ebf9101.

📒 Files selected for processing (3)
  • src/WeatherEditor/Weather/LightingTemplateWidget.cpp
  • src/WeatherEditor/Weather/LightingTemplateWidget.h
  • src/WeatherEditor/Weather/ReferenceEffectWidget.cpp

Comment thread src/WeatherEditor/Weather/LightingTemplateWidget.cpp
SkrubbySkrubInAShrub and others added 4 commits March 4, 2026 00:25
Automated formatting by clang-format, prettier, and other hooks.
See https://pre-commit.ci for details.
Automated formatting by clang-format, prettier, and other hooks.
See https://pre-commit.ci for details.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/WeatherEditor/Weather/CellLightingWidget.cpp (1)

207-219: ⚠️ Potential issue | 🟠 Major

Sanitize deserialized values before ApplyChanges().

Line 219 applies deserialized values directly into game lighting structs. A malformed JSON can inject out-of-range values (fog/fade/rotation), so add a clamp/sanitize pass after load and before apply.

Suggested fix (post-load sanitize)
+ auto sanitize = [](Settings& s) {
+   s.fogNear = std::clamp(s.fogNear, 0.0f, 10000.0f);
+   s.fogFar = std::clamp(s.fogFar, 0.0f, 50000.0f);
+   s.fogPower = std::clamp(s.fogPower, 0.0f, 10.0f);
+   s.fogClamp = std::clamp(s.fogClamp, 0.0f, 1.0f);
+   s.directionalFade = std::clamp(s.directionalFade, 0.0f, 1.0f);
+   s.clipDist = std::clamp(s.clipDist, 0.0f, 50000.0f);
+   s.lightFadeStart = std::clamp(s.lightFadeStart, 0.0f, 10000.0f);
+   s.lightFadeEnd = std::clamp(s.lightFadeEnd, 0.0f, 20000.0f);
+   s.directionalXY = std::min<uint32_t>(s.directionalXY, 360);
+   s.directionalZ = std::min<uint32_t>(s.directionalZ, 360);
+   s.dalc.fresnelPower = std::clamp(s.dalc.fresnelPower, 0.0f, 10.0f);
+ };
  try {
    if (!js.empty()) {
      settings = js;
    } else {
      settings = vanillaSettings;
    }
  } catch (const std::exception& e) {
    logger::error("CellLighting {}: Failed to load from JSON: {}", GetEditorID(), e.what());
    settings = vanillaSettings;
  }
+ sanitize(settings);

  originalSettings = settings;
  ApplyChanges();

As per coding guidelines "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/CellLightingWidget.cpp` around lines 207 - 219,
After deserializing into settings (or falling back to vanillaSettings) but
before assigning originalSettings and calling ApplyChanges(), add a
sanitize/clamp pass that validates and clamps any game lighting fields (e.g.,
fog, fade, rotation, intensity, color components) to safe ranges; update the
block that currently sets settings from js/vanillaSettings to run a
SanitizedSettings = ClampSettings(settings) (or call a new
ValidateAndClampSettings(settings) helper) and then assign originalSettings =
SanitizedSettings and call ApplyChanges(); keep the existing exception logger
(logger::error with GetEditorID()) but ensure malformed values never get passed
directly into ApplyChanges().
src/WeatherEditor/Weather/LightingTemplateWidget.cpp (1)

6-23: ⚠️ Potential issue | 🟠 Major

Replace NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE with _WITH_DEFAULT to gracefully handle missing fields in older or partial presets.

The current macros throw json.exception.out_of_range when any key is missing. Using _WITH_DEFAULT will skip missing fields and use member defaults instead. Additionally, wrap the assignment in LoadSettings() (line 259) with try-catch to handle any remaining parse errors:

Fix details
-NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LightingTemplateWidget::DirectionalColor, max, min)
-NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LightingTemplateWidget::DALC, specular, fresnelPower, directional)
-NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LightingTemplateWidget::Settings,
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(LightingTemplateWidget::DirectionalColor, max, min)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(LightingTemplateWidget::DALC, specular, fresnelPower, directional)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(LightingTemplateWidget::Settings,
 	ambient,
 	directional,
 	fogColorNear,
 	fogColorFar,
 	fogNear,
 	fogFar,
 	directionalXY,
 	directionalZ,
 	directionalFade,
 	clipDist,
 	fogPower,
 	fogClamp,
 	lightFadeStart,
 	lightFadeEnd,
 	dalc)
 void LightingTemplateWidget::LoadSettings()
 {
 	if (!js.empty()) {
-		settings = js;
+		try {
+			settings = js.get<Settings>();
+		} catch (const nlohmann::json::exception&) {
+			settings = vanillaSettings;
+		}
 	} else {
 		settings = vanillaSettings;
 	}
 	originalSettings = settings;
 	ApplyChanges();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/LightingTemplateWidget.cpp` around lines 6 - 23,
Replace the NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE macros for
LightingTemplateWidget::DirectionalColor, LightingTemplateWidget::DALC, and
LightingTemplateWidget::Settings with the corresponding
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT variants so missing JSON keys
fall back to member defaults; then, in LightingTemplateWidget::LoadSettings()
wrap the JSON-to-struct assignment (the code that deserializes into the Settings
instance) in a try-catch that catches nlohmann::json::exception and handles/logs
the error gracefully (e.g., preserve defaults and avoid throwing) to protect
against malformed or partial presets.
src/WeatherEditor/Weather/LensFlareWidget.cpp (1)

43-54: ⚠️ Potential issue | 🟠 Major

Validate loaded values before applying runtime changes

settings = js accepts parseable but out-of-range numbers, and Line 54 applies them immediately. This bypasses the UI bounds enforced by sliders and can propagate malformed config values into live weather data.

Suggested fix
 	try {
 		if (!js.empty()) {
 			settings = js;
 		} else {
 			settings = vanillaSettings;
 		}
+
+		if (settings.fadeDistRadiusScale < 0.0f)
+			settings.fadeDistRadiusScale = 0.0f;
+		else if (settings.fadeDistRadiusScale > 10.0f)
+			settings.fadeDistRadiusScale = 10.0f;
+
+		if (settings.colorInfluence < 0.0f)
+			settings.colorInfluence = 0.0f;
+		else if (settings.colorInfluence > 1.0f)
+			settings.colorInfluence = 1.0f;
 	} catch (const std::exception& e) {
 		logger::error("LensFlare {}: Failed to load from JSON: {}", GetEditorID(), e.what());
 		settings = vanillaSettings;
 	}

As per coding guidelines: "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/LensFlareWidget.cpp` around lines 43 - 54, The code
assigns settings = js and immediately calls ApplyChanges(), which allows
parseable but out-of-range values to be applied; modify the load path in
LensFlareWidget.cpp so that after parsing js you validate and clamp each
configurable field (the members stored in settings) against the UI slider bounds
or use vanillaSettings defaults for missing/invalid entries, log a warning via
logger::warn with GetEditorID() when values were out-of-range or replaced, only
then assign originalSettings = settings and call ApplyChanges(); ensure the
validation logic references the same settings members used by ApplyChanges() to
prevent malformed config propagation.
🧹 Nitpick comments (4)
src/WeatherEditor/Weather/ImageSpaceWidget.cpp (1)

88-105: Consider logging a warning on early exit for debugging purposes.

The null check is good defensive programming. However, silently returning when imageSpace is null could mask initialization issues during development/debugging.

📝 Optional: Add debug logging
 void ImageSpaceWidget::LoadSettings()
 {
-	if (!imageSpace)
+	if (!imageSpace) {
+		logger::warn("LoadSettings called with null imageSpace for {}", GetEditorID());
 		return;
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp` around lines 88 - 105, The
early-return in ImageSpaceWidget::LoadSettings silently hides when imageSpace is
null; add a warning log before the return so developers can see this during
debugging — e.g., in ImageSpaceWidget::LoadSettings, just before the existing
"if (!imageSpace) return;" call logger::warn (including context like
GetEditorID() or the widget's identity) to note that imageSpace is null and
LoadSettings was skipped, then keep the return; do not change the existing
exception handling or ApplyChanges call.
src/WeatherEditor/Weather/ReferenceEffectWidget.cpp (1)

7-12: Set current widget context after a successful ImGui::Begin().

Line 7 sets the global current widget before the early-return path on Lines 9-11. If the window is collapsed, this can leave an unintended global context for subsequent WeatherUtils fallback undo tracking.

♻️ Proposed adjustment
-	WeatherUtils::SetCurrentWidget(this);
 	ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX));
 	if (!ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings | kStickyHeaderFlags)) {
 		ImGui::End();
 		return;
 	}
+	WeatherUtils::SetCurrentWidget(this);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/ReferenceEffectWidget.cpp` around lines 7 - 12,
WeatherUtils::SetCurrentWidget is being called before the ImGui window is
confirmed open, which can leave a stale global widget when ImGui::Begin returns
false; move the call so it runs only after ImGui::Begin(GetEditorID().c_str(),
&open, ImGuiWindowFlags_NoSavedSettings | kStickyHeaderFlags) returns true
(i.e., after the early-return check), or add a corresponding
WeatherUtils::ClearCurrentWidget() before the early return; update references
around WeatherUtils::SetCurrentWidget, ImGui::Begin, GetEditorID, and the open
flag to ensure the global widget context is only set for windows that are
actually open.
src/WeatherEditor/Weather/WeatherWidget.cpp (1)

173-176: Code is syntactically correct; no extra closing brace issue exists.

The brace depth analysis confirms the tab-bar code at line 175 is properly nested inside DrawWidget() at depth 1, not at file scope. Line 172 closes an inner block, and line 175's if statement is correctly indented as part of the function body. No compilation errors from brace mismatch.

However, two separate functional concerns warrant attention:

  1. Search dropdown ImGui ID collision (line 79): The hardcoded "##SearchDropdown" ID can collide if multiple WeatherWidget windows are open simultaneously. Use a unique identifier based on window instance or index.

  2. Missing undo snapshot in records auto-apply (line 423): The ApplyChanges() call should be preceded by EditorWindow::GetSingleton()->PushUndoState() to maintain undo/redo consistency when auto-apply triggers, matching the behavior of manual apply.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` around lines 173 - 176, The
hardcoded ImGui ID "##SearchDropdown" in DrawWidget() can collide across
multiple WeatherWidget instances—replace it with a unique ID per instance (e.g.,
append the widget pointer/address or an instance index) so each widget's
dropdown uses a unique ImGui identifier; additionally, before automatic
ApplyChanges() (the auto-apply path that currently calls ApplyChanges() around
line 423), call EditorWindow::GetSingleton()->PushUndoState() to create an undo
snapshot so auto-applies match the manual Apply behavior and preserve undo/redo
consistency.
src/WeatherEditor/Weather/PrecipitationWidget.cpp (1)

1-1: Consider shortening the PR title to match repository convention length.

The title is conventional, but it exceeds the 50-character limit in the repo guideline. A concise option: refactor(weather): unify precipitation widget json io.
If this is tied to a tracker item, consider adding Related to #<id> in the PR body.

As per coding guidelines, "Format: type(scope): description" and "Length: 50 characters limit for title, 72 for body".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp` at line 1, The PR title
exceeds the repo's 50-character convention; rename it to follow "type(scope):
description" and keep it ≤50 characters (e.g., "refactor(weather): unify
precipitation widget json io"), and if this change is linked to a tracker
include "Related to #<id>" in the PR body; update the PR title and body
accordingly for the PrecipitationWidget changes so it matches the repository
guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/WeatherEditor/Weather/LensFlareWidget.cpp`:
- Around line 12-16: The call to WeatherUtils::SetCurrentWidget(this) is
happening before ImGui::Begin succeeds and can cause the global current widget
to point to an invalid widget on the early-return path; move the
WeatherUtils::SetCurrentWidget(this) call to immediately after the successful
ImGui::Begin(...) check (i.e., after the if-block that checks Begin and before
any UI code/early returns) so the global pointer is only set when Begin returns
true; update any cleanup/End() paths in LensFlareWidget:: whatever-function
contains this code to ensure SetCurrentWidget is not called when Begin fails.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp`:
- Around line 5-20: Deserialization currently overwrites
PrecipitationWidget::Settings with struct defaults when JSON keys are missing
(the `settings = js;` assignment), so change deserialization to merge incoming
keys into the existing settings instead of blindly replacing them: parse the
JSON into a temporary (e.g., tempSettings initialized from current Settings),
then for each setting key check js.contains("<key>") and copy only those present
into tempSettings (use the unique names like gravityVelocity, rotationVelocity,
particleSizeX, numSubtexturesX, numSubtexturesY, particleType, particleDensity,
etc.), then assign tempSettings back to settings; additionally add validation in
ApplyChanges() to clamp and validate fields (e.g., ensure
numSubtexturesX/numSubtexturesY >= 1, particleDensity >= 0, particleType within
expected enum range, sizes/velocities within sane ranges) before writing to the
shader runtime to avoid applying malformed or legacy-missing values (use
vanillaSettings values as fallbacks where appropriate).

In `@src/WeatherEditor/Weather/VolumetricLightingWidget.cpp`:
- Around line 102-107: The current assignment replaces missing JSON keys with
struct defaults; instead, initialize settings from vanillaSettings and then
apply only the keys present in js so untouched fields keep the vanilla
values—specifically, in VolumetricLightingWidget.cpp set settings =
vanillaSettings first and then use nlohmann::json::get_to (or an equivalent JSON
merge/patch) to write js into settings so only keys present in js overwrite the
initialized vanillaSettings; update the code paths using the symbols settings,
js, and vanillaSettings accordingly.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Around line 423-425: The auto-apply branch currently calls ApplyChanges()
directly when recordChanged and
EditorWindow::GetSingleton()->settings.autoApplyChanges are true, which bypasses
the undo system; modify the block so you create/capture an undo snapshot before
calling ApplyChanges() (use the same undo/transaction API used elsewhere in this
editor), ensuring the snapshot includes the record being edited and any relevant
state, then call ApplyChanges() and finalize/commit the undo snapshot so
auto-applied edits are properly undoable.
- Around line 79-81: The search dropdown uses a constant ImGui window ID
"##SearchDropdown" which will collide across multiple WeatherWidget instances;
change the window ID to be unique per-widget by incorporating a per-instance
identifier (e.g., the widget's this pointer, a member uint64 id, or
ImGui::GetID/PushID) into the string passed to ImGui::Begin (replace
"##SearchDropdown" with a formatted unique ID), or wrap the call with
ImGui::PushID/PopID using the widget instance identifier so each WeatherWidget
has its own popup state; update the call site in WeatherWidget.cpp where
ImGui::Begin("##SearchDropdown", ...) is used.

---

Outside diff comments:
In `@src/WeatherEditor/Weather/CellLightingWidget.cpp`:
- Around line 207-219: After deserializing into settings (or falling back to
vanillaSettings) but before assigning originalSettings and calling
ApplyChanges(), add a sanitize/clamp pass that validates and clamps any game
lighting fields (e.g., fog, fade, rotation, intensity, color components) to safe
ranges; update the block that currently sets settings from js/vanillaSettings to
run a SanitizedSettings = ClampSettings(settings) (or call a new
ValidateAndClampSettings(settings) helper) and then assign originalSettings =
SanitizedSettings and call ApplyChanges(); keep the existing exception logger
(logger::error with GetEditorID()) but ensure malformed values never get passed
directly into ApplyChanges().

In `@src/WeatherEditor/Weather/LensFlareWidget.cpp`:
- Around line 43-54: The code assigns settings = js and immediately calls
ApplyChanges(), which allows parseable but out-of-range values to be applied;
modify the load path in LensFlareWidget.cpp so that after parsing js you
validate and clamp each configurable field (the members stored in settings)
against the UI slider bounds or use vanillaSettings defaults for missing/invalid
entries, log a warning via logger::warn with GetEditorID() when values were
out-of-range or replaced, only then assign originalSettings = settings and call
ApplyChanges(); ensure the validation logic references the same settings members
used by ApplyChanges() to prevent malformed config propagation.

In `@src/WeatherEditor/Weather/LightingTemplateWidget.cpp`:
- Around line 6-23: Replace the NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE macros for
LightingTemplateWidget::DirectionalColor, LightingTemplateWidget::DALC, and
LightingTemplateWidget::Settings with the corresponding
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT variants so missing JSON keys
fall back to member defaults; then, in LightingTemplateWidget::LoadSettings()
wrap the JSON-to-struct assignment (the code that deserializes into the Settings
instance) in a try-catch that catches nlohmann::json::exception and handles/logs
the error gracefully (e.g., preserve defaults and avoid throwing) to protect
against malformed or partial presets.

---

Nitpick comments:
In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp`:
- Around line 88-105: The early-return in ImageSpaceWidget::LoadSettings
silently hides when imageSpace is null; add a warning log before the return so
developers can see this during debugging — e.g., in
ImageSpaceWidget::LoadSettings, just before the existing "if (!imageSpace)
return;" call logger::warn (including context like GetEditorID() or the widget's
identity) to note that imageSpace is null and LoadSettings was skipped, then
keep the return; do not change the existing exception handling or ApplyChanges
call.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp`:
- Line 1: The PR title exceeds the repo's 50-character convention; rename it to
follow "type(scope): description" and keep it ≤50 characters (e.g.,
"refactor(weather): unify precipitation widget json io"), and if this change is
linked to a tracker include "Related to #<id>" in the PR body; update the PR
title and body accordingly for the PrecipitationWidget changes so it matches the
repository guidelines.

In `@src/WeatherEditor/Weather/ReferenceEffectWidget.cpp`:
- Around line 7-12: WeatherUtils::SetCurrentWidget is being called before the
ImGui window is confirmed open, which can leave a stale global widget when
ImGui::Begin returns false; move the call so it runs only after
ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings |
kStickyHeaderFlags) returns true (i.e., after the early-return check), or add a
corresponding WeatherUtils::ClearCurrentWidget() before the early return; update
references around WeatherUtils::SetCurrentWidget, ImGui::Begin, GetEditorID, and
the open flag to ensure the global widget context is only set for windows that
are actually open.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Around line 173-176: The hardcoded ImGui ID "##SearchDropdown" in DrawWidget()
can collide across multiple WeatherWidget instances—replace it with a unique ID
per instance (e.g., append the widget pointer/address or an instance index) so
each widget's dropdown uses a unique ImGui identifier; additionally, before
automatic ApplyChanges() (the auto-apply path that currently calls
ApplyChanges() around line 423), call
EditorWindow::GetSingleton()->PushUndoState() to create an undo snapshot so
auto-applies match the manual Apply behavior and preserve undo/redo consistency.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebf9101 and 61c2bff.

📒 Files selected for processing (8)
  • src/WeatherEditor/Weather/CellLightingWidget.cpp
  • src/WeatherEditor/Weather/ImageSpaceWidget.cpp
  • src/WeatherEditor/Weather/LensFlareWidget.cpp
  • src/WeatherEditor/Weather/LightingTemplateWidget.cpp
  • src/WeatherEditor/Weather/PrecipitationWidget.cpp
  • src/WeatherEditor/Weather/ReferenceEffectWidget.cpp
  • src/WeatherEditor/Weather/VolumetricLightingWidget.cpp
  • src/WeatherEditor/Weather/WeatherWidget.cpp

Comment thread src/WeatherEditor/Weather/LensFlareWidget.cpp Outdated
Comment thread src/WeatherEditor/Weather/PrecipitationWidget.cpp
Comment thread src/WeatherEditor/Weather/VolumetricLightingWidget.cpp Outdated
Comment thread src/WeatherEditor/Weather/WeatherWidget.cpp Outdated
Comment thread src/WeatherEditor/Weather/WeatherWidget.cpp Outdated
@SkrubbySkrubInAShrub SkrubbySkrubInAShrub marked this pull request as draft March 4, 2026 13:31
SkrubbySkrubInAShrub added 2 commits March 4, 2026 16:39
The 'fix: compile' commit (9e44d10) removed the closing brace for
DrawWidget() while trying to undo damage from the auto-formatter, but
also dropped the function's own closing brace. This left the BeginTabBar
block at global indentation and made every subsequent function definition
appear to be inside DrawWidget, causing cascading compile errors.

Added the missing closing brace and re-applied clang-format to restore
correct indentation throughout the file.
@SkrubbySkrubInAShrub SkrubbySkrubInAShrub marked this pull request as ready for review March 4, 2026 15:57
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/WeatherEditor/Weather/WeatherWidget.cpp (1)

175-175: ⚠️ Potential issue | 🔴 Critical

Move ImGui::End() outside the BeginTabBar conditional to ensure the main window always closes.

The main window opened at line 59 with ImGui::Begin() must be closed regardless of whether BeginTabBar succeeds. Currently ImGui::End() at line 430 is nested inside the if (ImGui::BeginTabBar(...)) block at line 175. If BeginTabBar returns false, the entire block is skipped and the window never closes, causing ImGui state corruption.

Proposed fix
 	if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) {
 		...
 		ImGui::EndTabBar();
-		ImGui::End();
 	}
+	ImGui::End();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` at line 175, The ImGui window
started with ImGui::Begin() must always be closed: move the ImGui::End() call
that currently lives inside the if (ImGui::BeginTabBar("WeatherSettingsTabs",
...)) block so that it executes unconditionally after the tab bar conditional
ends; locate the ImGui::Begin() call near the top of WeatherWidget.cpp and the
matching ImGui::End() currently nested inside the BeginTabBar block and place
that End() immediately after the if-block (or ensure a single End() runs in all
code paths) to avoid leaving the main window unclosed.
♻️ Duplicate comments (1)
src/WeatherEditor/Weather/WeatherWidget.cpp (1)

79-81: ⚠️ Potential issue | 🟡 Minor

Use a per-widget ID for the search dropdown window.

Line 79 uses a constant "##SearchDropdown" window ID, so multiple open WeatherWidget instances can share popup state/focus unexpectedly.

Proposed fix
-		if (ImGui::Begin("##SearchDropdown", nullptr,
+		const std::string dropdownId = std::format("##SearchDropdown_{}", GetEditorID());
+		if (ImGui::Begin(dropdownId.c_str(), nullptr,
 				ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
 					ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) {
#!/bin/bash
# Verify the dropdown still uses a constant ImGui window ID.
rg -n 'ImGui::Begin\("##SearchDropdown"' src/WeatherEditor/Weather/WeatherWidget.cpp
# Expected: no matches after fix.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` around lines 79 - 81, The search
dropdown uses a constant ImGui window ID ("##SearchDropdown") which lets
multiple WeatherWidget instances share popup state; update the ImGui::Begin call
in WeatherWidget (where "##SearchDropdown" is used) to generate a per-instance
unique ID (for example by embedding a unique token such as the widget
pointer/instance id or using ImGui::GetID with a widget-unique string) so each
WeatherWidget gets its own window state; ensure the new ID is used consistently
for the matching ImGui::End and any related popup/child calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Line 175: The ImGui window started with ImGui::Begin() must always be closed:
move the ImGui::End() call that currently lives inside the if
(ImGui::BeginTabBar("WeatherSettingsTabs", ...)) block so that it executes
unconditionally after the tab bar conditional ends; locate the ImGui::Begin()
call near the top of WeatherWidget.cpp and the matching ImGui::End() currently
nested inside the BeginTabBar block and place that End() immediately after the
if-block (or ensure a single End() runs in all code paths) to avoid leaving the
main window unclosed.

---

Duplicate comments:
In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Around line 79-81: The search dropdown uses a constant ImGui window ID
("##SearchDropdown") which lets multiple WeatherWidget instances share popup
state; update the ImGui::Begin call in WeatherWidget (where "##SearchDropdown"
is used) to generate a per-instance unique ID (for example by embedding a unique
token such as the widget pointer/instance id or using ImGui::GetID with a
widget-unique string) so each WeatherWidget gets its own window state; ensure
the new ID is used consistently for the matching ImGui::End and any related
popup/child calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 70f048ad-8557-4143-9000-c246b2740c83

📥 Commits

Reviewing files that changed from the base of the PR and between 61c2bff and 712024f.

📒 Files selected for processing (1)
  • src/WeatherEditor/Weather/WeatherWidget.cpp

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/WeatherEditor/Weather/ImageSpaceWidget.cpp (1)

115-146: Consider centralizing the field mapping to avoid drift.

LoadFromGameSettings() and ApplyChanges() duplicate the same field map in opposite directions. A small mapper/helper would reduce maintenance risk when fields evolve.

Also applies to: 153-178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp` around lines 115 - 146,
LoadFromGameSettings duplicates mapping logic with ApplyChanges; extract a small
mapper helper that centralizes conversions between imageSpace->data and the
widget's settings so both functions call it. Implement two helper functions
(e.g., MapImageSpaceDataToSettings(const ImageSpaceData& src, Settings& dst) and
MapSettingsToImageSpaceData(const Settings& src, ImageSpaceData& dst)) that copy
hdr.*, cinematic.*, tint.*, and depthOfField.* fields, then replace the inline
assignments in ImageSpaceWidget::LoadFromGameSettings and
ImageSpaceWidget::ApplyChanges to call these helpers, ensuring all referenced
symbols (settings, imageSpace->data, hdrEyeAdaptSpeed, cinematicSaturation,
tintColor, dofStrength, etc.) are handled.
src/WeatherEditor/Weather/WeatherWidget.cpp (3)

944-946: Inconsistent undo handling across auto-apply paths.

This method calls ApplyChanges() without PushUndoState(), while DrawCloudSettings (line 1148) and the Records tab (line 422) do capture undo state. This creates inconsistent undo behavior—changes in DALC, Atmosphere Colors, Fog, and Basic tabs won't be undoable when auto-apply is enabled.

Since this PR is about consolidation, consider making undo handling consistent across all tabs.

🔧 Suggested pattern for all auto-apply blocks
 	if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) {
+		EditorWindow::GetSingleton()->PushUndoState(this);
 		ApplyChanges();
 	}

Apply similar changes to:

  • DrawWeatherColorSettings (lines 1005-1007)
  • DrawFogSettings (lines 1293-1295)
  • DrawProperties (lines 1379-1381)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` around lines 944 - 946, The
auto-apply path calls ApplyChanges() without recording an undo state, causing
inconsistent undo behavior; modify the auto-apply branches in the methods
DrawCloudSettings, DrawWeatherColorSettings, DrawFogSettings, DrawProperties
(and the DALC/Atmosphere/Fog/Basic tabs where the snippet lives) to call
PushUndoState() immediately before ApplyChanges() when
EditorWindow::GetSingleton()->settings.autoApplyChanges is true, mirroring the
pattern used in the Records tab and existing DrawCloudSettings code so every
auto-apply path captures the undo snapshot prior to applying changes.

127-127: Minor: Signed/unsigned comparison.

Using int i with widgets.size() (returns size_t) may trigger compiler warnings.

🔧 Suggested fix
-		for (int i = 0; i < widgets.size(); i++) {
+		for (size_t i = 0; i < widgets.size(); i++) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` at line 127, The for-loop uses a
signed int for the index while comparing against widgets.size() (unsigned),
causing a signed/unsigned comparison warning; change the loop index type to an
unsigned type matching widgets.size() (e.g., size_t or decltype(widgets.size()))
or use a range-based loop to iterate over widgets in the code around the for
(int i = 0; i < widgets.size(); i++) line so the comparison and types are
consistent.

2020-2023: Optional: Case-insensitive extension check.

The current check won't detect uppercase .DDS extensions, potentially resulting in malformed paths like "texture.DDS.dds".

🔧 Suggested fix
-	if (resourcePath.size() < 4 || resourcePath.substr(resourcePath.size() - 4) != ".dds") {
+	auto endsWithDds = [](const std::string& s) {
+		if (s.size() < 4) return false;
+		std::string ext = s.substr(s.size() - 4);
+		std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+		return ext == ".dds";
+	};
+	if (!endsWithDds(resourcePath)) {
 		resourcePath += ".dds";
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` around lines 2020 - 2023, The
extension check for resourcePath is case-sensitive and will miss uppercase
variants like ".DDS"; update the logic around resourcePath so the last four
characters are compared case-insensitively (e.g., convert
resourcePath.substr(resourcePath.size()-4) to lowercase before comparing to
".dds" or compare using a case-insensitive routine) and only append ".dds" when
no case-insensitive match exists; preserve the existing behavior of appending to
resourcePath when missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp`:
- Around line 153-178: ApplyChanges() currently writes deserialized values
directly into imageSpace->data (fields like hdr.eyeAdaptSpeed,
hdr.bloomBlurRadius, hdr.bloomThreshold, hdr.bloomScale, hdr.white,
hdr.sunlightScale, hdr.skyScale, cinematic.saturation/brightness/contrast,
tint.color.*, tint.amount, depthOfField.strength/distance/range) without
validation; add sanitization by clamping each value to safe ranges (use sensible
min/max per field), reject or default NaN/inf, ensure non-negative for
radii/strengths/distances, constrain tint color components to [0,1] and
cinematic/hdr params to their expected ranges, and on out-of-range or invalid
values substitute a predefined default (or log a warning) before writing into
imageSpace->data so malformed JSON cannot inject extreme values.

In `@src/WeatherEditor/Weather/LightingTemplateWidget.cpp`:
- Around line 258-270: The deserialized Settings (in the block that merges
vanillaSettings with js and assigns settings) can contain non-finite/malformed
float values that are not validated (only directionalXY/Z are hardened later);
add a ValidateSettings(const Settings& vanilla) method (or function) and call it
immediately after settings is set from JSON and before originalSettings =
settings / ApplyChanges(); ValidateSettings should iterate every float and
float3 member in Settings and replace any non-finite value (use std::isfinite)
with the corresponding value from vanillaSettings (at minimum for all scalar
floats and vec3-like members), ensuring robust fallback and preventing
propagation of bad data into ApplyChanges().

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp`:
- Around line 169-176: ApplyChanges() is mutating settings via clamping which
makes settings differ from the earlier LoadSettings() snapshot
(originalSettings) and triggers HasUnsavedChanges() immediately; move or
duplicate the normalization so the same clamped values are used when
snapshotting: perform the clamp/validation when loading/parsing settings (e.g.,
in LoadSettings() or a new NormalizeSettings() helper) and assign the normalized
values to both settings and originalSettings, or alternatively apply clamping
only at save/apply time but update originalSettings to the normalized result;
refer to ApplyChanges(), LoadSettings(), settings, and originalSettings to
locate where to adjust behavior.

In `@src/WeatherEditor/Weather/VolumetricLightingWidget.cpp`:
- Around line 102-107: After deserializing merged JSON into a Settings instance
(the variable settings assigned from merged.get<Settings>()), validate and clamp
all numeric fields (e.g., floats/doubles) and reject non-finite values before
calling ApplyChanges(); specifically sanitize any
intensity/scale/range/attenuation-like members to prescribed min/max bounds and
replace NaN/inf with safe defaults, then only pass the validated settings into
ApplyChanges() to prevent runtime crashes from malformed JSON.

---

Nitpick comments:
In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp`:
- Around line 115-146: LoadFromGameSettings duplicates mapping logic with
ApplyChanges; extract a small mapper helper that centralizes conversions between
imageSpace->data and the widget's settings so both functions call it. Implement
two helper functions (e.g., MapImageSpaceDataToSettings(const ImageSpaceData&
src, Settings& dst) and MapSettingsToImageSpaceData(const Settings& src,
ImageSpaceData& dst)) that copy hdr.*, cinematic.*, tint.*, and depthOfField.*
fields, then replace the inline assignments in
ImageSpaceWidget::LoadFromGameSettings and ImageSpaceWidget::ApplyChanges to
call these helpers, ensuring all referenced symbols (settings, imageSpace->data,
hdrEyeAdaptSpeed, cinematicSaturation, tintColor, dofStrength, etc.) are
handled.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Around line 944-946: The auto-apply path calls ApplyChanges() without
recording an undo state, causing inconsistent undo behavior; modify the
auto-apply branches in the methods DrawCloudSettings, DrawWeatherColorSettings,
DrawFogSettings, DrawProperties (and the DALC/Atmosphere/Fog/Basic tabs where
the snippet lives) to call PushUndoState() immediately before ApplyChanges()
when EditorWindow::GetSingleton()->settings.autoApplyChanges is true, mirroring
the pattern used in the Records tab and existing DrawCloudSettings code so every
auto-apply path captures the undo snapshot prior to applying changes.
- Line 127: The for-loop uses a signed int for the index while comparing against
widgets.size() (unsigned), causing a signed/unsigned comparison warning; change
the loop index type to an unsigned type matching widgets.size() (e.g., size_t or
decltype(widgets.size())) or use a range-based loop to iterate over widgets in
the code around the for (int i = 0; i < widgets.size(); i++) line so the
comparison and types are consistent.
- Around line 2020-2023: The extension check for resourcePath is case-sensitive
and will miss uppercase variants like ".DDS"; update the logic around
resourcePath so the last four characters are compared case-insensitively (e.g.,
convert resourcePath.substr(resourcePath.size()-4) to lowercase before comparing
to ".dds" or compare using a case-insensitive routine) and only append ".dds"
when no case-insensitive match exists; preserve the existing behavior of
appending to resourcePath when missing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7c08d5b7-beb4-4ebc-9708-7758ec554158

📥 Commits

Reviewing files that changed from the base of the PR and between 712024f and 395b660.

📒 Files selected for processing (8)
  • src/WeatherEditor/Weather/CellLightingWidget.cpp
  • src/WeatherEditor/Weather/ImageSpaceWidget.cpp
  • src/WeatherEditor/Weather/LensFlareWidget.cpp
  • src/WeatherEditor/Weather/LightingTemplateWidget.cpp
  • src/WeatherEditor/Weather/PrecipitationWidget.cpp
  • src/WeatherEditor/Weather/ReferenceEffectWidget.cpp
  • src/WeatherEditor/Weather/VolumetricLightingWidget.cpp
  • src/WeatherEditor/Weather/WeatherWidget.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/WeatherEditor/Weather/CellLightingWidget.cpp

Comment thread src/WeatherEditor/Weather/ImageSpaceWidget.cpp Outdated
Comment thread src/WeatherEditor/Weather/LightingTemplateWidget.cpp
Comment thread src/WeatherEditor/Weather/PrecipitationWidget.cpp
Comment thread src/WeatherEditor/Weather/VolumetricLightingWidget.cpp
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 4, 2026

✅ A pre-release build is available for this PR:
Download

SkrubbySkrubInAShrub and others added 2 commits March 4, 2026 19:29
Automated formatting by clang-format, prettier, and other hooks.
See https://pre-commit.ci for details.
@SkrubbySkrubInAShrub
Copy link
Copy Markdown
Collaborator Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

✅ Actions performed

Reviews resumed.

@SkrubbySkrubInAShrub
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
src/WeatherEditor/Weather/ImageSpaceWidget.cpp (1)

45-75: Consider centralizing value ranges used by UI and apply paths.

The same numeric bounds are duplicated in DrawWidget() and ApplyChanges(). A shared range table/constexprs would prevent drift and keep UI constraints aligned with runtime sanitization.

Also applies to: 161-183

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp` around lines 45 - 75,
DrawWidget() duplicates numeric bounds used later in ApplyChanges(), causing
drift; centralize those ranges into shared constants or a constexpr lookup
(e.g., HDR_EYE_ADAPT_MIN/MAX, BLOOM_BLUR_MIN/MAX, DOF_DISTANCE_MIN/MAX,
TINT_AMOUNT_MIN/MAX, etc.) and replace literal bounds in
PropertyDrawer::DrawFloat and any range-sanitizing logic in ApplyChanges() to
use those constants; update usages in ImageSpaceWidget::DrawWidget (the
PropertyDrawer::DrawFloat and DrawColor calls) and
ImageSpaceWidget::ApplyChanges to reference the shared range table/constexprs so
UI and runtime validation remain identical.
src/WeatherEditor/Weather/PrecipitationWidget.cpp (2)

130-137: Extract normalization into one helper to avoid drift

The same clamp/normalize block exists in both LoadSettings() and ApplyChanges(). Consolidating it into one helper reduces divergence risk.

Refactor sketch
+namespace
+{
+	void NormalizePrecipitationSettings(PrecipitationWidget::Settings& s)
+	{
+		s.numSubtexturesX = std::max(1u, s.numSubtexturesX);
+		s.numSubtexturesY = std::max(1u, s.numSubtexturesY);
+		s.particleDensity = std::max(0.0f, s.particleDensity);
+		s.particleType = std::min(s.particleType, 1u);
+		s.boxSize = std::max(0.0f, s.boxSize);
+		s.particleSizeX = std::clamp(s.particleSizeX, 0.0f, 10.0f);
+		s.particleSizeY = std::clamp(s.particleSizeY, 0.0f, 10.0f);
+	}
+}
@@
-	// Normalize settings so originalSettings matches the clamped values applied in ApplyChanges
-	settings.numSubtexturesX = std::max(1u, settings.numSubtexturesX);
-	settings.numSubtexturesY = std::max(1u, settings.numSubtexturesY);
-	settings.particleDensity = std::max(0.0f, settings.particleDensity);
-	settings.particleType = std::min(settings.particleType, 1u);
-	settings.boxSize = std::max(0.0f, settings.boxSize);
-	settings.particleSizeX = std::clamp(settings.particleSizeX, 0.0f, 10.0f);
-	settings.particleSizeY = std::clamp(settings.particleSizeY, 0.0f, 10.0f);
+	NormalizePrecipitationSettings(settings);
@@
-	settings.numSubtexturesX = std::max(1u, settings.numSubtexturesX);
-	settings.numSubtexturesY = std::max(1u, settings.numSubtexturesY);
-	settings.particleDensity = std::max(0.0f, settings.particleDensity);
-	settings.particleType = std::min(settings.particleType, 1u);
-	settings.boxSize = std::max(0.0f, settings.boxSize);
-	settings.particleSizeX = std::clamp(settings.particleSizeX, 0.0f, 10.0f);
-	settings.particleSizeY = std::clamp(settings.particleSizeY, 0.0f, 10.0f);
+	NormalizePrecipitationSettings(settings);

Also applies to: 178-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp` around lines 130 - 137,
Extract the repeated clamp/normalize logic into a single helper (e.g.,
NormalizePrecipitationSettings) and call it from both LoadSettings() and
ApplyChanges(); move the block that adjusts settings.numSubtexturesX,
settings.numSubtexturesY, settings.particleDensity, settings.particleType,
settings.boxSize, settings.particleSizeX, and settings.particleSizeY into that
helper (also update the identical block at the other occurrence around the
178-185 area to call the helper), ensure the function takes and mutates the same
settings type used by LoadSettings/ApplyChanges and keep behavior identical
(same std::max/std::min/std::clamp calls).

1-215: PR metadata suggestion: tighten title and add issue linkage

Suggested conventional title (≤50 chars):
refactor(weather): unify precipitation json io

If this maps to a tracked ticket, add one of: Addresses #<id> / Related to #<id> in the PR description.

As per coding guidelines, "When reviewing PRs, please provide suggestions for: 1. Conventional Commit Titles ... 2. Issue References ...".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp` around lines 1 - 215,
Update the PR metadata: set the PR title to a concise conventional commit style
(e.g., "refactor(weather): unify precipitation json io") and add an issue
reference in the PR description if this change maps to a tracked ticket (use
"Addresses #<id>" or "Related to #<id>"). Ensure the PR description includes the
short conventional title and the issue linkage line so reviewers can track the
change; no code changes are required in PrecipitationWidget.cpp itself.
src/WeatherEditor/Weather/WeatherWidget.cpp (1)

157-159: Capture inherit-all undo state before mutation and independent of auto-apply.

For consistency and safer undo semantics, snapshot before InheritAllFromParent() performs bulk mutations, instead of inside InheritAllFromParent() after changes and only under auto-apply.

Also applies to: 1477-1479

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` around lines 157 - 159, Before
calling InheritAllFromParent() (triggered by Util::ButtonWithFlash("Inherit
All")), create the undo snapshot so the pre-mutation state is preserved
regardless of auto-apply; move the undo-capture logic out of
InheritAllFromParent() and invoke it immediately when the button is pressed (and
similarly for the code paths at lines ~1477-1479), then call
InheritAllFromParent() to perform the bulk changes and only afterwards
mark/apply changes as appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp`:
- Around line 155-183: ApplyChanges() currently clamps values into local data
(using safeClamp) but leaves the original settings object unchanged, causing
persisted JSON and HasUnsavedChanges() to mismatch applied state; after clamping
each value into data (for HDR, Cinematic, Tint, Depth of Field), write the
sanitized values back into the corresponding settings members (e.g.,
settings.hdrEyeAdaptSpeed, settings.hdrBloomBlurRadius,
settings.cinematicSaturation, settings.tintColor.x/y/z, settings.dofStrength,
etc.) so settings mirrors data; keep using safeClamp/std::isfinite for
sanitization and ensure the same bounds are applied when updating settings to
maintain consistency between imageSpace->data and persisted settings.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp`:
- Around line 179-180: The current validation only enforces a lower bound on
settings.numSubtexturesX and settings.numSubtexturesY (uint32_t) but later they
are written into signed .i members, allowing large JSON values to overflow;
update the checks where settings.numSubtexturesX and settings.numSubtexturesY
are normalized (the two places currently using std::max(1u, ...)) to also clamp
the values to INT32_MAX (use INT32_MAX from <cstdint>/<climits>) before any
assignment to the signed .i fields so the values are safely converted to int32_t
without overflow.

In `@src/WeatherEditor/Weather/VolumetricLightingWidget.cpp`:
- Around line 105-107: The current use of nlohmann::json::merge_patch on
`merged` (initialized from `vanillaSettings`) will delete keys when `js`
contains explicit nulls, causing silent defaults after deserialization into
`Settings`; replace the single `merged.merge_patch(js)` call with explicit
iteration over `js`'s object members and copy only non-null entries into
`merged` (skip any key where `js[key].is_null()`), so `vanillaSettings` values
are preserved when `js` supplies nulls, then deserialize `merged` into
`Settings` and continue applying `safeClamp`/existing validation.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Around line 74-76: The dropdown is forcing focus every frame via
ImGui::SetNextWindowFocus(), which prevents the ImGui::IsWindowFocused() close
path from working; change the logic in WeatherWidget (where
ImGui::SetNextWindowFocus is called) to only call SetNextWindowFocus once when
the dropdown is opened (e.g., guard it with a one-time "justOpened" boolean or
check the previous open state / ImGui popup-open state) instead of every frame,
and remove or skip the unconditional call in the alternate location (lines
98-101) so the existing !ImGui::IsWindowFocused() close path can detect outside
clicks and close the dropdown normally.
- Around line 1380-1382: The block in DrawProperties currently calls
EditorWindow::GetSingleton()->PushUndoState(this) before ApplyChanges(), causing
duplicate undo snapshots because WeatherUtils property editors already push an
undo on interaction start; remove the PushUndoState(this) call from the
auto-apply path so DrawProperties only calls ApplyChanges(), or alternatively
guard PushUndoState with an "undo already pushed" check, leaving ApplyChanges()
unchanged.

---

Nitpick comments:
In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp`:
- Around line 45-75: DrawWidget() duplicates numeric bounds used later in
ApplyChanges(), causing drift; centralize those ranges into shared constants or
a constexpr lookup (e.g., HDR_EYE_ADAPT_MIN/MAX, BLOOM_BLUR_MIN/MAX,
DOF_DISTANCE_MIN/MAX, TINT_AMOUNT_MIN/MAX, etc.) and replace literal bounds in
PropertyDrawer::DrawFloat and any range-sanitizing logic in ApplyChanges() to
use those constants; update usages in ImageSpaceWidget::DrawWidget (the
PropertyDrawer::DrawFloat and DrawColor calls) and
ImageSpaceWidget::ApplyChanges to reference the shared range table/constexprs so
UI and runtime validation remain identical.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp`:
- Around line 130-137: Extract the repeated clamp/normalize logic into a single
helper (e.g., NormalizePrecipitationSettings) and call it from both
LoadSettings() and ApplyChanges(); move the block that adjusts
settings.numSubtexturesX, settings.numSubtexturesY, settings.particleDensity,
settings.particleType, settings.boxSize, settings.particleSizeX, and
settings.particleSizeY into that helper (also update the identical block at the
other occurrence around the 178-185 area to call the helper), ensure the
function takes and mutates the same settings type used by
LoadSettings/ApplyChanges and keep behavior identical (same
std::max/std::min/std::clamp calls).
- Around line 1-215: Update the PR metadata: set the PR title to a concise
conventional commit style (e.g., "refactor(weather): unify precipitation json
io") and add an issue reference in the PR description if this change maps to a
tracked ticket (use "Addresses #<id>" or "Related to #<id>"). Ensure the PR
description includes the short conventional title and the issue linkage line so
reviewers can track the change; no code changes are required in
PrecipitationWidget.cpp itself.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Around line 157-159: Before calling InheritAllFromParent() (triggered by
Util::ButtonWithFlash("Inherit All")), create the undo snapshot so the
pre-mutation state is preserved regardless of auto-apply; move the undo-capture
logic out of InheritAllFromParent() and invoke it immediately when the button is
pressed (and similarly for the code paths at lines ~1477-1479), then call
InheritAllFromParent() to perform the bulk changes and only afterwards
mark/apply changes as appropriate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 15db01d3-0946-452b-8916-7e3c87741313

📥 Commits

Reviewing files that changed from the base of the PR and between 395b660 and 37fb964.

📒 Files selected for processing (5)
  • src/WeatherEditor/Weather/ImageSpaceWidget.cpp
  • src/WeatherEditor/Weather/LightingTemplateWidget.cpp
  • src/WeatherEditor/Weather/PrecipitationWidget.cpp
  • src/WeatherEditor/Weather/VolumetricLightingWidget.cpp
  • src/WeatherEditor/Weather/WeatherWidget.cpp

Comment on lines +155 to +183
// Clamp/sanitize all fields; reject non-finite values by substituting the low bound
auto safeClamp = [](float v, float lo, float hi) {
return std::isfinite(v) ? std::clamp(v, lo, hi) : lo;
};

// HDR
data.hdr.eyeAdaptSpeed = safeClamp(settings.hdrEyeAdaptSpeed, 0.0f, 10.0f);
data.hdr.bloomBlurRadius = safeClamp(settings.hdrBloomBlurRadius, 0.0f, 10.0f);
data.hdr.bloomThreshold = safeClamp(settings.hdrBloomThreshold, 0.0f, 10.0f);
data.hdr.bloomScale = safeClamp(settings.hdrBloomScale, 0.0f, 10.0f);
data.hdr.white = safeClamp(settings.hdrWhite, 0.0f, 10.0f);
data.hdr.sunlightScale = safeClamp(settings.hdrSunlightScale, 0.0f, 50.0f);
data.hdr.skyScale = safeClamp(settings.hdrSkyScale, 0.0f, 10.0f);

// Cinematic
data.cinematic.saturation = safeClamp(settings.cinematicSaturation, 0.0f, 2.0f);
data.cinematic.brightness = safeClamp(settings.cinematicBrightness, 0.0f, 2.0f);
data.cinematic.contrast = safeClamp(settings.cinematicContrast, 0.0f, 2.0f);

// Tint
data.tint.color.red = safeClamp(settings.tintColor.x, 0.0f, 1.0f);
data.tint.color.green = safeClamp(settings.tintColor.y, 0.0f, 1.0f);
data.tint.color.blue = safeClamp(settings.tintColor.z, 0.0f, 1.0f);
data.tint.amount = safeClamp(settings.tintAmount, 0.0f, 1.0f);

// Depth of Field
data.depthOfField.strength = safeClamp(settings.dofStrength, 0.0f, 10.0f);
data.depthOfField.distance = safeClamp(settings.dofDistance, 0.0f, 10000.0f);
data.depthOfField.range = safeClamp(settings.dofRange, 0.0f, 10000.0f);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize settings after clamp to keep persisted and applied state consistent.

ApplyChanges() clamps before writing to imageSpace->data, but settings keeps original out-of-range values. That can leave JSON persistence and HasUnsavedChanges() out of sync with what is actually applied.

Proposed fix
 	// HDR
-	data.hdr.eyeAdaptSpeed = safeClamp(settings.hdrEyeAdaptSpeed, 0.0f, 10.0f);
-	data.hdr.bloomBlurRadius = safeClamp(settings.hdrBloomBlurRadius, 0.0f, 10.0f);
-	data.hdr.bloomThreshold = safeClamp(settings.hdrBloomThreshold, 0.0f, 10.0f);
-	data.hdr.bloomScale = safeClamp(settings.hdrBloomScale, 0.0f, 10.0f);
-	data.hdr.white = safeClamp(settings.hdrWhite, 0.0f, 10.0f);
-	data.hdr.sunlightScale = safeClamp(settings.hdrSunlightScale, 0.0f, 50.0f);
-	data.hdr.skyScale = safeClamp(settings.hdrSkyScale, 0.0f, 10.0f);
+	settings.hdrEyeAdaptSpeed = safeClamp(settings.hdrEyeAdaptSpeed, 0.0f, 10.0f);
+	settings.hdrBloomBlurRadius = safeClamp(settings.hdrBloomBlurRadius, 0.0f, 10.0f);
+	settings.hdrBloomThreshold = safeClamp(settings.hdrBloomThreshold, 0.0f, 10.0f);
+	settings.hdrBloomScale = safeClamp(settings.hdrBloomScale, 0.0f, 10.0f);
+	settings.hdrWhite = safeClamp(settings.hdrWhite, 0.0f, 10.0f);
+	settings.hdrSunlightScale = safeClamp(settings.hdrSunlightScale, 0.0f, 50.0f);
+	settings.hdrSkyScale = safeClamp(settings.hdrSkyScale, 0.0f, 10.0f);
+	data.hdr.eyeAdaptSpeed = settings.hdrEyeAdaptSpeed;
+	data.hdr.bloomBlurRadius = settings.hdrBloomBlurRadius;
+	data.hdr.bloomThreshold = settings.hdrBloomThreshold;
+	data.hdr.bloomScale = settings.hdrBloomScale;
+	data.hdr.white = settings.hdrWhite;
+	data.hdr.sunlightScale = settings.hdrSunlightScale;
+	data.hdr.skyScale = settings.hdrSkyScale;

 	// Cinematic
-	data.cinematic.saturation = safeClamp(settings.cinematicSaturation, 0.0f, 2.0f);
-	data.cinematic.brightness = safeClamp(settings.cinematicBrightness, 0.0f, 2.0f);
-	data.cinematic.contrast = safeClamp(settings.cinematicContrast, 0.0f, 2.0f);
+	settings.cinematicSaturation = safeClamp(settings.cinematicSaturation, 0.0f, 2.0f);
+	settings.cinematicBrightness = safeClamp(settings.cinematicBrightness, 0.0f, 2.0f);
+	settings.cinematicContrast = safeClamp(settings.cinematicContrast, 0.0f, 2.0f);
+	data.cinematic.saturation = settings.cinematicSaturation;
+	data.cinematic.brightness = settings.cinematicBrightness;
+	data.cinematic.contrast = settings.cinematicContrast;

 	// Tint
-	data.tint.color.red = safeClamp(settings.tintColor.x, 0.0f, 1.0f);
-	data.tint.color.green = safeClamp(settings.tintColor.y, 0.0f, 1.0f);
-	data.tint.color.blue = safeClamp(settings.tintColor.z, 0.0f, 1.0f);
-	data.tint.amount = safeClamp(settings.tintAmount, 0.0f, 1.0f);
+	settings.tintColor.x = safeClamp(settings.tintColor.x, 0.0f, 1.0f);
+	settings.tintColor.y = safeClamp(settings.tintColor.y, 0.0f, 1.0f);
+	settings.tintColor.z = safeClamp(settings.tintColor.z, 0.0f, 1.0f);
+	settings.tintAmount = safeClamp(settings.tintAmount, 0.0f, 1.0f);
+	data.tint.color.red = settings.tintColor.x;
+	data.tint.color.green = settings.tintColor.y;
+	data.tint.color.blue = settings.tintColor.z;
+	data.tint.amount = settings.tintAmount;

 	// Depth of Field
-	data.depthOfField.strength = safeClamp(settings.dofStrength, 0.0f, 10.0f);
-	data.depthOfField.distance = safeClamp(settings.dofDistance, 0.0f, 10000.0f);
-	data.depthOfField.range = safeClamp(settings.dofRange, 0.0f, 10000.0f);
+	settings.dofStrength = safeClamp(settings.dofStrength, 0.0f, 10.0f);
+	settings.dofDistance = safeClamp(settings.dofDistance, 0.0f, 10000.0f);
+	settings.dofRange = safeClamp(settings.dofRange, 0.0f, 10000.0f);
+	data.depthOfField.strength = settings.dofStrength;
+	data.depthOfField.distance = settings.dofDistance;
+	data.depthOfField.range = settings.dofRange;

As per coding guidelines "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp` around lines 155 - 183,
ApplyChanges() currently clamps values into local data (using safeClamp) but
leaves the original settings object unchanged, causing persisted JSON and
HasUnsavedChanges() to mismatch applied state; after clamping each value into
data (for HDR, Cinematic, Tint, Depth of Field), write the sanitized values back
into the corresponding settings members (e.g., settings.hdrEyeAdaptSpeed,
settings.hdrBloomBlurRadius, settings.cinematicSaturation,
settings.tintColor.x/y/z, settings.dofStrength, etc.) so settings mirrors data;
keep using safeClamp/std::isfinite for sanitization and ensure the same bounds
are applied when updating settings to maintain consistency between
imageSpace->data and persisted settings.

Comment on lines +179 to +180
settings.numSubtexturesX = std::max(1u, settings.numSubtexturesX);
settings.numSubtexturesY = std::max(1u, settings.numSubtexturesY);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if file exists and get its size
if [ ! -f "src/WeatherEditor/Weather/PrecipitationWidget.cpp" ]; then
  echo "File not found"
  exit 1
fi

echo "=== PrecipitationWidget.h - checking Settings struct ==="
cat -n src/WeatherEditor/Weather/PrecipitationWidget.h | head -120

echo ""
echo "=== PrecipitationWidget.cpp - Lines 170-210 (clamping and write logic) ==="
cat -n src/WeatherEditor/Weather/PrecipitationWidget.cpp | sed -n '170,210p'

echo ""
echo "=== Search for runtime.data definition ==="
rg -n "struct.*Data|union.*Data" src/WeatherEditor/ -A5 | head -40

echo ""
echo "=== Search for DataID enum ==="
rg -n "enum.*DataID|kNumSubtexturesX|kNumSubtexturesY" src/WeatherEditor/ -B2 -A2 | head -50

Repository: doodlum/skyrim-community-shaders

Length of output: 4536


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for runtime.data union/struct definition in Skyrim SDK headers ==="
find . -name "*.h" -o -name "*.hpp" | xargs rg -l "BGSShaderParticleGeometryData|struct.*runtime|union.*Data" 2>/dev/null | head -20

echo ""
echo "=== Look for existing upper-bound clamping patterns with subtextures ==="
rg -n "std::clamp.*numSubtextures|numSubtextures.*std::clamp" --type cpp

echo ""
echo "=== Check other Widget implementations for similar validation patterns ==="
ls -la src/WeatherEditor/Weather/ | grep -i widget

echo ""
echo "=== Check LoadSettings for subtexture validation (lines around 100-140) ==="
if [ -f "src/WeatherEditor/Weather/PrecipitationWidget.cpp" ]; then
  wc -l src/WeatherEditor/Weather/PrecipitationWidget.cpp
  sed -n '100,145p' src/WeatherEditor/Weather/PrecipitationWidget.cpp | cat -n
fi

Repository: doodlum/skyrim-community-shaders

Length of output: 277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== PrecipitationWidget.cpp LoadSettings method ==="
sed -n '100,172p' src/WeatherEditor/Weather/PrecipitationWidget.cpp | cat -n

echo ""
echo "=== Check if JSON parsing could produce > INT_MAX values ==="
rg -n "LoadSettings|json|parse" src/WeatherEditor/Weather/PrecipitationWidget.cpp | head -20

echo ""
echo "=== Check for any use of numeric_limits or INT_MAX in similar validation ==="
rg -n "numeric_limits|INT_MAX|INT32_MAX" src/WeatherEditor/ --type cpp | head -20

Repository: doodlum/skyrim-community-shaders

Length of output: 4031


Clamp subtexture counts to prevent signed integer overflow when writing to game data

Lines 179–180 and 132–133 validate only a lower bound. Since numSubtexturesX/Y are uint32_t (lines 39–40), but written to signed .i members (lines 197–198), large JSON values can overflow. Clamp to INT32_MAX before assignment.

Proposed fix
 void PrecipitationWidget::LoadSettings()
 {
 	if (!precipitation)
@@ -19,8 +19,10 @@ void PrecipitationWidget::LoadSettings()
 		}
 	}
 
+	constexpr uint32_t kMaxSignedInt = static_cast<uint32_t>(std::numeric_limits<int32_t>::max());
 	settings.numSubtexturesX = std::max(1u, settings.numSubtexturesX);
+	settings.numSubtexturesX = std::min(settings.numSubtexturesX, kMaxSignedInt);
 	settings.numSubtexturesY = std::max(1u, settings.numSubtexturesY);
+	settings.numSubtexturesY = std::min(settings.numSubtexturesY, kMaxSignedInt);
 	settings.particleDensity = std::max(0.0f, settings.particleDensity);
 	settings.particleType = std::min(settings.particleType, 1u);
 	settings.boxSize = std::max(0.0f, settings.boxSize);
@@ -25,8 +27,10 @@ void PrecipitationWidget::ApplyChanges()
 	if (!precipitation)
 		return;
 
+	constexpr uint32_t kMaxSignedInt = static_cast<uint32_t>(std::numeric_limits<int32_t>::max());
 	settings.numSubtexturesX = std::max(1u, settings.numSubtexturesX);
+	settings.numSubtexturesX = std::min(settings.numSubtexturesX, kMaxSignedInt);
 	settings.numSubtexturesY = std::max(1u, settings.numSubtexturesY);
+	settings.numSubtexturesY = std::min(settings.numSubtexturesY, kMaxSignedInt);
 	settings.particleDensity = std::max(0.0f, settings.particleDensity);
 	settings.particleType = std::min(settings.particleType, 1u);
 	settings.boxSize = std::max(0.0f, settings.boxSize);

Also applies to lines 132–133.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp` around lines 179 - 180,
The current validation only enforces a lower bound on settings.numSubtexturesX
and settings.numSubtexturesY (uint32_t) but later they are written into signed
.i members, allowing large JSON values to overflow; update the checks where
settings.numSubtexturesX and settings.numSubtexturesY are normalized (the two
places currently using std::max(1u, ...)) to also clamp the values to INT32_MAX
(use INT32_MAX from <cstdint>/<climits>) before any assignment to the signed .i
fields so the values are safely converted to int32_t without overflow.

Comment on lines +105 to +107
nlohmann::json merged = vanillaSettings;
merged.merge_patch(js);
settings = merged.get<Settings>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

nlohmann json merge_patch RFC 7396 null semantics key deletion

💡 Result:

Per RFC 7396 (JSON Merge Patch), when the patch is an object and a member’s value is null, that member is deleted from the target object (if it exists). If a member is absent from the patch, it is left unchanged. (datatracker.ietf.org)

nlohmann::json::merge_patch() implements this RFC behavior: in the project’s own example, applying a patch containing "f": null removes key f from the nested object. (github.com)

Key implications (often the “gotcha”):

  • You cannot use merge-patch to mean “set this field to JSON null” inside an object, because null means remove the key. (datatracker.ietf.org)
  • If the entire patch value is not an object (e.g., the patch is null, a string, an array, etc.), the result is replace the entire target with the patch. (datatracker.ietf.org)

Citations:


🏁 Script executed:

# Find and examine the file
fd VolumetricLightingWidget.cpp

Repository: doodlum/skyrim-community-shaders

Length of output: 130


🏁 Script executed:

# Read the specific file around the mentioned lines
wc -l $(fd VolumetricLightingWidget.cpp)

Repository: doodlum/skyrim-community-shaders

Length of output: 134


🏁 Script executed:

# Read the file around the mentioned lines with context
sed -n '95,115p' src/WeatherEditor/Weather/VolumetricLightingWidget.cpp | cat -n

Repository: doodlum/skyrim-community-shaders

Length of output: 1014


🏁 Script executed:

# Get more context to understand the function and data flow
sed -n '85,125p' src/WeatherEditor/Weather/VolumetricLightingWidget.cpp | cat -n

Repository: doodlum/skyrim-community-shaders

Length of output: 2394


🏁 Script executed:

# Find Settings struct definition
rg "struct Settings" src/WeatherEditor -A 15

Repository: doodlum/skyrim-community-shaders

Length of output: 10524


🏁 Script executed:

# Get exact line numbers - search for the merge_patch call
rg -n "merge_patch" src/WeatherEditor/Weather/VolumetricLightingWidget.cpp

Repository: doodlum/skyrim-community-shaders

Length of output: 106


🏁 Script executed:

# Check where js is initialized/populated to see if there's upstream validation
rg -n "js =" src/WeatherEditor/Weather/VolumetricLightingWidget.cpp -B 3 -A 1

Repository: doodlum/skyrim-community-shaders

Length of output: 190


🏁 Script executed:

# Check the full LoadSettings function to see all context
sed -n '98,145p' src/WeatherEditor/Weather/VolumetricLightingWidget.cpp | cat -n

Repository: doodlum/skyrim-community-shaders

Length of output: 2903


Add explicit validation to prevent null-value deletion during JSON merge.

On Line 106, merge_patch treats null values as key deletion per RFC 7396. When merged JSON contains explicit nulls (e.g., legacy or malformed configurations), deleted keys deserialize using struct defaults (e.g., intensity = 1.0f), which may differ from vanillaSettings values, causing silent behavior drift. The existing safeClamp validation only catches non-finite values, not finite struct defaults that silently diverge from vanilla.

Replace the merge_patch call with explicit object validation and conditional iteration to skip null values:

Proposed fix
-			nlohmann::json merged = vanillaSettings;
-			merged.merge_patch(js);
-			settings = merged.get<Settings>();
+			nlohmann::json merged = vanillaSettings;
+			if (!js.is_object()) {
+				throw std::runtime_error("VolumetricLighting JSON payload must be an object");
+			}
+			for (auto it = js.begin(); it != js.end(); ++it) {
+				if (!it.value().is_null()) {
+					merged[it.key()] = it.value();
+				}
+			}
+			settings = merged.get<Settings>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/VolumetricLightingWidget.cpp` around lines 105 -
107, The current use of nlohmann::json::merge_patch on `merged` (initialized
from `vanillaSettings`) will delete keys when `js` contains explicit nulls,
causing silent defaults after deserialization into `Settings`; replace the
single `merged.merge_patch(js)` call with explicit iteration over `js`'s object
members and copy only non-null entries into `merged` (skip any key where
`js[key].is_null()`), so `vanillaSettings` values are preserved when `js`
supplies nulls, then deserialize `merged` into `Settings` and continue applying
`safeClamp`/existing validation.

Comment on lines +74 to +76
ImGui::SetNextWindowPos(ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y));
ImGui::SetNextWindowSize(ImVec2(200.0f * 1.5f, 0));
ImGui::SetNextWindowFocus();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Dropdown focus is being force-set every frame, which breaks the outside-click close behavior.

Line 76 calls ImGui::SetNextWindowFocus() unconditionally, so the !ImGui::IsWindowFocused() close path at Line 99 is effectively neutralized.

Suggested fix
-		ImGui::SetNextWindowFocus();
+		// Do not force focus every frame; allows outside-click dismissal to work.

Also applies to: 98-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` around lines 74 - 76, The
dropdown is forcing focus every frame via ImGui::SetNextWindowFocus(), which
prevents the ImGui::IsWindowFocused() close path from working; change the logic
in WeatherWidget (where ImGui::SetNextWindowFocus is called) to only call
SetNextWindowFocus once when the dropdown is opened (e.g., guard it with a
one-time "justOpened" boolean or check the previous open state / ImGui
popup-open state) instead of every frame, and remove or skip the unconditional
call in the alternate location (lines 98-101) so the existing
!ImGui::IsWindowFocused() close path can detect outside clicks and close the
dropdown normally.

Comment on lines 1380 to 1382
if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) {
EditorWindow::GetSingleton()->PushUndoState(this);
ApplyChanges();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid double undo snapshots in DrawProperties auto-apply path.

This block pushes undo again even though property editors in WeatherUtils already snapshot undo on interaction start (e.g., color/float paths), causing duplicate undo entries and faster stack churn.

Suggested fix
 	if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) {
-		EditorWindow::GetSingleton()->PushUndoState(this);
 		ApplyChanges();
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` around lines 1380 - 1382, The
block in DrawProperties currently calls
EditorWindow::GetSingleton()->PushUndoState(this) before ApplyChanges(), causing
duplicate undo snapshots because WeatherUtils property editors already push an
undo on interaction start; remove the PushUndoState(this) call from the
auto-apply path so DrawProperties only calls ApplyChanges(), or alternatively
guard PushUndoState with an "undo already pushed" check, leaving ApplyChanges()
unchanged.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/WeatherEditor/Weather/ImageSpaceWidget.cpp (1)

160-183: ⚠️ Potential issue | 🟡 Minor

Sanitized runtime values are not written back to settings.

This keeps persisted JSON / HasUnsavedChanges() potentially out of sync with what is actually applied.

Suggested fix
-	data.hdr.eyeAdaptSpeed = safeClamp(settings.hdrEyeAdaptSpeed, 0.0f, 10.0f);
+	settings.hdrEyeAdaptSpeed = safeClamp(settings.hdrEyeAdaptSpeed, 0.0f, 10.0f);
+	data.hdr.eyeAdaptSpeed = settings.hdrEyeAdaptSpeed;
 	...
-	data.tint.amount = safeClamp(settings.tintAmount, 0.0f, 1.0f);
+	settings.tintAmount = safeClamp(settings.tintAmount, 0.0f, 1.0f);
+	data.tint.amount = settings.tintAmount;

As per coding guidelines "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp` around lines 160 - 183, The
code clamps runtime values into `data` but does not write the sanitized values
back into `settings`, leaving persisted JSON and `HasUnsavedChanges()` out of
sync; update the same fields on the `settings` object after clamping (for every
field set on `data` such as `hdrEyeAdaptSpeed`, `hdrBloomBlurRadius`,
`hdrBloomThreshold`, `hdrBloomScale`, `hdrWhite`, `hdrSunlightScale`,
`hdrSkyScale`, `cinematicSaturation`, `cinematicBrightness`,
`cinematicContrast`, `tintColor.x/y/z`, `tintAmount`, `dofStrength`,
`dofDistance`, `dofRange`) so that `settings` contains the sanitized values, and
ensure any JSON persistence or `HasUnsavedChanges()` logic will reflect the
applied/clamped values.
src/WeatherEditor/Weather/WeatherWidget.cpp (1)

74-77: ⚠️ Potential issue | 🟡 Minor

Dropdown focus is still forced every frame, so outside-click close stays unreliable.

Line 76 re-focuses the dropdown continuously, which undermines the close check on Line 99.

Suggested fix
-		ImGui::SetNextWindowFocus();
+		if (searchDropdownJustOpened) {
+			ImGui::SetNextWindowFocus();
+			searchDropdownJustOpened = false;
+		}

Also applies to: 98-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` around lines 74 - 77, The code
calls ImGui::SetNextWindowFocus every frame which forces focus and prevents
reliable outside-click closing; change logic to call ImGui::SetNextWindowFocus
only once when the dropdown is opened (e.g., on the state transition that sets
the dropdown open flag) instead of every frame, and remove or gate the
continuous calls to ImGui::SetNextWindowFocus at the other occurrence (the block
around ImGui::PushStyleVar/SetNextWindowSize); tie the focus call to your
dropdown-open boolean (or a one-time "requestFocus" flag) so subsequent frames
do not refocus the window.
src/WeatherEditor/Weather/PrecipitationWidget.cpp (1)

131-132: ⚠️ Potential issue | 🟠 Major

Clamp subtexture counts to int32_t max before writing .i runtime fields.

Current normalization only enforces a lower bound. Very large JSON values can overflow/truncate when assigned to runtime.data[..].i.

Suggested fix
+	constexpr uint32_t kMaxI32 = static_cast<uint32_t>(std::numeric_limits<int32_t>::max());
-	settings.numSubtexturesX = std::max(1u, settings.numSubtexturesX);
-	settings.numSubtexturesY = std::max(1u, settings.numSubtexturesY);
+	settings.numSubtexturesX = std::clamp(settings.numSubtexturesX, 1u, kMaxI32);
+	settings.numSubtexturesY = std::clamp(settings.numSubtexturesY, 1u, kMaxI32);

 	...
-	runtime.data[static_cast<uint32_t>(DataID::kNumSubtexturesX)].i = settings.numSubtexturesX;
-	runtime.data[static_cast<uint32_t>(DataID::kNumSubtexturesY)].i = settings.numSubtexturesY;
+	runtime.data[static_cast<uint32_t>(DataID::kNumSubtexturesX)].i = static_cast<int32_t>(settings.numSubtexturesX);
+	runtime.data[static_cast<uint32_t>(DataID::kNumSubtexturesY)].i = static_cast<int32_t>(settings.numSubtexturesY);

Use this script to verify field types and assignment sites:

#!/bin/bash
set -euo pipefail

echo "=== Locate subtexture runtime writes ==="
rg -n -C2 'kNumSubtexturesX|kNumSubtexturesY|runtime\.data\[.*\]\.i' src/WeatherEditor/Weather/PrecipitationWidget.cpp

echo
echo "=== Locate Data slot type definitions/usages ==="
rg -n -C3 'BGSShaderParticleGeometryData|union|struct|\.i\b' src --type cpp --type h --type hpp

As per coding guidelines "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations".

Also applies to: 179-180, 197-198

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp` around lines 131 - 132,
The code currently only enforces a lower bound for settings.numSubtexturesX/Y
(in PrecipitationWidget.cpp) which can still be extremely large and
overflow/truncate when later written into runtime.data[...].i (the integer slot,
referenced by kNumSubtexturesX/kNumSubtexturesY); fix by clamping
settings.numSubtexturesX and settings.numSubtexturesY to int32_t maximum before
any assignment into runtime.data[].i (use std::min(settings.numSubtexturesX,
static_cast<uint32_t>(std::numeric_limits<int32_t>::max())) or equivalent) and
then cast to int32_t when writing to runtime.data[..].i so the stored value
cannot overflow the .i union field. Ensure the same clamp is applied at all
assignment sites that write into runtime.data[..].i (including the instances
around the other mentioned lines).
src/WeatherEditor/Weather/VolumetricLightingWidget.cpp (1)

105-107: ⚠️ Potential issue | 🟠 Major

merge_patch + _WITH_DEFAULT can still drift from vanilla when payload contains explicit null.

merge_patch removes null-valued keys; then deserialization fills missing members from struct defaults (finite), which bypasses your current fallback logic.

Suggested fix
 			nlohmann::json merged = vanillaSettings;
-			merged.merge_patch(js);
+			if (!js.is_object()) {
+				throw std::runtime_error("VolumetricLighting settings JSON must be an object");
+			}
+			for (auto it = js.begin(); it != js.end(); ++it) {
+				if (!it.value().is_null()) {
+					merged[it.key()] = it.value();
+				}
+			}
 			settings = merged.get<Settings>();
For nlohmann::json merge_patch (RFC 7396), does a member with value null delete that key from the target object? Also, with NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT, how are missing keys populated during from_json?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/VolumetricLightingWidget.cpp` around lines 105 -
107, The current use of merged.merge_patch(js) removes keys when js contains
explicit null, so deserialization later fills those missing members from struct
defaults (via NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT) and you lose the
intended vanilla fallback; after performing merged.merge_patch(js) (vars:
merged, vanillaSettings, js, Settings), iterate the keys in vanillaSettings and
for any key missing from merged set merged[key] = vanillaSettings[key] (thereby
restoring vanilla values for keys deleted by null in js) before calling settings
= merged.get<Settings>(); This preserves explicit-null semantics while ensuring
fallback to vanillaSettings instead of struct defaults.
🧹 Nitpick comments (1)
src/WeatherEditor/Weather/WeatherWidget.cpp (1)

1-1: Optional: tighten the PR title to match repo commit-title constraints.

Current title is clear, but it appears over the 50-char title target. Consider: refactor(weather-editor): unify widget json io.
If there is a tracking issue, add Related to #<id> in the PR description.

As per coding guidelines "When reviewing PRs, please provide suggestions for: Conventional Commit Titles ... Length: 50 characters ... and Issue References ...".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` at line 1, PR title exceeds the
50-character conventional-commit target; update the PR/commit title to a
shorter, conventional form (for example "refactor(weather-editor): unify widget
json io") and, if applicable, add a tracking issue reference like "Related to
#<id>" in the PR description to satisfy the repo's commit-title and
issue-reference guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Around line 419-421: The undo snapshot is taken after mutating settings, so
PushUndoState (which snapshots widget->js) can capture a stale state; before any
auto-apply path (where EditorWindow::GetSingleton()->settings.autoApplyChanges
is checked and ApplyChanges is called) capture a pre-edit snapshot: call
EditorWindow::GetSingleton()->PushUndoState(this) before you mutate
settings/other fields or, better, extend PushUndoState to also copy the current
settings state (or add an overload like PushUndoState(this, preEditSettings))
and use that pre-edit snapshot in the auto-apply flows (the branches that call
ApplyChanges and PushUndoState, e.g. the blocks around ApplyChanges,
settings.autoApplyChanges, PushUndoState, and widget->js) so the undo history
reflects the exact state prior to the mutation.

---

Duplicate comments:
In `@src/WeatherEditor/Weather/ImageSpaceWidget.cpp`:
- Around line 160-183: The code clamps runtime values into `data` but does not
write the sanitized values back into `settings`, leaving persisted JSON and
`HasUnsavedChanges()` out of sync; update the same fields on the `settings`
object after clamping (for every field set on `data` such as `hdrEyeAdaptSpeed`,
`hdrBloomBlurRadius`, `hdrBloomThreshold`, `hdrBloomScale`, `hdrWhite`,
`hdrSunlightScale`, `hdrSkyScale`, `cinematicSaturation`, `cinematicBrightness`,
`cinematicContrast`, `tintColor.x/y/z`, `tintAmount`, `dofStrength`,
`dofDistance`, `dofRange`) so that `settings` contains the sanitized values, and
ensure any JSON persistence or `HasUnsavedChanges()` logic will reflect the
applied/clamped values.

In `@src/WeatherEditor/Weather/PrecipitationWidget.cpp`:
- Around line 131-132: The code currently only enforces a lower bound for
settings.numSubtexturesX/Y (in PrecipitationWidget.cpp) which can still be
extremely large and overflow/truncate when later written into
runtime.data[...].i (the integer slot, referenced by
kNumSubtexturesX/kNumSubtexturesY); fix by clamping settings.numSubtexturesX and
settings.numSubtexturesY to int32_t maximum before any assignment into
runtime.data[].i (use std::min(settings.numSubtexturesX,
static_cast<uint32_t>(std::numeric_limits<int32_t>::max())) or equivalent) and
then cast to int32_t when writing to runtime.data[..].i so the stored value
cannot overflow the .i union field. Ensure the same clamp is applied at all
assignment sites that write into runtime.data[..].i (including the instances
around the other mentioned lines).

In `@src/WeatherEditor/Weather/VolumetricLightingWidget.cpp`:
- Around line 105-107: The current use of merged.merge_patch(js) removes keys
when js contains explicit null, so deserialization later fills those missing
members from struct defaults (via
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT) and you lose the intended
vanilla fallback; after performing merged.merge_patch(js) (vars: merged,
vanillaSettings, js, Settings), iterate the keys in vanillaSettings and for any
key missing from merged set merged[key] = vanillaSettings[key] (thereby
restoring vanilla values for keys deleted by null in js) before calling settings
= merged.get<Settings>(); This preserves explicit-null semantics while ensuring
fallback to vanillaSettings instead of struct defaults.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Around line 74-77: The code calls ImGui::SetNextWindowFocus every frame which
forces focus and prevents reliable outside-click closing; change logic to call
ImGui::SetNextWindowFocus only once when the dropdown is opened (e.g., on the
state transition that sets the dropdown open flag) instead of every frame, and
remove or gate the continuous calls to ImGui::SetNextWindowFocus at the other
occurrence (the block around ImGui::PushStyleVar/SetNextWindowSize); tie the
focus call to your dropdown-open boolean (or a one-time "requestFocus" flag) so
subsequent frames do not refocus the window.

---

Nitpick comments:
In `@src/WeatherEditor/Weather/WeatherWidget.cpp`:
- Line 1: PR title exceeds the 50-character conventional-commit target; update
the PR/commit title to a shorter, conventional form (for example
"refactor(weather-editor): unify widget json io") and, if applicable, add a
tracking issue reference like "Related to #<id>" in the PR description to
satisfy the repo's commit-title and issue-reference guidelines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9e5c24fd-8ef7-4873-8995-7afcb7e9a586

📥 Commits

Reviewing files that changed from the base of the PR and between 395b660 and 37fb964.

📒 Files selected for processing (5)
  • src/WeatherEditor/Weather/ImageSpaceWidget.cpp
  • src/WeatherEditor/Weather/LightingTemplateWidget.cpp
  • src/WeatherEditor/Weather/PrecipitationWidget.cpp
  • src/WeatherEditor/Weather/VolumetricLightingWidget.cpp
  • src/WeatherEditor/Weather/WeatherWidget.cpp

Comment on lines 419 to 421
if (recordChanged && EditorWindow::GetSingleton()->settings.autoApplyChanges) {
EditorWindow::GetSingleton()->PushUndoState(this);
ApplyChanges();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Undo snapshots in these auto-apply paths can collapse to stale state.

PushUndoState() snapshots widget->js, while these flows mutate settings and immediately apply. Without a guaranteed pre-edit snapshot source, repeated edits can produce duplicate/incorrect undo history.

Suggested direction
+	// capture pre-edit settings before mutating controls in this section
+	const Settings preEdit = settings;
 	...
 	if (changed && editorWindow->settings.autoApplyChanges) {
-		editorWindow->PushUndoState(this);
+		const auto jsBackup = js;
+		js = preEdit;  // push true pre-edit state
+		editorWindow->PushUndoState(this);
+		js = jsBackup;
 		ApplyChanges();
 	}

Also applies to: 942-944, 1004-1006, 1293-1295, 1380-1382, 1477-1479

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/Weather/WeatherWidget.cpp` around lines 419 - 421, The undo
snapshot is taken after mutating settings, so PushUndoState (which snapshots
widget->js) can capture a stale state; before any auto-apply path (where
EditorWindow::GetSingleton()->settings.autoApplyChanges is checked and
ApplyChanges is called) capture a pre-edit snapshot: call
EditorWindow::GetSingleton()->PushUndoState(this) before you mutate
settings/other fields or, better, extend PushUndoState to also copy the current
settings state (or add an overload like PushUndoState(this, preEditSettings))
and use that pre-edit snapshot in the auto-apply flows (the branches that call
ApplyChanges and PushUndoState, e.g. the blocks around ApplyChanges,
settings.autoApplyChanges, PushUndoState, and widget->js) so the undo history
reflects the exact state prior to the mutation.

@SkrubbySkrubInAShrub SkrubbySkrubInAShrub marked this pull request as draft March 4, 2026 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant