From 1e93926e1fc8f6d866c3e47a870ed197efbd7a5b Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 30 Sep 2025 18:50:24 +1000 Subject: [PATCH 01/11] json printer 9000 --- .../InverseSquareLighting/LightEditor.cpp | 178 ++++++++++++++++++ .../InverseSquareLighting/LightEditor.h | 3 + 2 files changed, 181 insertions(+) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index d6dfe2723..da8d34dc9 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -1,6 +1,13 @@ #include "Features/InverseSquareLighting/LightEditor.h" #include "Features/LightLimitFix.h" #include "Menu.h" +#include "Util.h" +#include +#include +#include +#include +#include +#include void LightEditor::DrawSettings() { @@ -35,6 +42,13 @@ void LightEditor::DrawSettings() sortOption = static_cast(selectedSort); } + if (ImGui::Button("Export Lights to JSON")) { + ExportLightsToJson(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Export all visible lights to JSON file with RefID, timestamp, and light data"); + } + if (ImGui::BeginCombo("Lights", selected.isSelected ? GetLightName(selected).c_str() : "Select a light")) { for (auto& light : lights) { const auto displayName = GetLightName(light); @@ -336,4 +350,168 @@ void LightEditor::SortLights() default: break; } +} + +void LightEditor::ExportLightsToJson() +{ + if (lights.empty()) { + logger::warn("No lights available to export"); + return; + } + + json exportData; + + // Add timestamp + const auto now = std::chrono::system_clock::now(); + const auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S"); + exportData["timestamp"] = ss.str(); + + // Add current scene context + const auto* tes = RE::TES::GetSingleton(); + const auto* currentCell = tes ? tes->interiorCell : nullptr; + if (!currentCell && tes) { + const auto* player = RE::PlayerCharacter::GetSingleton(); + if (player) { + currentCell = player->GetParentCell(); + } + } + + // Cell information + if (currentCell) { + exportData["cell"] = { + {"formID", fmt::format("0x{:08X}", currentCell->GetFormID())}, + {"editorID", currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown"}, + {"isInterior", currentCell->IsInteriorCell()} + }; + } else { + exportData["cell"] = { + {"formID", "0x00000000"}, + {"editorID", "Unknown"}, + {"isInterior", false} + }; + } + + // Player position for reference + const auto* player = RE::PlayerCharacter::GetSingleton(); + if (player) { + const auto playerPos = player->GetPosition(); + exportData["playerPosition"] = { + {"x", playerPos.x}, + {"y", playerPos.y}, + {"z", playerPos.z} + }; + } + + // Filter and sort settings used for this export + exportData["exportSettings"] = { + {"filterOption", FilterOptionLabels[static_cast(filterOption)]}, + {"sortOption", SortOptionLabels[static_cast(sortOption)]}, + {"lightCount", lights.size()} + }; + + // Add light data + exportData["lights"] = json::array(); + for (const auto& light : lights) { + exportData["lights"].push_back(CreateLightJsonData(light)); + } + + // Generate filename with timestamp + const auto exportPath = Util::PathHelpers::GetCommunityShaderPath() / "LightExports"; + try { + std::filesystem::create_directories(exportPath); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Failed to create export directory: {}", e.what()); + return; + } + + const auto filename = fmt::format("lights_export_{:%Y%m%d_%H%M%S}.json", + std::chrono::system_clock::now()); + const auto filePath = exportPath / filename; + + std::ofstream outFile(filePath); + if (!outFile.is_open()) { + logger::warn("Failed to create export file: {}", filePath.string()); + return; + } + + outFile << exportData.dump(4); + outFile.close(); + + logger::info("Successfully exported {} lights to: {}", lights.size(), filePath.string()); +} + +json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) +{ + json lightData; + + // Basic light info - using refID as the main identifier for compatibility + lightData["refID"] = fmt::format("0x{:08X}", lightInfo.id); + lightData["editorID"] = lightInfo.name; + lightData["index"] = lightInfo.index; + lightData["type"] = lightInfo.isRef ? "Reference" : (lightInfo.isAttached ? "Attached" : "Other"); + lightData["memoryAddress"] = fmt::format("{:p}", lightInfo.ptr); + + // Position - using standard light placer format + lightData["position"] = { + {"x", lightInfo.position.x}, + {"y", lightInfo.position.y}, + {"z", lightInfo.position.z} + }; + + // If this is the selected light, include detailed settings + if (lightInfo == selected && lightInfo.isSelected) { + lightData["settings"] = { + {"color", { + {"r", current.data.diffuse.red}, + {"g", current.data.diffuse.green}, + {"b", current.data.diffuse.blue} + }}, + {"intensity", current.data.fade}, + {"radius", current.data.radius}, + {"size", current.data.size}, + {"cutoffOverride", current.data.cutoffOverride}, + {"isInverseSquare", current.data.flags.any(LightLimitFix::LightFlags::InverseSquare)} + }; + + // Position offset if applicable + if (!lightInfo.isOther) { + lightData["settings"]["positionOffset"] = { + {"x", current.pos.x}, + {"y", current.pos.y}, + {"z", current.pos.z} + }; + } + + // TES flags if applicable + if (!lightInfo.isOther && displayInfo.ownerFormId != 0) { + lightData["settings"]["tesFlags"] = { + {"dynamic", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kDynamic)}, + {"negative", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kNegative)}, + {"flicker", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kFlicker)}, + {"flickerSlow", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kFlickerSlow)}, + {"pulse", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kPulse)}, + {"pulseSlow", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kPulseSlow)}, + {"hemiShadow", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kHemiShadow)}, + {"omniShadow", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kOmniShadow)}, + {"portalStrict", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kPortalStrict)} + }; + } + + // Additional metadata for reference/attached lights + if (lightInfo.isRef || lightInfo.isAttached) { + lightData["metadata"] = { + {"ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId)}, + {"ownerEditorID", displayInfo.ownerEditorId}, + {"baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId)}, + {"ownerLastEditedBy", displayInfo.ownerLastEditedBy}, + {"cellEditorID", displayInfo.cellEditorId}, + {"lightFormID", fmt::format("0x{:08X}", displayInfo.lighFormId)}, + {"lightEditorID", displayInfo.lighEditorId} + }; + } + } + + return lightData; } \ No newline at end of file diff --git a/src/Features/InverseSquareLighting/LightEditor.h b/src/Features/InverseSquareLighting/LightEditor.h index b3c2b8294..f1b164e4d 100644 --- a/src/Features/InverseSquareLighting/LightEditor.h +++ b/src/Features/InverseSquareLighting/LightEditor.h @@ -100,4 +100,7 @@ struct LightEditor static std::string GetLightName(LightInfo& lightInfo); void UpdateSelectedLight(RE::TESObjectREFR* refr, RE::TESObjectLIGH* ligh, RE::NiLight* niLight); + + void ExportLightsToJson(); + json CreateLightJsonData(const LightInfo& lightInfo); }; From e9123841776f16583d1e355c2903454964f816b5 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 30 Sep 2025 19:13:38 +1000 Subject: [PATCH 02/11] Update LightEditor.cpp --- .../InverseSquareLighting/LightEditor.cpp | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index da8d34dc9..8711141f1 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -426,8 +426,9 @@ void LightEditor::ExportLightsToJson() return; } - const auto filename = fmt::format("lights_export_{:%Y%m%d_%H%M%S}.json", - std::chrono::system_clock::now()); + std::stringstream timeStream; + timeStream << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S"); + const auto filename = fmt::format("lights_export_{}.json", timeStream.str()); const auto filePath = exportPath / filename; std::ofstream outFile(filePath); @@ -472,7 +473,7 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) {"radius", current.data.radius}, {"size", current.data.size}, {"cutoffOverride", current.data.cutoffOverride}, - {"isInverseSquare", current.data.flags.any(LightLimitFix::LightFlags::InverseSquare)} + {"isInverseSquare", static_cast(*reinterpret_cast(¤t.data.flags) & static_cast(LightLimitFix::LightFlags::InverseSquare))} }; // Position offset if applicable @@ -486,16 +487,17 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // TES flags if applicable if (!lightInfo.isOther && displayInfo.ownerFormId != 0) { + auto flagsValue = *reinterpret_cast(¤t.tesFlags); lightData["settings"]["tesFlags"] = { - {"dynamic", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kDynamic)}, - {"negative", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kNegative)}, - {"flicker", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kFlicker)}, - {"flickerSlow", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kFlickerSlow)}, - {"pulse", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kPulse)}, - {"pulseSlow", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kPulseSlow)}, - {"hemiShadow", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kHemiShadow)}, - {"omniShadow", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kOmniShadow)}, - {"portalStrict", current.tesFlags.any(RE::TES_LIGHT_FLAGS::kPortalStrict)} + {"dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic))}, + {"negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative))}, + {"flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker))}, + {"flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow))}, + {"pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse))}, + {"pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow))}, + {"hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow))}, + {"omniShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow))}, + {"portalStrict", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict))} }; } From 717daba06f2496b793ec26fa89633184e83a1272 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 30 Sep 2025 21:25:09 +1000 Subject: [PATCH 03/11] extra button --- .../InverseSquareLighting/LightEditor.cpp | 120 +++++++++++++++++- .../InverseSquareLighting/LightEditor.h | 1 + 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index 8711141f1..25df6712b 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -42,11 +42,19 @@ void LightEditor::DrawSettings() sortOption = static_cast(selectedSort); } - if (ImGui::Button("Export Lights to JSON")) { + if (ImGui::Button("Export All Lights to JSON")) { ExportLightsToJson(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Export all visible lights to JSON file with RefID, timestamp, and light data"); + ImGui::Text("Export all visible lights with metadata to JSON file with RefID, timestamp, and light data"); + } + + ImGui::SameLine(); + if (ImGui::Button("Export Selected Light")) { + ExportSelectedLightToJson(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Export only the currently selected light to JSON file"); } if (ImGui::BeginCombo("Lights", selected.isSelected ? GetLightName(selected).c_str() : "Select a light")) { @@ -411,11 +419,20 @@ void LightEditor::ExportLightsToJson() {"lightCount", lights.size()} }; - // Add light data + // Add light data - only include lights with metadata (edited/meaningful lights) exportData["lights"] = json::array(); + int metadataLightCount = 0; for (const auto& light : lights) { - exportData["lights"].push_back(CreateLightJsonData(light)); + // Only export lights that have metadata (isRef or isAttached) + if (light.isRef || light.isAttached) { + exportData["lights"].push_back(CreateLightJsonData(light)); + metadataLightCount++; + } } + + // Update the light count to reflect only exported lights + exportData["exportSettings"]["lightCount"] = metadataLightCount; + exportData["exportSettings"]["totalSceneLights"] = lights.size(); // Generate filename with timestamp const auto exportPath = Util::PathHelpers::GetCommunityShaderPath() / "LightExports"; @@ -440,7 +457,7 @@ void LightEditor::ExportLightsToJson() outFile << exportData.dump(4); outFile.close(); - logger::info("Successfully exported {} lights to: {}", lights.size(), filePath.string()); + logger::info("Successfully exported {} lights with metadata to: {}", metadataLightCount, filePath.string()); } json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) @@ -516,4 +533,97 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) } return lightData; +} + +void LightEditor::ExportSelectedLightToJson() +{ + if (!selected.isSelected) { + logger::warn("No light is currently selected for export"); + return; + } + + json exportData; + + // Add timestamp + const auto now = std::chrono::system_clock::now(); + const auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S"); + exportData["timestamp"] = ss.str(); + + // Add current scene context + const auto* tes = RE::TES::GetSingleton(); + const auto* currentCell = tes ? tes->interiorCell : nullptr; + if (!currentCell && tes) { + const auto* player = RE::PlayerCharacter::GetSingleton(); + if (player) { + currentCell = player->GetParentCell(); + } + } + + // Cell information + if (currentCell) { + exportData["cell"] = { + {"formID", fmt::format("0x{:08X}", currentCell->GetFormID())}, + {"editorID", currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown"}, + {"isInterior", currentCell->IsInteriorCell()} + }; + } else { + exportData["cell"] = { + {"formID", "0x00000000"}, + {"editorID", "Unknown"}, + {"isInterior", false} + }; + } + + // Player position for reference + const auto* player = RE::PlayerCharacter::GetSingleton(); + if (player) { + const auto playerPos = player->GetPosition(); + exportData["playerPosition"] = { + {"x", playerPos.x}, + {"y", playerPos.y}, + {"z", playerPos.z} + }; + } + + // Export settings for selected light + exportData["exportSettings"] = { + {"exportType", "selected_light"}, + {"lightCount", 1}, + {"selectedLightInfo", { + {"refID", fmt::format("0x{:08X}", selected.id)}, + {"name", selected.name}, + {"type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other")} + }} + }; + + // Add the selected light data + exportData["lights"] = json::array(); + exportData["lights"].push_back(CreateLightJsonData(selected)); + + // Generate filename with timestamp + const auto exportPath = Util::PathHelpers::GetCommunityShaderPath() / "LightExports"; + try { + std::filesystem::create_directories(exportPath); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Failed to create export directory: {}", e.what()); + return; + } + + std::stringstream timeStream; + timeStream << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S"); + const auto filename = fmt::format("selected_light_export_{}.json", timeStream.str()); + const auto filePath = exportPath / filename; + + std::ofstream outFile(filePath); + if (!outFile.is_open()) { + logger::warn("Failed to create export file: {}", filePath.string()); + return; + } + + outFile << exportData.dump(4); + outFile.close(); + + logger::info("Successfully exported selected light to: {}", filePath.string()); } \ No newline at end of file diff --git a/src/Features/InverseSquareLighting/LightEditor.h b/src/Features/InverseSquareLighting/LightEditor.h index f1b164e4d..5d3e47360 100644 --- a/src/Features/InverseSquareLighting/LightEditor.h +++ b/src/Features/InverseSquareLighting/LightEditor.h @@ -102,5 +102,6 @@ struct LightEditor void UpdateSelectedLight(RE::TESObjectREFR* refr, RE::TESObjectLIGH* ligh, RE::NiLight* niLight); void ExportLightsToJson(); + void ExportSelectedLightToJson(); json CreateLightJsonData(const LightInfo& lightInfo); }; From e1ea508b420ebb4dde543ff77fb11eafb1463834 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 08:01:29 +0000 Subject: [PATCH 04/11] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../InverseSquareLighting/LightEditor.cpp | 132 +++++++++--------- 1 file changed, 64 insertions(+), 68 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index 25df6712b..9f954b88c 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -3,11 +3,11 @@ #include "Menu.h" #include "Util.h" #include -#include #include -#include -#include #include +#include +#include +#include void LightEditor::DrawSettings() { @@ -368,7 +368,7 @@ void LightEditor::ExportLightsToJson() } json exportData; - + // Add timestamp const auto now = std::chrono::system_clock::now(); const auto time_t = std::chrono::system_clock::to_time_t(now); @@ -385,19 +385,19 @@ void LightEditor::ExportLightsToJson() currentCell = player->GetParentCell(); } } - + // Cell information if (currentCell) { exportData["cell"] = { - {"formID", fmt::format("0x{:08X}", currentCell->GetFormID())}, - {"editorID", currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown"}, - {"isInterior", currentCell->IsInteriorCell()} + { "formID", fmt::format("0x{:08X}", currentCell->GetFormID()) }, + { "editorID", currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown" }, + { "isInterior", currentCell->IsInteriorCell() } }; } else { exportData["cell"] = { - {"formID", "0x00000000"}, - {"editorID", "Unknown"}, - {"isInterior", false} + { "formID", "0x00000000" }, + { "editorID", "Unknown" }, + { "isInterior", false } }; } @@ -406,17 +406,17 @@ void LightEditor::ExportLightsToJson() if (player) { const auto playerPos = player->GetPosition(); exportData["playerPosition"] = { - {"x", playerPos.x}, - {"y", playerPos.y}, - {"z", playerPos.z} + { "x", playerPos.x }, + { "y", playerPos.y }, + { "z", playerPos.z } }; } // Filter and sort settings used for this export exportData["exportSettings"] = { - {"filterOption", FilterOptionLabels[static_cast(filterOption)]}, - {"sortOption", SortOptionLabels[static_cast(sortOption)]}, - {"lightCount", lights.size()} + { "filterOption", FilterOptionLabels[static_cast(filterOption)] }, + { "sortOption", SortOptionLabels[static_cast(sortOption)] }, + { "lightCount", lights.size() } }; // Add light data - only include lights with metadata (edited/meaningful lights) @@ -429,7 +429,7 @@ void LightEditor::ExportLightsToJson() metadataLightCount++; } } - + // Update the light count to reflect only exported lights exportData["exportSettings"]["lightCount"] = metadataLightCount; exportData["exportSettings"]["totalSceneLights"] = lights.size(); @@ -473,32 +473,30 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // Position - using standard light placer format lightData["position"] = { - {"x", lightInfo.position.x}, - {"y", lightInfo.position.y}, - {"z", lightInfo.position.z} + { "x", lightInfo.position.x }, + { "y", lightInfo.position.y }, + { "z", lightInfo.position.z } }; // If this is the selected light, include detailed settings if (lightInfo == selected && lightInfo.isSelected) { lightData["settings"] = { - {"color", { - {"r", current.data.diffuse.red}, - {"g", current.data.diffuse.green}, - {"b", current.data.diffuse.blue} - }}, - {"intensity", current.data.fade}, - {"radius", current.data.radius}, - {"size", current.data.size}, - {"cutoffOverride", current.data.cutoffOverride}, - {"isInverseSquare", static_cast(*reinterpret_cast(¤t.data.flags) & static_cast(LightLimitFix::LightFlags::InverseSquare))} + { "color", { { "r", current.data.diffuse.red }, + { "g", current.data.diffuse.green }, + { "b", current.data.diffuse.blue } } }, + { "intensity", current.data.fade }, + { "radius", current.data.radius }, + { "size", current.data.size }, + { "cutoffOverride", current.data.cutoffOverride }, + { "isInverseSquare", static_cast(*reinterpret_cast(¤t.data.flags) & static_cast(LightLimitFix::LightFlags::InverseSquare)) } }; // Position offset if applicable if (!lightInfo.isOther) { lightData["settings"]["positionOffset"] = { - {"x", current.pos.x}, - {"y", current.pos.y}, - {"z", current.pos.z} + { "x", current.pos.x }, + { "y", current.pos.y }, + { "z", current.pos.z } }; } @@ -506,28 +504,28 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) if (!lightInfo.isOther && displayInfo.ownerFormId != 0) { auto flagsValue = *reinterpret_cast(¤t.tesFlags); lightData["settings"]["tesFlags"] = { - {"dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic))}, - {"negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative))}, - {"flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker))}, - {"flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow))}, - {"pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse))}, - {"pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow))}, - {"hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow))}, - {"omniShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow))}, - {"portalStrict", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict))} + { "dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic)) }, + { "negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative)) }, + { "flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker)) }, + { "flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)) }, + { "pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse)) }, + { "pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)) }, + { "hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)) }, + { "omniShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)) }, + { "portalStrict", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)) } }; } // Additional metadata for reference/attached lights if (lightInfo.isRef || lightInfo.isAttached) { lightData["metadata"] = { - {"ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId)}, - {"ownerEditorID", displayInfo.ownerEditorId}, - {"baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId)}, - {"ownerLastEditedBy", displayInfo.ownerLastEditedBy}, - {"cellEditorID", displayInfo.cellEditorId}, - {"lightFormID", fmt::format("0x{:08X}", displayInfo.lighFormId)}, - {"lightEditorID", displayInfo.lighEditorId} + { "ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId) }, + { "ownerEditorID", displayInfo.ownerEditorId }, + { "baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId) }, + { "ownerLastEditedBy", displayInfo.ownerLastEditedBy }, + { "cellEditorID", displayInfo.cellEditorId }, + { "lightFormID", fmt::format("0x{:08X}", displayInfo.lighFormId) }, + { "lightEditorID", displayInfo.lighEditorId } }; } } @@ -543,7 +541,7 @@ void LightEditor::ExportSelectedLightToJson() } json exportData; - + // Add timestamp const auto now = std::chrono::system_clock::now(); const auto time_t = std::chrono::system_clock::to_time_t(now); @@ -560,19 +558,19 @@ void LightEditor::ExportSelectedLightToJson() currentCell = player->GetParentCell(); } } - + // Cell information if (currentCell) { exportData["cell"] = { - {"formID", fmt::format("0x{:08X}", currentCell->GetFormID())}, - {"editorID", currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown"}, - {"isInterior", currentCell->IsInteriorCell()} + { "formID", fmt::format("0x{:08X}", currentCell->GetFormID()) }, + { "editorID", currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown" }, + { "isInterior", currentCell->IsInteriorCell() } }; } else { exportData["cell"] = { - {"formID", "0x00000000"}, - {"editorID", "Unknown"}, - {"isInterior", false} + { "formID", "0x00000000" }, + { "editorID", "Unknown" }, + { "isInterior", false } }; } @@ -581,21 +579,19 @@ void LightEditor::ExportSelectedLightToJson() if (player) { const auto playerPos = player->GetPosition(); exportData["playerPosition"] = { - {"x", playerPos.x}, - {"y", playerPos.y}, - {"z", playerPos.z} + { "x", playerPos.x }, + { "y", playerPos.y }, + { "z", playerPos.z } }; } // Export settings for selected light exportData["exportSettings"] = { - {"exportType", "selected_light"}, - {"lightCount", 1}, - {"selectedLightInfo", { - {"refID", fmt::format("0x{:08X}", selected.id)}, - {"name", selected.name}, - {"type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other")} - }} + { "exportType", "selected_light" }, + { "lightCount", 1 }, + { "selectedLightInfo", { { "refID", fmt::format("0x{:08X}", selected.id) }, + { "name", selected.name }, + { "type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other") } } } }; // Add the selected light data From 618ae1f19dbfab6663c41fa788b22a815cda78a1 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 4 Oct 2025 18:30:57 +1000 Subject: [PATCH 05/11] redid semantics, matches LP closer --- .../InverseSquareLighting/LightEditor.cpp | 237 ++++++++++++++++-- 1 file changed, 219 insertions(+), 18 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index 9f954b88c..a0918f94a 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -367,6 +367,7 @@ void LightEditor::ExportLightsToJson() return; } +<<<<<<< Updated upstream json exportData; // Add timestamp @@ -421,18 +422,74 @@ void LightEditor::ExportLightsToJson() // Add light data - only include lights with metadata (edited/meaningful lights) exportData["lights"] = json::array(); +======= + // Create Light Placer compatible format: array of light configurations + json exportArray = json::array(); + + // Group lights by model/reference to create proper Light Placer structure + std::map> lightsByModel; + +>>>>>>> Stashed changes int metadataLightCount = 0; for (const auto& light : lights) { // Only export lights that have metadata (isRef or isAttached) if (light.isRef || light.isAttached) { - exportData["lights"].push_back(CreateLightJsonData(light)); + // Use a model identifier - for actual game objects this would be the model path + // For now, group by owner/type for demo purposes + std::string modelKey = fmt::format("ISL_Export_Group_{}", + light.isRef ? "Reference" : "Attached"); + lightsByModel[modelKey].push_back(&light); metadataLightCount++; } } +<<<<<<< Updated upstream // Update the light count to reflect only exported lights exportData["exportSettings"]["lightCount"] = metadataLightCount; exportData["exportSettings"]["totalSceneLights"] = lights.size(); +======= + + // Create Light Placer entries for each model group + for (const auto& [modelKey, modelLights] : lightsByModel) { + json modelEntry; + + // Add ISL metadata as a comment (not part of Light Placer spec) + const auto now = std::chrono::system_clock::now(); + const auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S"); + + // Get current cell info for context + const auto* tes = RE::TES::GetSingleton(); + const auto* currentCell = tes ? tes->interiorCell : nullptr; + if (!currentCell && tes) { + const auto* player = RE::PlayerCharacter::GetSingleton(); + if (player) { + currentCell = player->GetParentCell(); + } + } + + // Models array - in real usage this would be actual .nif paths + modelEntry["models"] = json::array({modelKey + ".nif"}); + + // Add export metadata (custom extension) + modelEntry["_islExportInfo"] = { + {"timestamp", ss.str()}, + {"cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown"}, + {"filterOption", FilterOptionLabels[static_cast(filterOption)]}, + {"sortOption", SortOptionLabels[static_cast(sortOption)]} + }; + + // Lights array + modelEntry["lights"] = json::array(); + + for (const auto* light : modelLights) { + modelEntry["lights"].push_back(CreateLightJsonData(*light)); + } + + exportArray.push_back(modelEntry); + } +>>>>>>> Stashed changes // Generate filename with timestamp const auto exportPath = Util::PathHelpers::GetCommunityShaderPath() / "LightExports"; @@ -443,9 +500,11 @@ void LightEditor::ExportLightsToJson() return; } + const auto now = std::chrono::system_clock::now(); + const auto time_t = std::chrono::system_clock::to_time_t(now); std::stringstream timeStream; timeStream << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S"); - const auto filename = fmt::format("lights_export_{}.json", timeStream.str()); + const auto filename = fmt::format("ISL_Export_{}.json", timeStream.str()); const auto filePath = exportPath / filename; std::ofstream outFile(filePath); @@ -454,7 +513,7 @@ void LightEditor::ExportLightsToJson() return; } - outFile << exportData.dump(4); + outFile << exportArray.dump(2); // Use 2-space indent like the example outFile.close(); logger::info("Successfully exported {} lights with metadata to: {}", metadataLightCount, filePath.string()); @@ -462,7 +521,12 @@ void LightEditor::ExportLightsToJson() json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) { + // Create Light Placer compatible format + json lightEntry; + + // Light data section json lightData; +<<<<<<< Updated upstream // Basic light info - using refID as the main identifier for compatibility lightData["refID"] = fmt::format("0x{:08X}", lightInfo.id); @@ -476,10 +540,38 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) { "x", lightInfo.position.x }, { "y", lightInfo.position.y }, { "z", lightInfo.position.z } +======= + + // Basic light properties - using display info when available + if (lightInfo.isRef || lightInfo.isAttached) { + lightData["light"] = displayInfo.lighEditorId.empty() ? "DefaultPointLight01" : displayInfo.lighEditorId; + } else { + lightData["light"] = "DefaultPointLight01"; // Default for non-ref lights + } + + // Color in Light Placer format [r, g, b] as 0-1 normalized values + lightData["color"] = { + current.data.diffuse.red, + current.data.diffuse.green, + current.data.diffuse.blue +>>>>>>> Stashed changes }; - - // If this is the selected light, include detailed settings + + // Radius and fade + lightData["radius"] = current.data.radius; + lightData["fade"] = current.data.fade; + + // Add custom metadata for ISL tracking (non-standard but useful) + lightData["_islMetadata"] = { + {"refID", fmt::format("0x{:08X}", lightInfo.id)}, + {"editorID", lightInfo.name}, + {"type", lightInfo.isRef ? "Reference" : (lightInfo.isAttached ? "Attached" : "Other")}, + {"memoryAddress", fmt::format("{:p}", lightInfo.ptr)} + }; + + // Additional settings if this is the selected light if (lightInfo == selected && lightInfo.isSelected) { +<<<<<<< Updated upstream lightData["settings"] = { { "color", { { "r", current.data.diffuse.red }, { "g", current.data.diffuse.green }, @@ -497,12 +589,33 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) { "x", current.pos.x }, { "y", current.pos.y }, { "z", current.pos.z } +======= + // Add size and cutoff if different from default + if (current.data.size != 0.0f) { + lightData["size"] = current.data.size; + } + + // Position offset + if (current.pos.x != 0.0f || current.pos.y != 0.0f || current.pos.z != 0.0f) { + lightData["offset"] = { + current.pos.x, + current.pos.y, + current.pos.z +>>>>>>> Stashed changes }; } - - // TES flags if applicable + + // Flags in Light Placer format + std::vector flags; + if (static_cast(*reinterpret_cast(¤t.data.flags) & static_cast(LightLimitFix::LightFlags::InverseSquare))) { + // Note: InverseSquare is not a standard Light Placer flag + lightData["_islMetadata"]["isInverseSquare"] = true; + } + + // TES flags converted to Light Placer equivalents where possible if (!lightInfo.isOther && displayInfo.ownerFormId != 0) { auto flagsValue = *reinterpret_cast(¤t.tesFlags); +<<<<<<< Updated upstream lightData["settings"]["tesFlags"] = { { "dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic)) }, { "negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative)) }, @@ -513,11 +626,38 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) { "hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)) }, { "omniShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)) }, { "portalStrict", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)) } +======= + if (flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)) { + flags.push_back("PortalStrict"); + } + if (flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)) { + flags.push_back("Shadow"); + } + // Store other TES flags in metadata + lightData["_islMetadata"]["tesFlags"] = { + {"dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic))}, + {"negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative))}, + {"flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker))}, + {"flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow))}, + {"pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse))}, + {"pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow))}, + {"hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow))} +>>>>>>> Stashed changes }; } - - // Additional metadata for reference/attached lights + + if (!flags.empty()) { + std::string flagsStr; + for (size_t i = 0; i < flags.size(); ++i) { + if (i > 0) flagsStr += "|"; + flagsStr += flags[i]; + } + lightData["flags"] = flagsStr; + } + + // Additional reference metadata if (lightInfo.isRef || lightInfo.isAttached) { +<<<<<<< Updated upstream lightData["metadata"] = { { "ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId) }, { "ownerEditorID", displayInfo.ownerEditorId }, @@ -526,11 +666,29 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) { "cellEditorID", displayInfo.cellEditorId }, { "lightFormID", fmt::format("0x{:08X}", displayInfo.lighFormId) }, { "lightEditorID", displayInfo.lighEditorId } +======= + lightData["_islMetadata"]["ownerInfo"] = { + {"ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId)}, + {"ownerEditorID", displayInfo.ownerEditorId}, + {"baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId)}, + {"ownerLastEditedBy", displayInfo.ownerLastEditedBy}, + {"cellEditorID", displayInfo.cellEditorId} +>>>>>>> Stashed changes }; } } - - return lightData; + + // Create the light entry with points array (Light Placer format) + lightEntry["data"] = lightData; + lightEntry["points"] = json::array({ + json::array({ + lightInfo.position.x, + lightInfo.position.y, + lightInfo.position.z + }) + }); + + return lightEntry; } void LightEditor::ExportSelectedLightToJson() @@ -540,16 +698,22 @@ void LightEditor::ExportSelectedLightToJson() return; } +<<<<<<< Updated upstream json exportData; // Add timestamp +======= + // Create Light Placer compatible format: array with single entry + json exportArray = json::array(); + + // Add timestamp and context metadata +>>>>>>> Stashed changes const auto now = std::chrono::system_clock::now(); const auto time_t = std::chrono::system_clock::to_time_t(now); std::stringstream ss; ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S"); - exportData["timestamp"] = ss.str(); - // Add current scene context + // Get current cell info for context const auto* tes = RE::TES::GetSingleton(); const auto* currentCell = tes ? tes->interiorCell : nullptr; if (!currentCell && tes) { @@ -558,6 +722,7 @@ void LightEditor::ExportSelectedLightToJson() currentCell = player->GetParentCell(); } } +<<<<<<< Updated upstream // Cell information if (currentCell) { @@ -592,11 +757,47 @@ void LightEditor::ExportSelectedLightToJson() { "selectedLightInfo", { { "refID", fmt::format("0x{:08X}", selected.id) }, { "name", selected.name }, { "type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other") } } } +======= + + // Create single model entry for selected light + json modelEntry; + + // Use a descriptive model name for the selected light + std::string modelKey = fmt::format("ISL_Selected_Light_Export_{}_{}", + selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other"), + selected.id); + + modelEntry["models"] = json::array({modelKey + ".nif"}); + + // Add export metadata (custom extension) + modelEntry["_islExportInfo"] = { + {"timestamp", ss.str()}, + {"exportType", "selected_light"}, + {"cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown"}, + {"selectedLightInfo", { + {"refID", fmt::format("0x{:08X}", selected.id)}, + {"name", selected.name}, + {"type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other")} + }} +>>>>>>> Stashed changes }; + + // Add player position for reference + const auto* player = RE::PlayerCharacter::GetSingleton(); + if (player) { + const auto playerPos = player->GetPosition(); + modelEntry["_islExportInfo"]["playerPosition"] = { + {"x", playerPos.x}, + {"y", playerPos.y}, + {"z", playerPos.z} + }; + } - // Add the selected light data - exportData["lights"] = json::array(); - exportData["lights"].push_back(CreateLightJsonData(selected)); + // Lights array with single light + modelEntry["lights"] = json::array(); + modelEntry["lights"].push_back(CreateLightJsonData(selected)); + + exportArray.push_back(modelEntry); // Generate filename with timestamp const auto exportPath = Util::PathHelpers::GetCommunityShaderPath() / "LightExports"; @@ -609,7 +810,7 @@ void LightEditor::ExportSelectedLightToJson() std::stringstream timeStream; timeStream << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S"); - const auto filename = fmt::format("selected_light_export_{}.json", timeStream.str()); + const auto filename = fmt::format("ISL_Selected_{}.json", timeStream.str()); const auto filePath = exportPath / filename; std::ofstream outFile(filePath); @@ -618,7 +819,7 @@ void LightEditor::ExportSelectedLightToJson() return; } - outFile << exportData.dump(4); + outFile << exportArray.dump(2); // Use 2-space indent like the example outFile.close(); logger::info("Successfully exported selected light to: {}", filePath.string()); From dd7554803bce9c199c33e21ba63f0be97b80e9ce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 08:31:21 +0000 Subject: [PATCH 06/11] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../InverseSquareLighting/LightEditor.cpp | 147 +++++++++--------- 1 file changed, 71 insertions(+), 76 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index a0918f94a..7e13cf1a9 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -425,10 +425,10 @@ void LightEditor::ExportLightsToJson() ======= // Create Light Placer compatible format: array of light configurations json exportArray = json::array(); - + // Group lights by model/reference to create proper Light Placer structure std::map> lightsByModel; - + >>>>>>> Stashed changes int metadataLightCount = 0; for (const auto& light : lights) { @@ -436,7 +436,7 @@ void LightEditor::ExportLightsToJson() if (light.isRef || light.isAttached) { // Use a model identifier - for actual game objects this would be the model path // For now, group by owner/type for demo purposes - std::string modelKey = fmt::format("ISL_Export_Group_{}", + std::string modelKey = fmt::format("ISL_Export_Group_{}", light.isRef ? "Reference" : "Attached"); lightsByModel[modelKey].push_back(&light); metadataLightCount++; @@ -448,17 +448,17 @@ void LightEditor::ExportLightsToJson() exportData["exportSettings"]["lightCount"] = metadataLightCount; exportData["exportSettings"]["totalSceneLights"] = lights.size(); ======= - + // Create Light Placer entries for each model group for (const auto& [modelKey, modelLights] : lightsByModel) { json modelEntry; - + // Add ISL metadata as a comment (not part of Light Placer spec) const auto now = std::chrono::system_clock::now(); const auto time_t = std::chrono::system_clock::to_time_t(now); std::stringstream ss; ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S"); - + // Get current cell info for context const auto* tes = RE::TES::GetSingleton(); const auto* currentCell = tes ? tes->interiorCell : nullptr; @@ -468,25 +468,25 @@ void LightEditor::ExportLightsToJson() currentCell = player->GetParentCell(); } } - + // Models array - in real usage this would be actual .nif paths - modelEntry["models"] = json::array({modelKey + ".nif"}); - + modelEntry["models"] = json::array({ modelKey + ".nif" }); + // Add export metadata (custom extension) modelEntry["_islExportInfo"] = { - {"timestamp", ss.str()}, - {"cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown"}, - {"filterOption", FilterOptionLabels[static_cast(filterOption)]}, - {"sortOption", SortOptionLabels[static_cast(sortOption)]} + { "timestamp", ss.str() }, + { "cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown" }, + { "filterOption", FilterOptionLabels[static_cast(filterOption)] }, + { "sortOption", SortOptionLabels[static_cast(sortOption)] } }; - + // Lights array modelEntry["lights"] = json::array(); - + for (const auto* light : modelLights) { modelEntry["lights"].push_back(CreateLightJsonData(*light)); } - + exportArray.push_back(modelEntry); } >>>>>>> Stashed changes @@ -513,7 +513,7 @@ void LightEditor::ExportLightsToJson() return; } - outFile << exportArray.dump(2); // Use 2-space indent like the example + outFile << exportArray.dump(2); // Use 2-space indent like the example outFile.close(); logger::info("Successfully exported {} lights with metadata to: {}", metadataLightCount, filePath.string()); @@ -523,7 +523,7 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) { // Create Light Placer compatible format json lightEntry; - + // Light data section json lightData; <<<<<<< Updated upstream @@ -541,14 +541,14 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) { "y", lightInfo.position.y }, { "z", lightInfo.position.z } ======= - + // Basic light properties - using display info when available if (lightInfo.isRef || lightInfo.isAttached) { lightData["light"] = displayInfo.lighEditorId.empty() ? "DefaultPointLight01" : displayInfo.lighEditorId; } else { - lightData["light"] = "DefaultPointLight01"; // Default for non-ref lights + lightData["light"] = "DefaultPointLight01"; // Default for non-ref lights } - + // Color in Light Placer format [r, g, b] as 0-1 normalized values lightData["color"] = { current.data.diffuse.red, @@ -556,19 +556,19 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) current.data.diffuse.blue >>>>>>> Stashed changes }; - + // Radius and fade lightData["radius"] = current.data.radius; lightData["fade"] = current.data.fade; - + // Add custom metadata for ISL tracking (non-standard but useful) lightData["_islMetadata"] = { - {"refID", fmt::format("0x{:08X}", lightInfo.id)}, - {"editorID", lightInfo.name}, - {"type", lightInfo.isRef ? "Reference" : (lightInfo.isAttached ? "Attached" : "Other")}, - {"memoryAddress", fmt::format("{:p}", lightInfo.ptr)} + { "refID", fmt::format("0x{:08X}", lightInfo.id) }, + { "editorID", lightInfo.name }, + { "type", lightInfo.isRef ? "Reference" : (lightInfo.isAttached ? "Attached" : "Other") }, + { "memoryAddress", fmt::format("{:p}", lightInfo.ptr) } }; - + // Additional settings if this is the selected light if (lightInfo == selected && lightInfo.isSelected) { <<<<<<< Updated upstream @@ -594,7 +594,7 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) if (current.data.size != 0.0f) { lightData["size"] = current.data.size; } - + // Position offset if (current.pos.x != 0.0f || current.pos.y != 0.0f || current.pos.z != 0.0f) { lightData["offset"] = { @@ -604,14 +604,14 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) >>>>>>> Stashed changes }; } - + // Flags in Light Placer format std::vector flags; if (static_cast(*reinterpret_cast(¤t.data.flags) & static_cast(LightLimitFix::LightFlags::InverseSquare))) { // Note: InverseSquare is not a standard Light Placer flag lightData["_islMetadata"]["isInverseSquare"] = true; } - + // TES flags converted to Light Placer equivalents where possible if (!lightInfo.isOther && displayInfo.ownerFormId != 0) { auto flagsValue = *reinterpret_cast(¤t.tesFlags); @@ -635,26 +635,27 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) } // Store other TES flags in metadata lightData["_islMetadata"]["tesFlags"] = { - {"dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic))}, - {"negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative))}, - {"flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker))}, - {"flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow))}, - {"pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse))}, - {"pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow))}, - {"hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow))} + { "dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic)) }, + { "negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative)) }, + { "flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker)) }, + { "flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)) }, + { "pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse)) }, + { "pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)) }, + { "hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)) } >>>>>>> Stashed changes }; } - + if (!flags.empty()) { std::string flagsStr; for (size_t i = 0; i < flags.size(); ++i) { - if (i > 0) flagsStr += "|"; + if (i > 0) + flagsStr += "|"; flagsStr += flags[i]; } lightData["flags"] = flagsStr; } - + // Additional reference metadata if (lightInfo.isRef || lightInfo.isAttached) { <<<<<<< Updated upstream @@ -668,26 +669,22 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) { "lightEditorID", displayInfo.lighEditorId } ======= lightData["_islMetadata"]["ownerInfo"] = { - {"ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId)}, - {"ownerEditorID", displayInfo.ownerEditorId}, - {"baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId)}, - {"ownerLastEditedBy", displayInfo.ownerLastEditedBy}, - {"cellEditorID", displayInfo.cellEditorId} + { "ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId) }, + { "ownerEditorID", displayInfo.ownerEditorId }, + { "baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId) }, + { "ownerLastEditedBy", displayInfo.ownerLastEditedBy }, + { "cellEditorID", displayInfo.cellEditorId } >>>>>>> Stashed changes }; } } - + // Create the light entry with points array (Light Placer format) lightEntry["data"] = lightData; - lightEntry["points"] = json::array({ - json::array({ - lightInfo.position.x, - lightInfo.position.y, - lightInfo.position.z - }) - }); - + lightEntry["points"] = json::array({ json::array({ lightInfo.position.x, + lightInfo.position.y, + lightInfo.position.z }) }); + return lightEntry; } @@ -705,7 +702,7 @@ void LightEditor::ExportSelectedLightToJson() ======= // Create Light Placer compatible format: array with single entry json exportArray = json::array(); - + // Add timestamp and context metadata >>>>>>> Stashed changes const auto now = std::chrono::system_clock::now(); @@ -758,45 +755,43 @@ void LightEditor::ExportSelectedLightToJson() { "name", selected.name }, { "type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other") } } } ======= - + // Create single model entry for selected light json modelEntry; - + // Use a descriptive model name for the selected light - std::string modelKey = fmt::format("ISL_Selected_Light_Export_{}_{}", + std::string modelKey = fmt::format("ISL_Selected_Light_Export_{}_{}", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other"), selected.id); - - modelEntry["models"] = json::array({modelKey + ".nif"}); - + + modelEntry["models"] = json::array({ modelKey + ".nif" }); + // Add export metadata (custom extension) modelEntry["_islExportInfo"] = { - {"timestamp", ss.str()}, - {"exportType", "selected_light"}, - {"cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown"}, - {"selectedLightInfo", { - {"refID", fmt::format("0x{:08X}", selected.id)}, - {"name", selected.name}, - {"type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other")} - }} + { "timestamp", ss.str() }, + { "exportType", "selected_light" }, + { "cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown" }, + { "selectedLightInfo", { { "refID", fmt::format("0x{:08X}", selected.id) }, + { "name", selected.name }, + { "type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other") } } } >>>>>>> Stashed changes }; - + // Add player position for reference const auto* player = RE::PlayerCharacter::GetSingleton(); if (player) { const auto playerPos = player->GetPosition(); modelEntry["_islExportInfo"]["playerPosition"] = { - {"x", playerPos.x}, - {"y", playerPos.y}, - {"z", playerPos.z} + { "x", playerPos.x }, + { "y", playerPos.y }, + { "z", playerPos.z } }; } // Lights array with single light modelEntry["lights"] = json::array(); modelEntry["lights"].push_back(CreateLightJsonData(selected)); - + exportArray.push_back(modelEntry); // Generate filename with timestamp @@ -819,7 +814,7 @@ void LightEditor::ExportSelectedLightToJson() return; } - outFile << exportArray.dump(2); // Use 2-space indent like the example + outFile << exportArray.dump(2); // Use 2-space indent like the example outFile.close(); logger::info("Successfully exported selected light to: {}", filePath.string()); From 7966138dae9423ad931b05914f1a816c805b06cb Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 4 Oct 2025 18:34:04 +1000 Subject: [PATCH 07/11] redid semantics, matches LP closer --- .../InverseSquareLighting/LightEditor.cpp | 167 ------------------ 1 file changed, 167 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index a0918f94a..55dc81eef 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -367,69 +367,12 @@ void LightEditor::ExportLightsToJson() return; } -<<<<<<< Updated upstream - json exportData; - - // Add timestamp - const auto now = std::chrono::system_clock::now(); - const auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S"); - exportData["timestamp"] = ss.str(); - - // Add current scene context - const auto* tes = RE::TES::GetSingleton(); - const auto* currentCell = tes ? tes->interiorCell : nullptr; - if (!currentCell && tes) { - const auto* player = RE::PlayerCharacter::GetSingleton(); - if (player) { - currentCell = player->GetParentCell(); - } - } - - // Cell information - if (currentCell) { - exportData["cell"] = { - { "formID", fmt::format("0x{:08X}", currentCell->GetFormID()) }, - { "editorID", currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown" }, - { "isInterior", currentCell->IsInteriorCell() } - }; - } else { - exportData["cell"] = { - { "formID", "0x00000000" }, - { "editorID", "Unknown" }, - { "isInterior", false } - }; - } - - // Player position for reference - const auto* player = RE::PlayerCharacter::GetSingleton(); - if (player) { - const auto playerPos = player->GetPosition(); - exportData["playerPosition"] = { - { "x", playerPos.x }, - { "y", playerPos.y }, - { "z", playerPos.z } - }; - } - - // Filter and sort settings used for this export - exportData["exportSettings"] = { - { "filterOption", FilterOptionLabels[static_cast(filterOption)] }, - { "sortOption", SortOptionLabels[static_cast(sortOption)] }, - { "lightCount", lights.size() } - }; - - // Add light data - only include lights with metadata (edited/meaningful lights) - exportData["lights"] = json::array(); -======= // Create Light Placer compatible format: array of light configurations json exportArray = json::array(); // Group lights by model/reference to create proper Light Placer structure std::map> lightsByModel; ->>>>>>> Stashed changes int metadataLightCount = 0; for (const auto& light : lights) { // Only export lights that have metadata (isRef or isAttached) @@ -442,12 +385,6 @@ void LightEditor::ExportLightsToJson() metadataLightCount++; } } -<<<<<<< Updated upstream - - // Update the light count to reflect only exported lights - exportData["exportSettings"]["lightCount"] = metadataLightCount; - exportData["exportSettings"]["totalSceneLights"] = lights.size(); -======= // Create Light Placer entries for each model group for (const auto& [modelKey, modelLights] : lightsByModel) { @@ -489,7 +426,6 @@ void LightEditor::ExportLightsToJson() exportArray.push_back(modelEntry); } ->>>>>>> Stashed changes // Generate filename with timestamp const auto exportPath = Util::PathHelpers::GetCommunityShaderPath() / "LightExports"; @@ -526,21 +462,6 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // Light data section json lightData; -<<<<<<< Updated upstream - - // Basic light info - using refID as the main identifier for compatibility - lightData["refID"] = fmt::format("0x{:08X}", lightInfo.id); - lightData["editorID"] = lightInfo.name; - lightData["index"] = lightInfo.index; - lightData["type"] = lightInfo.isRef ? "Reference" : (lightInfo.isAttached ? "Attached" : "Other"); - lightData["memoryAddress"] = fmt::format("{:p}", lightInfo.ptr); - - // Position - using standard light placer format - lightData["position"] = { - { "x", lightInfo.position.x }, - { "y", lightInfo.position.y }, - { "z", lightInfo.position.z } -======= // Basic light properties - using display info when available if (lightInfo.isRef || lightInfo.isAttached) { @@ -554,7 +475,6 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) current.data.diffuse.red, current.data.diffuse.green, current.data.diffuse.blue ->>>>>>> Stashed changes }; // Radius and fade @@ -571,25 +491,6 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // Additional settings if this is the selected light if (lightInfo == selected && lightInfo.isSelected) { -<<<<<<< Updated upstream - lightData["settings"] = { - { "color", { { "r", current.data.diffuse.red }, - { "g", current.data.diffuse.green }, - { "b", current.data.diffuse.blue } } }, - { "intensity", current.data.fade }, - { "radius", current.data.radius }, - { "size", current.data.size }, - { "cutoffOverride", current.data.cutoffOverride }, - { "isInverseSquare", static_cast(*reinterpret_cast(¤t.data.flags) & static_cast(LightLimitFix::LightFlags::InverseSquare)) } - }; - - // Position offset if applicable - if (!lightInfo.isOther) { - lightData["settings"]["positionOffset"] = { - { "x", current.pos.x }, - { "y", current.pos.y }, - { "z", current.pos.z } -======= // Add size and cutoff if different from default if (current.data.size != 0.0f) { lightData["size"] = current.data.size; @@ -601,7 +502,6 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) current.pos.x, current.pos.y, current.pos.z ->>>>>>> Stashed changes }; } @@ -615,18 +515,6 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // TES flags converted to Light Placer equivalents where possible if (!lightInfo.isOther && displayInfo.ownerFormId != 0) { auto flagsValue = *reinterpret_cast(¤t.tesFlags); -<<<<<<< Updated upstream - lightData["settings"]["tesFlags"] = { - { "dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic)) }, - { "negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative)) }, - { "flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker)) }, - { "flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)) }, - { "pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse)) }, - { "pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)) }, - { "hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)) }, - { "omniShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)) }, - { "portalStrict", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)) } -======= if (flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)) { flags.push_back("PortalStrict"); } @@ -642,7 +530,6 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) {"pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse))}, {"pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow))}, {"hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow))} ->>>>>>> Stashed changes }; } @@ -657,23 +544,12 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // Additional reference metadata if (lightInfo.isRef || lightInfo.isAttached) { -<<<<<<< Updated upstream - lightData["metadata"] = { - { "ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId) }, - { "ownerEditorID", displayInfo.ownerEditorId }, - { "baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId) }, - { "ownerLastEditedBy", displayInfo.ownerLastEditedBy }, - { "cellEditorID", displayInfo.cellEditorId }, - { "lightFormID", fmt::format("0x{:08X}", displayInfo.lighFormId) }, - { "lightEditorID", displayInfo.lighEditorId } -======= lightData["_islMetadata"]["ownerInfo"] = { {"ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId)}, {"ownerEditorID", displayInfo.ownerEditorId}, {"baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId)}, {"ownerLastEditedBy", displayInfo.ownerLastEditedBy}, {"cellEditorID", displayInfo.cellEditorId} ->>>>>>> Stashed changes }; } } @@ -698,16 +574,10 @@ void LightEditor::ExportSelectedLightToJson() return; } -<<<<<<< Updated upstream - json exportData; - - // Add timestamp -======= // Create Light Placer compatible format: array with single entry json exportArray = json::array(); // Add timestamp and context metadata ->>>>>>> Stashed changes const auto now = std::chrono::system_clock::now(); const auto time_t = std::chrono::system_clock::to_time_t(now); std::stringstream ss; @@ -722,42 +592,6 @@ void LightEditor::ExportSelectedLightToJson() currentCell = player->GetParentCell(); } } -<<<<<<< Updated upstream - - // Cell information - if (currentCell) { - exportData["cell"] = { - { "formID", fmt::format("0x{:08X}", currentCell->GetFormID()) }, - { "editorID", currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown" }, - { "isInterior", currentCell->IsInteriorCell() } - }; - } else { - exportData["cell"] = { - { "formID", "0x00000000" }, - { "editorID", "Unknown" }, - { "isInterior", false } - }; - } - - // Player position for reference - const auto* player = RE::PlayerCharacter::GetSingleton(); - if (player) { - const auto playerPos = player->GetPosition(); - exportData["playerPosition"] = { - { "x", playerPos.x }, - { "y", playerPos.y }, - { "z", playerPos.z } - }; - } - - // Export settings for selected light - exportData["exportSettings"] = { - { "exportType", "selected_light" }, - { "lightCount", 1 }, - { "selectedLightInfo", { { "refID", fmt::format("0x{:08X}", selected.id) }, - { "name", selected.name }, - { "type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other") } } } -======= // Create single model entry for selected light json modelEntry; @@ -779,7 +613,6 @@ void LightEditor::ExportSelectedLightToJson() {"name", selected.name}, {"type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other")} }} ->>>>>>> Stashed changes }; // Add player position for reference From 48b7eb2c1173609ec8cf620c32824bbb797b6681 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 08:38:50 +0000 Subject: [PATCH 08/11] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../InverseSquareLighting/LightEditor.cpp | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index b7a2c606f..7a0d32ed0 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -372,7 +372,7 @@ void LightEditor::ExportLightsToJson() // Group lights by model/reference to create proper Light Placer structure std::map> lightsByModel; - + int metadataLightCount = 0; for (const auto& light : lights) { // Only export lights that have metadata (isRef or isAttached) @@ -385,7 +385,7 @@ void LightEditor::ExportLightsToJson() metadataLightCount++; } } - + // Create Light Placer entries for each model group for (const auto& [modelKey, modelLights] : lightsByModel) { json modelEntry; @@ -462,7 +462,7 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // Light data section json lightData; - + // Basic light properties - using display info when available if (lightInfo.isRef || lightInfo.isAttached) { lightData["light"] = displayInfo.lighEditorId.empty() ? "DefaultPointLight01" : displayInfo.lighEditorId; @@ -523,13 +523,13 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) } // Store other TES flags in metadata lightData["_islMetadata"]["tesFlags"] = { - {"dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic))}, - {"negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative))}, - {"flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker))}, - {"flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow))}, - {"pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse))}, - {"pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow))}, - {"hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow))} + { "dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic)) }, + { "negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative)) }, + { "flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker)) }, + { "flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)) }, + { "pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse)) }, + { "pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)) }, + { "hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)) } }; } @@ -546,11 +546,11 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // Additional reference metadata if (lightInfo.isRef || lightInfo.isAttached) { lightData["_islMetadata"]["ownerInfo"] = { - {"ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId)}, - {"ownerEditorID", displayInfo.ownerEditorId}, - {"baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId)}, - {"ownerLastEditedBy", displayInfo.ownerLastEditedBy}, - {"cellEditorID", displayInfo.cellEditorId} + { "ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId) }, + { "ownerEditorID", displayInfo.ownerEditorId }, + { "baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId) }, + { "ownerLastEditedBy", displayInfo.ownerLastEditedBy }, + { "cellEditorID", displayInfo.cellEditorId } }; } } @@ -589,7 +589,7 @@ void LightEditor::ExportSelectedLightToJson() currentCell = player->GetParentCell(); } } - + // Create single model entry for selected light json modelEntry; @@ -602,14 +602,12 @@ void LightEditor::ExportSelectedLightToJson() // Add export metadata (custom extension) modelEntry["_islExportInfo"] = { - {"timestamp", ss.str()}, - {"exportType", "selected_light"}, - {"cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown"}, - {"selectedLightInfo", { - {"refID", fmt::format("0x{:08X}", selected.id)}, - {"name", selected.name}, - {"type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other")} - }} + { "timestamp", ss.str() }, + { "exportType", "selected_light" }, + { "cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown" }, + { "selectedLightInfo", { { "refID", fmt::format("0x{:08X}", selected.id) }, + { "name", selected.name }, + { "type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other") } } } }; // Add player position for reference From 6ab3fb55d273290335b00dd7b0b08665e590a36d Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 6 Oct 2025 17:00:22 +1000 Subject: [PATCH 09/11] rabbit fixes --- .../InverseSquareLighting/LightEditor.cpp | 158 +++++++++++------- .../InverseSquareLighting/LightEditor.h | 16 ++ 2 files changed, 110 insertions(+), 64 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index 7a0d32ed0..45df6b5cd 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -228,6 +228,9 @@ void LightEditor::GatherLights() current.isSelected = selected == current; + // Populate runtime data for each light (needed for correct JSON export) + PopulateLightRuntimeData(current, refr, ligh, niLight); + lights.push_back(current); if (!current.isSelected) @@ -330,6 +333,34 @@ void LightEditor::UpdateSelectedLight(RE::TESObjectREFR* refr, RE::TESObjectLIGH displayInfo.lighEditorId = ligh ? clib_util::editorID::get_editorID(ligh) : "Unknown"; } +void LightEditor::PopulateLightRuntimeData(LightInfo& lightInfo, RE::TESObjectREFR* refr, RE::TESObjectLIGH* ligh, RE::NiLight* niLight) +{ + const auto runtimeData = ISLCommon::RuntimeLightDataExt::Get(niLight); + auto tesFlags = ligh ? &ligh->data.flags : nullptr; + + // Capture runtime light data + lightInfo.runtimeData = *runtimeData; + lightInfo.tesFlags = tesFlags ? static_cast(tesFlags->underlying()) : static_cast(0); + + // Capture position offset (for ref lights this would be difference from original position) + if (lightInfo.isRef) { + lightInfo.positionOffset = { 0, 0, 0 }; // Ref lights use world position directly + } else if (lightInfo.isAttached) { + lightInfo.positionOffset = niLight->parent->local.translate; + } else { + lightInfo.positionOffset = { 0, 0, 0 }; + } + + // Capture display metadata + lightInfo.ownerFormId = refr ? refr->GetFormID() : 0; + lightInfo.ownerEditorId = refr ? clib_util::editorID::get_editorID(refr) : "Unknown"; + lightInfo.baseObjectFormId = refr && refr->GetBaseObject() ? refr->GetBaseObject()->formID : 0; + lightInfo.ownerLastEditedBy = refr && refr->GetDescriptionOwnerFile() ? refr->GetDescriptionOwnerFile()->fileName : "Unknown"; + lightInfo.cellEditorId = refr && refr->GetParentCell() ? refr->GetParentCell()->GetFormEditorID() : "Unknown"; + lightInfo.lighFormId = ligh ? ligh->GetFormID() : 0; + lightInfo.lighEditorId = ligh ? clib_util::editorID::get_editorID(ligh) : "Unknown"; +} + void LightEditor::SortLights() { if (filterOption == FilterOption::OtherLights && (sortOption == SortOption::FormID || sortOption == SortOption::EditorID)) @@ -463,23 +494,23 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // Light data section json lightData; - // Basic light properties - using display info when available + // Basic light properties - using per-light data if (lightInfo.isRef || lightInfo.isAttached) { - lightData["light"] = displayInfo.lighEditorId.empty() ? "DefaultPointLight01" : displayInfo.lighEditorId; + lightData["light"] = lightInfo.lighEditorId.empty() ? "DefaultPointLight01" : lightInfo.lighEditorId; } else { lightData["light"] = "DefaultPointLight01"; // Default for non-ref lights } // Color in Light Placer format [r, g, b] as 0-1 normalized values lightData["color"] = { - current.data.diffuse.red, - current.data.diffuse.green, - current.data.diffuse.blue + lightInfo.runtimeData.diffuse.red, + lightInfo.runtimeData.diffuse.green, + lightInfo.runtimeData.diffuse.blue }; // Radius and fade - lightData["radius"] = current.data.radius; - lightData["fade"] = current.data.fade; + lightData["radius"] = lightInfo.runtimeData.radius; + lightData["fade"] = lightInfo.runtimeData.fade; // Add custom metadata for ISL tracking (non-standard but useful) lightData["_islMetadata"] = { @@ -489,70 +520,69 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) { "memoryAddress", fmt::format("{:p}", lightInfo.ptr) } }; - // Additional settings if this is the selected light - if (lightInfo == selected && lightInfo.isSelected) { - // Add size and cutoff if different from default - if (current.data.size != 0.0f) { - lightData["size"] = current.data.size; - } + // Additional settings for all lights + // Add size and cutoff if different from default + if (lightInfo.runtimeData.size != 0.0f) { + lightData["size"] = lightInfo.runtimeData.size; + } - // Position offset - if (current.pos.x != 0.0f || current.pos.y != 0.0f || current.pos.z != 0.0f) { - lightData["offset"] = { - current.pos.x, - current.pos.y, - current.pos.z - }; - } + // Position offset + if (lightInfo.positionOffset.x != 0.0f || lightInfo.positionOffset.y != 0.0f || lightInfo.positionOffset.z != 0.0f) { + lightData["offset"] = { + lightInfo.positionOffset.x, + lightInfo.positionOffset.y, + lightInfo.positionOffset.z + }; + } - // Flags in Light Placer format - std::vector flags; - if (static_cast(*reinterpret_cast(¤t.data.flags) & static_cast(LightLimitFix::LightFlags::InverseSquare))) { - // Note: InverseSquare is not a standard Light Placer flag - lightData["_islMetadata"]["isInverseSquare"] = true; - } + // Flags in Light Placer format + std::vector flags; + if (static_cast(*reinterpret_cast(&lightInfo.runtimeData.flags) & static_cast(LightLimitFix::LightFlags::InverseSquare))) { + // Note: InverseSquare is not a standard Light Placer flag + lightData["_islMetadata"]["isInverseSquare"] = true; + } - // TES flags converted to Light Placer equivalents where possible - if (!lightInfo.isOther && displayInfo.ownerFormId != 0) { - auto flagsValue = *reinterpret_cast(¤t.tesFlags); - if (flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)) { - flags.push_back("PortalStrict"); - } - if (flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)) { - flags.push_back("Shadow"); - } - // Store other TES flags in metadata - lightData["_islMetadata"]["tesFlags"] = { - { "dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic)) }, - { "negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative)) }, - { "flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker)) }, - { "flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)) }, - { "pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse)) }, - { "pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)) }, - { "hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)) } - }; + // TES flags converted to Light Placer equivalents where possible + if (!lightInfo.isOther && lightInfo.ownerFormId != 0) { + auto flagsValue = *reinterpret_cast(&lightInfo.tesFlags); + if (flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)) { + flags.push_back("PortalStrict"); } - - if (!flags.empty()) { - std::string flagsStr; - for (size_t i = 0; i < flags.size(); ++i) { - if (i > 0) - flagsStr += "|"; - flagsStr += flags[i]; - } - lightData["flags"] = flagsStr; + if (flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)) { + flags.push_back("Shadow"); } + // Store other TES flags in metadata + lightData["_islMetadata"]["tesFlags"] = { + { "dynamic", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kDynamic)) }, + { "negative", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kNegative)) }, + { "flicker", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlicker)) }, + { "flickerSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)) }, + { "pulse", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulse)) }, + { "pulseSlow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)) }, + { "hemiShadow", static_cast(flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)) } + }; + } - // Additional reference metadata - if (lightInfo.isRef || lightInfo.isAttached) { - lightData["_islMetadata"]["ownerInfo"] = { - { "ownerFormID", fmt::format("0x{:08X}", displayInfo.ownerFormId) }, - { "ownerEditorID", displayInfo.ownerEditorId }, - { "baseObjectFormID", fmt::format("0x{:08X}", displayInfo.baseObjectFormId) }, - { "ownerLastEditedBy", displayInfo.ownerLastEditedBy }, - { "cellEditorID", displayInfo.cellEditorId } - }; + + if (!flags.empty()) { + std::string flagsStr; + for (size_t i = 0; i < flags.size(); ++i) { + if (i > 0) + flagsStr += "|"; + flagsStr += flags[i]; } + lightData["flags"] = flagsStr; + } + + // Additional reference metadata + if (lightInfo.isRef || lightInfo.isAttached) { + lightData["_islMetadata"]["ownerInfo"] = { + { "ownerFormID", fmt::format("0x{:08X}", lightInfo.ownerFormId) }, + { "ownerEditorID", lightInfo.ownerEditorId }, + { "baseObjectFormID", fmt::format("0x{:08X}", lightInfo.baseObjectFormId) }, + { "ownerLastEditedBy", lightInfo.ownerLastEditedBy }, + { "cellEditorID", lightInfo.cellEditorId } + }; } // Create the light entry with points array (Light Placer format) diff --git a/src/Features/InverseSquareLighting/LightEditor.h b/src/Features/InverseSquareLighting/LightEditor.h index 5d3e47360..17d6db962 100644 --- a/src/Features/InverseSquareLighting/LightEditor.h +++ b/src/Features/InverseSquareLighting/LightEditor.h @@ -23,6 +23,20 @@ struct LightEditor bool isOther; RE::NiPoint3 position; + // Runtime light data for per-light JSON export + ISLCommon::RuntimeLightDataExt runtimeData = {}; + stl::enumeration tesFlags = {}; + RE::NiPoint3 positionOffset = {}; + + // Display metadata for per-light JSON export + RE::FormID ownerFormId = 0; + std::string ownerEditorId; + RE::FormID baseObjectFormId = 0; + std::string ownerLastEditedBy; + std::string cellEditorId; + RE::FormID lighFormId = 0; + std::string lighEditorId; + bool operator==(const LightInfo& other) const noexcept { return id == other.id && index == other.index; @@ -101,6 +115,8 @@ struct LightEditor void UpdateSelectedLight(RE::TESObjectREFR* refr, RE::TESObjectLIGH* ligh, RE::NiLight* niLight); + void PopulateLightRuntimeData(LightInfo& lightInfo, RE::TESObjectREFR* refr, RE::TESObjectLIGH* ligh, RE::NiLight* niLight); + void ExportLightsToJson(); void ExportSelectedLightToJson(); json CreateLightJsonData(const LightInfo& lightInfo); From 8d29e818d523df44281c6b8eca1ac697a6732900 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 07:00:43 +0000 Subject: [PATCH 10/11] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/Features/InverseSquareLighting/LightEditor.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index 45df6b5cd..2e4b2410d 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -341,10 +341,10 @@ void LightEditor::PopulateLightRuntimeData(LightInfo& lightInfo, RE::TESObjectRE // Capture runtime light data lightInfo.runtimeData = *runtimeData; lightInfo.tesFlags = tesFlags ? static_cast(tesFlags->underlying()) : static_cast(0); - + // Capture position offset (for ref lights this would be difference from original position) if (lightInfo.isRef) { - lightInfo.positionOffset = { 0, 0, 0 }; // Ref lights use world position directly + lightInfo.positionOffset = { 0, 0, 0 }; // Ref lights use world position directly } else if (lightInfo.isAttached) { lightInfo.positionOffset = niLight->parent->local.translate; } else { @@ -563,7 +563,6 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) }; } - if (!flags.empty()) { std::string flagsStr; for (size_t i = 0; i < flags.size(); ++i) { From ccc89ef8fb7b9635e404097a5a2c65d8a84464a4 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 6 Oct 2025 18:25:08 +1000 Subject: [PATCH 11/11] Update LightEditor.cpp --- .../InverseSquareLighting/LightEditor.cpp | 78 ++++++++++++++----- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/Features/InverseSquareLighting/LightEditor.cpp index 2e4b2410d..bfc22e61d 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/Features/InverseSquareLighting/LightEditor.cpp @@ -102,7 +102,14 @@ void LightEditor::DrawSettings() ImGui::Spacing(); ImGui::Spacing(); - ImGui::CheckboxFlags("Inverse Square Light", reinterpret_cast(¤t.data.flags), static_cast(LightLimitFix::LightFlags::InverseSquare)); + // Use accessor to check/modify inverse square flag + bool isInvSqChecked = current.data.flags.any(LightLimitFix::LightFlags::InverseSquare); + if (ImGui::Checkbox("Inverse Square Light", &isInvSqChecked)) { + if (isInvSqChecked) + current.data.flags.set(LightLimitFix::LightFlags::InverseSquare); + else + current.data.flags.reset(LightLimitFix::LightFlags::InverseSquare); + } ImGui::Spacing(); ImGui::Spacing(); @@ -134,18 +141,21 @@ void LightEditor::DrawSettings() ImGui::Spacing(); ImGui::Spacing(); - auto* flags = reinterpret_cast(¤t.tesFlags); + // Use underlying() to get raw TES flag mask for ImGui + auto flagsValue = current.tesFlags.underlying(); ImGui::Spacing(); ImGui::Text("Light Flags"); - ImGui::CheckboxFlags("Dynamic", flags, static_cast(RE::TES_LIGHT_FLAGS::kDynamic)); - ImGui::CheckboxFlags("Negative", flags, static_cast(RE::TES_LIGHT_FLAGS::kNegative)); - ImGui::CheckboxFlags("Flicker", flags, static_cast(RE::TES_LIGHT_FLAGS::kFlicker)); - ImGui::CheckboxFlags("Flicker Slow", flags, static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)); - ImGui::CheckboxFlags("Pulse", flags, static_cast(RE::TES_LIGHT_FLAGS::kPulse)); - ImGui::CheckboxFlags("Pulse Slow", flags, static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)); - ImGui::CheckboxFlags("Hemi Shadow", flags, static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)); - ImGui::CheckboxFlags("Omni Shadow", flags, static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)); - ImGui::CheckboxFlags("Portal Strict", flags, static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)); + ImGui::CheckboxFlags("Dynamic", &flagsValue, static_cast(RE::TES_LIGHT_FLAGS::kDynamic)); + ImGui::CheckboxFlags("Negative", &flagsValue, static_cast(RE::TES_LIGHT_FLAGS::kNegative)); + ImGui::CheckboxFlags("Flicker", &flagsValue, static_cast(RE::TES_LIGHT_FLAGS::kFlicker)); + ImGui::CheckboxFlags("Flicker Slow", &flagsValue, static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)); + ImGui::CheckboxFlags("Pulse", &flagsValue, static_cast(RE::TES_LIGHT_FLAGS::kPulse)); + ImGui::CheckboxFlags("Pulse Slow", &flagsValue, static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)); + ImGui::CheckboxFlags("Hemi Shadow", &flagsValue, static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)); + ImGui::CheckboxFlags("Omni Shadow", &flagsValue, static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)); + ImGui::CheckboxFlags("Portal Strict", &flagsValue, static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)); + // Update the enumeration wrapper from the modified value + current.tesFlags = stl::enumeration(static_cast(flagsValue)); } } @@ -408,8 +418,9 @@ void LightEditor::ExportLightsToJson() for (const auto& light : lights) { // Only export lights that have metadata (isRef or isAttached) if (light.isRef || light.isAttached) { - // Use a model identifier - for actual game objects this would be the model path - // For now, group by owner/type for demo purposes + // Group by type for organizational purposes + // Note: Using placeholder keys, not actual base object model paths + // This is intentional as not all lights have associated .nif models std::string modelKey = fmt::format("ISL_Export_Group_{}", light.isRef ? "Reference" : "Attached"); lightsByModel[modelKey].push_back(&light); @@ -437,7 +448,8 @@ void LightEditor::ExportLightsToJson() } } - // Models array - in real usage this would be actual .nif paths + // Models array - using placeholder keys (NOT actual .nif paths) + // WARNING: This is ISL-specific export format, not directly compatible with Light Placer modelEntry["models"] = json::array({ modelKey + ".nif" }); // Add export metadata (custom extension) @@ -445,7 +457,9 @@ void LightEditor::ExportLightsToJson() { "timestamp", ss.str() }, { "cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown" }, { "filterOption", FilterOptionLabels[static_cast(filterOption)] }, - { "sortOption", SortOptionLabels[static_cast(sortOption)] } + { "sortOption", SortOptionLabels[static_cast(sortOption)] }, + { "warning", "This is an ISL-specific export format. Model keys are placeholders, not actual .nif paths. Not directly compatible with Light Placer imports." }, + { "description", "Exported lights are grouped by type (Reference/Attached) for organizational purposes. Use _islMetadata fields for detailed light information." } }; // Lights array @@ -480,8 +494,14 @@ void LightEditor::ExportLightsToJson() return; } - outFile << exportArray.dump(2); // Use 2-space indent like the example - outFile.close(); + try { + outFile << exportArray.dump(2); // Use 2-space indent like the example + outFile.close(); + } catch (const json::exception& e) { + logger::warn("Failed to serialize JSON for export: {}", e.what()); + outFile.close(); + return; + } logger::info("Successfully exported {} lights with metadata to: {}", metadataLightCount, filePath.string()); } @@ -512,6 +532,11 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) lightData["radius"] = lightInfo.runtimeData.radius; lightData["fade"] = lightInfo.runtimeData.fade; + // Add cutoffOverride (important for ISL lights) + if (lightInfo.runtimeData.cutoffOverride != 0.0f) { + lightData["cutoffOverride"] = lightInfo.runtimeData.cutoffOverride; + } + // Add custom metadata for ISL tracking (non-standard but useful) lightData["_islMetadata"] = { { "refID", fmt::format("0x{:08X}", lightInfo.id) }, @@ -537,14 +562,16 @@ json LightEditor::CreateLightJsonData(const LightInfo& lightInfo) // Flags in Light Placer format std::vector flags; - if (static_cast(*reinterpret_cast(&lightInfo.runtimeData.flags) & static_cast(LightLimitFix::LightFlags::InverseSquare))) { + // Use accessor to check inverse square flag + if (lightInfo.runtimeData.flags.any(LightLimitFix::LightFlags::InverseSquare)) { // Note: InverseSquare is not a standard Light Placer flag lightData["_islMetadata"]["isInverseSquare"] = true; } // TES flags converted to Light Placer equivalents where possible if (!lightInfo.isOther && lightInfo.ownerFormId != 0) { - auto flagsValue = *reinterpret_cast(&lightInfo.tesFlags); + // Use underlying() to get raw TES flag mask + auto flagsValue = lightInfo.tesFlags.underlying(); if (flagsValue & static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)) { flags.push_back("PortalStrict"); } @@ -623,6 +650,7 @@ void LightEditor::ExportSelectedLightToJson() json modelEntry; // Use a descriptive model name for the selected light + // WARNING: This is a placeholder key, not an actual .nif path std::string modelKey = fmt::format("ISL_Selected_Light_Export_{}_{}", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other"), selected.id); @@ -634,6 +662,8 @@ void LightEditor::ExportSelectedLightToJson() { "timestamp", ss.str() }, { "exportType", "selected_light" }, { "cellEditorID", currentCell && currentCell->GetFormEditorID() ? currentCell->GetFormEditorID() : "Unknown" }, + { "warning", "This is an ISL-specific single light export. The model key is a placeholder, not an actual .nif path. Not directly compatible with Light Placer imports." }, + { "description", "Use _islMetadata fields for detailed light information and original reference data." }, { "selectedLightInfo", { { "refID", fmt::format("0x{:08X}", selected.id) }, { "name", selected.name }, { "type", selected.isRef ? "Reference" : (selected.isAttached ? "Attached" : "Other") } } } @@ -676,8 +706,14 @@ void LightEditor::ExportSelectedLightToJson() return; } - outFile << exportArray.dump(2); // Use 2-space indent like the example - outFile.close(); + try { + outFile << exportArray.dump(2); // Use 2-space indent like the example + outFile.close(); + } catch (const json::exception& e) { + logger::warn("Failed to serialize JSON for selected light export: {}", e.what()); + outFile.close(); + return; + } logger::info("Successfully exported selected light to: {}", filePath.string()); } \ No newline at end of file