From 81fb6e76f6f4280b9ed053209754d526b0e737f5 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 5 Jan 2026 15:02:39 +0000 Subject: [PATCH] feat: Add configuration service and JSON handling to script engine --- src/app/service_based_app.cpp | 1 + src/services/impl/json_config_service.cpp | 137 ++++++++++++++++++- src/services/impl/json_config_service.hpp | 13 +- src/services/impl/script_engine_service.cpp | 110 ++++++++++++++- src/services/impl/script_engine_service.hpp | 6 + src/services/interfaces/i_config_service.hpp | 7 + 6 files changed, 266 insertions(+), 8 deletions(-) diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index c3f8c3b..1d11fde 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -264,6 +264,7 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService(), registry_.GetService(), registry_.GetService(), + registry_.GetService(), runtimeConfig_.luaDebug); // Script-facing services diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index 881f1f3..ed5ecb6 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -25,11 +26,13 @@ JsonConfigService::JsonConfigService(std::shared_ptr logger, const char "argv0=" + std::string(argv0 ? argv0 : "")); } config_.scriptPath = FindScriptPath(argv0); + configJson_ = BuildConfigJson(config_, {}); logger_->Info("JsonConfigService initialized with default configuration"); } JsonConfigService::JsonConfigService(std::shared_ptr logger, const std::filesystem::path& configPath, bool dumpConfig) - : logger_(std::move(logger)), config_(LoadFromJson(logger_, configPath, dumpConfig)) { + : logger_(std::move(logger)), + config_(LoadFromJson(logger_, configPath, dumpConfig, &configJson_)) { if (logger_) { logger_->Trace("JsonConfigService", "JsonConfigService", "configPath=" + configPath.string() + @@ -39,7 +42,7 @@ JsonConfigService::JsonConfigService(std::shared_ptr logger, const std: } JsonConfigService::JsonConfigService(std::shared_ptr logger, const RuntimeConfig& config) - : logger_(std::move(logger)), config_(config) { + : logger_(std::move(logger)), config_(config), configJson_(BuildConfigJson(config, {})) { if (logger_) { logger_->Trace("JsonConfigService", "JsonConfigService", "config.width=" + std::to_string(config.width) + @@ -80,7 +83,10 @@ std::filesystem::path JsonConfigService::FindScriptPath(const char* argv0) { return scriptPath; } -RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, const std::filesystem::path& configPath, bool dumpConfig) { +RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, + const std::filesystem::path& configPath, + bool dumpConfig, + std::string* configJson) { std::string args = "configPath=" + configPath.string() + ", dumpConfig=" + (dumpConfig ? "true" : "false"); logger->Trace("JsonConfigService", "LoadFromJson", args); @@ -100,13 +106,18 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, c throw std::runtime_error("JSON config must contain an object at the root"); } - if (dumpConfig) { + if (dumpConfig || configJson) { rapidjson::StringBuffer buffer; rapidjson::PrettyWriter writer(buffer); writer.SetIndent(' ', 2); document.Accept(writer); - std::cout << "Loaded runtime config (" << configPath << "):\n" - << buffer.GetString() << '\n'; + if (dumpConfig) { + std::cout << "Loaded runtime config (" << configPath << "):\n" + << buffer.GetString() << '\n'; + } + if (configJson) { + *configJson = buffer.GetString(); + } } const char* scriptField = "lua_script"; @@ -352,4 +363,118 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, c return config; } +std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, + const std::filesystem::path& configPath) { + rapidjson::Document document; + document.SetObject(); + auto& allocator = document.GetAllocator(); + + auto addStringMember = [&](const char* name, const std::string& value) { + rapidjson::Value nameValue(name, allocator); + rapidjson::Value stringValue(value.c_str(), allocator); + document.AddMember(nameValue, stringValue, allocator); + }; + + document.AddMember("window_width", config.width, allocator); + document.AddMember("window_height", config.height, allocator); + addStringMember("lua_script", config.scriptPath.string()); + addStringMember("window_title", config.windowTitle); + document.AddMember("lua_debug", config.luaDebug, allocator); + + std::filesystem::path scriptsDir = config.scriptPath.parent_path(); + if (!scriptsDir.empty()) { + addStringMember("scripts_directory", scriptsDir.string()); + } + + rapidjson::Value mouseGrabObject(rapidjson::kObjectType); + mouseGrabObject.AddMember("enabled", config.mouseGrab.enabled, allocator); + mouseGrabObject.AddMember("grab_on_click", config.mouseGrab.grabOnClick, allocator); + mouseGrabObject.AddMember("release_on_escape", config.mouseGrab.releaseOnEscape, allocator); + mouseGrabObject.AddMember("start_grabbed", config.mouseGrab.startGrabbed, allocator); + mouseGrabObject.AddMember("hide_cursor", config.mouseGrab.hideCursor, allocator); + mouseGrabObject.AddMember("relative_mode", config.mouseGrab.relativeMode, allocator); + mouseGrabObject.AddMember("grab_mouse_button", + rapidjson::Value(config.mouseGrab.grabMouseButton.c_str(), allocator), + allocator); + mouseGrabObject.AddMember("release_key", + rapidjson::Value(config.mouseGrab.releaseKey.c_str(), allocator), + allocator); + document.AddMember("mouse_grab", mouseGrabObject, allocator); + + rapidjson::Value bindingsObject(rapidjson::kObjectType); + auto addBindingMember = [&](const char* name, const std::string& value) { + rapidjson::Value nameValue(name, allocator); + rapidjson::Value stringValue(value.c_str(), allocator); + bindingsObject.AddMember(nameValue, stringValue, allocator); + }; + struct BindingSpec { + const char* name; + std::string InputBindings::* member; + }; + const std::array bindingSpecs = {{ + {"move_forward", &InputBindings::moveForwardKey}, + {"move_back", &InputBindings::moveBackKey}, + {"move_left", &InputBindings::moveLeftKey}, + {"move_right", &InputBindings::moveRightKey}, + {"fly_up", &InputBindings::flyUpKey}, + {"fly_down", &InputBindings::flyDownKey}, + {"jump", &InputBindings::jumpKey}, + {"noclip_toggle", &InputBindings::noclipToggleKey}, + {"music_toggle", &InputBindings::musicToggleKey}, + {"music_toggle_gamepad", &InputBindings::musicToggleGamepadButton}, + {"gamepad_move_x_axis", &InputBindings::gamepadMoveXAxis}, + {"gamepad_move_y_axis", &InputBindings::gamepadMoveYAxis}, + {"gamepad_look_x_axis", &InputBindings::gamepadLookXAxis}, + {"gamepad_look_y_axis", &InputBindings::gamepadLookYAxis}, + {"gamepad_dpad_up", &InputBindings::gamepadDpadUpButton}, + {"gamepad_dpad_down", &InputBindings::gamepadDpadDownButton}, + {"gamepad_dpad_left", &InputBindings::gamepadDpadLeftButton}, + {"gamepad_dpad_right", &InputBindings::gamepadDpadRightButton}, + }}; + for (const auto& spec : bindingSpecs) { + addBindingMember(spec.name, config.inputBindings.*(spec.member)); + } + + auto addMappingObject = [&](const char* name, + const std::unordered_map& mappings, + rapidjson::Value& target) { + rapidjson::Value mappingObject(rapidjson::kObjectType); + for (const auto& [key, value] : mappings) { + rapidjson::Value keyValue(key.c_str(), allocator); + rapidjson::Value stringValue(value.c_str(), allocator); + mappingObject.AddMember(keyValue, stringValue, allocator); + } + target.AddMember(rapidjson::Value(name, allocator), mappingObject, allocator); + }; + + addMappingObject("gamepad_button_actions", config.inputBindings.gamepadButtonActions, bindingsObject); + addMappingObject("gamepad_axis_actions", config.inputBindings.gamepadAxisActions, bindingsObject); + bindingsObject.AddMember("gamepad_axis_action_threshold", + config.inputBindings.gamepadAxisActionThreshold, allocator); + document.AddMember("input_bindings", bindingsObject, allocator); + + std::filesystem::path projectRoot = scriptsDir.parent_path(); + if (!projectRoot.empty()) { + addStringMember("project_root", projectRoot.string()); + addStringMember("shaders_directory", (projectRoot / "shaders").string()); + } else { + addStringMember("shaders_directory", "shaders"); + } + + rapidjson::Value extensionArray(rapidjson::kArrayType); + for (const char* extension : kDeviceExtensions) { + rapidjson::Value extensionValue(extension, allocator); + extensionArray.PushBack(extensionValue, allocator); + } + document.AddMember("device_extensions", extensionArray, allocator); + if (!configPath.empty()) { + addStringMember("config_file", configPath.string()); + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + return buffer.GetString(); +} + } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp index c5c4498..f910fcb 100644 --- a/src/services/impl/json_config_service.hpp +++ b/src/services/impl/json_config_service.hpp @@ -88,6 +88,12 @@ public: } return config_.mouseGrab; } + const std::string& GetConfigJson() const override { + if (logger_) { + logger_->Trace("JsonConfigService", "GetConfigJson"); + } + return configJson_; + } /** * @brief Get the full runtime configuration. @@ -104,10 +110,15 @@ public: private: std::shared_ptr logger_; RuntimeConfig config_; + std::string configJson_; // Helper methods moved from main.cpp std::filesystem::path FindScriptPath(const char* argv0); - static RuntimeConfig LoadFromJson(std::shared_ptr logger, const std::filesystem::path& configPath, bool dumpConfig); + static RuntimeConfig LoadFromJson(std::shared_ptr logger, + const std::filesystem::path& configPath, + bool dumpConfig, + std::string* configJson); + static std::string BuildConfigJson(const RuntimeConfig& config, const std::filesystem::path& configPath); }; } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/script_engine_service.cpp b/src/services/impl/script_engine_service.cpp index e3c4b97..cfeb684 100644 --- a/src/services/impl/script_engine_service.cpp +++ b/src/services/impl/script_engine_service.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include @@ -61,6 +63,54 @@ SDL_Keycode ReadKeycodeFromLua(lua_State* L, int index, bool& ok) { ok = false; return SDLK_UNKNOWN; } + +void PushJsonValue(lua_State* L, const rapidjson::Value& value) { + if (value.IsObject()) { + lua_newtable(L); + for (auto it = value.MemberBegin(); it != value.MemberEnd(); ++it) { + lua_pushlstring(L, it->name.GetString(), it->name.GetStringLength()); + PushJsonValue(L, it->value); + lua_settable(L, -3); + } + return; + } + if (value.IsArray()) { + lua_newtable(L); + for (rapidjson::SizeType i = 0; i < value.Size(); ++i) { + PushJsonValue(L, value[i]); + lua_rawseti(L, -2, static_cast(i + 1)); + } + return; + } + if (value.IsString()) { + lua_pushlstring(L, value.GetString(), value.GetStringLength()); + return; + } + if (value.IsBool()) { + lua_pushboolean(L, value.GetBool()); + return; + } + if (value.IsNumber()) { + lua_pushnumber(L, value.GetDouble()); + return; + } + lua_pushnil(L); +} + +bool PushConfigTableFromJson(lua_State* L, const std::string& json, std::string& error) { + if (json.empty()) { + error = "Config JSON not available"; + return false; + } + rapidjson::Document document; + rapidjson::ParseResult result = document.Parse(json.c_str()); + if (!result) { + error = std::string("Config JSON parse failed: ") + rapidjson::GetParseError_En(result.Code()); + return false; + } + PushJsonValue(L, document); + return true; +} } // namespace namespace sdl3cpp::services::impl { @@ -72,6 +122,7 @@ ScriptEngineService::ScriptEngineService(const std::filesystem::path& scriptPath std::shared_ptr physicsBridgeService, std::shared_ptr inputService, std::shared_ptr windowService, + std::shared_ptr configService, bool debugEnabled) : logger_(std::move(logger)), meshService_(std::move(meshService)), @@ -79,6 +130,7 @@ ScriptEngineService::ScriptEngineService(const std::filesystem::path& scriptPath physicsBridgeService_(std::move(physicsBridgeService)), inputService_(std::move(inputService)), windowService_(std::move(windowService)), + configService_(std::move(configService)), scriptPath_(scriptPath), debugEnabled_(debugEnabled) { if (logger_) { @@ -89,7 +141,8 @@ ScriptEngineService::ScriptEngineService(const std::filesystem::path& scriptPath ", audioCommandService=" + std::string(audioCommandService_ ? "set" : "null") + ", physicsBridgeService=" + std::string(physicsBridgeService_ ? "set" : "null") + ", inputService=" + std::string(inputService_ ? "set" : "null") + - ", windowService=" + std::string(windowService_ ? "set" : "null")); + ", windowService=" + std::string(windowService_ ? "set" : "null") + + ", configService=" + std::string(configService_ ? "set" : "null")); } } @@ -116,6 +169,7 @@ void ScriptEngineService::Initialize() { bindingContext_->physicsBridgeService = physicsBridgeService_; bindingContext_->inputService = inputService_; bindingContext_->windowService = windowService_; + bindingContext_->configService = configService_; bindingContext_->logger = logger_; luaState_ = luaL_newstate(); @@ -130,6 +184,25 @@ void ScriptEngineService::Initialize() { lua_pushboolean(luaState_, debugEnabled_); lua_setglobal(luaState_, "lua_debug"); + if (configService_) { + std::string error; + if (PushConfigTableFromJson(luaState_, configService_->GetConfigJson(), error)) { + lua_setglobal(luaState_, "config"); + } else { + if (logger_) { + logger_->Error("ScriptEngineService: " + error); + } + lua_newtable(luaState_); + lua_setglobal(luaState_, "config"); + } + } else { + if (logger_) { + logger_->Error("ScriptEngineService: Config service not available"); + } + lua_newtable(luaState_); + lua_setglobal(luaState_, "config"); + } + scriptDirectory_ = scriptPath_.parent_path(); if (!scriptDirectory_.empty()) { lua_getglobal(luaState_, "package"); @@ -227,6 +300,8 @@ void ScriptEngineService::RegisterBindings(lua_State* L) { bind("input_is_action_down", &ScriptEngineService::InputIsActionDown); bind("input_is_mouse_down", &ScriptEngineService::InputIsMouseDown); bind("input_get_text", &ScriptEngineService::InputGetText); + bind("config_get_json", &ScriptEngineService::ConfigGetJson); + bind("config_get_table", &ScriptEngineService::ConfigGetTable); bind("window_get_size", &ScriptEngineService::WindowGetSize); bind("window_set_title", &ScriptEngineService::WindowSetTitle); bind("window_is_minimized", &ScriptEngineService::WindowIsMinimized); @@ -564,6 +639,39 @@ int ScriptEngineService::InputGetText(lua_State* L) { return 1; } +int ScriptEngineService::ConfigGetJson(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->configService) { + lua_pushnil(L); + lua_pushstring(L, "Config service not available"); + return 2; + } + const std::string& json = context->configService->GetConfigJson(); + if (json.empty()) { + lua_pushnil(L); + lua_pushstring(L, "Config JSON not available"); + return 2; + } + lua_pushlstring(L, json.c_str(), json.size()); + return 1; +} + +int ScriptEngineService::ConfigGetTable(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->configService) { + lua_pushnil(L); + lua_pushstring(L, "Config service not available"); + return 2; + } + std::string error; + if (!PushConfigTableFromJson(L, context->configService->GetConfigJson(), error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + return 1; +} + int ScriptEngineService::WindowGetSize(lua_State* L) { auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); if (!context || !context->windowService) { diff --git a/src/services/impl/script_engine_service.hpp b/src/services/impl/script_engine_service.hpp index e7b5b75..3ca09e0 100644 --- a/src/services/impl/script_engine_service.hpp +++ b/src/services/impl/script_engine_service.hpp @@ -6,6 +6,7 @@ #include "../interfaces/i_physics_bridge_service.hpp" #include "../interfaces/i_input_service.hpp" #include "../interfaces/i_window_service.hpp" +#include "../interfaces/i_config_service.hpp" #include "../interfaces/i_logger.hpp" #include "../../di/lifecycle.hpp" #include @@ -29,6 +30,7 @@ public: std::shared_ptr physicsBridgeService, std::shared_ptr inputService, std::shared_ptr windowService, + std::shared_ptr configService, bool debugEnabled = false); ~ScriptEngineService() override; @@ -53,6 +55,7 @@ private: std::shared_ptr physicsBridgeService; std::shared_ptr inputService; std::shared_ptr windowService; + std::shared_ptr configService; std::shared_ptr logger; }; @@ -72,6 +75,8 @@ private: static int InputIsActionDown(lua_State* L); static int InputIsMouseDown(lua_State* L); static int InputGetText(lua_State* L); + static int ConfigGetJson(lua_State* L); + static int ConfigGetTable(lua_State* L); static int WindowGetSize(lua_State* L); static int WindowSetTitle(lua_State* L); static int WindowIsMinimized(lua_State* L); @@ -88,6 +93,7 @@ private: std::shared_ptr physicsBridgeService_; std::shared_ptr inputService_; std::shared_ptr windowService_; + std::shared_ptr configService_; std::filesystem::path scriptPath_; std::filesystem::path scriptDirectory_; bool debugEnabled_ = false; diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index 71e4039..122e852 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -65,6 +65,13 @@ public: * @return Mouse grab configuration */ virtual const MouseGrabConfig& GetMouseGrabConfig() const = 0; + + /** + * @brief Get the full JSON configuration as a string. + * + * @return JSON string (may be empty if unavailable) + */ + virtual const std::string& GetConfigJson() const = 0; }; } // namespace sdl3cpp::services