C++ Compilation!
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -11,6 +11,3 @@
|
|||||||
[submodule "src/ThirdParty/assimp"]
|
[submodule "src/ThirdParty/assimp"]
|
||||||
path = src/ThirdParty/assimp
|
path = src/ThirdParty/assimp
|
||||||
url = https://github.com/assimp/assimp.git
|
url = https://github.com/assimp/assimp.git
|
||||||
[submodule "src/ThirdParty/gl3d"]
|
|
||||||
path = src/ThirdParty/gl3d
|
|
||||||
url = https://github.com/meemknight/gl3d.git
|
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ if(NOT WIN32)
|
|||||||
Xinerama
|
Xinerama
|
||||||
Xcursor
|
Xcursor
|
||||||
)
|
)
|
||||||
|
# Export symbols so runtime-loaded scripts can resolve ImGui/engine symbols.
|
||||||
|
target_link_options(Modularity PRIVATE "-rdynamic")
|
||||||
else()
|
else()
|
||||||
target_link_libraries(Modularity PRIVATE core glfw OpenGL::GL)
|
target_link_libraries(Modularity PRIVATE core glfw OpenGL::GL)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
45
Scripts/SampleInspector.cpp
Normal file
45
Scripts/SampleInspector.cpp
Normal file
@@ -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 <string>
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -709,8 +709,8 @@ void Engine::OpenProjectPath(const std::string& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadRecentScenes();
|
loadRecentScenes();
|
||||||
fileBrowser.setProjectRoot(projectManager.currentProject.assetsPath);
|
fileBrowser.setProjectRoot(projectManager.currentProject.projectPath);
|
||||||
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
|
fileBrowser.currentPath = projectManager.currentProject.projectPath;
|
||||||
fileBrowser.needsRefresh = true;
|
fileBrowser.needsRefresh = true;
|
||||||
showLauncher = false;
|
showLauncher = false;
|
||||||
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
|
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");
|
addObject(ObjectType::Cube, "Cube");
|
||||||
|
|
||||||
fileBrowser.setProjectRoot(projectManager.currentProject.assetsPath);
|
fileBrowser.setProjectRoot(projectManager.currentProject.projectPath);
|
||||||
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
|
fileBrowser.currentPath = projectManager.currentProject.projectPath;
|
||||||
fileBrowser.needsRefresh = true;
|
fileBrowser.needsRefresh = true;
|
||||||
|
|
||||||
showLauncher = false;
|
showLauncher = false;
|
||||||
@@ -784,8 +784,8 @@ void Engine::loadRecentScenes() {
|
|||||||
}
|
}
|
||||||
recordState("sceneLoaded");
|
recordState("sceneLoaded");
|
||||||
|
|
||||||
fileBrowser.setProjectRoot(projectManager.currentProject.assetsPath);
|
fileBrowser.setProjectRoot(projectManager.currentProject.projectPath);
|
||||||
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
|
fileBrowser.currentPath = projectManager.currentProject.projectPath;
|
||||||
fileBrowser.needsRefresh = true;
|
fileBrowser.needsRefresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -993,6 +993,91 @@ void Engine::logToConsole(const std::string& message) {
|
|||||||
addConsoleMessage(message, ConsoleMessageType::Info);
|
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() {
|
void Engine::setupImGui() {
|
||||||
std::cerr << "[DEBUG] setupImGui: getting primary monitor..." << std::endl;
|
std::cerr << "[DEBUG] setupImGui: getting primary monitor..." << std::endl;
|
||||||
float mainScale = 1.0f;
|
float mainScale = 1.0f;
|
||||||
|
|||||||
12
src/Engine.h
12
src/Engine.h
@@ -7,6 +7,8 @@
|
|||||||
#include "ProjectManager.h"
|
#include "ProjectManager.h"
|
||||||
#include "EditorUI.h"
|
#include "EditorUI.h"
|
||||||
#include "MeshBuilder.h"
|
#include "MeshBuilder.h"
|
||||||
|
#include "ScriptCompiler.h"
|
||||||
|
#include "ScriptRuntime.h"
|
||||||
#include "../include/Window/Window.h"
|
#include "../include/Window/Window.h"
|
||||||
|
|
||||||
void window_size_callback(GLFWwindow* window, int width, int height);
|
void window_size_callback(GLFWwindow* window, int width, int height);
|
||||||
@@ -101,6 +103,12 @@ private:
|
|||||||
std::vector<int> meshEditSelectedFaces; // indices into mesh faces
|
std::vector<int> meshEditSelectedFaces; // indices into mesh faces
|
||||||
enum class MeshEditSelectionMode { Vertex = 0, Edge = 1, Face = 2 };
|
enum class MeshEditSelectionMode { Vertex = 0, Edge = 1, Face = 2 };
|
||||||
MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex;
|
MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex;
|
||||||
|
ScriptCompiler scriptCompiler;
|
||||||
|
ScriptRuntime scriptRuntime;
|
||||||
|
bool showCompilePopup = false;
|
||||||
|
bool lastCompileSuccess = false;
|
||||||
|
std::string lastCompileStatus;
|
||||||
|
std::string lastCompileLog;
|
||||||
|
|
||||||
// Private methods
|
// Private methods
|
||||||
SceneObject* getSelectedObject();
|
SceneObject* getSelectedObject();
|
||||||
@@ -134,6 +142,7 @@ private:
|
|||||||
void renderDialogs();
|
void renderDialogs();
|
||||||
void renderProjectBrowserPanel();
|
void renderProjectBrowserPanel();
|
||||||
Camera makeCameraFromObject(const SceneObject& obj) const;
|
Camera makeCameraFromObject(const SceneObject& obj) const;
|
||||||
|
void compileScriptFile(const fs::path& scriptPath);
|
||||||
|
|
||||||
void renderFileBrowserToolbar();
|
void renderFileBrowserToolbar();
|
||||||
void renderFileBrowserBreadcrumb();
|
void renderFileBrowserBreadcrumb();
|
||||||
@@ -188,4 +197,7 @@ public:
|
|||||||
bool init();
|
bool init();
|
||||||
void run();
|
void run();
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
SceneObject* findObjectByName(const std::string& name);
|
||||||
|
SceneObject* findObjectById(int id);
|
||||||
|
fs::path resolveScriptBinary(const fs::path& sourcePath);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -618,6 +618,11 @@ void Engine::renderFileBrowserPanel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (fileBrowser.getFileCategory(entry) == FileCategory::Script) {
|
||||||
|
if (ImGui::MenuItem("Compile Script")) {
|
||||||
|
compileScriptFile(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem("Show in Explorer")) {
|
if (ImGui::MenuItem("Show in Explorer")) {
|
||||||
#ifdef _WIN32
|
#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();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem("Show in Explorer")) {
|
if (ImGui::MenuItem("Show in Explorer")) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -2294,6 +2304,120 @@ void Engine::renderInspectorPanel() {
|
|||||||
ImGui::PopStyleColor();
|
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<int>(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<long>(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<int>(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<long>(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) {
|
if (browserHasMaterial) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
renderMaterialAssetPanel("Material Asset (File Browser)", true);
|
renderMaterialAssetPanel("Material Asset (File Browser)", true);
|
||||||
@@ -3682,6 +3806,26 @@ void Engine::renderDialogs() {
|
|||||||
ImGui::End();
|
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) {
|
if (showSaveSceneAsDialog) {
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Project::Project(const std::string& projectName, const fs::path& basePath)
|
|||||||
scenesPath = projectPath / "Scenes";
|
scenesPath = projectPath / "Scenes";
|
||||||
assetsPath = projectPath / "Assets";
|
assetsPath = projectPath / "Assets";
|
||||||
scriptsPath = projectPath / "Scripts";
|
scriptsPath = projectPath / "Scripts";
|
||||||
|
scriptsConfigPath = projectPath / "Scripts.modu";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Project::create() {
|
bool Project::create() {
|
||||||
@@ -20,9 +21,29 @@ bool Project::create() {
|
|||||||
fs::create_directories(assetsPath / "Models");
|
fs::create_directories(assetsPath / "Models");
|
||||||
fs::create_directories(assetsPath / "Shaders");
|
fs::create_directories(assetsPath / "Shaders");
|
||||||
fs::create_directories(scriptsPath);
|
fs::create_directories(scriptsPath);
|
||||||
|
fs::create_directories(projectPath / "Cache" / "ScriptBin");
|
||||||
|
|
||||||
saveProjectFile();
|
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";
|
currentSceneName = "Main";
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -38,6 +59,7 @@ bool Project::load(const fs::path& projectFilePath) {
|
|||||||
scenesPath = projectPath / "Scenes";
|
scenesPath = projectPath / "Scenes";
|
||||||
assetsPath = projectPath / "Assets";
|
assetsPath = projectPath / "Assets";
|
||||||
scriptsPath = projectPath / "Scripts";
|
scriptsPath = projectPath / "Scripts";
|
||||||
|
scriptsConfigPath = projectPath / "Scripts.modu";
|
||||||
|
|
||||||
std::ifstream file(projectFilePath);
|
std::ifstream file(projectFilePath);
|
||||||
if (!file.is_open()) return false;
|
if (!file.is_open()) return false;
|
||||||
@@ -262,6 +284,15 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
|||||||
file << "vertexShader=" << obj.vertexShaderPath << "\n";
|
file << "vertexShader=" << obj.vertexShaderPath << "\n";
|
||||||
file << "fragmentShader=" << obj.fragmentShaderPath << "\n";
|
file << "fragmentShader=" << obj.fragmentShaderPath << "\n";
|
||||||
file << "useOverlay=" << (obj.useOverlay ? 1 : 0) << "\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 << "lightColor=" << obj.light.color.r << "," << obj.light.color.g << "," << obj.light.color.b << "\n";
|
||||||
file << "lightIntensity=" << obj.light.intensity << "\n";
|
file << "lightIntensity=" << obj.light.intensity << "\n";
|
||||||
file << "lightRange=" << obj.light.range << "\n";
|
file << "lightRange=" << obj.light.range << "\n";
|
||||||
@@ -288,6 +319,16 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
|||||||
file << "postMotionBlurStrength=" << obj.postFx.motionBlurStrength << "\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()) {
|
if ((obj.type == ObjectType::OBJMesh || obj.type == ObjectType::Model) && !obj.meshPath.empty()) {
|
||||||
file << "meshPath=" << obj.meshPath << "\n";
|
file << "meshPath=" << obj.meshPath << "\n";
|
||||||
}
|
}
|
||||||
@@ -397,6 +438,35 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
|
|||||||
currentObj->fragmentShaderPath = value;
|
currentObj->fragmentShaderPath = value;
|
||||||
} else if (key == "useOverlay") {
|
} else if (key == "useOverlay") {
|
||||||
currentObj->useOverlay = (std::stoi(value) != 0);
|
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") {
|
} else if (key == "lightColor") {
|
||||||
sscanf(value.c_str(), "%f,%f,%f",
|
sscanf(value.c_str(), "%f,%f,%f",
|
||||||
¤tObj->light.color.r,
|
¤tObj->light.color.r,
|
||||||
@@ -451,6 +521,36 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
|
|||||||
currentObj->postFx.motionBlurEnabled = (std::stoi(value) != 0);
|
currentObj->postFx.motionBlurEnabled = (std::stoi(value) != 0);
|
||||||
} else if (key == "postMotionBlurStrength") {
|
} else if (key == "postMotionBlurStrength") {
|
||||||
currentObj->postFx.motionBlurStrength = std::stof(value);
|
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") {
|
} else if (key == "meshPath") {
|
||||||
currentObj->meshPath = value;
|
currentObj->meshPath = value;
|
||||||
if (!value.empty() && currentObj->type == ObjectType::OBJMesh) {
|
if (!value.empty() && currentObj->type == ObjectType::OBJMesh) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public:
|
|||||||
fs::path scenesPath;
|
fs::path scenesPath;
|
||||||
fs::path assetsPath;
|
fs::path assetsPath;
|
||||||
fs::path scriptsPath;
|
fs::path scriptsPath;
|
||||||
|
fs::path scriptsConfigPath;
|
||||||
std::string currentSceneName;
|
std::string currentSceneName;
|
||||||
bool isLoaded = false;
|
bool isLoaded = false;
|
||||||
bool hasUnsavedChanges = false;
|
bool hasUnsavedChanges = false;
|
||||||
|
|||||||
@@ -78,6 +78,17 @@ enum class ConsoleMessageType {
|
|||||||
Success
|
Success
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ScriptSetting {
|
||||||
|
std::string key;
|
||||||
|
std::string value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScriptComponent {
|
||||||
|
std::string path;
|
||||||
|
std::vector<ScriptSetting> settings;
|
||||||
|
std::string lastBinaryPath;
|
||||||
|
};
|
||||||
|
|
||||||
class SceneObject {
|
class SceneObject {
|
||||||
public:
|
public:
|
||||||
std::string name;
|
std::string name;
|
||||||
@@ -102,6 +113,7 @@ public:
|
|||||||
LightComponent light; // Only used when type is a light
|
LightComponent light; // Only used when type is a light
|
||||||
CameraComponent camera; // Only used when type is camera
|
CameraComponent camera; // Only used when type is camera
|
||||||
PostFXSettings postFx; // Only used when type is PostFXNode
|
PostFXSettings postFx; // Only used when type is PostFXNode
|
||||||
|
std::vector<ScriptComponent> scripts;
|
||||||
|
|
||||||
SceneObject(const std::string& name, ObjectType type, int id)
|
SceneObject(const std::string& name, ObjectType type, int id)
|
||||||
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}
|
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}
|
||||||
|
|||||||
286
src/ScriptCompiler.cpp
Normal file
286
src/ScriptCompiler.cpp
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
#include "ScriptCompiler.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <windows.h>
|
||||||
|
#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<unsigned char>(value[start]))) {
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
size_t end = value.size();
|
||||||
|
while (end > start && std::isspace(static_cast<unsigned char>(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<char, 256> 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<int>(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;
|
||||||
|
}
|
||||||
40
src/ScriptCompiler.h
Normal file
40
src/ScriptCompiler.h
Normal file
@@ -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<fs::path> includeDirs;
|
||||||
|
std::vector<std::string> defines;
|
||||||
|
std::vector<std::string> linuxLinkLibs;
|
||||||
|
std::vector<std::string> 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);
|
||||||
|
};
|
||||||
97
src/ScriptRuntime.cpp
Normal file
97
src/ScriptRuntime.cpp
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#include "ScriptRuntime.h"
|
||||||
|
#include "Engine.h"
|
||||||
|
#include "SceneObject.h"
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <Windows.h>
|
||||||
|
#else
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#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<HMODULE>(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<InspectorFn>(GetProcAddress(static_cast<HMODULE>(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<InspectorFn>(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<HMODULE>(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<HMODULE>(kv.second.handle));
|
||||||
|
#else
|
||||||
|
dlclose(kv.second.handle);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
loaded.clear();
|
||||||
|
}
|
||||||
35
src/ScriptRuntime.h
Normal file
35
src/ScriptRuntime.h
Normal file
@@ -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<std::string, Module> loaded;
|
||||||
|
std::string lastError;
|
||||||
|
};
|
||||||
1
src/ThirdParty/gl3d
vendored
1
src/ThirdParty/gl3d
vendored
Submodule src/ThirdParty/gl3d deleted from a99e29fc20
Reference in New Issue
Block a user