From ee90559e8c746c2fe4132cbee5ca919f5a5bbd04 Mon Sep 17 00:00:00 2001 From: Anemunt <69436164+darkresident55@users.noreply.github.com> Date: Fri, 12 Dec 2025 00:06:21 -0500 Subject: [PATCH] C++ Compilation! --- .gitmodules | 3 - CMakeLists.txt | 2 + Scripts/SampleInspector.cpp | 45 ++++++ src/Engine.cpp | 97 +++++++++++- src/Engine.h | 12 ++ src/EnginePanels.cpp | 144 ++++++++++++++++++ src/ProjectManager.cpp | 100 +++++++++++++ src/ProjectManager.h | 1 + src/SceneObject.h | 12 ++ src/ScriptCompiler.cpp | 286 ++++++++++++++++++++++++++++++++++++ src/ScriptCompiler.h | 40 +++++ src/ScriptRuntime.cpp | 97 ++++++++++++ src/ScriptRuntime.h | 35 +++++ src/ThirdParty/gl3d | 1 - 14 files changed, 865 insertions(+), 10 deletions(-) create mode 100644 Scripts/SampleInspector.cpp create mode 100644 src/ScriptCompiler.cpp create mode 100644 src/ScriptCompiler.h create mode 100644 src/ScriptRuntime.cpp create mode 100644 src/ScriptRuntime.h delete mode 160000 src/ThirdParty/gl3d diff --git a/.gitmodules b/.gitmodules index d3dd102..d89afde 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,6 +11,3 @@ [submodule "src/ThirdParty/assimp"] path = src/ThirdParty/assimp url = https://github.com/assimp/assimp.git -[submodule "src/ThirdParty/gl3d"] - path = src/ThirdParty/gl3d - url = https://github.com/meemknight/gl3d.git diff --git a/CMakeLists.txt b/CMakeLists.txt index c173bd4..dca231b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,8 @@ if(NOT WIN32) Xinerama Xcursor ) + # Export symbols so runtime-loaded scripts can resolve ImGui/engine symbols. + target_link_options(Modularity PRIVATE "-rdynamic") else() target_link_libraries(Modularity PRIVATE core glfw OpenGL::GL) endif() diff --git a/Scripts/SampleInspector.cpp b/Scripts/SampleInspector.cpp new file mode 100644 index 0000000..b6a373a --- /dev/null +++ b/Scripts/SampleInspector.cpp @@ -0,0 +1,45 @@ +// Minimal sample script demonstrating Script_OnInspector usage. +// Build via the engine’s “Compile Script” action or: +// Linux: g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Cache/ScriptBin/SampleInspector.o +// g++ -shared ../Cache/ScriptBin/SampleInspector.o -o ../Cache/ScriptBin/SampleInspector.so -ldl -lpthread +// Windows: cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\src /I ..\include /c SampleInspector.cpp /Fo ..\Cache\ScriptBin\SampleInspector.obj +// link /nologo /DLL ..\Cache\ScriptBin\SampleInspector.obj /OUT:..\Cache\ScriptBin\SampleInspector.dll User32.lib Advapi32.lib + +#include "ScriptRuntime.h" +#include "SceneObject.h" +#include "ThirdParty/imgui/imgui.h" +#include + +extern "C" void Script_OnInspector(ScriptContext& ctx) { + static bool autoRotate = false; + static glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f); + static glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f); + static char targetName[128] = "MyTarget"; + + ImGui::TextUnformatted("SampleInspector"); + ImGui::Separator(); + + ImGui::Checkbox("Auto Rotate", &autoRotate); + ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f); + ImGui::DragFloat3("Offset", &offset.x, 0.1f); + + ImGui::InputText("Target Name", targetName, sizeof(targetName)); + + if (ctx.object) { + ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id); + + if (ImGui::Button("Apply Offset To Self")) { + ctx.SetPosition(ctx.object->position + offset); + } + } + + if (ImGui::Button("Nudge Target")) { + if (SceneObject* target = ctx.FindObjectByName(targetName)) { + target->position += offset; + } + } + + if (autoRotate && ctx.object) { + ctx.SetRotation(ctx.object->rotation + spinSpeed * (1.0f / 60.0f)); + } +} diff --git a/src/Engine.cpp b/src/Engine.cpp index e707247..f777cec 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -709,8 +709,8 @@ void Engine::OpenProjectPath(const std::string& path) { } loadRecentScenes(); - fileBrowser.setProjectRoot(projectManager.currentProject.assetsPath); - fileBrowser.currentPath = projectManager.currentProject.assetsPath; + fileBrowser.setProjectRoot(projectManager.currentProject.projectPath); + fileBrowser.currentPath = projectManager.currentProject.projectPath; fileBrowser.needsRefresh = true; showLauncher = false; addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info); @@ -747,8 +747,8 @@ void Engine::createNewProject(const char* name, const char* location) { addObject(ObjectType::Cube, "Cube"); - fileBrowser.setProjectRoot(projectManager.currentProject.assetsPath); - fileBrowser.currentPath = projectManager.currentProject.assetsPath; + fileBrowser.setProjectRoot(projectManager.currentProject.projectPath); + fileBrowser.currentPath = projectManager.currentProject.projectPath; fileBrowser.needsRefresh = true; showLauncher = false; @@ -784,8 +784,8 @@ void Engine::loadRecentScenes() { } recordState("sceneLoaded"); - fileBrowser.setProjectRoot(projectManager.currentProject.assetsPath); - fileBrowser.currentPath = projectManager.currentProject.assetsPath; + fileBrowser.setProjectRoot(projectManager.currentProject.projectPath); + fileBrowser.currentPath = projectManager.currentProject.projectPath; fileBrowser.needsRefresh = true; } @@ -993,6 +993,91 @@ void Engine::logToConsole(const std::string& message) { addConsoleMessage(message, ConsoleMessageType::Info); } +SceneObject* Engine::findObjectByName(const std::string& name) { + auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(), [&](const SceneObject& o) { + return o.name == name; + }); + if (it != sceneObjects.end()) return &(*it); + return nullptr; +} + +SceneObject* Engine::findObjectById(int id) { + auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(), [&](const SceneObject& o) { + return o.id == id; + }); + if (it != sceneObjects.end()) return &(*it); + return nullptr; +} + +fs::path Engine::resolveScriptBinary(const fs::path& sourcePath) { + ScriptBuildConfig config; + std::string error; + fs::path cfg = projectManager.currentProject.scriptsConfigPath.empty() + ? projectManager.currentProject.projectPath / "Scripts.modu" + : projectManager.currentProject.scriptsConfigPath; + if (!scriptCompiler.loadConfig(cfg, config, error)) { + return {}; + } + ScriptBuildCommands cmds; + if (!scriptCompiler.makeCommands(config, sourcePath, cmds, error)) { + return {}; + } + return cmds.binaryPath; +} + +void Engine::compileScriptFile(const fs::path& scriptPath) { + if (!projectManager.currentProject.isLoaded) { + addConsoleMessage("No project is loaded", ConsoleMessageType::Warning); + return; + } + + showCompilePopup = true; + lastCompileLog.clear(); + lastCompileStatus = "Compiling " + scriptPath.filename().string(); + + fs::path configPath = projectManager.currentProject.scriptsConfigPath; + if (configPath.empty()) { + configPath = projectManager.currentProject.projectPath / "Scripts.modu"; + } + + ScriptBuildConfig config; + std::string error; + if (!scriptCompiler.loadConfig(configPath, config, error)) { + lastCompileSuccess = false; + lastCompileLog = error; + addConsoleMessage("Script config error: " + error, ConsoleMessageType::Error); + return; + } + + ScriptBuildCommands commands; + if (!scriptCompiler.makeCommands(config, scriptPath, commands, error)) { + lastCompileSuccess = false; + lastCompileLog = error; + addConsoleMessage("Script build error: " + error, ConsoleMessageType::Error); + return; + } + + ScriptCompileOutput output; + if (!scriptCompiler.compile(commands, output, error)) { + lastCompileSuccess = false; + lastCompileStatus = "Compile failed"; + lastCompileLog = output.compileLog + output.linkLog + error; + addConsoleMessage("Compile failed: " + error, ConsoleMessageType::Error); + if (!output.compileLog.empty()) addConsoleMessage(output.compileLog, ConsoleMessageType::Info); + if (!output.linkLog.empty()) addConsoleMessage(output.linkLog, ConsoleMessageType::Info); + return; + } + + scriptRuntime.unloadAll(); + + lastCompileSuccess = true; + lastCompileStatus = "Reloading EngineRoot"; + lastCompileLog = output.compileLog + output.linkLog; + addConsoleMessage("Compiled script -> " + commands.binaryPath.string(), ConsoleMessageType::Success); + if (!output.compileLog.empty()) addConsoleMessage(output.compileLog, ConsoleMessageType::Info); + if (!output.linkLog.empty()) addConsoleMessage(output.linkLog, ConsoleMessageType::Info); +} + void Engine::setupImGui() { std::cerr << "[DEBUG] setupImGui: getting primary monitor..." << std::endl; float mainScale = 1.0f; diff --git a/src/Engine.h b/src/Engine.h index 5c6f00f..b65f8ff 100644 --- a/src/Engine.h +++ b/src/Engine.h @@ -7,6 +7,8 @@ #include "ProjectManager.h" #include "EditorUI.h" #include "MeshBuilder.h" +#include "ScriptCompiler.h" +#include "ScriptRuntime.h" #include "../include/Window/Window.h" void window_size_callback(GLFWwindow* window, int width, int height); @@ -101,6 +103,12 @@ private: std::vector meshEditSelectedFaces; // indices into mesh faces enum class MeshEditSelectionMode { Vertex = 0, Edge = 1, Face = 2 }; MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex; + ScriptCompiler scriptCompiler; + ScriptRuntime scriptRuntime; + bool showCompilePopup = false; + bool lastCompileSuccess = false; + std::string lastCompileStatus; + std::string lastCompileLog; // Private methods SceneObject* getSelectedObject(); @@ -134,6 +142,7 @@ private: void renderDialogs(); void renderProjectBrowserPanel(); Camera makeCameraFromObject(const SceneObject& obj) const; + void compileScriptFile(const fs::path& scriptPath); void renderFileBrowserToolbar(); void renderFileBrowserBreadcrumb(); @@ -188,4 +197,7 @@ public: bool init(); void run(); void shutdown(); + SceneObject* findObjectByName(const std::string& name); + SceneObject* findObjectById(int id); + fs::path resolveScriptBinary(const fs::path& sourcePath); }; diff --git a/src/EnginePanels.cpp b/src/EnginePanels.cpp index a35518a..d63e33c 100644 --- a/src/EnginePanels.cpp +++ b/src/EnginePanels.cpp @@ -618,6 +618,11 @@ void Engine::renderFileBrowserPanel() { } } } + if (fileBrowser.getFileCategory(entry) == FileCategory::Script) { + if (ImGui::MenuItem("Compile Script")) { + compileScriptFile(entry.path()); + } + } ImGui::Separator(); if (ImGui::MenuItem("Show in Explorer")) { #ifdef _WIN32 @@ -726,6 +731,11 @@ void Engine::renderFileBrowserPanel() { } } } + if (fileBrowser.getFileCategory(entry) == FileCategory::Script) { + if (ImGui::MenuItem("Compile Script")) { + compileScriptFile(entry.path()); + } + } ImGui::Separator(); if (ImGui::MenuItem("Show in Explorer")) { #ifdef _WIN32 @@ -2294,6 +2304,120 @@ void Engine::renderInspectorPanel() { ImGui::PopStyleColor(); } + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.4f, 0.35f, 0.55f, 1.0f)); + if (ImGui::CollapsingHeader("Scripts", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(10.0f); + + bool changed = false; + if (ImGui::Button("Add Script", ImVec2(-1, 0))) { + obj.scripts.push_back(ScriptComponent{}); + changed = true; + } + + for (size_t i = 0; i < obj.scripts.size(); ++i) { + ImGui::Separator(); + ImGui::PushID(static_cast(i)); + ScriptComponent& sc = obj.scripts[i]; + + char pathBuf[512] = {}; + std::snprintf(pathBuf, sizeof(pathBuf), "%s", sc.path.c_str()); + ImGui::Text("Script %zu", i + 1); + ImGui::SetNextItemWidth(-140); + if (ImGui::InputText("##ScriptPath", pathBuf, sizeof(pathBuf))) { + sc.path = pathBuf; + changed = true; + } + + ImGui::SameLine(); + if (ImGui::SmallButton("Use Selection")) { + if (!fileBrowser.selectedFile.empty()) { + fs::directory_entry entry(fileBrowser.selectedFile); + if (fileBrowser.getFileCategory(entry) == FileCategory::Script) { + sc.path = entry.path().string(); + changed = true; + } + } + } + + ImGui::SameLine(); + ImGui::BeginDisabled(sc.path.empty()); + if (ImGui::SmallButton("Compile")) { + compileScriptFile(sc.path); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::SmallButton("Remove")) { + obj.scripts.erase(obj.scripts.begin() + static_cast(i)); + changed = true; + ImGui::PopID(); + break; + } + + if (!sc.path.empty()) { + fs::path binary = resolveScriptBinary(sc.path); + sc.lastBinaryPath = binary.string(); + ScriptRuntime::InspectorFn inspector = scriptRuntime.getInspector(binary); + if (inspector) { + ImGui::Separator(); + ImGui::TextDisabled("Inspector (from script)"); + ScriptContext ctx; + ctx.engine = this; + ctx.object = &obj; + inspector(ctx); + } else if (!scriptRuntime.getLastError().empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed"); + ImGui::TextWrapped("%s", scriptRuntime.getLastError().c_str()); + } else { + ImGui::TextDisabled("No inspector exported (Script_OnInspector)"); + } + } + + ImGui::TextDisabled("Settings"); + for (size_t s = 0; s < sc.settings.size(); ++s) { + ImGui::PushID(static_cast(s)); + char keyBuf[128] = {}; + char valBuf[256] = {}; + std::snprintf(keyBuf, sizeof(keyBuf), "%s", sc.settings[s].key.c_str()); + std::snprintf(valBuf, sizeof(valBuf), "%s", sc.settings[s].value.c_str()); + ImGui::SetNextItemWidth(140); + if (ImGui::InputText("##Key", keyBuf, sizeof(keyBuf))) { + sc.settings[s].key = keyBuf; + changed = true; + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(-100); + if (ImGui::InputText("##Value", valBuf, sizeof(valBuf))) { + sc.settings[s].value = valBuf; + changed = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton("X")) { + sc.settings.erase(sc.settings.begin() + static_cast(s)); + changed = true; + ImGui::PopID(); + break; + } + ImGui::PopID(); + } + + if (ImGui::SmallButton("Add Setting")) { + sc.settings.push_back(ScriptSetting{"", ""}); + changed = true; + } + + ImGui::PopID(); + } + + if (changed) { + projectManager.currentProject.hasUnsavedChanges = true; + } + + ImGui::Unindent(10.0f); + } + ImGui::PopStyleColor(); + if (browserHasMaterial) { ImGui::Spacing(); renderMaterialAssetPanel("Material Asset (File Browser)", true); @@ -3682,6 +3806,26 @@ void Engine::renderDialogs() { ImGui::End(); } + if (showCompilePopup) { + ImGuiIO& io = ImGui::GetIO(); + ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(520, 240), ImGuiCond_FirstUseEver); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoSavedSettings; + if (ImGui::Begin("Script Compile", &showCompilePopup, flags)) { + ImGui::TextWrapped("%s", lastCompileStatus.c_str()); + ImGui::Separator(); + ImGui::BeginChild("CompileLog", ImVec2(0, -40), true); + ImGui::TextUnformatted(lastCompileLog.c_str()); + ImGui::EndChild(); + ImGui::Spacing(); + if (ImGui::Button("Close", ImVec2(80, 0))) { + showCompilePopup = false; + } + } + ImGui::End(); + } + if (showSaveSceneAsDialog) { ImGuiIO& io = ImGui::GetIO(); ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); diff --git a/src/ProjectManager.cpp b/src/ProjectManager.cpp index 057a7c3..8e4427b 100644 --- a/src/ProjectManager.cpp +++ b/src/ProjectManager.cpp @@ -9,6 +9,7 @@ Project::Project(const std::string& projectName, const fs::path& basePath) scenesPath = projectPath / "Scenes"; assetsPath = projectPath / "Assets"; scriptsPath = projectPath / "Scripts"; + scriptsConfigPath = projectPath / "Scripts.modu"; } bool Project::create() { @@ -20,9 +21,29 @@ bool Project::create() { fs::create_directories(assetsPath / "Models"); fs::create_directories(assetsPath / "Shaders"); fs::create_directories(scriptsPath); + fs::create_directories(projectPath / "Cache" / "ScriptBin"); saveProjectFile(); + // Initialize a default scripting build file + fs::path engineRoot = fs::current_path(); + std::ofstream scriptCfg(scriptsConfigPath); + scriptCfg << "# Scripts.modu\n"; + scriptCfg << "cppStandard=c++20\n"; + scriptCfg << "scriptsDir=Scripts\n"; + scriptCfg << "outDir=Cache/ScriptBin\n"; + scriptCfg << "includeDir=" << (engineRoot / "src").string() << "\n"; + scriptCfg << "includeDir=" << (engineRoot / "include").string() << "\n"; + scriptCfg << "includeDir=" << (engineRoot / "src/ThirdParty").string() << "\n"; + scriptCfg << "includeDir=" << (engineRoot / "src/ThirdParty/glm").string() << "\n"; + scriptCfg << "define=MODU_SCRIPTING=1\n"; + scriptCfg << "define=MODU_PROJECT_NAME=\"" << name << "\"\n"; + scriptCfg << "linux.linkLib=pthread\n"; + scriptCfg << "linux.linkLib=dl\n"; + scriptCfg << "win.linkLib=User32.lib\n"; + scriptCfg << "win.linkLib=Advapi32.lib\n"; + scriptCfg.close(); + currentSceneName = "Main"; isLoaded = true; return true; @@ -38,6 +59,7 @@ bool Project::load(const fs::path& projectFilePath) { scenesPath = projectPath / "Scenes"; assetsPath = projectPath / "Assets"; scriptsPath = projectPath / "Scripts"; + scriptsConfigPath = projectPath / "Scripts.modu"; std::ifstream file(projectFilePath); if (!file.is_open()) return false; @@ -262,6 +284,15 @@ bool SceneSerializer::saveScene(const fs::path& filePath, file << "vertexShader=" << obj.vertexShaderPath << "\n"; file << "fragmentShader=" << obj.fragmentShaderPath << "\n"; file << "useOverlay=" << (obj.useOverlay ? 1 : 0) << "\n"; + file << "scripts=" << obj.scripts.size() << "\n"; + for (size_t si = 0; si < obj.scripts.size(); ++si) { + const auto& sc = obj.scripts[si]; + file << "script" << si << "_path=" << sc.path << "\n"; + file << "script" << si << "_settings=" << sc.settings.size() << "\n"; + for (size_t k = 0; k < sc.settings.size(); ++k) { + file << "script" << si << "_setting" << k << "=" << sc.settings[k].key << ":" << sc.settings[k].value << "\n"; + } + } file << "lightColor=" << obj.light.color.r << "," << obj.light.color.g << "," << obj.light.color.b << "\n"; file << "lightIntensity=" << obj.light.intensity << "\n"; file << "lightRange=" << obj.light.range << "\n"; @@ -287,6 +318,16 @@ bool SceneSerializer::saveScene(const fs::path& filePath, file << "postMotionBlurEnabled=" << (obj.postFx.motionBlurEnabled ? 1 : 0) << "\n"; file << "postMotionBlurStrength=" << obj.postFx.motionBlurStrength << "\n"; } + + file << "scriptCount=" << obj.scripts.size() << "\n"; + for (size_t s = 0; s < obj.scripts.size(); ++s) { + const auto& sc = obj.scripts[s]; + file << "script" << s << "_path=" << sc.path << "\n"; + file << "script" << s << "_settingCount=" << sc.settings.size() << "\n"; + for (size_t si = 0; si < sc.settings.size(); ++si) { + file << "script" << s << "_setting" << si << "=" << sc.settings[si].key << ":" << sc.settings[si].value << "\n"; + } + } if ((obj.type == ObjectType::OBJMesh || obj.type == ObjectType::Model) && !obj.meshPath.empty()) { file << "meshPath=" << obj.meshPath << "\n"; @@ -397,6 +438,35 @@ bool SceneSerializer::loadScene(const fs::path& filePath, currentObj->fragmentShaderPath = value; } else if (key == "useOverlay") { currentObj->useOverlay = (std::stoi(value) != 0); + } else if (key == "scripts") { + int count = std::stoi(value); + currentObj->scripts.resize(std::max(0, count)); + } else if (key.rfind("script", 0) == 0) { + size_t underscore = key.find('_'); + if (underscore != std::string::npos && underscore > 6) { + int idx = std::stoi(key.substr(6, underscore - 6)); + if (idx >= 0 && idx < (int)currentObj->scripts.size()) { + std::string sub = key.substr(underscore + 1); + ScriptComponent& sc = currentObj->scripts[idx]; + if (sub == "path") { + sc.path = value; + } else if (sub == "settings") { + int cnt = std::stoi(value); + sc.settings.resize(std::max(0, cnt)); + } else if (sub.rfind("setting", 0) == 0) { + int sIdx = std::stoi(sub.substr(7)); + if (sIdx >= 0 && sIdx < (int)sc.settings.size()) { + size_t sep = value.find(':'); + if (sep != std::string::npos) { + sc.settings[sIdx].key = value.substr(0, sep); + sc.settings[sIdx].value = value.substr(sep + 1); + } else { + sc.settings[sIdx].value = value; + } + } + } + } + } } else if (key == "lightColor") { sscanf(value.c_str(), "%f,%f,%f", ¤tObj->light.color.r, @@ -451,6 +521,36 @@ bool SceneSerializer::loadScene(const fs::path& filePath, currentObj->postFx.motionBlurEnabled = (std::stoi(value) != 0); } else if (key == "postMotionBlurStrength") { currentObj->postFx.motionBlurStrength = std::stof(value); + } else if (key == "scriptCount") { + int count = std::stoi(value); + currentObj->scripts.resize(std::max(0, count)); + } else if (key.rfind("script", 0) == 0) { + size_t underscore = key.find('_'); + if (underscore != std::string::npos && underscore > 6) { + int idx = std::stoi(key.substr(6, underscore - 6)); + if (idx >= 0 && idx < (int)currentObj->scripts.size()) { + std::string subKey = key.substr(underscore + 1); + ScriptComponent& sc = currentObj->scripts[idx]; + if (subKey == "path") { + sc.path = value; + } else if (subKey == "settingCount") { + int cnt = std::stoi(value); + sc.settings.resize(std::max(0, cnt)); + } else if (subKey.rfind("setting", 0) == 0) { + int sIdx = std::stoi(subKey.substr(7)); + if (sIdx >= 0 && sIdx < (int)sc.settings.size()) { + size_t sep = value.find(':'); + if (sep != std::string::npos) { + sc.settings[sIdx].key = value.substr(0, sep); + sc.settings[sIdx].value = value.substr(sep + 1); + } else { + sc.settings[sIdx].key.clear(); + sc.settings[sIdx].value = value; + } + } + } + } + } } else if (key == "meshPath") { currentObj->meshPath = value; if (!value.empty() && currentObj->type == ObjectType::OBJMesh) { diff --git a/src/ProjectManager.h b/src/ProjectManager.h index 30d1a98..2032648 100644 --- a/src/ProjectManager.h +++ b/src/ProjectManager.h @@ -16,6 +16,7 @@ public: fs::path scenesPath; fs::path assetsPath; fs::path scriptsPath; + fs::path scriptsConfigPath; std::string currentSceneName; bool isLoaded = false; bool hasUnsavedChanges = false; diff --git a/src/SceneObject.h b/src/SceneObject.h index 3cc89e8..53b33d5 100644 --- a/src/SceneObject.h +++ b/src/SceneObject.h @@ -78,6 +78,17 @@ enum class ConsoleMessageType { Success }; +struct ScriptSetting { + std::string key; + std::string value; +}; + +struct ScriptComponent { + std::string path; + std::vector settings; + std::string lastBinaryPath; +}; + class SceneObject { public: std::string name; @@ -102,6 +113,7 @@ public: LightComponent light; // Only used when type is a light CameraComponent camera; // Only used when type is camera PostFXSettings postFx; // Only used when type is PostFXNode + std::vector scripts; SceneObject(const std::string& name, ObjectType type, int id) : name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {} diff --git a/src/ScriptCompiler.cpp b/src/ScriptCompiler.cpp new file mode 100644 index 0000000..db9bfbb --- /dev/null +++ b/src/ScriptCompiler.cpp @@ -0,0 +1,286 @@ +#include "ScriptCompiler.h" + +#include +#include +#include +#include +#include +#if defined(_WIN32) + #include +#endif + +namespace { + fs::path makeAbsolute(const fs::path& base, const fs::path& value) { + if (value.is_absolute()) return value; + std::error_code ec; + fs::path normalized = fs::weakly_canonical(base / value, ec); + if (ec) { + return fs::absolute(base / value); + } + return normalized; + } +} + +std::string ScriptCompiler::trim(const std::string& value) { + size_t start = 0; + while (start < value.size() && std::isspace(static_cast(value[start]))) { + start++; + } + size_t end = value.size(); + while (end > start && std::isspace(static_cast(value[end - 1]))) { + end--; + } + return value.substr(start, end - start); +} + +std::string ScriptCompiler::escapeDefine(const std::string& def) { + std::string escaped; + escaped.reserve(def.size()); + for (char c : def) { + if (c == '"') { + escaped += "\\\""; + } else { + escaped += c; + } + } + return escaped; +} + +bool ScriptCompiler::loadConfig(const fs::path& configPath, ScriptBuildConfig& outConfig, + std::string& error) const { + outConfig = ScriptBuildConfig(); + + if (!fs::exists(configPath)) { + error = "Config file not found: " + configPath.string(); + return false; + } + + std::ifstream file(configPath); + if (!file.is_open()) { + error = "Unable to open config file: " + configPath.string(); + return false; + } + + fs::path baseDir = configPath.parent_path(); + std::string line; + size_t lineNumber = 0; + while (std::getline(file, line)) { + lineNumber++; + std::string cleaned = trim(line); + if (cleaned.empty() || cleaned[0] == '#') continue; + + size_t pos = cleaned.find('='); + if (pos == std::string::npos) continue; + + std::string key = trim(cleaned.substr(0, pos)); + std::string value = trim(cleaned.substr(pos + 1)); + + if (key == "cppStandard") { + outConfig.cppStandard = value; + } else if (key == "scriptsDir") { + outConfig.scriptsDir = makeAbsolute(baseDir, value); + } else if (key == "outDir") { + outConfig.outDir = makeAbsolute(baseDir, value); + } else if (key == "includeDir") { + outConfig.includeDirs.push_back(makeAbsolute(baseDir, value)); + } else if (key == "define") { + outConfig.defines.push_back(value); + } else if (key == "linux.linkLib") { + outConfig.linuxLinkLibs.push_back(value); + } else if (key == "win.linkLib") { + outConfig.windowsLinkLibs.push_back(value); + } else { + // Ignore unknown keys for now + } + } + + outConfig.scriptsDir = makeAbsolute(baseDir, outConfig.scriptsDir); + outConfig.outDir = makeAbsolute(baseDir, outConfig.outDir); + for (auto& dir : outConfig.includeDirs) { + dir = makeAbsolute(baseDir, dir); + } + + // Heuristic: auto-add engine include roots if ScriptRuntime.h is discoverable nearby. + auto tryAddEngineRoot = [&](const fs::path& start) { + std::error_code ec; + fs::path candidate = start; + for (int depth = 0; depth < 5 && !candidate.empty(); ++depth) { + if (fs::exists(candidate / "src" / "ScriptRuntime.h", ec)) { + outConfig.includeDirs.push_back(candidate / "src"); + outConfig.includeDirs.push_back(candidate / "include"); + outConfig.includeDirs.push_back(candidate / "src/ThirdParty"); + outConfig.includeDirs.push_back(candidate / "src/ThirdParty/glm"); + outConfig.includeDirs.push_back(candidate / "src/ThirdParty/glad"); + outConfig.includeDirs.push_back(candidate / "src/ThirdParty/imgui"); + outConfig.includeDirs.push_back(candidate / "src/ThirdParty/imgui/backends"); + return true; + } + candidate = candidate.parent_path(); + } + return false; + }; + + tryAddEngineRoot(configPath.parent_path()); + tryAddEngineRoot(fs::current_path()); + tryAddEngineRoot(fs::current_path().parent_path()); +#if defined(__linux__) + { + std::error_code ec; + fs::path exe = fs::read_symlink("/proc/self/exe", ec); + if (!ec) { + tryAddEngineRoot(exe.parent_path()); + tryAddEngineRoot(exe.parent_path().parent_path()); + } + } +#elif defined(_WIN32) + { + wchar_t buffer[MAX_PATH]; + DWORD len = GetModuleFileNameW(nullptr, buffer, MAX_PATH); + if (len > 0) { + fs::path exe(buffer); + tryAddEngineRoot(exe.parent_path()); + tryAddEngineRoot(exe.parent_path().parent_path()); + } + } +#endif + + return true; +} + +std::string ScriptCompiler::formatLinkFlag(const std::string& lib) { + if (lib.rfind("-l", 0) == 0 || lib.rfind("-L", 0) == 0) return lib; + if (lib.find('/') != std::string::npos || lib.find('\\') != std::string::npos) { + return "\"" + lib + "\""; + } + return "-l" + lib; +} + +bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::path& scriptPath, + ScriptBuildCommands& outCommands, std::string& error) const { + if (!fs::exists(scriptPath)) { + error = "Script file not found: " + scriptPath.string(); + return false; + } + + std::error_code ec; + fs::path scriptAbs = fs::absolute(scriptPath, ec); + if (ec) scriptAbs = scriptPath; + + fs::path relToScripts; + relToScripts = fs::relative(scriptAbs, config.scriptsDir, ec); + if (ec) { + relToScripts.clear(); + } + + fs::path relativeParent = relToScripts.has_parent_path() ? relToScripts.parent_path() : fs::path(); + std::string baseName = scriptAbs.stem().string(); + fs::path objectPath = config.outDir / relativeParent / (baseName + ".o"); + + fs::path binaryPath = config.outDir / relativeParent; +#ifdef _WIN32 + objectPath = config.outDir / relativeParent / (baseName + ".obj"); + binaryPath /= baseName + ".dll"; +#else + binaryPath /= baseName + ".so"; +#endif + + std::ostringstream compileCmd; +#ifdef _WIN32 + compileCmd << "cl /nologo /std:" << config.cppStandard << " /EHsc /MD /Zi /Od"; + for (const auto& inc : config.includeDirs) { + compileCmd << " /I\"" << inc.string() << "\""; + } + for (const auto& def : config.defines) { + compileCmd << " /D" << escapeDefine(def); + } + compileCmd << " /c \"" << scriptAbs.string() << "\" /Fo\"" << objectPath.string() << "\""; +#else + compileCmd << "g++ -std=" << config.cppStandard << " -fPIC -O0 -g"; + for (const auto& inc : config.includeDirs) { + compileCmd << " -I\"" << inc.string() << "\""; + } + auto formatDefine = [&](const std::string& def) { + std::string escaped = def; + for (size_t pos = 0; pos < escaped.size(); ++pos) { + if (escaped[pos] == '"') { + escaped.insert(pos, "\\"); + ++pos; + } + } + return std::string(" -D\"") + escaped + "\""; + }; + for (const auto& def : config.defines) { + compileCmd << formatDefine(def); + } + compileCmd << " -c \"" << scriptAbs.string() << "\" -o \"" << objectPath.string() << "\""; +#endif + + std::ostringstream linkCmd; +#ifdef _WIN32 + linkCmd << "link /nologo /DLL \"" << objectPath.string() << "\" /OUT:\"" + << binaryPath.string() << "\""; + for (const auto& lib : config.windowsLinkLibs) { + linkCmd << " " << lib; + } +#else + linkCmd << "g++ -shared \"" << objectPath.string() << "\" -o \"" << binaryPath.string() << "\""; + for (const auto& lib : config.linuxLinkLibs) { + linkCmd << " " << formatLinkFlag(lib); + } +#endif + + outCommands.compile = compileCmd.str(); + outCommands.link = linkCmd.str(); + outCommands.objectPath = objectPath; + outCommands.binaryPath = binaryPath; + return true; +} + +bool ScriptCompiler::runCommand(const std::string& command, std::string& output) { + std::array buffer{}; +#ifdef _WIN32 + FILE* pipe = _popen(command.c_str(), "r"); +#else + FILE* pipe = popen(command.c_str(), "r"); +#endif + if (!pipe) { + output = "Failed to spawn process: " + command; + return false; + } + + while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { + output += buffer.data(); + } + +#ifdef _WIN32 + int returnCode = _pclose(pipe); +#else + int returnCode = pclose(pipe); +#endif + if (returnCode != 0) { + return false; + } + return true; +} + +bool ScriptCompiler::compile(const ScriptBuildCommands& commands, ScriptCompileOutput& output, + std::string& error) const { + if (!commands.objectPath.empty()) { + std::error_code ec; + fs::create_directories(commands.objectPath.parent_path(), ec); + } + if (!commands.binaryPath.empty()) { + std::error_code ec; + fs::create_directories(commands.binaryPath.parent_path(), ec); + } + + if (!runCommand(commands.compile + " 2>&1", output.compileLog)) { + error = "Compile failed"; + return false; + } + if (!runCommand(commands.link + " 2>&1", output.linkLog)) { + error = "Link failed"; + return false; + } + return true; +} diff --git a/src/ScriptCompiler.h b/src/ScriptCompiler.h new file mode 100644 index 0000000..70e49fa --- /dev/null +++ b/src/ScriptCompiler.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Common.h" + +struct ScriptBuildConfig { + std::string cppStandard = "c++20"; + fs::path scriptsDir = "Scripts"; + fs::path outDir = "Cache/ScriptBin"; + std::vector includeDirs; + std::vector defines; + std::vector linuxLinkLibs; + std::vector windowsLinkLibs; +}; + +struct ScriptBuildCommands { + std::string compile; + std::string link; + fs::path objectPath; + fs::path binaryPath; +}; + +struct ScriptCompileOutput { + std::string compileLog; + std::string linkLog; +}; + +class ScriptCompiler { +public: + bool loadConfig(const fs::path& configPath, ScriptBuildConfig& outConfig, std::string& error) const; + bool makeCommands(const ScriptBuildConfig& config, const fs::path& scriptPath, + ScriptBuildCommands& outCommands, std::string& error) const; + bool compile(const ScriptBuildCommands& commands, ScriptCompileOutput& output, + std::string& error) const; + +private: + static std::string trim(const std::string& value); + static std::string escapeDefine(const std::string& def); + static bool runCommand(const std::string& command, std::string& output); + static std::string formatLinkFlag(const std::string& lib); +}; diff --git a/src/ScriptRuntime.cpp b/src/ScriptRuntime.cpp new file mode 100644 index 0000000..6adaebc --- /dev/null +++ b/src/ScriptRuntime.cpp @@ -0,0 +1,97 @@ +#include "ScriptRuntime.h" +#include "Engine.h" +#include "SceneObject.h" + +#if defined(_WIN32) + #include +#else + #include +#endif + +SceneObject* ScriptContext::FindObjectByName(const std::string& name) { + if (!engine) return nullptr; + return engine->findObjectByName(name); +} + +SceneObject* ScriptContext::FindObjectById(int id) { + if (!engine) return nullptr; + return engine->findObjectById(id); +} + +void ScriptContext::SetPosition(const glm::vec3& pos) { + if (object) object->position = pos; +} + +void ScriptContext::SetRotation(const glm::vec3& rot) { + if (object) object->rotation = rot; +} + +void ScriptContext::SetScale(const glm::vec3& scl) { + if (object) object->scale = scl; +} + +ScriptRuntime::InspectorFn ScriptRuntime::getInspector(const fs::path& binaryPath) { + lastError.clear(); + if (binaryPath.empty()) return nullptr; + auto key = binaryPath.string(); + auto it = loaded.find(key); + if (it != loaded.end()) { + if (it->second.inspector) return it->second.inspector; + // Previously loaded but missing inspector; try reloading. +#if defined(_WIN32) + if (it->second.handle) FreeLibrary(static_cast(it->second.handle)); +#else + if (it->second.handle) dlclose(it->second.handle); +#endif + loaded.erase(it); + } + + Module mod{}; +#if defined(_WIN32) + mod.handle = LoadLibraryA(binaryPath.string().c_str()); + if (!mod.handle) { + lastError = "LoadLibrary failed"; + return nullptr; + } + mod.inspector = reinterpret_cast(GetProcAddress(static_cast(mod.handle), "Script_OnInspector")); +#else + mod.handle = dlopen(binaryPath.string().c_str(), RTLD_NOW); + if (!mod.handle) { + const char* err = dlerror(); + if (err) lastError = err; + return nullptr; + } + mod.inspector = reinterpret_cast(dlsym(mod.handle, "Script_OnInspector")); +#if !defined(_WIN32) + { + const char* err = dlerror(); + if (err && !mod.inspector) lastError = err; + } +#endif +#endif + + if (!mod.inspector) { +#if defined(_WIN32) + FreeLibrary(static_cast(mod.handle)); +#else + dlclose(mod.handle); +#endif + if (lastError.empty()) lastError = "Script_OnInspector not found"; + return nullptr; + } + + loaded[key] = mod; + return mod.inspector; +} + +void ScriptRuntime::unloadAll() { + for (auto& kv : loaded) { + if (!kv.second.handle) continue; +#if defined(_WIN32) + FreeLibrary(static_cast(kv.second.handle)); +#else + dlclose(kv.second.handle); +#endif + } + loaded.clear(); +} diff --git a/src/ScriptRuntime.h b/src/ScriptRuntime.h new file mode 100644 index 0000000..9da7570 --- /dev/null +++ b/src/ScriptRuntime.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Common.h" +#include "SceneObject.h" + +class Engine; + +struct ScriptContext { + Engine* engine = nullptr; + SceneObject* object = nullptr; + + // Convenience helpers for scripts + SceneObject* FindObjectByName(const std::string& name); + SceneObject* FindObjectById(int id); + void SetPosition(const glm::vec3& pos); + void SetRotation(const glm::vec3& rot); + void SetScale(const glm::vec3& scl); +}; + +class ScriptRuntime { +public: + using InspectorFn = void(*)(ScriptContext&); + + InspectorFn getInspector(const fs::path& binaryPath); + void unloadAll(); + const std::string& getLastError() const { return lastError; } + +private: + struct Module { + void* handle = nullptr; + InspectorFn inspector = nullptr; + }; + std::unordered_map loaded; + std::string lastError; +}; diff --git a/src/ThirdParty/gl3d b/src/ThirdParty/gl3d deleted file mode 160000 index a99e29f..0000000 --- a/src/ThirdParty/gl3d +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a99e29fc20692689e9576001c4ba7ba9605c7e3d