#include "script/script_engine.hpp" #include "app/audio_player.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace sdl3cpp::script { namespace detail { std::array ReadVector3(lua_State* L, int index) { std::array result{}; int absIndex = lua_absindex(L, index); size_t len = lua_rawlen(L, absIndex); if (len != 3) { throw std::runtime_error("Expected vector with 3 components"); } for (size_t i = 1; i <= 3; ++i) { lua_rawgeti(L, absIndex, static_cast(i)); if (!lua_isnumber(L, -1)) { lua_pop(L, 1); throw std::runtime_error("Vector component is not a number"); } result[i - 1] = static_cast(lua_tonumber(L, -1)); lua_pop(L, 1); } return result; } std::array ReadMatrix(lua_State* L, int index) { std::array result{}; int absIndex = lua_absindex(L, index); size_t len = lua_rawlen(L, absIndex); if (len != 16) { throw std::runtime_error("Expected 4x4 matrix with 16 components"); } for (size_t i = 1; i <= 16; ++i) { lua_rawgeti(L, absIndex, static_cast(i)); if (!lua_isnumber(L, -1)) { lua_pop(L, 1); throw std::runtime_error("Matrix component is not a number"); } result[i - 1] = static_cast(lua_tonumber(L, -1)); lua_pop(L, 1); } return result; } std::array ReadQuaternion(lua_State* L, int index) { std::array result{}; int absIndex = lua_absindex(L, index); size_t len = lua_rawlen(L, absIndex); if (len != 4) { throw std::runtime_error("Expected quaternion with 4 components"); } for (size_t i = 1; i <= 4; ++i) { lua_rawgeti(L, absIndex, static_cast(i)); if (!lua_isnumber(L, -1)) { lua_pop(L, 1); throw std::runtime_error("Quaternion component is not a number"); } result[i - 1] = static_cast(lua_tonumber(L, -1)); lua_pop(L, 1); } return result; } } // namespace detail namespace { struct MeshPayload { std::vector> positions; std::vector> colors; std::vector indices; }; bool TryLoadMeshPayload(const ScriptEngine* script, const std::string& requestedPath, MeshPayload& payload, std::string& error) { std::filesystem::path resolved(requestedPath); if (!resolved.is_absolute()) { resolved = script->GetScriptDirectory() / resolved; } std::error_code ec; resolved = std::filesystem::weakly_canonical(resolved, ec); if (ec) { error = "Failed to resolve mesh path: " + ec.message(); return false; } if (!std::filesystem::exists(resolved)) { error = "Mesh file not found: " + resolved.string(); return false; } Assimp::Importer importer; const aiScene* scene = importer.ReadFile( resolved.string(), aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_PreTransformVertices); if (!scene) { error = importer.GetErrorString() ? importer.GetErrorString() : "Assimp failed to load mesh"; return false; } if (scene->mNumMeshes == 0) { error = "Scene contains no meshes"; return false; } const aiMesh* mesh = scene->mMeshes[0]; if (!mesh->mNumVertices) { error = "Mesh contains no vertices"; return false; } payload.positions.reserve(mesh->mNumVertices); payload.colors.reserve(mesh->mNumVertices); payload.indices.reserve(mesh->mNumFaces * 3); aiColor3D defaultColor(0.6f, 0.8f, 1.0f); aiColor3D materialColor = defaultColor; if (mesh->mMaterialIndex < scene->mNumMaterials) { const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; aiColor4D diffuse; if (material && material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse) == AI_SUCCESS) { materialColor = aiColor3D(diffuse.r, diffuse.g, diffuse.b); } } for (unsigned i = 0; i < mesh->mNumVertices; ++i) { const aiVector3D& vertex = mesh->mVertices[i]; payload.positions.push_back({vertex.x, vertex.y, vertex.z}); aiColor3D color = materialColor; if (mesh->HasVertexColors(0) && mesh->mColors[0]) { const aiColor4D& vertexColor = mesh->mColors[0][i]; color = aiColor3D(vertexColor.r, vertexColor.g, vertexColor.b); } payload.colors.push_back({color.r, color.g, color.b}); } for (unsigned faceIndex = 0; faceIndex < mesh->mNumFaces; ++faceIndex) { const aiFace& face = mesh->mFaces[faceIndex]; if (face.mNumIndices != 3) { continue; } payload.indices.push_back(face.mIndices[0]); payload.indices.push_back(face.mIndices[1]); payload.indices.push_back(face.mIndices[2]); } if (payload.indices.empty()) { error = "Mesh contains no triangle faces"; return false; } return true; } glm::vec3 ToVec3(const std::array& value) { return glm::vec3(value[0], value[1], value[2]); } glm::quat ToQuat(const std::array& value) { return glm::quat(value[3], value[0], value[1], value[2]); } void PushMatrix(lua_State* L, const glm::mat4& matrix) { lua_newtable(L); const float* ptr = glm::value_ptr(matrix); for (int i = 0; i < 16; ++i) { lua_pushnumber(L, ptr[i]); lua_rawseti(L, -2, i + 1); } } int PushMeshToLua(lua_State* L, const MeshPayload& payload) { lua_newtable(L); lua_newtable(L); for (size_t vertexIndex = 0; vertexIndex < payload.positions.size(); ++vertexIndex) { lua_newtable(L); lua_newtable(L); for (int component = 0; component < 3; ++component) { lua_pushnumber(L, payload.positions[vertexIndex][component]); lua_rawseti(L, -2, component + 1); } lua_setfield(L, -2, "position"); lua_newtable(L); for (int component = 0; component < 3; ++component) { lua_pushnumber(L, payload.colors[vertexIndex][component]); lua_rawseti(L, -2, component + 1); } lua_setfield(L, -2, "color"); lua_rawseti(L, -2, static_cast(vertexIndex + 1)); } lua_setfield(L, -2, "vertices"); lua_newtable(L); for (size_t index = 0; index < payload.indices.size(); ++index) { lua_pushinteger(L, static_cast(payload.indices[index]) + 1); lua_rawseti(L, -2, static_cast(index + 1)); } lua_setfield(L, -2, "indices"); return 1; } int LuaLoadMeshFromFile(lua_State* L) { auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); const char* path = luaL_checkstring(L, 1); MeshPayload payload; std::string error; if (!TryLoadMeshPayload(script, path, payload, error)) { lua_pushnil(L); lua_pushstring(L, error.c_str()); return 2; } PushMeshToLua(L, payload); lua_pushnil(L); return 2; } int LuaPhysicsCreateBox(lua_State* L) { auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); const char* name = luaL_checkstring(L, 1); if (!lua_istable(L, 2) || !lua_istable(L, 4) || !lua_istable(L, 5)) { luaL_error(L, "physics_create_box expects vector tables for half extents, origin, and rotation"); } std::array halfExtents = detail::ReadVector3(L, 2); float mass = static_cast(luaL_checknumber(L, 3)); std::array origin = detail::ReadVector3(L, 4); std::array rotation = detail::ReadQuaternion(L, 5); btTransform transform; transform.setIdentity(); transform.setOrigin(btVector3(origin[0], origin[1], origin[2])); transform.setRotation(btQuaternion(rotation[0], rotation[1], rotation[2], rotation[3])); std::string error; if (!script->GetPhysicsBridge().addBoxRigidBody( name, btVector3(halfExtents[0], halfExtents[1], halfExtents[2]), mass, transform, error)) { lua_pushnil(L); lua_pushstring(L, error.c_str()); return 2; } lua_pushboolean(L, 1); return 1; } int LuaPhysicsStepSimulation(lua_State* L) { auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); float deltaTime = static_cast(luaL_checknumber(L, 1)); int steps = script->GetPhysicsBridge().stepSimulation(deltaTime); lua_pushinteger(L, steps); return 1; } int LuaPhysicsGetTransform(lua_State* L) { auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); const char* name = luaL_checkstring(L, 1); btTransform transform; std::string error; if (!script->GetPhysicsBridge().getRigidBodyTransform(name, transform, error)) { lua_pushnil(L); lua_pushstring(L, error.c_str()); return 2; } lua_newtable(L); lua_newtable(L); const btVector3& origin = transform.getOrigin(); lua_pushnumber(L, origin.x()); lua_rawseti(L, -2, 1); lua_pushnumber(L, origin.y()); lua_rawseti(L, -2, 2); lua_pushnumber(L, origin.z()); lua_rawseti(L, -2, 3); lua_setfield(L, -2, "position"); lua_newtable(L); const btQuaternion& orientation = transform.getRotation(); lua_pushnumber(L, orientation.x()); lua_rawseti(L, -2, 1); lua_pushnumber(L, orientation.y()); lua_rawseti(L, -2, 2); lua_pushnumber(L, orientation.z()); lua_rawseti(L, -2, 3); lua_pushnumber(L, orientation.w()); lua_rawseti(L, -2, 4); lua_setfield(L, -2, "rotation"); return 1; } int LuaAudioPlayBackground(lua_State* L) { auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); const char* path = luaL_checkstring(L, 1); bool loop = true; if (lua_gettop(L) >= 2 && lua_isboolean(L, 2)) { loop = lua_toboolean(L, 2); } std::string error; if (!script->QueueAudioCommand(ScriptEngine::AudioCommandType::Background, path, loop, error)) { lua_pushnil(L); lua_pushstring(L, error.c_str()); return 2; } lua_pushboolean(L, 1); return 1; } int LuaAudioPlaySound(lua_State* L) { auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); const char* path = luaL_checkstring(L, 1); bool loop = false; if (lua_gettop(L) >= 2 && lua_isboolean(L, 2)) { loop = lua_toboolean(L, 2); } std::string error; if (!script->QueueAudioCommand(ScriptEngine::AudioCommandType::Effect, path, loop, error)) { lua_pushnil(L); lua_pushstring(L, error.c_str()); return 2; } lua_pushboolean(L, 1); return 1; } int LuaGlmMatrixFromTransform(lua_State* L) { std::array translation = detail::ReadVector3(L, 1); std::array rotation = detail::ReadQuaternion(L, 2); glm::vec3 pos = ToVec3(translation); glm::quat quat = ToQuat(rotation); glm::mat4 matrix = glm::translate(glm::mat4(1.0f), pos) * glm::mat4_cast(quat); PushMatrix(L, matrix); return 1; } std::array IdentityMatrix() { return {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f}; } } // namespace ScriptEngine::ScriptEngine(const std::filesystem::path& scriptPath, bool debugEnabled) : L_(luaL_newstate()), scriptDirectory_(scriptPath.parent_path()), debugEnabled_(debugEnabled), physicsBridge_(std::make_unique()) { if (!L_) { throw std::runtime_error("Failed to create Lua state"); } luaL_openlibs(L_); lua_pushlightuserdata(L_, this); lua_pushcclosure(L_, &LuaLoadMeshFromFile, 1); lua_setglobal(L_, "load_mesh_from_file"); lua_pushlightuserdata(L_, this); lua_pushcclosure(L_, &LuaPhysicsCreateBox, 1); lua_setglobal(L_, "physics_create_box"); lua_pushlightuserdata(L_, this); lua_pushcclosure(L_, &LuaPhysicsStepSimulation, 1); lua_setglobal(L_, "physics_step_simulation"); lua_pushlightuserdata(L_, this); lua_pushcclosure(L_, &LuaPhysicsGetTransform, 1); lua_setglobal(L_, "physics_get_transform"); lua_pushlightuserdata(L_, this); lua_pushcclosure(L_, &LuaGlmMatrixFromTransform, 1); lua_setglobal(L_, "glm_matrix_from_transform"); lua_pushlightuserdata(L_, this); lua_pushcclosure(L_, &LuaAudioPlayBackground, 1); lua_setglobal(L_, "audio_play_background"); lua_pushlightuserdata(L_, this); lua_pushcclosure(L_, &LuaAudioPlaySound, 1); lua_setglobal(L_, "audio_play_sound"); lua_pushboolean(L_, debugEnabled_); lua_setglobal(L_, "lua_debug"); auto scriptDir = scriptPath.parent_path(); if (!scriptDir.empty()) { lua_getglobal(L_, "package"); if (lua_istable(L_, -1)) { lua_getfield(L_, -1, "path"); const char* currentPath = lua_tostring(L_, -1); std::string newPath = scriptDir.string() + "/?.lua;"; if (currentPath) { newPath += currentPath; } lua_pop(L_, 1); lua_pushstring(L_, newPath.c_str()); lua_setfield(L_, -2, "path"); } lua_pop(L_, 1); } if (luaL_dofile(L_, scriptPath.string().c_str()) != LUA_OK) { std::string message = LuaErrorMessage(L_); lua_pop(L_, 1); lua_close(L_); L_ = nullptr; throw std::runtime_error("Failed to load Lua script: " + message); } lua_getglobal(L_, "gui_input"); if (!lua_isnil(L_, -1)) { guiInputRef_ = luaL_ref(L_, LUA_REGISTRYINDEX); } else { lua_pop(L_, 1); } lua_getglobal(L_, "get_gui_commands"); if (lua_isfunction(L_, -1)) { guiCommandsFnRef_ = luaL_ref(L_, LUA_REGISTRYINDEX); } else { lua_pop(L_, 1); } } ScriptEngine::~ScriptEngine() { if (L_) { if (guiInputRef_ != LUA_REFNIL) { luaL_unref(L_, LUA_REGISTRYINDEX, guiInputRef_); } if (guiCommandsFnRef_ != LUA_REFNIL) { luaL_unref(L_, LUA_REGISTRYINDEX, guiCommandsFnRef_); } lua_close(L_); } } std::vector ScriptEngine::LoadSceneObjects() { lua_getglobal(L_, "get_scene_objects"); if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("Lua function 'get_scene_objects' is missing"); } if (lua_pcall(L_, 0, 1, 0) != LUA_OK) { std::string message = LuaErrorMessage(L_); lua_pop(L_, 1); throw std::runtime_error("Lua get_scene_objects failed: " + message); } if (!lua_istable(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("'get_scene_objects' did not return a table"); } size_t count = lua_rawlen(L_, -1); std::vector objects; objects.reserve(count); for (size_t i = 1; i <= count; ++i) { lua_rawgeti(L_, -1, static_cast(i)); if (!lua_istable(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("Scene object at index " + std::to_string(i) + " is not a table"); } SceneObject object; lua_getfield(L_, -1, "vertices"); object.vertices = ReadVertexArray(L_, -1); lua_pop(L_, 1); if (object.vertices.empty()) { lua_pop(L_, 1); throw std::runtime_error("Scene object " + std::to_string(i) + " must supply at least one vertex"); } lua_getfield(L_, -1, "indices"); object.indices = ReadIndexArray(L_, -1); lua_pop(L_, 1); if (object.indices.empty()) { lua_pop(L_, 1); throw std::runtime_error("Scene object " + std::to_string(i) + " must supply indices"); } lua_getfield(L_, -1, "compute_model_matrix"); if (lua_isfunction(L_, -1)) { object.computeModelMatrixRef = luaL_ref(L_, LUA_REGISTRYINDEX); } else { lua_pop(L_, 1); object.computeModelMatrixRef = LUA_REFNIL; } lua_getfield(L_, -1, "shader_key"); if (lua_isstring(L_, -1)) { object.shaderKey = lua_tostring(L_, -1); } lua_pop(L_, 1); objects.push_back(std::move(object)); lua_pop(L_, 1); } lua_pop(L_, 1); return objects; } std::array ScriptEngine::ComputeModelMatrix(int functionRef, float time) { if (functionRef == LUA_REFNIL) { lua_getglobal(L_, "compute_model_matrix"); if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); return IdentityMatrix(); } } else { lua_rawgeti(L_, LUA_REGISTRYINDEX, functionRef); } lua_pushnumber(L_, time); if (lua_pcall(L_, 1, 1, 0) != LUA_OK) { std::string message = LuaErrorMessage(L_); lua_pop(L_, 1); throw std::runtime_error("Lua compute_model_matrix failed: " + message); } if (!lua_istable(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("'compute_model_matrix' did not return a table"); } std::array matrix = detail::ReadMatrix(L_, -1); lua_pop(L_, 1); return matrix; } std::array ScriptEngine::GetViewProjectionMatrix(float aspect) { lua_getglobal(L_, "get_view_projection"); if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("Lua function 'get_view_projection' is missing"); } lua_pushnumber(L_, aspect); if (lua_pcall(L_, 1, 1, 0) != LUA_OK) { std::string message = LuaErrorMessage(L_); lua_pop(L_, 1); throw std::runtime_error("Lua get_view_projection failed: " + message); } if (!lua_istable(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("'get_view_projection' did not return a table"); } std::array matrix = detail::ReadMatrix(L_, -1); lua_pop(L_, 1); return matrix; } std::vector ScriptEngine::ReadVertexArray(lua_State* L, int index) { int absIndex = lua_absindex(L, index); if (!lua_istable(L, absIndex)) { throw std::runtime_error("Expected table for vertex data"); } size_t count = lua_rawlen(L, absIndex); std::vector vertices; vertices.reserve(count); for (size_t i = 1; i <= count; ++i) { lua_rawgeti(L, absIndex, static_cast(i)); if (!lua_istable(L, -1)) { lua_pop(L, 1); throw std::runtime_error("Vertex entry at index " + std::to_string(i) + " is not a table"); } int vertexIndex = lua_gettop(L); core::Vertex vertex{}; lua_getfield(L, vertexIndex, "position"); vertex.position = detail::ReadVector3(L, -1); lua_pop(L, 1); lua_getfield(L, vertexIndex, "color"); vertex.color = detail::ReadVector3(L, -1); lua_pop(L, 1); lua_pop(L, 1); vertices.push_back(vertex); } return vertices; } std::vector ScriptEngine::ReadIndexArray(lua_State* L, int index) { int absIndex = lua_absindex(L, index); if (!lua_istable(L, absIndex)) { throw std::runtime_error("Expected table for index data"); } size_t count = lua_rawlen(L, absIndex); std::vector indices; indices.reserve(count); for (size_t i = 1; i <= count; ++i) { lua_rawgeti(L, absIndex, static_cast(i)); if (!lua_isinteger(L, -1)) { lua_pop(L, 1); throw std::runtime_error("Index entry at position " + std::to_string(i) + " is not an integer"); } lua_Integer value = lua_tointeger(L, -1); lua_pop(L, 1); if (value < 1) { throw std::runtime_error("Index values must be 1 or greater"); } indices.push_back(static_cast(value - 1)); } return indices; } std::unordered_map ScriptEngine::LoadShaderPathsMap() { lua_getglobal(L_, "get_shader_paths"); if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("Lua function 'get_shader_paths' is missing"); } if (lua_pcall(L_, 0, 1, 0) != LUA_OK) { std::string message = LuaErrorMessage(L_); lua_pop(L_, 1); throw std::runtime_error("Lua get_shader_paths failed: " + message); } if (!lua_istable(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("'get_shader_paths' did not return a table"); } std::unordered_map shaderMap; lua_pushnil(L_); while (lua_next(L_, -2) != 0) { if (lua_isstring(L_, -2) && lua_istable(L_, -1)) { std::string key = lua_tostring(L_, -2); shaderMap.emplace(key, ReadShaderPathsTable(L_, -1)); } lua_pop(L_, 1); } lua_pop(L_, 1); if (shaderMap.empty()) { throw std::runtime_error("'get_shader_paths' did not return any shader variants"); } return shaderMap; } ScriptEngine::ShaderPaths ScriptEngine::ReadShaderPathsTable(lua_State* L, int index) { ShaderPaths paths; int absIndex = lua_absindex(L, index); lua_getfield(L, absIndex, "vertex"); if (!lua_isstring(L, -1)) { lua_pop(L, 1); throw std::runtime_error("Shader path 'vertex' must be a string"); } paths.vertex = lua_tostring(L, -1); lua_pop(L, 1); lua_getfield(L, absIndex, "fragment"); if (!lua_isstring(L, -1)) { lua_pop(L, 1); throw std::runtime_error("Shader path 'fragment' must be a string"); } paths.fragment = lua_tostring(L, -1); lua_pop(L, 1); return paths; } std::string ScriptEngine::LuaErrorMessage(lua_State* L) { const char* message = lua_tostring(L, -1); return message ? message : "unknown lua error"; } PhysicsBridge& ScriptEngine::GetPhysicsBridge() { return *physicsBridge_; } std::vector ScriptEngine::LoadGuiCommands() { std::vector commands; if (guiCommandsFnRef_ == LUA_REFNIL) { return commands; } lua_rawgeti(L_, LUA_REGISTRYINDEX, guiCommandsFnRef_); if (lua_pcall(L_, 0, 1, 0) != LUA_OK) { std::string message = LuaErrorMessage(L_); lua_pop(L_, 1); throw std::runtime_error("Lua get_gui_commands failed: " + message); } if (!lua_istable(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("'get_gui_commands' did not return a table"); } size_t count = lua_rawlen(L_, -1); commands.reserve(count); for (size_t i = 1; i <= count; ++i) { lua_rawgeti(L_, -1, static_cast(i)); if (!lua_istable(L_, -1)) { lua_pop(L_, 1); throw std::runtime_error("GUI command at index " + std::to_string(i) + " is not a table"); } int commandIndex = lua_gettop(L_); lua_getfield(L_, commandIndex, "type"); const char* typeName = lua_tostring(L_, -1); if (!typeName) { lua_pop(L_, 2); throw std::runtime_error("GUI command at index " + std::to_string(i) + " is missing a type"); } GuiCommand command{}; if (std::strcmp(typeName, "rect") == 0) { command.type = GuiCommand::Type::Rect; command.rect = ReadRect(L_, commandIndex); command.color = ReadColor(L_, commandIndex, GuiColor{0.0f, 0.0f, 0.0f, 1.0f}); command.borderColor = ReadColor(L_, commandIndex, GuiColor{0.0f, 0.0f, 0.0f, 0.0f}); lua_getfield(L_, commandIndex, "borderWidth"); if (lua_isnumber(L_, -1)) { command.borderWidth = static_cast(lua_tonumber(L_, -1)); } lua_pop(L_, 1); } else if (std::strcmp(typeName, "text") == 0) { command.type = GuiCommand::Type::Text; ReadStringField(L_, commandIndex, "text", command.text); lua_getfield(L_, commandIndex, "fontSize"); if (lua_isnumber(L_, -1)) { command.fontSize = static_cast(lua_tonumber(L_, -1)); } lua_pop(L_, 1); std::string align; if (ReadStringField(L_, commandIndex, "alignX", align)) { command.alignX = align; } if (ReadStringField(L_, commandIndex, "alignY", align)) { command.alignY = align; } lua_getfield(L_, commandIndex, "clipRect"); if (lua_istable(L_, -1)) { command.clipRect = ReadRect(L_, -1); command.hasClipRect = true; } lua_pop(L_, 1); lua_getfield(L_, commandIndex, "bounds"); if (lua_istable(L_, -1)) { command.bounds = ReadRect(L_, -1); command.hasBounds = true; } lua_pop(L_, 1); command.color = ReadColor(L_, commandIndex, GuiColor{1.0f, 1.0f, 1.0f, 1.0f}); } else if (std::strcmp(typeName, "clip_push") == 0) { command.type = GuiCommand::Type::ClipPush; command.rect = ReadRect(L_, commandIndex); } else if (std::strcmp(typeName, "clip_pop") == 0) { command.type = GuiCommand::Type::ClipPop; } else if (std::strcmp(typeName, "svg") == 0) { command.type = GuiCommand::Type::Svg; ReadStringField(L_, commandIndex, "path", command.svgPath); command.rect = ReadRect(L_, commandIndex); command.svgTint = ReadColor(L_, commandIndex, GuiColor{1.0f, 1.0f, 1.0f, 0.0f}); lua_getfield(L_, commandIndex, "tint"); if (lua_istable(L_, -1)) { command.svgTint = ReadColor(L_, -1, command.svgTint); } lua_pop(L_, 1); } lua_pop(L_, 1); lua_pop(L_, 1); commands.push_back(std::move(command)); } lua_pop(L_, 1); return commands; } void ScriptEngine::UpdateGuiInput(const GuiInputSnapshot& input) { if (guiInputRef_ == LUA_REFNIL) { return; } lua_rawgeti(L_, LUA_REGISTRYINDEX, guiInputRef_); int stateIndex = lua_gettop(L_); lua_getfield(L_, stateIndex, "resetTransient"); lua_pushvalue(L_, stateIndex); lua_call(L_, 1, 0); lua_getfield(L_, stateIndex, "setMouse"); lua_pushvalue(L_, stateIndex); lua_pushnumber(L_, input.mouseX); lua_pushnumber(L_, input.mouseY); lua_pushboolean(L_, input.mouseDown); lua_call(L_, 4, 0); lua_getfield(L_, stateIndex, "setWheel"); lua_pushvalue(L_, stateIndex); lua_pushnumber(L_, input.wheel); lua_call(L_, 2, 0); if (!input.textInput.empty()) { lua_getfield(L_, stateIndex, "addTextInput"); lua_pushvalue(L_, stateIndex); lua_pushstring(L_, input.textInput.c_str()); lua_call(L_, 2, 0); } for (const auto& [key, pressed] : input.keyStates) { lua_getfield(L_, stateIndex, "setKey"); lua_pushvalue(L_, stateIndex); lua_pushstring(L_, key.c_str()); lua_pushboolean(L_, pressed); lua_call(L_, 3, 0); } lua_pop(L_, 1); } bool ScriptEngine::HasGuiCommands() const { return guiCommandsFnRef_ != LUA_REFNIL; } std::filesystem::path ScriptEngine::GetScriptDirectory() const { return scriptDirectory_; } GuiCommand::RectData ScriptEngine::ReadRect(lua_State* L, int index) { GuiCommand::RectData rect{}; if (!lua_istable(L, index)) { return rect; } int absIndex = lua_absindex(L, index); auto readField = [&](const char* name, float defaultValue) -> float { lua_getfield(L, absIndex, name); float value = defaultValue; if (lua_isnumber(L, -1)) { value = static_cast(lua_tonumber(L, -1)); } lua_pop(L, 1); return value; }; rect.x = readField("x", rect.x); rect.y = readField("y", rect.y); rect.width = readField("width", rect.width); rect.height = readField("height", rect.height); return rect; } GuiColor ScriptEngine::ReadColor(lua_State* L, int index, const GuiColor& defaultColor) { GuiColor color = defaultColor; if (!lua_istable(L, index)) { return color; } int absIndex = lua_absindex(L, index); for (int component = 0; component < 4; ++component) { lua_rawgeti(L, absIndex, component + 1); if (lua_isnumber(L, -1)) { float value = static_cast(lua_tonumber(L, -1)); switch (component) { case 0: color.r = value; break; case 1: color.g = value; break; case 2: color.b = value; break; case 3: color.a = value; break; } } lua_pop(L, 1); } return color; } bool ScriptEngine::ReadStringField(lua_State* L, int index, const char* name, std::string& outString) { int absIndex = lua_absindex(L, index); lua_getfield(L, absIndex, name); if (lua_isstring(L, -1)) { outString = lua_tostring(L, -1); lua_pop(L, 1); return true; } lua_pop(L, 1); return false; } void ScriptEngine::SetAudioPlayer(app::AudioPlayer* audioPlayer) { audioPlayer_ = audioPlayer; if (!audioPlayer_) { return; } for (const auto& command : pendingAudioCommands_) { try { ExecuteAudioCommand(audioPlayer_, command); } catch (const std::exception& exc) { std::cerr << "AudioPlayer: " << exc.what() << '\n'; } } pendingAudioCommands_.clear(); } bool ScriptEngine::QueueAudioCommand(AudioCommandType type, std::string path, bool loop, std::string& error) { if (audioPlayer_) { try { AudioCommand command{type, std::move(path), loop}; ExecuteAudioCommand(audioPlayer_, command); return true; } catch (const std::exception& exc) { error = exc.what(); return false; } } pendingAudioCommands_.push_back(AudioCommand{type, std::move(path), loop}); return true; } void ScriptEngine::ExecuteAudioCommand(app::AudioPlayer* player, const AudioCommand& command) { auto resolved = ResolveScriptPath(command.path); if (!std::filesystem::exists(resolved)) { throw std::runtime_error("Audio file not found: " + resolved.string()); } switch (command.type) { case AudioCommandType::Background: player->PlayBackground(resolved, command.loop); break; case AudioCommandType::Effect: player->PlayEffect(resolved, command.loop); break; } } std::filesystem::path ScriptEngine::ResolveScriptPath(const std::string& requested) const { std::filesystem::path resolved(requested); if (!resolved.is_absolute()) { resolved = scriptDirectory_ / resolved; } std::error_code ec; auto canonical = std::filesystem::weakly_canonical(resolved, ec); if (!ec) { resolved = canonical; } return resolved; } } // namespace sdl3cpp::script