#include "json_config_service.hpp" #include "../interfaces/i_logger.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace sdl3cpp::services::impl { // Default Vulkan device extensions static const std::vector kDeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME, }; GraphicsBackendType ParseBackendType(const std::string& value) { std::string lower = value; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (lower == "vulkan") { return GraphicsBackendType::Vulkan; } if (lower == "bgfx") { return GraphicsBackendType::Bgfx; } throw std::runtime_error("graphics_backend.type must be 'vulkan' or 'bgfx'"); } std::string BackendTypeToString(GraphicsBackendType type) { switch (type) { case GraphicsBackendType::Vulkan: return "vulkan"; case GraphicsBackendType::Bgfx: return "bgfx"; } return "vulkan"; } JsonConfigService::JsonConfigService(std::shared_ptr logger, const char* argv0) : logger_(std::move(logger)), configJson_(), config_(RuntimeConfig{}) { if (logger_) { logger_->Trace("JsonConfigService", "JsonConfigService", "argv0=" + std::string(argv0 ? argv0 : "")); } config_.scriptPath = FindScriptPath(argv0); configJson_ = BuildConfigJson(config_, {}); logger_->Info("JsonConfigService initialized with default configuration"); } JsonConfigService::JsonConfigService(std::shared_ptr logger, const std::filesystem::path& configPath, bool dumpConfig) : logger_(std::move(logger)), configJson_(), config_(LoadFromJson(logger_, configPath, dumpConfig, &configJson_)) { if (logger_) { logger_->Trace("JsonConfigService", "JsonConfigService", "configPath=" + configPath.string() + ", dumpConfig=" + std::string(dumpConfig ? "true" : "false")); } logger_->Info("JsonConfigService initialized from config file: " + configPath.string()); } JsonConfigService::JsonConfigService(std::shared_ptr logger, const RuntimeConfig& config) : logger_(std::move(logger)), configJson_(BuildConfigJson(config, {})), config_(config) { if (logger_) { logger_->Trace("JsonConfigService", "JsonConfigService", "config.width=" + std::to_string(config.width) + ", config.height=" + std::to_string(config.height) + ", config.scriptPath=" + config.scriptPath.string() + ", config.luaDebug=" + std::string(config.luaDebug ? "true" : "false") + ", config.windowTitle=" + config.windowTitle); } logger_->Info("JsonConfigService initialized with explicit configuration"); } std::vector JsonConfigService::GetDeviceExtensions() const { if (logger_) { logger_->Trace("JsonConfigService", "GetDeviceExtensions"); } return kDeviceExtensions; } std::filesystem::path JsonConfigService::FindScriptPath(const char* argv0) { if (logger_) { logger_->Trace("JsonConfigService", "FindScriptPath", "argv0=" + std::string(argv0 ? argv0 : "")); } std::filesystem::path executable; if (argv0 && *argv0 != '\0') { executable = std::filesystem::path(argv0); if (executable.is_relative()) { executable = std::filesystem::current_path() / executable; } } else { executable = std::filesystem::current_path(); } executable = std::filesystem::weakly_canonical(executable); std::filesystem::path scriptPath = executable.parent_path() / "scripts" / "cube_logic.lua"; if (!std::filesystem::exists(scriptPath)) { throw std::runtime_error("Could not find Lua script at " + scriptPath.string()); } return scriptPath; } RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, const std::filesystem::path& configPath, bool dumpConfig, std::string* configJson) { std::string args = "configPath=" + configPath.string() + ", dumpConfig=" + (dumpConfig ? "true" : "false"); logger->Trace("JsonConfigService", "LoadFromJson", args); std::ifstream configStream(configPath); if (!configStream) { throw std::runtime_error("Failed to open config file: " + configPath.string()); } rapidjson::IStreamWrapper inputWrapper(configStream); rapidjson::Document document; document.ParseStream(inputWrapper); if (document.HasParseError()) { throw std::runtime_error("Failed to parse JSON config at " + configPath.string()); } if (!document.IsObject()) { throw std::runtime_error("JSON config must contain an object at the root"); } if (dumpConfig || configJson) { rapidjson::StringBuffer buffer; rapidjson::PrettyWriter writer(buffer); writer.SetIndent(' ', 2); document.Accept(writer); if (dumpConfig) { std::cout << "Loaded runtime config (" << configPath << "):\n" << buffer.GetString() << '\n'; } if (configJson) { *configJson = buffer.GetString(); } } const char* scriptField = "lua_script"; if (!document.HasMember(scriptField) || !document[scriptField].IsString()) { throw std::runtime_error("JSON config requires a string member '" + std::string(scriptField) + "'"); } std::optional projectRoot; const char* projectRootField = "project_root"; if (document.HasMember(projectRootField) && document[projectRootField].IsString()) { std::filesystem::path candidate(document[projectRootField].GetString()); if (candidate.is_absolute()) { projectRoot = std::filesystem::weakly_canonical(candidate); } else { projectRoot = std::filesystem::weakly_canonical(configPath.parent_path() / candidate); } } RuntimeConfig config; const auto& scriptValue = document[scriptField]; std::filesystem::path scriptPath(scriptValue.GetString()); if (!scriptPath.is_absolute()) { if (projectRoot) { scriptPath = *projectRoot / scriptPath; } else { scriptPath = configPath.parent_path() / scriptPath; } } scriptPath = std::filesystem::weakly_canonical(scriptPath); if (!std::filesystem::exists(scriptPath)) { throw std::runtime_error("Lua script not found at " + scriptPath.string()); } config.scriptPath = scriptPath; auto parseDimension = [&](const char* name, uint32_t defaultValue) -> uint32_t { if (!document.HasMember(name)) { return defaultValue; } const auto& value = document[name]; if (value.IsUint()) { return value.GetUint(); } if (value.IsInt()) { int maybeValue = value.GetInt(); if (maybeValue >= 0) { return static_cast(maybeValue); } } throw std::runtime_error(std::string("JSON member '") + name + "' must be a non-negative integer"); }; config.width = parseDimension("window_width", config.width); config.height = parseDimension("window_height", config.height); if (document.HasMember("lua_debug")) { const auto& value = document["lua_debug"]; if (!value.IsBool()) { throw std::runtime_error("JSON member 'lua_debug' must be a boolean"); } config.luaDebug = value.GetBool(); } if (document.HasMember("window_title")) { const auto& value = document["window_title"]; if (!value.IsString()) { throw std::runtime_error("JSON member 'window_title' must be a string"); } config.windowTitle = value.GetString(); } if (document.HasMember("mouse_grab")) { const auto& mouseGrabValue = document["mouse_grab"]; if (!mouseGrabValue.IsObject()) { throw std::runtime_error("JSON member 'mouse_grab' must be an object"); } auto readBool = [&](const char* name, bool& target) { if (!mouseGrabValue.HasMember(name)) { return; } const auto& value = mouseGrabValue[name]; if (!value.IsBool()) { throw std::runtime_error("JSON member 'mouse_grab." + std::string(name) + "' must be a boolean"); } target = value.GetBool(); }; auto readString = [&](const char* name, std::string& target) { if (!mouseGrabValue.HasMember(name)) { return; } const auto& value = mouseGrabValue[name]; if (!value.IsString()) { throw std::runtime_error("JSON member 'mouse_grab." + std::string(name) + "' must be a string"); } target = value.GetString(); }; readBool("enabled", config.mouseGrab.enabled); readBool("grab_on_click", config.mouseGrab.grabOnClick); readBool("release_on_escape", config.mouseGrab.releaseOnEscape); readBool("start_grabbed", config.mouseGrab.startGrabbed); readBool("hide_cursor", config.mouseGrab.hideCursor); readBool("relative_mode", config.mouseGrab.relativeMode); readString("grab_mouse_button", config.mouseGrab.grabMouseButton); readString("release_key", config.mouseGrab.releaseKey); } if (document.HasMember("input_bindings")) { const auto& bindingsValue = document["input_bindings"]; if (!bindingsValue.IsObject()) { throw std::runtime_error("JSON member 'input_bindings' must be an object"); } struct BindingSpec { const char* name; std::string InputBindings::* member; }; const std::array bindingSpecs = {{ {"move_forward", &InputBindings::moveForwardKey}, {"move_back", &InputBindings::moveBackKey}, {"move_left", &InputBindings::moveLeftKey}, {"move_right", &InputBindings::moveRightKey}, {"fly_up", &InputBindings::flyUpKey}, {"fly_down", &InputBindings::flyDownKey}, {"jump", &InputBindings::jumpKey}, {"noclip_toggle", &InputBindings::noclipToggleKey}, {"music_toggle", &InputBindings::musicToggleKey}, {"music_toggle_gamepad", &InputBindings::musicToggleGamepadButton}, {"gamepad_move_x_axis", &InputBindings::gamepadMoveXAxis}, {"gamepad_move_y_axis", &InputBindings::gamepadMoveYAxis}, {"gamepad_look_x_axis", &InputBindings::gamepadLookXAxis}, {"gamepad_look_y_axis", &InputBindings::gamepadLookYAxis}, {"gamepad_dpad_up", &InputBindings::gamepadDpadUpButton}, {"gamepad_dpad_down", &InputBindings::gamepadDpadDownButton}, {"gamepad_dpad_left", &InputBindings::gamepadDpadLeftButton}, {"gamepad_dpad_right", &InputBindings::gamepadDpadRightButton}, }}; auto readBinding = [&](const BindingSpec& spec) { if (!bindingsValue.HasMember(spec.name)) { return; } const auto& value = bindingsValue[spec.name]; if (!value.IsString()) { throw std::runtime_error("JSON member 'input_bindings." + std::string(spec.name) + "' must be a string"); } config.inputBindings.*(spec.member) = value.GetString(); }; for (const auto& spec : bindingSpecs) { readBinding(spec); } auto readMapping = [&](const char* name, std::unordered_map& target) { if (!bindingsValue.HasMember(name)) { return; } const auto& mappingValue = bindingsValue[name]; if (!mappingValue.IsObject()) { throw std::runtime_error("JSON member 'input_bindings." + std::string(name) + "' must be an object"); } for (auto it = mappingValue.MemberBegin(); it != mappingValue.MemberEnd(); ++it) { if (!it->name.IsString() || !it->value.IsString()) { throw std::runtime_error("JSON member 'input_bindings." + std::string(name) + "' must contain string pairs"); } std::string key = it->name.GetString(); std::string value = it->value.GetString(); if (value.empty()) { target.erase(key); } else { target[key] = value; } } }; readMapping("gamepad_button_actions", config.inputBindings.gamepadButtonActions); readMapping("gamepad_axis_actions", config.inputBindings.gamepadAxisActions); if (bindingsValue.HasMember("gamepad_axis_action_threshold")) { const auto& value = bindingsValue["gamepad_axis_action_threshold"]; if (!value.IsNumber()) { throw std::runtime_error("JSON member 'input_bindings.gamepad_axis_action_threshold' must be a number"); } config.inputBindings.gamepadAxisActionThreshold = static_cast(value.GetDouble()); } } if (document.HasMember("atmospherics")) { const auto& atmosphericsValue = document["atmospherics"]; if (!atmosphericsValue.IsObject()) { throw std::runtime_error("JSON member 'atmospherics' must be an object"); } auto readFloat = [&](const char* name, float& target) { if (!atmosphericsValue.HasMember(name)) { return; } const auto& value = atmosphericsValue[name]; if (!value.IsNumber()) { throw std::runtime_error("JSON member 'atmospherics." + std::string(name) + "' must be a number"); } target = static_cast(value.GetDouble()); }; auto readBool = [&](const char* name, bool& target) { if (!atmosphericsValue.HasMember(name)) { return; } const auto& value = atmosphericsValue[name]; if (!value.IsBool()) { throw std::runtime_error("JSON member 'atmospherics." + std::string(name) + "' must be a boolean"); } target = value.GetBool(); }; auto readFloatArray3 = [&](const char* name, std::array& target) { if (!atmosphericsValue.HasMember(name)) { return; } const auto& value = atmosphericsValue[name]; if (!value.IsArray() || value.Size() != 3) { throw std::runtime_error("JSON member 'atmospherics." + std::string(name) + "' must be an array of 3 numbers"); } for (rapidjson::SizeType i = 0; i < 3; ++i) { if (!value[i].IsNumber()) { throw std::runtime_error("JSON member 'atmospherics." + std::string(name) + "[" + std::to_string(i) + "]' must be a number"); } target[i] = static_cast(value[i].GetDouble()); } }; readFloat("ambient_strength", config.atmospherics.ambientStrength); readFloat("fog_density", config.atmospherics.fogDensity); readFloatArray3("fog_color", config.atmospherics.fogColor); readFloatArray3("sky_color", config.atmospherics.skyColor); readFloat("gamma", config.atmospherics.gamma); readFloat("exposure", config.atmospherics.exposure); readBool("enable_tone_mapping", config.atmospherics.enableToneMapping); readBool("enable_shadows", config.atmospherics.enableShadows); readBool("enable_ssgi", config.atmospherics.enableSSGI); readBool("enable_volumetric_lighting", config.atmospherics.enableVolumetricLighting); readFloat("pbr_roughness", config.atmospherics.pbrRoughness); readFloat("pbr_metallic", config.atmospherics.pbrMetallic); } if (document.HasMember("render_graph")) { const auto& renderGraphValue = document["render_graph"]; if (!renderGraphValue.IsObject()) { throw std::runtime_error("JSON member 'render_graph' must be an object"); } if (renderGraphValue.HasMember("enabled")) { const auto& value = renderGraphValue["enabled"]; if (!value.IsBool()) { throw std::runtime_error("JSON member 'render_graph.enabled' must be a boolean"); } config.renderGraph.enabled = value.GetBool(); } if (renderGraphValue.HasMember("function")) { const auto& value = renderGraphValue["function"]; if (!value.IsString()) { throw std::runtime_error("JSON member 'render_graph.function' must be a string"); } config.renderGraph.functionName = value.GetString(); } } if (document.HasMember("graphics_backend")) { const auto& backendValue = document["graphics_backend"]; if (!backendValue.IsObject()) { throw std::runtime_error("JSON member 'graphics_backend' must be an object"); } if (backendValue.HasMember("type")) { const auto& value = backendValue["type"]; if (!value.IsString()) { throw std::runtime_error("JSON member 'graphics_backend.type' must be a string"); } config.graphicsBackend.backend = ParseBackendType(value.GetString()); } if (backendValue.HasMember("bgfx_renderer")) { const auto& value = backendValue["bgfx_renderer"]; if (!value.IsString()) { throw std::runtime_error("JSON member 'graphics_backend.bgfx_renderer' must be a string"); } config.graphicsBackend.bgfxRenderer = value.GetString(); } } if (document.HasMember("materialx")) { const auto& materialValue = document["materialx"]; if (!materialValue.IsObject()) { throw std::runtime_error("JSON member 'materialx' must be an object"); } if (materialValue.HasMember("enabled")) { const auto& value = materialValue["enabled"]; if (!value.IsBool()) { throw std::runtime_error("JSON member 'materialx.enabled' must be a boolean"); } config.materialX.enabled = value.GetBool(); } if (materialValue.HasMember("document")) { const auto& value = materialValue["document"]; if (!value.IsString()) { throw std::runtime_error("JSON member 'materialx.document' must be a string"); } config.materialX.documentPath = value.GetString(); } if (materialValue.HasMember("shader_key")) { const auto& value = materialValue["shader_key"]; if (!value.IsString()) { throw std::runtime_error("JSON member 'materialx.shader_key' must be a string"); } config.materialX.shaderKey = value.GetString(); } if (materialValue.HasMember("material")) { const auto& value = materialValue["material"]; if (!value.IsString()) { throw std::runtime_error("JSON member 'materialx.material' must be a string"); } config.materialX.materialName = value.GetString(); } if (materialValue.HasMember("library_path")) { const auto& value = materialValue["library_path"]; if (!value.IsString()) { throw std::runtime_error("JSON member 'materialx.library_path' must be a string"); } config.materialX.libraryPath = value.GetString(); } if (materialValue.HasMember("library_folders")) { const auto& value = materialValue["library_folders"]; if (!value.IsArray()) { throw std::runtime_error("JSON member 'materialx.library_folders' must be an array"); } config.materialX.libraryFolders.clear(); for (rapidjson::SizeType i = 0; i < value.Size(); ++i) { if (!value[i].IsString()) { throw std::runtime_error("JSON member 'materialx.library_folders[" + std::to_string(i) + "]' must be a string"); } config.materialX.libraryFolders.emplace_back(value[i].GetString()); } } if (materialValue.HasMember("use_constant_color")) { const auto& value = materialValue["use_constant_color"]; if (!value.IsBool()) { throw std::runtime_error("JSON member 'materialx.use_constant_color' must be a boolean"); } config.materialX.useConstantColor = value.GetBool(); } if (materialValue.HasMember("constant_color")) { const auto& value = materialValue["constant_color"]; if (!value.IsArray() || value.Size() != 3) { throw std::runtime_error("JSON member 'materialx.constant_color' must be an array of 3 numbers"); } for (rapidjson::SizeType i = 0; i < 3; ++i) { if (!value[i].IsNumber()) { throw std::runtime_error("JSON member 'materialx.constant_color[" + std::to_string(i) + "]' must be a number"); } config.materialX.constantColor[i] = static_cast(value[i].GetDouble()); } } } if (document.HasMember("gui_font")) { const auto& fontValue = document["gui_font"]; if (!fontValue.IsObject()) { throw std::runtime_error("JSON member 'gui_font' must be an object"); } if (fontValue.HasMember("use_freetype")) { const auto& value = fontValue["use_freetype"]; if (!value.IsBool()) { throw std::runtime_error("JSON member 'gui_font.use_freetype' must be a boolean"); } config.guiFont.useFreeType = value.GetBool(); } if (fontValue.HasMember("font_path")) { const auto& value = fontValue["font_path"]; if (!value.IsString()) { throw std::runtime_error("JSON member 'gui_font.font_path' must be a string"); } config.guiFont.fontPath = value.GetString(); } if (fontValue.HasMember("font_size")) { const auto& value = fontValue["font_size"]; if (!value.IsNumber()) { throw std::runtime_error("JSON member 'gui_font.font_size' must be a number"); } config.guiFont.fontSize = static_cast(value.GetDouble()); } } if (document.HasMember("gui_opacity")) { const auto& value = document["gui_opacity"]; if (!value.IsNumber()) { throw std::runtime_error("JSON member 'gui_opacity' must be a number"); } config.guiOpacity = static_cast(value.GetDouble()); } return config; } std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, const std::filesystem::path& configPath) { rapidjson::Document document; document.SetObject(); auto& allocator = document.GetAllocator(); auto addStringMember = [&](const char* name, const std::string& value) { rapidjson::Value nameValue(name, allocator); rapidjson::Value stringValue(value.c_str(), allocator); document.AddMember(nameValue, stringValue, allocator); }; document.AddMember("window_width", config.width, allocator); document.AddMember("window_height", config.height, allocator); addStringMember("lua_script", config.scriptPath.string()); addStringMember("window_title", config.windowTitle); document.AddMember("lua_debug", config.luaDebug, allocator); std::filesystem::path scriptsDir = config.scriptPath.parent_path(); if (!scriptsDir.empty()) { addStringMember("scripts_directory", scriptsDir.string()); } rapidjson::Value mouseGrabObject(rapidjson::kObjectType); mouseGrabObject.AddMember("enabled", config.mouseGrab.enabled, allocator); mouseGrabObject.AddMember("grab_on_click", config.mouseGrab.grabOnClick, allocator); mouseGrabObject.AddMember("release_on_escape", config.mouseGrab.releaseOnEscape, allocator); mouseGrabObject.AddMember("start_grabbed", config.mouseGrab.startGrabbed, allocator); mouseGrabObject.AddMember("hide_cursor", config.mouseGrab.hideCursor, allocator); mouseGrabObject.AddMember("relative_mode", config.mouseGrab.relativeMode, allocator); mouseGrabObject.AddMember("grab_mouse_button", rapidjson::Value(config.mouseGrab.grabMouseButton.c_str(), allocator), allocator); mouseGrabObject.AddMember("release_key", rapidjson::Value(config.mouseGrab.releaseKey.c_str(), allocator), allocator); document.AddMember("mouse_grab", mouseGrabObject, allocator); rapidjson::Value renderGraphObject(rapidjson::kObjectType); renderGraphObject.AddMember("enabled", config.renderGraph.enabled, allocator); renderGraphObject.AddMember("function", rapidjson::Value(config.renderGraph.functionName.c_str(), allocator), allocator); document.AddMember("render_graph", renderGraphObject, allocator); rapidjson::Value backendObject(rapidjson::kObjectType); backendObject.AddMember("type", rapidjson::Value(BackendTypeToString(config.graphicsBackend.backend).c_str(), allocator), allocator); backendObject.AddMember("bgfx_renderer", rapidjson::Value(config.graphicsBackend.bgfxRenderer.c_str(), allocator), allocator); document.AddMember("graphics_backend", backendObject, allocator); rapidjson::Value materialObject(rapidjson::kObjectType); materialObject.AddMember("enabled", config.materialX.enabled, allocator); materialObject.AddMember("document", rapidjson::Value(config.materialX.documentPath.string().c_str(), allocator), allocator); materialObject.AddMember("shader_key", rapidjson::Value(config.materialX.shaderKey.c_str(), allocator), allocator); materialObject.AddMember("material", rapidjson::Value(config.materialX.materialName.c_str(), allocator), allocator); materialObject.AddMember("library_path", rapidjson::Value(config.materialX.libraryPath.string().c_str(), allocator), allocator); rapidjson::Value libraryFolders(rapidjson::kArrayType); for (const auto& folder : config.materialX.libraryFolders) { libraryFolders.PushBack(rapidjson::Value(folder.c_str(), allocator), allocator); } materialObject.AddMember("library_folders", libraryFolders, allocator); materialObject.AddMember("use_constant_color", config.materialX.useConstantColor, allocator); rapidjson::Value constantColor(rapidjson::kArrayType); constantColor.PushBack(config.materialX.constantColor[0], allocator); constantColor.PushBack(config.materialX.constantColor[1], allocator); constantColor.PushBack(config.materialX.constantColor[2], allocator); materialObject.AddMember("constant_color", constantColor, allocator); document.AddMember("materialx", materialObject, allocator); rapidjson::Value fontObject(rapidjson::kObjectType); fontObject.AddMember("use_freetype", config.guiFont.useFreeType, allocator); fontObject.AddMember("font_path", rapidjson::Value(config.guiFont.fontPath.string().c_str(), allocator), allocator); fontObject.AddMember("font_size", config.guiFont.fontSize, allocator); document.AddMember("gui_font", fontObject, allocator); rapidjson::Value bindingsObject(rapidjson::kObjectType); auto addBindingMember = [&](const char* name, const std::string& value) { rapidjson::Value nameValue(name, allocator); rapidjson::Value stringValue(value.c_str(), allocator); bindingsObject.AddMember(nameValue, stringValue, allocator); }; struct BindingSpec { const char* name; std::string InputBindings::* member; }; const std::array bindingSpecs = {{ {"move_forward", &InputBindings::moveForwardKey}, {"move_back", &InputBindings::moveBackKey}, {"move_left", &InputBindings::moveLeftKey}, {"move_right", &InputBindings::moveRightKey}, {"fly_up", &InputBindings::flyUpKey}, {"fly_down", &InputBindings::flyDownKey}, {"jump", &InputBindings::jumpKey}, {"noclip_toggle", &InputBindings::noclipToggleKey}, {"music_toggle", &InputBindings::musicToggleKey}, {"music_toggle_gamepad", &InputBindings::musicToggleGamepadButton}, {"gamepad_move_x_axis", &InputBindings::gamepadMoveXAxis}, {"gamepad_move_y_axis", &InputBindings::gamepadMoveYAxis}, {"gamepad_look_x_axis", &InputBindings::gamepadLookXAxis}, {"gamepad_look_y_axis", &InputBindings::gamepadLookYAxis}, {"gamepad_dpad_up", &InputBindings::gamepadDpadUpButton}, {"gamepad_dpad_down", &InputBindings::gamepadDpadDownButton}, {"gamepad_dpad_left", &InputBindings::gamepadDpadLeftButton}, {"gamepad_dpad_right", &InputBindings::gamepadDpadRightButton}, }}; for (const auto& spec : bindingSpecs) { addBindingMember(spec.name, config.inputBindings.*(spec.member)); } auto addMappingObject = [&](const char* name, const std::unordered_map& mappings, rapidjson::Value& target) { rapidjson::Value mappingObject(rapidjson::kObjectType); for (const auto& [key, value] : mappings) { rapidjson::Value keyValue(key.c_str(), allocator); rapidjson::Value stringValue(value.c_str(), allocator); mappingObject.AddMember(keyValue, stringValue, allocator); } target.AddMember(rapidjson::Value(name, allocator), mappingObject, allocator); }; addMappingObject("gamepad_button_actions", config.inputBindings.gamepadButtonActions, bindingsObject); addMappingObject("gamepad_axis_actions", config.inputBindings.gamepadAxisActions, bindingsObject); bindingsObject.AddMember("gamepad_axis_action_threshold", config.inputBindings.gamepadAxisActionThreshold, allocator); document.AddMember("input_bindings", bindingsObject, allocator); std::filesystem::path projectRoot = scriptsDir.parent_path(); if (!projectRoot.empty()) { addStringMember("project_root", projectRoot.string()); addStringMember("shaders_directory", (projectRoot / "shaders").string()); } else { addStringMember("shaders_directory", "shaders"); } document.AddMember("gui_opacity", config.guiOpacity, allocator); rapidjson::Value extensionArray(rapidjson::kArrayType); for (const char* extension : kDeviceExtensions) { rapidjson::Value extensionValue(extension, allocator); extensionArray.PushBack(extensionValue, allocator); } document.AddMember("device_extensions", extensionArray, allocator); if (!configPath.empty()) { addStringMember("config_file", configPath.string()); } rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); document.Accept(writer); return buffer.GetString(); } } // namespace sdl3cpp::services::impl