Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
344 changes: 344 additions & 0 deletions src/Features/InverseSquareLighting/LightEditor.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
#include "Features/InverseSquareLighting/LightEditor.h"
#include "Features/LightLimitFix.h"
#include "Menu.h"
#include "Util.h"
#include <chrono>
#include <filesystem>
#include <format>
#include <fstream>
#include <iomanip>
#include <sstream>

void LightEditor::DrawSettings()
{
Expand Down Expand Up @@ -35,6 +42,21 @@ void LightEditor::DrawSettings()
sortOption = static_cast<SortOption>(selectedSort);
}

if (ImGui::Button("Export All Lights to JSON")) {
ExportLightsToJson();
}
if (auto _tt = Util::HoverTooltipWrapper()) {
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")) {
for (auto& light : lights) {
const auto displayName = GetLightName(light);
Expand Down Expand Up @@ -206,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)
Expand Down Expand Up @@ -308,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<ISLCommon::TES_LIGHT_FLAGS_EXT>(tesFlags->underlying()) : static_cast<ISLCommon::TES_LIGHT_FLAGS_EXT>(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))
Expand Down Expand Up @@ -336,4 +389,295 @@ void LightEditor::SortLights()
default:
break;
}
}

void LightEditor::ExportLightsToJson()
{
if (lights.empty()) {
logger::warn("No lights available to export");
return;
}

// 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<std::string, std::vector<const LightInfo*>> lightsByModel;

int metadataLightCount = 0;
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
std::string modelKey = fmt::format("ISL_Export_Group_{}",
light.isRef ? "Reference" : "Attached");
lightsByModel[modelKey].push_back(&light);
metadataLightCount++;
}
}

// 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<int>(filterOption)] },
{ "sortOption", SortOptionLabels[static_cast<int>(sortOption)] }
};

// Lights array
modelEntry["lights"] = json::array();

for (const auto* light : modelLights) {
modelEntry["lights"].push_back(CreateLightJsonData(*light));
}

exportArray.push_back(modelEntry);
}

// 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 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("ISL_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 << exportArray.dump(2); // Use 2-space indent like the example
outFile.close();

logger::info("Successfully exported {} lights with metadata to: {}", metadataLightCount, filePath.string());
}

json LightEditor::CreateLightJsonData(const LightInfo& lightInfo)
{
// Create Light Placer compatible format
json lightEntry;

// Light data section
json lightData;

// Basic light properties - using per-light data
if (lightInfo.isRef || lightInfo.isAttached) {
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"] = {
lightInfo.runtimeData.diffuse.red,
lightInfo.runtimeData.diffuse.green,
lightInfo.runtimeData.diffuse.blue
};

// Radius and fade
lightData["radius"] = lightInfo.runtimeData.radius;
lightData["fade"] = lightInfo.runtimeData.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 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 (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<std::string> flags;
if (static_cast<bool>(*reinterpret_cast<const uint32_t*>(&lightInfo.runtimeData.flags) & static_cast<uint32_t>(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<const uint32_t*>(&lightInfo.tesFlags);
if (flagsValue & static_cast<uint32_t>(RE::TES_LIGHT_FLAGS::kPortalStrict)) {
flags.push_back("PortalStrict");
}
if (flagsValue & static_cast<uint32_t>(RE::TES_LIGHT_FLAGS::kOmniShadow)) {
flags.push_back("Shadow");
}
// Store other TES flags in metadata
lightData["_islMetadata"]["tesFlags"] = {
{ "dynamic", static_cast<bool>(flagsValue & static_cast<uint32_t>(RE::TES_LIGHT_FLAGS::kDynamic)) },
{ "negative", static_cast<bool>(flagsValue & static_cast<uint32_t>(RE::TES_LIGHT_FLAGS::kNegative)) },
{ "flicker", static_cast<bool>(flagsValue & static_cast<uint32_t>(RE::TES_LIGHT_FLAGS::kFlicker)) },
{ "flickerSlow", static_cast<bool>(flagsValue & static_cast<uint32_t>(RE::TES_LIGHT_FLAGS::kFlickerSlow)) },
{ "pulse", static_cast<bool>(flagsValue & static_cast<uint32_t>(RE::TES_LIGHT_FLAGS::kPulse)) },
{ "pulseSlow", static_cast<bool>(flagsValue & static_cast<uint32_t>(RE::TES_LIGHT_FLAGS::kPulseSlow)) },
{ "hemiShadow", static_cast<bool>(flagsValue & static_cast<uint32_t>(RE::TES_LIGHT_FLAGS::kHemiShadow)) }
};
}

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)
lightEntry["data"] = lightData;
lightEntry["points"] = json::array({ json::array({ lightInfo.position.x,
lightInfo.position.y,
lightInfo.position.z }) });

return lightEntry;
}

void LightEditor::ExportSelectedLightToJson()
{
if (!selected.isSelected) {
logger::warn("No light is currently selected for export");
return;
}

// Create Light Placer compatible format: array with single entry
json exportArray = json::array();

// Add timestamp and context metadata
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();
}
}

// 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") } } }
};

// 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 }
};
}

// 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";
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("ISL_Selected_{}.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 << exportArray.dump(2); // Use 2-space indent like the example
outFile.close();

logger::info("Successfully exported selected light to: {}", filePath.string());
}
Loading