refactor: Introduce command line and JSON config writer services, enhance runtime configuration handling

This commit is contained in:
2026-01-04 18:51:53 +00:00
parent 7503dc440a
commit 7328eaa517
12 changed files with 393 additions and 323 deletions

View File

@@ -121,6 +121,8 @@ if(BUILD_SDL3_APP)
src/di/service_registry.cpp
src/events/event_bus.cpp
src/services/impl/json_config_service.cpp
src/services/impl/command_line_service.cpp
src/services/impl/json_config_writer_service.cpp
src/services/impl/logger_service.cpp
src/services/impl/platform_service.cpp
src/services/impl/script_engine_service.cpp

View File

@@ -35,16 +35,17 @@
#include "services/interfaces/i_platform_service.hpp"
#include <iostream>
#include <stdexcept>
#include <utility>
namespace sdl3cpp::app {
ServiceBasedApp::ServiceBasedApp(const std::filesystem::path& scriptPath)
: scriptPath_(scriptPath) {
ServiceBasedApp::ServiceBasedApp(services::RuntimeConfig runtimeConfig)
: runtimeConfig_(std::move(runtimeConfig)) {
// Register logger service first
registry_.RegisterService<services::ILogger, services::impl::LoggerService>();
logger_ = registry_.GetService<services::ILogger>();
logger_->Trace("ServiceBasedApp", "ServiceBasedApp", "scriptPath=" + scriptPath_.string(), "constructor starting");
logger_->Trace("ServiceBasedApp", "ServiceBasedApp", "scriptPath=" + runtimeConfig_.scriptPath.string(), "constructor starting");
try {
logger_->Info("ServiceBasedApp::ServiceBasedApp: Setting up SDL");
@@ -100,11 +101,18 @@ void ServiceBasedApp::Run() {
// Create the window
auto windowService = registry_.GetService<services::IWindowService>();
auto configService = registry_.GetService<services::IConfigService>();
if (windowService) {
services::WindowConfig config;
config.width = 1024;
config.height = 768;
config.title = "SDL3 + Vulkan Application";
if (configService) {
config.width = configService->GetWindowWidth();
config.height = configService->GetWindowHeight();
config.title = configService->GetWindowTitle();
} else {
config.width = runtimeConfig_.width;
config.height = runtimeConfig_.height;
config.title = runtimeConfig_.windowTitle;
}
config.resizable = true;
windowService->CreateWindow(config);
}
@@ -113,7 +121,11 @@ void ServiceBasedApp::Run() {
auto graphicsService = registry_.GetService<services::IGraphicsService>();
if (graphicsService && windowService) {
services::GraphicsConfig graphicsConfig;
graphicsConfig.deviceExtensions = {"VK_KHR_swapchain"};
if (configService) {
graphicsConfig.deviceExtensions = configService->GetDeviceExtensions();
} else {
graphicsConfig.deviceExtensions = {"VK_KHR_swapchain"};
}
graphicsConfig.enableValidationLayers = false;
graphicsService->InitializeDevice(windowService->GetNativeHandle(), graphicsConfig);
graphicsService->InitializeSwapchain();
@@ -197,10 +209,8 @@ void ServiceBasedApp::RegisterServices() {
registry_.RegisterService<events::IEventBus, events::EventBus>();
// Configuration service
services::impl::RuntimeConfig runtimeConfig;
runtimeConfig.scriptPath = scriptPath_;
registry_.RegisterService<services::IConfigService, services::impl::JsonConfigService>(
registry_.GetService<services::ILogger>(), runtimeConfig);
registry_.GetService<services::ILogger>(), runtimeConfig_);
// Window service
registry_.RegisterService<services::IWindowService, services::impl::SdlWindowService>(
@@ -229,12 +239,12 @@ void ServiceBasedApp::RegisterServices() {
// Script engine service (shared Lua runtime)
registry_.RegisterService<services::IScriptEngineService, services::impl::ScriptEngineService>(
scriptPath_,
runtimeConfig_.scriptPath,
registry_.GetService<services::ILogger>(),
registry_.GetService<services::IMeshService>(),
registry_.GetService<services::IAudioCommandService>(),
registry_.GetService<services::IPhysicsBridgeService>(),
runtimeConfig.luaDebug);
runtimeConfig_.luaDebug);
// Script-facing services
registry_.RegisterService<services::ISceneScriptService, services::impl::SceneScriptService>(

View File

@@ -1,6 +1,5 @@
#pragma once
#include <filesystem>
#include <memory>
#include <SDL3/SDL.h>
#include "di/service_registry.hpp"
@@ -10,6 +9,7 @@
#include "services/interfaces/i_application_service.hpp"
#include "services/interfaces/i_logger.hpp"
#include "services/interfaces/i_crash_recovery_service.hpp"
#include "services/interfaces/config_types.hpp"
namespace sdl3cpp::app {
@@ -20,7 +20,7 @@ namespace sdl3cpp::app {
*/
class ServiceBasedApp : public services::IApplicationService {
public:
explicit ServiceBasedApp(const std::filesystem::path& scriptPath);
explicit ServiceBasedApp(services::RuntimeConfig runtimeConfig);
~ServiceBasedApp();
ServiceBasedApp(const ServiceBasedApp&) = delete;
@@ -51,7 +51,7 @@ private:
void RegisterServices();
void SetupSDL();
std::filesystem::path scriptPath_;
services::RuntimeConfig runtimeConfig_;
di::ServiceRegistry registry_;
std::shared_ptr<services::ILifecycleService> lifecycleService_;
std::shared_ptr<services::IApplicationLoopService> applicationLoopService_;

View File

@@ -1,39 +1,25 @@
#include <CLI/CLI.hpp>
#include <rapidjson/document.h>
#include <rapidjson/istreamwrapper.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <rapidjson/prettywriter.h>
#include <atomic>
#include <csignal>
#include <cstdlib>
#include <cstdint>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <SDL3/SDL_main.h>
#include "app/service_based_app.hpp"
#include <SDL3/SDL_main.h>
#include "services/interfaces/i_logger.hpp"
#include "services/impl/command_line_service.hpp"
#include "services/impl/json_config_writer_service.hpp"
#include "services/impl/logger_service.hpp"
#include "services/impl/platform_service.hpp"
using namespace sdl3cpp::services;
using namespace sdl3cpp::services;
#include "services/interfaces/i_logger.hpp"
namespace sdl3cpp::app {
std::atomic<bool> g_signalReceived{false};
constexpr uint32_t kWidth = 1024;
constexpr uint32_t kHeight = 768;
const std::vector<const char*> kDeviceExtensions = {"VK_KHR_swapchain"};
}
namespace {
@@ -49,303 +35,60 @@ void SetupSignalHandlers() {
std::signal(SIGTERM, SignalHandler);
}
std::filesystem::path FindScriptPath(const char* 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;
}
struct RuntimeConfig {
uint32_t width = sdl3cpp::app::kWidth;
uint32_t height = sdl3cpp::app::kHeight;
std::filesystem::path scriptPath;
bool luaDebug = false;
};
RuntimeConfig GenerateDefaultRuntimeConfig(const char* argv0) {
RuntimeConfig config;
config.scriptPath = FindScriptPath(argv0);
return config;
}
RuntimeConfig LoadRuntimeConfigFromJson(const std::filesystem::path& configPath, bool dumpConfig) {
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) {
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetIndent(' ', 2);
document.Accept(writer);
std::cout << "Loaded runtime config (" << configPath << "):\n"
<< buffer.GetString() << '\n';
}
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<std::filesystem::path> 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<uint32_t>(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();
}
return config;
}
std::optional<std::filesystem::path> GetDefaultConfigPath() {
sdl3cpp::services::impl::PlatformService platformService;
if (auto dir = platformService.GetUserConfigDirectory()) {
return *dir / "default_runtime.json";
}
return std::nullopt;
}
struct AppOptions {
RuntimeConfig runtimeConfig;
std::optional<std::filesystem::path> seedOutput;
bool saveDefaultJson = false;
bool dumpRuntimeJson = false;
bool traceEnabled = false;
};
AppOptions ParseCommandLine(int argc, char** argv) {
std::string jsonInputText;
std::string seedOutputText;
std::string setDefaultJsonPath;
bool dumpRuntimeJson = false;
bool traceRuntime = false;
CLI::App app("SDL3 + Vulkan runtime helper");
app.add_option("-j,--json-file-in", jsonInputText, "Path to a runtime JSON config")
->check(CLI::ExistingFile);
app.add_option("-s,--create-seed-json", seedOutputText,
"Write a template runtime JSON file");
auto* setDefaultJsonOption = app.add_option(
"-d,--set-default-json", setDefaultJsonPath,
"Persist the runtime JSON to the platform default location (XDG/APPDATA); "
"provide PATH to copy that JSON instead of using the default contents");
setDefaultJsonOption->type_name("PATH");
setDefaultJsonOption->type_size(1, 1);
setDefaultJsonOption->expected(0, 1);
app.add_flag("--dump-json", dumpRuntimeJson, "Print the runtime JSON that was loaded");
app.add_flag("--trace", traceRuntime, "Emit a log line when key functions/methods run");
try {
app.parse(argc, argv);
} catch (const CLI::CallForHelp& e) {
std::exit(app.exit(e));
} catch (const CLI::CallForVersion& e) {
std::exit(app.exit(e));
} catch (const CLI::ParseError& e) {
app.exit(e);
throw;
}
bool shouldSaveDefault = setDefaultJsonOption->count() > 0;
std::optional<std::filesystem::path> providedDefaultPath;
if (shouldSaveDefault && !setDefaultJsonPath.empty()) {
providedDefaultPath = std::filesystem::absolute(setDefaultJsonPath);
}
RuntimeConfig runtimeConfig;
if (!jsonInputText.empty()) {
runtimeConfig = LoadRuntimeConfigFromJson(std::filesystem::absolute(jsonInputText), dumpRuntimeJson);
} else if (providedDefaultPath) {
runtimeConfig = LoadRuntimeConfigFromJson(*providedDefaultPath, dumpRuntimeJson);
} else if (auto defaultPath = GetDefaultConfigPath();
defaultPath && std::filesystem::exists(*defaultPath)) {
runtimeConfig = LoadRuntimeConfigFromJson(*defaultPath, dumpRuntimeJson);
} else {
runtimeConfig = GenerateDefaultRuntimeConfig(argc > 0 ? argv[0] : nullptr);
}
AppOptions options;
options.runtimeConfig = std::move(runtimeConfig);
if (!seedOutputText.empty()) {
options.seedOutput = std::filesystem::absolute(seedOutputText);
}
options.saveDefaultJson = shouldSaveDefault;
options.dumpRuntimeJson = dumpRuntimeJson;
options.traceEnabled = traceRuntime;
return options;
}
void LogRuntimeConfig(const RuntimeConfig& config, std::shared_ptr<sdl3cpp::services::ILogger> logger) {
if (logger) {
logger->TraceVariable("config.width", static_cast<int>(config.width));
logger->TraceVariable("config.height", static_cast<int>(config.height));
logger->TraceVariable("config.scriptPath", config.scriptPath.string());
}
}
void WriteRuntimeConfigJson(const RuntimeConfig& runtimeConfig,
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", runtimeConfig.width, allocator);
document.AddMember("window_height", runtimeConfig.height, allocator);
addStringMember("lua_script", runtimeConfig.scriptPath.string());
std::filesystem::path scriptsDir = runtimeConfig.scriptPath.parent_path();
addStringMember("scripts_directory", scriptsDir.string());
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");
}
rapidjson::Value extensionArray(rapidjson::kArrayType);
for (const char* extension : sdl3cpp::app::kDeviceExtensions) {
rapidjson::Value extensionValue(extension, allocator);
extensionArray.PushBack(extensionValue, allocator);
}
document.AddMember("device_extensions", extensionArray, allocator);
addStringMember("config_file", configPath.string());
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
auto parentDir = configPath.parent_path();
if (!parentDir.empty()) {
std::filesystem::create_directories(parentDir);
}
std::ofstream outFile(configPath);
if (!outFile) {
throw std::runtime_error("Failed to open config output file: " + configPath.string());
}
outFile << buffer.GetString();
}
} // namespace
int main(int argc, char** argv) {
SDL_SetMainReady();
SetupSignalHandlers();
try {
AppOptions options = ParseCommandLine(argc, argv);
sdl3cpp::app::ServiceBasedApp app(options.runtimeConfig.scriptPath);
// Configure logging
sdl3cpp::services::LogLevel logLevel = options.traceEnabled ? sdl3cpp::services::LogLevel::TRACE : sdl3cpp::services::LogLevel::INFO;
auto startupLogger = std::make_shared<sdl3cpp::services::impl::LoggerService>();
auto platformService = std::make_shared<sdl3cpp::services::impl::PlatformService>(startupLogger);
sdl3cpp::services::impl::CommandLineService commandLineService(startupLogger, platformService);
sdl3cpp::services::CommandLineOptions options = commandLineService.Parse(argc, argv);
sdl3cpp::app::ServiceBasedApp app(options.runtimeConfig);
sdl3cpp::services::LogLevel logLevel = options.traceEnabled
? sdl3cpp::services::LogLevel::TRACE
: sdl3cpp::services::LogLevel::INFO;
app.ConfigureLogging(logLevel, true, "sdl3_app.log");
// Log startup information using service-based logging
auto logger = app.GetLogger();
if (logger) {
logger->Info("Application starting");
LogRuntimeConfig(options.runtimeConfig, logger);
logger->TraceVariable("config.width", static_cast<int>(options.runtimeConfig.width));
logger->TraceVariable("config.height", static_cast<int>(options.runtimeConfig.height));
logger->TraceVariable("config.scriptPath", options.runtimeConfig.scriptPath.string());
logger->TraceVariable("config.luaDebug", options.runtimeConfig.luaDebug);
logger->TraceVariable("config.windowTitle", options.runtimeConfig.windowTitle);
}
if (options.seedOutput) {
WriteRuntimeConfigJson(options.runtimeConfig, *options.seedOutput);
}
if (options.saveDefaultJson) {
if (auto defaultPath = GetDefaultConfigPath()) {
WriteRuntimeConfigJson(options.runtimeConfig, *defaultPath);
} else {
throw std::runtime_error("Unable to determine platform config directory");
if (options.seedOutput || options.saveDefaultJson) {
sdl3cpp::services::impl::JsonConfigWriterService configWriter(logger);
if (options.seedOutput) {
configWriter.WriteConfig(options.runtimeConfig, *options.seedOutput);
}
if (options.saveDefaultJson) {
if (auto configDir = platformService->GetUserConfigDirectory()) {
configWriter.WriteConfig(options.runtimeConfig, *configDir / "default_runtime.json");
} else {
throw std::runtime_error("Unable to determine platform config directory");
}
}
}
app.Run();
} catch (const std::runtime_error& e) {
std::string errorMsg = e.what();
// For early errors before app is created, we can't use service logger
// Fall back to console output
std::cerr << "Runtime error: " << errorMsg << std::endl;
// Check if this is a timeout/hang error - show simpler message for these
bool isTimeoutError = errorMsg.find("timeout") != std::string::npos ||
errorMsg.find("Launch timeout") != std::string::npos ||
errorMsg.find("Swapchain recreation loop") != std::string::npos;
errorMsg.find("Launch timeout") != std::string::npos ||
errorMsg.find("Swapchain recreation loop") != std::string::npos;
if (!isTimeoutError) {
// For non-timeout errors, show full error dialog
SDL_ShowSimpleMessageBox(

View File

@@ -0,0 +1,123 @@
#include "command_line_service.hpp"
#include "json_config_service.hpp"
#include <CLI/CLI.hpp>
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <utility>
namespace sdl3cpp::services::impl {
CommandLineService::CommandLineService(std::shared_ptr<ILogger> logger,
std::shared_ptr<IPlatformService> platformService)
: logger_(std::move(logger)),
platformService_(std::move(platformService)) {
if (!logger_) {
throw std::runtime_error("CommandLineService requires a logger");
}
logger_->Trace("CommandLineService", "CommandLineService", "", "Created");
}
CommandLineOptions CommandLineService::Parse(int argc, char** argv) {
bool traceRequested = false;
for (int i = 1; i < argc; ++i) {
if (argv[i] && std::string(argv[i]) == "--trace") {
traceRequested = true;
break;
}
}
if (traceRequested) {
logger_->SetLevel(LogLevel::TRACE);
}
logger_->Trace("CommandLineService", "Parse", "argc=" + std::to_string(argc), "Entering");
std::string jsonInputText;
std::string seedOutputText;
std::string setDefaultJsonPath;
bool dumpRuntimeJson = false;
bool traceRuntime = false;
CLI::App app("SDL3 + Vulkan runtime helper");
app.add_option("-j,--json-file-in", jsonInputText, "Path to a runtime JSON config")
->check(CLI::ExistingFile);
app.add_option("-s,--create-seed-json", seedOutputText,
"Write a template runtime JSON file");
auto* setDefaultJsonOption = app.add_option(
"-d,--set-default-json", setDefaultJsonPath,
"Persist the runtime JSON to the platform default location (XDG/APPDATA); "
"provide PATH to copy that JSON instead of using the default contents");
setDefaultJsonOption->type_name("PATH");
setDefaultJsonOption->type_size(1, 1);
setDefaultJsonOption->expected(0, 1);
app.add_flag("--dump-json", dumpRuntimeJson, "Print the runtime JSON that was loaded");
app.add_flag("--trace", traceRuntime, "Emit a log line when key functions/methods run");
try {
app.parse(argc, argv);
} catch (const CLI::CallForHelp& e) {
std::exit(app.exit(e));
} catch (const CLI::CallForVersion& e) {
std::exit(app.exit(e));
} catch (const CLI::ParseError& e) {
app.exit(e);
throw;
}
bool shouldSaveDefault = setDefaultJsonOption->count() > 0;
std::optional<std::filesystem::path> providedDefaultPath;
if (shouldSaveDefault && !setDefaultJsonPath.empty()) {
providedDefaultPath = std::filesystem::absolute(setDefaultJsonPath);
}
RuntimeConfig runtimeConfig;
if (!jsonInputText.empty()) {
runtimeConfig = LoadConfigFromJson(std::filesystem::absolute(jsonInputText), dumpRuntimeJson);
} else if (providedDefaultPath) {
runtimeConfig = LoadConfigFromJson(*providedDefaultPath, dumpRuntimeJson);
} else if (auto defaultPath = GetDefaultConfigPath();
defaultPath && std::filesystem::exists(*defaultPath)) {
runtimeConfig = LoadConfigFromJson(*defaultPath, dumpRuntimeJson);
} else {
runtimeConfig = LoadDefaultConfig(argc > 0 ? argv[0] : nullptr);
}
CommandLineOptions options;
options.runtimeConfig = std::move(runtimeConfig);
if (!seedOutputText.empty()) {
options.seedOutput = std::filesystem::absolute(seedOutputText);
}
options.saveDefaultJson = shouldSaveDefault;
options.dumpRuntimeJson = dumpRuntimeJson;
options.traceEnabled = traceRuntime;
logger_->Trace("CommandLineService", "Parse", "", "Exiting");
return options;
}
std::optional<std::filesystem::path> CommandLineService::GetDefaultConfigPath() const {
if (!platformService_) {
logger_->Warn("CommandLineService::GetDefaultConfigPath: Platform service not available");
return std::nullopt;
}
if (auto dir = platformService_->GetUserConfigDirectory()) {
return *dir / "default_runtime.json";
}
return std::nullopt;
}
RuntimeConfig CommandLineService::LoadConfigFromJson(const std::filesystem::path& configPath, bool dumpConfig) {
logger_->Trace("CommandLineService", "LoadConfigFromJson", "configPath=" + configPath.string());
JsonConfigService configService(logger_, configPath, dumpConfig);
return configService.GetConfig();
}
RuntimeConfig CommandLineService::LoadDefaultConfig(const char* argv0) {
logger_->Trace("CommandLineService", "LoadDefaultConfig");
JsonConfigService configService(logger_, argv0);
return configService.GetConfig();
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,31 @@
#pragma once
#include "../interfaces/i_command_line_service.hpp"
#include "../interfaces/i_logger.hpp"
#include "../interfaces/i_platform_service.hpp"
#include <filesystem>
#include <memory>
#include <optional>
namespace sdl3cpp::services::impl {
/**
* @brief CLI11-based command line parsing service.
*/
class CommandLineService : public ICommandLineService {
public:
CommandLineService(std::shared_ptr<ILogger> logger,
std::shared_ptr<IPlatformService> platformService);
CommandLineOptions Parse(int argc, char** argv) override;
private:
std::shared_ptr<ILogger> logger_;
std::shared_ptr<IPlatformService> platformService_;
std::optional<std::filesystem::path> GetDefaultConfigPath() const;
RuntimeConfig LoadConfigFromJson(const std::filesystem::path& configPath, bool dumpConfig);
RuntimeConfig LoadDefaultConfig(const char* argv0);
};
} // namespace sdl3cpp::services::impl

View File

@@ -2,6 +2,7 @@
#include "../interfaces/i_config_service.hpp"
#include "../interfaces/i_logger.hpp"
#include "../interfaces/config_types.hpp"
#include <cstdint>
#include <filesystem>
#include <string>
@@ -9,17 +10,6 @@
namespace sdl3cpp::services::impl {
/**
* @brief Runtime configuration structure.
*/
struct RuntimeConfig {
uint32_t width = 1024;
uint32_t height = 768;
std::filesystem::path scriptPath;
bool luaDebug = false;
std::string windowTitle = "SDL3 Vulkan Demo";
};
/**
* @brief JSON-based configuration service implementation.
*

View File

@@ -0,0 +1,83 @@
#include "json_config_writer_service.hpp"
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <vulkan/vulkan.h>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <utility>
#include <vector>
namespace sdl3cpp::services::impl {
JsonConfigWriterService::JsonConfigWriterService(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {
if (logger_) {
logger_->Trace("JsonConfigWriterService", "JsonConfigWriterService");
}
}
void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std::filesystem::path& configPath) {
if (logger_) {
logger_->Trace("JsonConfigWriterService", "WriteConfig", "configPath=" + configPath.string(), "Entering");
}
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());
std::filesystem::path scriptsDir = config.scriptPath.parent_path();
addStringMember("scripts_directory", scriptsDir.string());
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");
}
std::vector<const char*> deviceExtensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
rapidjson::Value extensionArray(rapidjson::kArrayType);
for (const char* extension : deviceExtensions) {
rapidjson::Value extensionValue(extension, allocator);
extensionArray.PushBack(extensionValue, allocator);
}
document.AddMember("device_extensions", extensionArray, allocator);
addStringMember("config_file", configPath.string());
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
auto parentDir = configPath.parent_path();
if (!parentDir.empty()) {
std::filesystem::create_directories(parentDir);
}
std::ofstream outFile(configPath);
if (!outFile) {
throw std::runtime_error("Failed to open config output file: " + configPath.string());
}
outFile << buffer.GetString();
if (logger_) {
logger_->Info("JsonConfigWriterService: Wrote runtime config to " + configPath.string());
logger_->Trace("JsonConfigWriterService", "WriteConfig", "", "Exiting");
}
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,22 @@
#pragma once
#include "../interfaces/i_config_writer_service.hpp"
#include "../interfaces/i_logger.hpp"
#include <memory>
namespace sdl3cpp::services::impl {
/**
* @brief JSON-based configuration writer.
*/
class JsonConfigWriterService : public IConfigWriterService {
public:
explicit JsonConfigWriterService(std::shared_ptr<ILogger> logger);
void WriteConfig(const RuntimeConfig& config, const std::filesystem::path& configPath) override;
private:
std::shared_ptr<ILogger> logger_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <string>
namespace sdl3cpp::services {
/**
* @brief Runtime configuration values used across services.
*/
struct RuntimeConfig {
uint32_t width = 1024;
uint32_t height = 768;
std::filesystem::path scriptPath;
bool luaDebug = false;
std::string windowTitle = "SDL3 Vulkan Demo";
};
} // namespace sdl3cpp::services

View File

@@ -0,0 +1,29 @@
#pragma once
#include "config_types.hpp"
#include <filesystem>
#include <optional>
namespace sdl3cpp::services {
/**
* @brief Parsed command-line options.
*/
struct CommandLineOptions {
RuntimeConfig runtimeConfig;
std::optional<std::filesystem::path> seedOutput;
bool saveDefaultJson = false;
bool dumpRuntimeJson = false;
bool traceEnabled = false;
};
/**
* @brief Command line parsing service interface.
*/
class ICommandLineService {
public:
virtual ~ICommandLineService() = default;
virtual CommandLineOptions Parse(int argc, char** argv) = 0;
};
} // namespace sdl3cpp::services

View File

@@ -0,0 +1,17 @@
#pragma once
#include "config_types.hpp"
#include <filesystem>
namespace sdl3cpp::services {
/**
* @brief Configuration writer service interface.
*/
class IConfigWriterService {
public:
virtual ~IConfigWriterService() = default;
virtual void WriteConfig(const RuntimeConfig& config, const std::filesystem::path& configPath) = 0;
};
} // namespace sdl3cpp::services