diff --git a/CMakeLists.txt b/CMakeLists.txt index 18a3175..4899a29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index 23ac64c..462a0ed 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -35,16 +35,17 @@ #include "services/interfaces/i_platform_service.hpp" #include #include +#include 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(); logger_ = registry_.GetService(); - 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(); + auto configService = registry_.GetService(); 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(); 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(); // Configuration service - services::impl::RuntimeConfig runtimeConfig; - runtimeConfig.scriptPath = scriptPath_; registry_.RegisterService( - registry_.GetService(), runtimeConfig); + registry_.GetService(), runtimeConfig_); // Window service registry_.RegisterService( @@ -229,12 +239,12 @@ void ServiceBasedApp::RegisterServices() { // Script engine service (shared Lua runtime) registry_.RegisterService( - scriptPath_, + runtimeConfig_.scriptPath, registry_.GetService(), registry_.GetService(), registry_.GetService(), registry_.GetService(), - runtimeConfig.luaDebug); + runtimeConfig_.luaDebug); // Script-facing services registry_.RegisterService( diff --git a/src/app/service_based_app.hpp b/src/app/service_based_app.hpp index 9d167b0..fed6fbf 100644 --- a/src/app/service_based_app.hpp +++ b/src/app/service_based_app.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #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 lifecycleService_; std::shared_ptr applicationLoopService_; diff --git a/src/main.cpp b/src/main.cpp index e5af87a..4c490b2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,39 +1,25 @@ -#include -#include -#include -#include -#include -#include - #include #include #include -#include #include #include -#include #include +#include #include #include #include -#include + +#include #include "app/service_based_app.hpp" -#include -#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 g_signalReceived{false}; - -constexpr uint32_t kWidth = 1024; -constexpr uint32_t kHeight = 768; -const std::vector 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 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 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(); - } - - return config; -} - -std::optional 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 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 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 logger) { - if (logger) { - logger->TraceVariable("config.width", static_cast(config.width)); - logger->TraceVariable("config.height", static_cast(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 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(); + auto platformService = std::make_shared(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(options.runtimeConfig.width)); + logger->TraceVariable("config.height", static_cast(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( diff --git a/src/services/impl/command_line_service.cpp b/src/services/impl/command_line_service.cpp new file mode 100644 index 0000000..a65539b --- /dev/null +++ b/src/services/impl/command_line_service.cpp @@ -0,0 +1,123 @@ +#include "command_line_service.hpp" + +#include "json_config_service.hpp" +#include + +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +CommandLineService::CommandLineService(std::shared_ptr logger, + std::shared_ptr 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 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 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 diff --git a/src/services/impl/command_line_service.hpp b/src/services/impl/command_line_service.hpp new file mode 100644 index 0000000..4b21cc7 --- /dev/null +++ b/src/services/impl/command_line_service.hpp @@ -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 +#include +#include + +namespace sdl3cpp::services::impl { + +/** + * @brief CLI11-based command line parsing service. + */ +class CommandLineService : public ICommandLineService { +public: + CommandLineService(std::shared_ptr logger, + std::shared_ptr platformService); + + CommandLineOptions Parse(int argc, char** argv) override; + +private: + std::shared_ptr logger_; + std::shared_ptr platformService_; + + std::optional GetDefaultConfigPath() const; + RuntimeConfig LoadConfigFromJson(const std::filesystem::path& configPath, bool dumpConfig); + RuntimeConfig LoadDefaultConfig(const char* argv0); +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp index 7bd7a66..7cb2123 100644 --- a/src/services/impl/json_config_service.hpp +++ b/src/services/impl/json_config_service.hpp @@ -2,6 +2,7 @@ #include "../interfaces/i_config_service.hpp" #include "../interfaces/i_logger.hpp" +#include "../interfaces/config_types.hpp" #include #include #include @@ -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. * diff --git a/src/services/impl/json_config_writer_service.cpp b/src/services/impl/json_config_writer_service.cpp new file mode 100644 index 0000000..b214ea8 --- /dev/null +++ b/src/services/impl/json_config_writer_service.cpp @@ -0,0 +1,83 @@ +#include "json_config_writer_service.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +JsonConfigWriterService::JsonConfigWriterService(std::shared_ptr 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 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 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 diff --git a/src/services/impl/json_config_writer_service.hpp b/src/services/impl/json_config_writer_service.hpp new file mode 100644 index 0000000..68b0049 --- /dev/null +++ b/src/services/impl/json_config_writer_service.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "../interfaces/i_config_writer_service.hpp" +#include "../interfaces/i_logger.hpp" +#include + +namespace sdl3cpp::services::impl { + +/** + * @brief JSON-based configuration writer. + */ +class JsonConfigWriterService : public IConfigWriterService { +public: + explicit JsonConfigWriterService(std::shared_ptr logger); + + void WriteConfig(const RuntimeConfig& config, const std::filesystem::path& configPath) override; + +private: + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp new file mode 100644 index 0000000..dca1b72 --- /dev/null +++ b/src/services/interfaces/config_types.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +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 diff --git a/src/services/interfaces/i_command_line_service.hpp b/src/services/interfaces/i_command_line_service.hpp new file mode 100644 index 0000000..9213b69 --- /dev/null +++ b/src/services/interfaces/i_command_line_service.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "config_types.hpp" +#include +#include + +namespace sdl3cpp::services { + +/** + * @brief Parsed command-line options. + */ +struct CommandLineOptions { + RuntimeConfig runtimeConfig; + std::optional 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 diff --git a/src/services/interfaces/i_config_writer_service.hpp b/src/services/interfaces/i_config_writer_service.hpp new file mode 100644 index 0000000..0a11420 --- /dev/null +++ b/src/services/interfaces/i_config_writer_service.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "config_types.hpp" +#include + +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