C++ Compilation!

This commit is contained in:
Anemunt
2025-12-12 00:06:21 -05:00
parent a7c89193b4
commit ee90559e8c
14 changed files with 865 additions and 10 deletions

3
.gitmodules vendored
View File

@@ -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

View File

@@ -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()

View File

@@ -0,0 +1,45 @@
// Minimal sample script demonstrating Script_OnInspector usage.
// Build via the engines “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));
}
}

View File

@@ -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;

View File

@@ -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<int> 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);
};

View File

@@ -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<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) {
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);

View File

@@ -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",
&currentObj->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) {

View File

@@ -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;

View File

@@ -78,6 +78,17 @@ enum class ConsoleMessageType {
Success
};
struct ScriptSetting {
std::string key;
std::string value;
};
struct ScriptComponent {
std::string path;
std::vector<ScriptSetting> 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<ScriptComponent> 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) {}

286
src/ScriptCompiler.cpp Normal file
View 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
View 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
View 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
View 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

Submodule src/ThirdParty/gl3d deleted from a99e29fc20