Skip to content
116 changes: 92 additions & 24 deletions src/WeatherEditor/Weather/WeatherWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,10 @@ void WeatherWidget::DrawWidget()
// Inherit checkbox
if (hasParent) {
bool& inheritFlag = settings.inheritFlags[inheritKey];
if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) {
if (inheritFlag && parentWidget) {
weather->imageSpaces[i] = parentWidget->weather->imageSpaces[i];
ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag);
if (inheritFlag && parentWidget) {
if (settings.imageSpaceRefs[i] != parentWidget->settings.imageSpaceRefs[i]) {
settings.imageSpaceRefs[i] = parentWidget->settings.imageSpaceRefs[i];
recordChanged = true;
}
}
Expand All @@ -255,14 +256,14 @@ void WeatherWidget::DrawWidget()

ImGui::Text("%s:", label.c_str());
ImGui::SameLine(todLabelOffset);
if (WeatherUtils::DrawFormPickerCached("##ImageSpace", weather->imageSpaces[i], editorWindow->imageSpaceWidgets, false, true, pickerWidth)) {
if (WeatherUtils::DrawFormPickerCached("##ImageSpace", settings.imageSpaceRefs[i], editorWindow->imageSpaceWidgets, false, true, pickerWidth)) {
recordChanged = true;
} // Add "Open" button
if (weather->imageSpaces[i]) {
if (settings.imageSpaceRefs[i]) {
ImGui::SameLine();
if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) {
for (auto& widget : editorWindow->imageSpaceWidgets) {
if (widget->form == weather->imageSpaces[i]) {
if (widget->form == settings.imageSpaceRefs[i]) {
widget->SetOpen(true);
break;
}
Expand All @@ -288,9 +289,10 @@ void WeatherWidget::DrawWidget()
// Inherit checkbox
if (hasParent) {
bool& inheritFlag = settings.inheritFlags[inheritKey];
if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) {
if (inheritFlag && parentWidget) {
weather->volumetricLighting[i] = parentWidget->weather->volumetricLighting[i];
ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag);
if (inheritFlag && parentWidget) {
if (settings.volumetricLightingRefs[i] != parentWidget->settings.volumetricLightingRefs[i]) {
settings.volumetricLightingRefs[i] = parentWidget->settings.volumetricLightingRefs[i];
recordChanged = true;
}
}
Expand All @@ -302,14 +304,14 @@ void WeatherWidget::DrawWidget()

ImGui::Text("%s:", label.c_str());
ImGui::SameLine(todLabelOffset);
if (WeatherUtils::DrawFormPickerCached("##VolumetricLighting", weather->volumetricLighting[i], editorWindow->volumetricLightingWidgets, false, true, pickerWidth)) {
if (WeatherUtils::DrawFormPickerCached("##VolumetricLighting", settings.volumetricLightingRefs[i], editorWindow->volumetricLightingWidgets, false, true, pickerWidth)) {
recordChanged = true;
} // Add "Open" button
if (weather->volumetricLighting[i]) {
if (settings.volumetricLightingRefs[i]) {
ImGui::SameLine();
if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) {
for (auto& widget : editorWindow->volumetricLightingWidgets) {
if (widget->form == weather->volumetricLighting[i]) {
if (widget->form == settings.volumetricLightingRefs[i]) {
widget->SetOpen(true);
break;
}
Expand All @@ -330,9 +332,10 @@ void WeatherWidget::DrawWidget()
// Inherit checkbox
if (hasParent) {
bool& inheritFlag = settings.inheritFlags["Precipitation"];
if (ImGui::Checkbox("##inherit_Precipitation", &inheritFlag)) {
if (inheritFlag && parentWidget) {
weather->precipitationData = parentWidget->weather->precipitationData;
ImGui::Checkbox("##inherit_Precipitation", &inheritFlag);
if (inheritFlag && parentWidget) {
if (settings.precipitationData != parentWidget->settings.precipitationData) {
settings.precipitationData = parentWidget->settings.precipitationData;
recordChanged = true;
}
}
Expand All @@ -344,14 +347,14 @@ void WeatherWidget::DrawWidget()

ImGui::Text("Particle Shader:");
ImGui::SameLine(formLabelOffset);
if (WeatherUtils::DrawFormPickerCached("##Precipitation", weather->precipitationData, editorWindow->precipitationWidgets, false, true, pickerWidth)) {
if (WeatherUtils::DrawFormPickerCached("##Precipitation", settings.precipitationData, editorWindow->precipitationWidgets, false, true, pickerWidth)) {
recordChanged = true;
} // Add "Open" button
if (weather->precipitationData) {
if (settings.precipitationData) {
ImGui::SameLine();
if (ImGui::SmallButton("Open##Precip")) {
for (auto& widget : editorWindow->precipitationWidgets) {
if (widget->form == weather->precipitationData) {
if (widget->form == settings.precipitationData) {
widget->SetOpen(true);
break;
}
Expand All @@ -370,9 +373,10 @@ void WeatherWidget::DrawWidget()
// Inherit checkbox
if (hasParent) {
bool& inheritFlag = settings.inheritFlags["ReferenceEffect"];
if (ImGui::Checkbox("##inherit_ReferenceEffect", &inheritFlag)) {
if (inheritFlag && parentWidget) {
weather->referenceEffect = parentWidget->weather->referenceEffect;
ImGui::Checkbox("##inherit_ReferenceEffect", &inheritFlag);
if (inheritFlag && parentWidget) {
if (settings.referenceEffect != parentWidget->settings.referenceEffect) {
settings.referenceEffect = parentWidget->settings.referenceEffect;
recordChanged = true;
}
}
Expand All @@ -384,14 +388,14 @@ void WeatherWidget::DrawWidget()

ImGui::Text("Reference Effect:");
ImGui::SameLine(formLabelOffset);
if (WeatherUtils::DrawFormPickerCached("##ReferenceEffect", weather->referenceEffect, editorWindow->referenceEffectWidgets, false, true, pickerWidth)) {
if (WeatherUtils::DrawFormPickerCached("##ReferenceEffect", settings.referenceEffect, editorWindow->referenceEffectWidgets, false, true, pickerWidth)) {
recordChanged = true;
} // Add "Open" button
if (weather->referenceEffect) {
if (settings.referenceEffect) {
ImGui::SameLine();
if (ImGui::SmallButton("Open##RefEffect")) {
for (auto& widget : editorWindow->referenceEffectWidgets) {
if (widget->form == weather->referenceEffect) {
if (widget->form == settings.referenceEffect) {
widget->SetOpen(true);
break;
}
Expand Down Expand Up @@ -454,6 +458,25 @@ void WeatherWidget::LoadSettings()
settings.fogProperties.size());
}

// Record form references (resolved by matching widget EditorID)
// Three cases: key missing -> vanilla, key present with "" -> explicit None (nullptr), key present with id -> lookup form
auto* editorWindow = EditorWindow::GetSingleton();
auto loadRef = [&]<typename T>(const std::string& key, const std::vector<std::unique_ptr<Widget>>& widgets, T* vanillaValue) -> T* {
if (!js.contains(key) || !js[key].is_string())
return vanillaValue;
const std::string id = js[key].get<std::string>();
if (id.empty())
return nullptr;
auto* f = WeatherUtils::FindFormByEditorID(id, widgets);
return f ? static_cast<T*>(f) : vanillaValue;
};
for (size_t i = 0; i < ColorTimes::kTotal; i++) {
settings.imageSpaceRefs[i] = loadRef(std::format("imageSpaceRef_{}", i), editorWindow->imageSpaceWidgets, vanillaSettings.imageSpaceRefs[i]);
settings.volumetricLightingRefs[i] = loadRef(std::format("volumetricLightingRef_{}", i), editorWindow->volumetricLightingWidgets, vanillaSettings.volumetricLightingRefs[i]);
}
settings.precipitationData = loadRef("precipitationDataRef", editorWindow->precipitationWidgets, vanillaSettings.precipitationData);
settings.referenceEffect = loadRef("referenceEffectRef", editorWindow->referenceEffectWidgets, vanillaSettings.referenceEffect);

} catch (const nlohmann::json::exception& e) {
logger::error("Weather {}: Failed to deserialize settings from JSON: {}", GetEditorID(), e.what());
// Fallback to vanilla/game values on exception
Expand Down Expand Up @@ -482,6 +505,15 @@ void WeatherWidget::SaveSettings()
try {
js = settings;

// Record form references (serialized as widget EditorIDs for load-order independence)
auto* editorWindow = EditorWindow::GetSingleton();
for (size_t i = 0; i < ColorTimes::kTotal; i++) {
js[std::format("imageSpaceRef_{}", i)] = WeatherUtils::FindEditorIDByForm(settings.imageSpaceRefs[i], editorWindow->imageSpaceWidgets);
js[std::format("volumetricLightingRef_{}", i)] = WeatherUtils::FindEditorIDByForm(settings.volumetricLightingRefs[i], editorWindow->volumetricLightingWidgets);
}
js["precipitationDataRef"] = WeatherUtils::FindEditorIDByForm(settings.precipitationData, editorWindow->precipitationWidgets);
js["referenceEffectRef"] = WeatherUtils::FindEditorIDByForm(settings.referenceEffect, editorWindow->referenceEffectWidgets);

if (js.is_null()) {
logger::error("Weather {}: Serialization produced null JSON!", GetEditorID());
} else if (!js.contains("weatherProperties")) {
Expand Down Expand Up @@ -609,6 +641,14 @@ void WeatherWidget::SetWeatherValues()
}
weather->cloudLayerDisabledBits = disabledBits;

// Record form references
for (size_t i = 0; i < ColorTimes::kTotal; i++) {
weather->imageSpaces[i] = settings.imageSpaceRefs[i];
weather->volumetricLighting[i] = settings.volumetricLightingRefs[i];
}
weather->precipitationData = settings.precipitationData;
weather->referenceEffect = settings.referenceEffect;

// If this weather is currently active, immediately apply feature settings to game memory
auto* weatherManager = WeatherManager::GetSingleton();
if (weatherManager->GetCurrentWeathers().currentWeather == weather) {
Expand Down Expand Up @@ -771,6 +811,14 @@ void WeatherWidget::LoadWeatherValues()
ColorToFloat3(cloudColors[j], settingsCloud.color[j]);
}
}

// Record form references
for (size_t i = 0; i < ColorTimes::kTotal; i++) {
settings.imageSpaceRefs[i] = weather->imageSpaces[i];
settings.volumetricLightingRefs[i] = weather->volumetricLighting[i];
}
settings.precipitationData = weather->precipitationData;
settings.referenceEffect = weather->referenceEffect;
}

void WeatherWidget::DrawDALCSettings()
Expand Down Expand Up @@ -1442,6 +1490,14 @@ void WeatherWidget::InheritAllFromParent()
settings.clouds[i] = parentWidget->settings.clouds[i];
}

// Copy records (form references)
for (size_t i = 0; i < ColorTimes::kTotal; i++) {
settings.imageSpaceRefs[i] = parentWidget->settings.imageSpaceRefs[i];
settings.volumetricLightingRefs[i] = parentWidget->settings.volumetricLightingRefs[i];
}
settings.precipitationData = parentWidget->settings.precipitationData;
settings.referenceEffect = parentWidget->settings.referenceEffect;

// Set all inherit flags to true
settings.inheritFlags["DALC_Specular"] = true;
settings.inheritFlags["DALC_Fresnel"] = true;
Expand Down Expand Up @@ -1471,6 +1527,14 @@ void WeatherWidget::InheritAllFromParent()
settings.inheritFlags[std::format("Cloud{}_Alpha", i)] = true;
}

// Records
for (size_t i = 0; i < ColorTimes::kTotal; i++) {
settings.inheritFlags["ImageSpace_" + std::to_string(i)] = true;
settings.inheritFlags["VolumetricLighting_" + std::to_string(i)] = true;
}
settings.inheritFlags["Precipitation"] = true;
settings.inheritFlags["ReferenceEffect"] = true;

// Apply the changes
if (EditorWindow::GetSingleton()->settings.autoApplyChanges) {
ApplyChanges();
Expand Down Expand Up @@ -1643,6 +1707,10 @@ bool WeatherWidget::Settings::operator==(const Settings& o) const
std::equal(std::begin(atmosphereColors), std::end(atmosphereColors), std::begin(o.atmosphereColors)) &&
std::equal(std::begin(dalc), std::end(dalc), std::begin(o.dalc)) &&
std::equal(std::begin(clouds), std::end(clouds), std::begin(o.clouds)) &&
std::equal(std::begin(imageSpaceRefs), std::end(imageSpaceRefs), std::begin(o.imageSpaceRefs)) &&
std::equal(std::begin(volumetricLightingRefs), std::end(volumetricLightingRefs), std::begin(o.volumetricLightingRefs)) &&
precipitationData == o.precipitationData &&
referenceEffect == o.referenceEffect &&
featureSettings == o.featureSettings;
}

Expand Down
6 changes: 6 additions & 0 deletions src/WeatherEditor/Weather/WeatherWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ class WeatherWidget : public Widget
DALC dalc[ColorTimes::kTotal];
Cloud clouds[TESWeather::kTotalLayers];

// Record form references
RE::TESImageSpace* imageSpaceRefs[ColorTimes::kTotal] = {};
RE::BGSVolumetricLighting* volumetricLightingRefs[ColorTimes::kTotal] = {};
RE::BGSShaderParticleGeometryData* precipitationData = nullptr;
RE::BGSReferenceEffect* referenceEffect = nullptr;

// Per-feature settings storage
std::map<std::string, json> featureSettings;

Expand Down
41 changes: 31 additions & 10 deletions src/WeatherEditor/WeatherUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,65 @@ namespace WeatherUtils::TexturePath

bool HasDdsExtension(std::string_view path)
{
return Normalize(path).ends_with(".dds");
return Normalize(path).ends_with(kDdsExtension);
}

bool ExistsOnDisk(std::string_view path)
{
const std::string lower = Normalize(path);
if (!lower.ends_with(".dds"))
if (!lower.ends_with(kDdsExtension))
return false;

// Reject absolute paths
// Reject absolute paths and ".." traversal
const std::filesystem::path fsPath(lower);
if (fsPath.is_absolute())
return false;

// Reject any ".." component
for (const auto& part : fsPath)
if (part == "..")
return false;

const std::filesystem::path dataPath = Util::PathHelpers::GetDataPath();
const std::filesystem::path fullPath = lower.starts_with("textures\\") ?
const std::filesystem::path fullPath = lower.starts_with(kTexturePrefix) ?
dataPath / lower :
dataPath / "textures" / lower;
dataPath / kTexturePrefix / lower;

std::error_code ec;
return std::filesystem::exists(fullPath, ec) && !ec;
}

std::string BuildResourcePath(std::string_view path)
{
std::string result = "Textures\\";
std::string result(kResourcePrefix);
result.append(path);
if (!Normalize(result).ends_with(".dds"))
result += ".dds";
if (!Normalize(result).ends_with(kDdsExtension))
result += kDdsExtension;
return result;
}
}

namespace WeatherUtils
{
RE::TESForm* FindFormByEditorID(std::string_view editorID, const std::vector<std::unique_ptr<Widget>>& widgets)
{
if (editorID.empty())
return nullptr;
for (const auto& w : widgets)
if (w->GetEditorID() == editorID)
return w->form;
return nullptr;
}

std::string FindEditorIDByForm(const RE::TESForm* form, const std::vector<std::unique_ptr<Widget>>& widgets)
{
if (!form)
return "";
for (const auto& w : widgets)
if (w->form == form)
return w->GetEditorID();
return "";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

// Global widget context for undo tracking
static Widget* g_currentWidget = nullptr;

Expand Down
8 changes: 8 additions & 0 deletions src/WeatherEditor/WeatherUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ namespace WeatherUtils
// Texture path helpers shared by precipitation and cloud layer widgets.
namespace TexturePath
{
inline constexpr std::string_view kTexturePrefix = "textures\\";
inline constexpr std::string_view kResourcePrefix = "Textures\\";
inline constexpr std::string_view kDdsExtension = ".dds";

// Lowercase + convert forward slashes to backslashes.
std::string Normalize(std::string_view path);

Expand All @@ -370,6 +374,10 @@ namespace WeatherUtils
std::string BuildResourcePath(std::string_view path);
}

// Lookup helpers for form↔widget ref serialization (load-order independent).
RE::TESForm* FindFormByEditorID(std::string_view editorID, const std::vector<std::unique_ptr<Widget>>& widgets);
std::string FindEditorIDByForm(const RE::TESForm* form, const std::vector<std::unique_ptr<Widget>>& widgets);

// Set the current widget for undo tracking (should be called at start of widget Draw())
void SetCurrentWidget(Widget* widget);

Expand Down
Loading