C++ Compilation!
This commit is contained in:
@@ -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;
|
||||
|
||||
12
src/Engine.h
12
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<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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
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