mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 21:55:09 +00:00
refactor: Introduce command line and JSON config writer services, enhance runtime configuration handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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_;
|
||||
|
||||
337
src/main.cpp
337
src/main.cpp
@@ -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(
|
||||
|
||||
123
src/services/impl/command_line_service.cpp
Normal file
123
src/services/impl/command_line_service.cpp
Normal 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
|
||||
31
src/services/impl/command_line_service.hpp
Normal file
31
src/services/impl/command_line_service.hpp
Normal 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
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
83
src/services/impl/json_config_writer_service.cpp
Normal file
83
src/services/impl/json_config_writer_service.cpp
Normal 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
|
||||
22
src/services/impl/json_config_writer_service.hpp
Normal file
22
src/services/impl/json_config_writer_service.hpp
Normal 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
|
||||
20
src/services/interfaces/config_types.hpp
Normal file
20
src/services/interfaces/config_types.hpp
Normal 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
|
||||
29
src/services/interfaces/i_command_line_service.hpp
Normal file
29
src/services/interfaces/i_command_line_service.hpp
Normal 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
|
||||
17
src/services/interfaces/i_config_writer_service.hpp
Normal file
17
src/services/interfaces/i_config_writer_service.hpp
Normal 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
|
||||
Reference in New Issue
Block a user