More Compilation stuff lol.

This commit is contained in:
Anemunt
2025-12-12 15:36:40 -05:00
parent ee90559e8c
commit 6ee17f52ee
9 changed files with 358 additions and 29 deletions

View File

@@ -9,21 +9,100 @@
#include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
#include <string>
#include <algorithm>
#include <sstream>
namespace {
bool autoRotate = false;
glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f);
glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f);
char targetName[128] = "MyTarget";
int settingsLoadedForId = -1;
ScriptComponent* settingsLoadedForScript = nullptr;
void setSetting(ScriptContext& ctx, const std::string& key, const std::string& value) {
if (!ctx.script) return;
auto it = std::find_if(ctx.script->settings.begin(), ctx.script->settings.end(),
[&](const ScriptSetting& s) { return s.key == key; });
if (it != ctx.script->settings.end()) {
it->value = value;
} else {
ctx.script->settings.push_back({key, value});
}
}
std::string getSetting(const ScriptContext& ctx, const std::string& key, const std::string& fallback = "") {
if (!ctx.script) return fallback;
auto it = std::find_if(ctx.script->settings.begin(), ctx.script->settings.end(),
[&](const ScriptSetting& s) { return s.key == key; });
return (it != ctx.script->settings.end()) ? it->value : fallback;
}
void loadSettings(ScriptContext& ctx) {
if (!ctx.script || !ctx.object) return;
if (settingsLoadedForId == ctx.object->id && settingsLoadedForScript == ctx.script) return;
settingsLoadedForId = ctx.object->id;
settingsLoadedForScript = ctx.script;
auto parseBool = [](const std::string& v, bool def) {
if (v == "1" || v == "true") return true;
if (v == "0" || v == "false") return false;
return def;
};
auto parseVec3 = [](const std::string& v, const glm::vec3& def) {
glm::vec3 out = def;
std::stringstream ss(v);
std::string part;
for (int i = 0; i < 3 && std::getline(ss, part, ','); ++i) {
try { out[i] = std::stof(part); } catch (...) {}
}
return out;
};
autoRotate = parseBool(getSetting(ctx, "autoRotate", autoRotate ? "1" : "0"), autoRotate);
spinSpeed = parseVec3(getSetting(ctx, "spinSpeed", ""), spinSpeed);
offset = parseVec3(getSetting(ctx, "offset", ""), offset);
std::string tgt = getSetting(ctx, "targetName", targetName);
if (!tgt.empty()) {
std::snprintf(targetName, sizeof(targetName), "%s", tgt.c_str());
}
}
void persistSettings(ScriptContext& ctx) {
setSetting(ctx, "autoRotate", autoRotate ? "1" : "0");
setSetting(ctx, "spinSpeed",
std::to_string(spinSpeed.x) + "," + std::to_string(spinSpeed.y) + "," + std::to_string(spinSpeed.z));
setSetting(ctx, "offset",
std::to_string(offset.x) + "," + std::to_string(offset.y) + "," + std::to_string(offset.z));
setSetting(ctx, "targetName", targetName);
ctx.MarkDirty();
}
void applyAutoRotate(ScriptContext& ctx, float deltaTime) {
if (!autoRotate || !ctx.object) return;
ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime);
}
} // namespace
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";
loadSettings(ctx);
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);
if (ImGui::Checkbox("Auto Rotate", &autoRotate)) {
persistSettings(ctx);
}
if (ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f)) {
persistSettings(ctx);
}
if (ImGui::DragFloat3("Offset", &offset.x, 0.1f)) {
persistSettings(ctx);
}
ImGui::InputText("Target Name", targetName, sizeof(targetName));
persistSettings(ctx);
if (ctx.object) {
ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id);
@@ -38,8 +117,24 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
target->position += offset;
}
}
}
if (autoRotate && ctx.object) {
ctx.SetRotation(ctx.object->rotation + spinSpeed * (1.0f / 60.0f));
// New lifecycle hooks supported by the compiler wrapper. These are optional stubs demonstrating usage.
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
// Initialize per-script state here.
loadSettings(ctx);
}
void Spec(ScriptContext& ctx, float deltaTime) {
// Special/speculative mode logic could go here.
applyAutoRotate(ctx, deltaTime);
}
void TestEditor(ScriptContext& ctx, float deltaTime) {
// Editor-time behavior entry point.
applyAutoRotate(ctx, deltaTime);
}
void TickUpdate(ScriptContext& ctx, float deltaTime) {
applyAutoRotate(ctx, deltaTime);
}

View File

@@ -303,6 +303,11 @@ void Engine::run() {
camera.processKeyboard(deltaTime, editorWindow);
}
// Run script tick/update even when the object is not selected.
if (projectManager.currentProject.isLoaded) {
updateScripts(deltaTime);
}
if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) {
glm::mat4 view = camera.getViewMatrix();
float aspect = static_cast<float>(viewportWidth) / static_cast<float>(viewportHeight);
@@ -691,6 +696,24 @@ void Engine::handleKeyboardShortcuts() {
}
}
void Engine::updateScripts(float delta) {
if (sceneObjects.empty()) return;
for (auto& obj : sceneObjects) {
for (auto& sc : obj.scripts) {
if (sc.path.empty()) continue;
fs::path binary = resolveScriptBinary(sc.path);
if (binary.empty() || !fs::exists(binary)) continue;
ScriptContext ctx;
ctx.engine = this;
ctx.object = &obj;
ctx.script = &sc;
scriptRuntime.tickModule(binary, ctx, delta, specMode, testMode);
}
}
}
void Engine::OpenProjectPath(const std::string& path) {
try {
if (projectManager.loadProject(path)) {
@@ -1025,6 +1048,10 @@ fs::path Engine::resolveScriptBinary(const fs::path& sourcePath) {
return cmds.binaryPath;
}
void Engine::markProjectDirty() {
projectManager.currentProject.hasUnsavedChanges = true;
}
void Engine::compileScriptFile(const fs::path& scriptPath) {
if (!projectManager.currentProject.isLoaded) {
addConsoleMessage("No project is loaded", ConsoleMessageType::Warning);

View File

@@ -109,6 +109,8 @@ private:
bool lastCompileSuccess = false;
std::string lastCompileStatus;
std::string lastCompileLog;
bool specMode = false;
bool testMode = false;
// Private methods
SceneObject* getSelectedObject();
@@ -143,6 +145,7 @@ private:
void renderProjectBrowserPanel();
Camera makeCameraFromObject(const SceneObject& obj) const;
void compileScriptFile(const fs::path& scriptPath);
void updateScripts(float delta);
void renderFileBrowserToolbar();
void renderFileBrowserBreadcrumb();
@@ -200,4 +203,5 @@ public:
SceneObject* findObjectByName(const std::string& name);
SceneObject* findObjectById(int id);
fs::path resolveScriptBinary(const fs::path& sourcePath);
void markProjectDirty();
};

View File

@@ -1306,6 +1306,12 @@ void Engine::renderMainMenuBar() {
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Scripts")) {
ImGui::MenuItem("Spec Mode (run Script_Spec)", nullptr, &specMode);
ImGui::MenuItem("Test Mode (run Script_TestEditor)", nullptr, &testMode);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Create")) {
if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");

View File

@@ -361,8 +361,17 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
SceneObject* currentObj = nullptr;
while (std::getline(file, line)) {
line.erase(0, line.find_first_not_of(" \t\r\n"));
line.erase(line.find_last_not_of(" \t\r\n") + 1);
size_t first = line.find_first_not_of(" \t\r\n");
if (first == std::string::npos) {
continue;
}
line.erase(0, first);
size_t last = line.find_last_not_of(" \t\r\n");
if (last != std::string::npos) {
line.erase(last + 1);
} else {
continue;
}
if (line.empty() || line[0] == '#') continue;
@@ -450,11 +459,13 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
ScriptComponent& sc = currentObj->scripts[idx];
if (sub == "path") {
sc.path = value;
} else if (sub == "settings") {
} else if (sub == "settings" || sub == "settingCount") {
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));
std::string idxStr = sub.substr(7);
if (!idxStr.empty() && std::all_of(idxStr.begin(), idxStr.end(), ::isdigit)) {
int sIdx = std::stoi(idxStr);
if (sIdx >= 0 && sIdx < (int)sc.settings.size()) {
size_t sep = value.find(':');
if (sep != std::string::npos) {
@@ -467,6 +478,7 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
}
}
}
}
} else if (key == "lightColor") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->light.color.r,

View File

@@ -5,6 +5,7 @@
#include <cstdio>
#include <fstream>
#include <sstream>
#include <regex>
#if defined(_WIN32)
#include <windows.h>
#endif
@@ -184,6 +185,118 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
binaryPath /= baseName + ".so";
#endif
// Build a lightweight wrapper that exposes expected entry points with C linkage and optional deltaTime.
auto readFileToString = [](const fs::path& path, std::string& contents) -> bool {
std::ifstream f(path);
if (!f.is_open()) return false;
std::ostringstream ss;
ss << f.rdbuf();
contents = ss.str();
return true;
};
struct FunctionSpec {
bool present = false;
bool takesDelta = false;
bool takesContext = false;
};
auto detectFunction = [](const std::string& source, const std::string& name) -> FunctionSpec {
FunctionSpec spec;
try {
std::regex ctxDeltaPattern("\\bvoid\\s+" + name + "\\s*\\(\\s*ScriptContext\\s*[&*][^,\\)]*,[^\\)]*(float|double)[^\\)]*\\)");
std::regex ctxOnlyPattern("\\bvoid\\s+" + name + "\\s*\\(\\s*ScriptContext\\s*[&*][^\\)]*\\)");
std::regex deltaPattern("\\bvoid\\s+" + name + "\\s*\\(\\s*(float|double)[^\\)]*\\)");
std::regex basicPattern("\\bvoid\\s+" + name + "\\s*\\(\\s*\\)");
if (std::regex_search(source, ctxDeltaPattern)) {
spec.present = true;
spec.takesDelta = true;
spec.takesContext = true;
return spec;
}
if (std::regex_search(source, ctxOnlyPattern)) {
spec.present = true;
spec.takesContext = true;
return spec;
}
if (std::regex_search(source, deltaPattern)) {
spec.present = true;
spec.takesDelta = true;
return spec;
}
if (std::regex_search(source, basicPattern)) {
spec.present = true;
}
} catch (...) {
// If regex throws for any reason, fall through and treat as not present.
}
return spec;
};
std::string scriptSource;
if (!readFileToString(scriptAbs, scriptSource)) {
error = "Unable to read script file: " + scriptAbs.string();
return false;
}
FunctionSpec beginSpec = detectFunction(scriptSource, "Begin");
FunctionSpec specSpec = detectFunction(scriptSource, "Spec");
FunctionSpec testEditorSpec = detectFunction(scriptSource, "TestEditor");
FunctionSpec updateSpec = detectFunction(scriptSource, "Update");
FunctionSpec tickUpdateSpec = detectFunction(scriptSource, "TickUpdate");
fs::path wrapperPath;
bool useWrapper = beginSpec.present || specSpec.present || testEditorSpec.present
|| updateSpec.present || tickUpdateSpec.present;
fs::path sourceToCompile = scriptAbs;
if (useWrapper) {
wrapperPath = config.outDir / relativeParent / (baseName + ".wrap.cpp");
std::error_code createErr;
fs::create_directories(wrapperPath.parent_path(), createErr);
std::ofstream wrapper(wrapperPath);
if (!wrapper.is_open()) {
error = "Unable to write wrapper file: " + wrapperPath.string();
return false;
}
std::string includePath = scriptAbs.lexically_normal().generic_string();
wrapper << "#include \"ScriptRuntime.h\"\n";
wrapper << "#include \"" << includePath << "\"\n\n";
wrapper << "extern \"C\" {\n";
auto emitWrapper = [&](const char* exportedName, const char* implName,
const FunctionSpec& spec) {
if (!spec.present) return;
wrapper << "void " << exportedName << "(ScriptContext& ctx, float deltaTime) {\n";
if (spec.takesContext && spec.takesDelta) {
wrapper << " " << implName << "(ctx, deltaTime);\n";
} else if (spec.takesContext) {
wrapper << " (void)deltaTime;\n";
wrapper << " " << implName << "(ctx);\n";
} else if (spec.takesDelta) {
wrapper << " (void)ctx;\n";
wrapper << " " << implName << "(deltaTime);\n";
} else {
wrapper << " (void)ctx;\n";
wrapper << " (void)deltaTime;\n";
wrapper << " " << implName << "();\n";
}
wrapper << "}\n\n";
};
emitWrapper("Script_Begin", "Begin", beginSpec);
emitWrapper("Script_Spec", "Spec", specSpec);
emitWrapper("Script_TestEditor", "TestEditor", testEditorSpec);
emitWrapper("Script_Update", "Update", updateSpec);
emitWrapper("Script_TickUpdate", "TickUpdate", tickUpdateSpec);
wrapper << "}\n";
sourceToCompile = wrapperPath;
}
std::ostringstream compileCmd;
#ifdef _WIN32
compileCmd << "cl /nologo /std:" << config.cppStandard << " /EHsc /MD /Zi /Od";
@@ -193,7 +306,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
for (const auto& def : config.defines) {
compileCmd << " /D" << escapeDefine(def);
}
compileCmd << " /c \"" << scriptAbs.string() << "\" /Fo\"" << objectPath.string() << "\"";
compileCmd << " /c \"" << sourceToCompile.string() << "\" /Fo\"" << objectPath.string() << "\"";
#else
compileCmd << "g++ -std=" << config.cppStandard << " -fPIC -O0 -g";
for (const auto& inc : config.includeDirs) {
@@ -212,7 +325,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
for (const auto& def : config.defines) {
compileCmd << formatDefine(def);
}
compileCmd << " -c \"" << scriptAbs.string() << "\" -o \"" << objectPath.string() << "\"";
compileCmd << " -c \"" << sourceToCompile.string() << "\" -o \"" << objectPath.string() << "\"";
#endif
std::ostringstream linkCmd;
@@ -233,6 +346,8 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
outCommands.link = linkCmd.str();
outCommands.objectPath = objectPath;
outCommands.binaryPath = binaryPath;
outCommands.wrapperPath = wrapperPath;
outCommands.usedWrapper = useWrapper;
return true;
}

View File

@@ -17,6 +17,8 @@ struct ScriptBuildCommands {
std::string link;
fs::path objectPath;
fs::path binaryPath;
fs::path wrapperPath;
bool usedWrapper = false;
};
struct ScriptCompileOutput {

View File

@@ -30,13 +30,25 @@ void ScriptContext::SetScale(const glm::vec3& scl) {
if (object) object->scale = scl;
}
void ScriptContext::MarkDirty() {
if (engine) {
engine->markProjectDirty();
}
}
ScriptRuntime::InspectorFn ScriptRuntime::getInspector(const fs::path& binaryPath) {
lastError.clear();
Module* mod = getModule(binaryPath);
return mod ? mod->inspector : nullptr;
}
ScriptRuntime::Module* ScriptRuntime::getModule(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;
return &it->second;
// Previously loaded but missing inspector; try reloading.
#if defined(_WIN32)
if (it->second.handle) FreeLibrary(static_cast<HMODULE>(it->second.handle));
@@ -54,6 +66,11 @@ ScriptRuntime::InspectorFn ScriptRuntime::getInspector(const fs::path& binaryPat
return nullptr;
}
mod.inspector = reinterpret_cast<InspectorFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_OnInspector"));
mod.begin = reinterpret_cast<BeginFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_Begin"));
mod.spec = reinterpret_cast<SpecFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_Spec"));
mod.testEditor = reinterpret_cast<TestEditorFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_TestEditor"));
mod.update = reinterpret_cast<UpdateFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_Update"));
mod.tickUpdate = reinterpret_cast<TickUpdateFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_TickUpdate"));
#else
mod.handle = dlopen(binaryPath.string().c_str(), RTLD_NOW);
if (!mod.handle) {
@@ -62,26 +79,60 @@ ScriptRuntime::InspectorFn ScriptRuntime::getInspector(const fs::path& binaryPat
return nullptr;
}
mod.inspector = reinterpret_cast<InspectorFn>(dlsym(mod.handle, "Script_OnInspector"));
mod.begin = reinterpret_cast<BeginFn>(dlsym(mod.handle, "Script_Begin"));
mod.spec = reinterpret_cast<SpecFn>(dlsym(mod.handle, "Script_Spec"));
mod.testEditor = reinterpret_cast<TestEditorFn>(dlsym(mod.handle, "Script_TestEditor"));
mod.update = reinterpret_cast<UpdateFn>(dlsym(mod.handle, "Script_Update"));
mod.tickUpdate = reinterpret_cast<TickUpdateFn>(dlsym(mod.handle, "Script_TickUpdate"));
#if !defined(_WIN32)
{
const char* err = dlerror();
if (err && !mod.inspector) lastError = err;
if (err && !mod.inspector && !mod.begin && !mod.spec && !mod.testEditor
&& !mod.update && !mod.tickUpdate) {
lastError = err;
}
}
#endif
#endif
if (!mod.inspector) {
if (!mod.inspector && !mod.begin && !mod.spec && !mod.testEditor
&& !mod.update && !mod.tickUpdate) {
#if defined(_WIN32)
FreeLibrary(static_cast<HMODULE>(mod.handle));
#else
dlclose(mod.handle);
#endif
if (lastError.empty()) lastError = "Script_OnInspector not found";
if (lastError.empty()) lastError = "No script exports found";
return nullptr;
}
loaded[key] = mod;
return mod.inspector;
return &loaded[key];
}
void ScriptRuntime::tickModule(const fs::path& binaryPath, ScriptContext& ctx, float deltaTime,
bool runSpec, bool runTest) {
Module* mod = getModule(binaryPath);
if (!mod) return;
int objId = ctx.object ? ctx.object->id : -1;
if (objId >= 0 && mod->begin && mod->beginCalledObjects.find(objId) == mod->beginCalledObjects.end()) {
mod->begin(ctx, deltaTime);
mod->beginCalledObjects.insert(objId);
}
if (mod->tickUpdate) {
mod->tickUpdate(ctx, deltaTime);
} else if (mod->update) {
mod->update(ctx, deltaTime);
}
if (runSpec && mod->spec) {
mod->spec(ctx, deltaTime);
}
if (runTest && mod->testEditor) {
mod->testEditor(ctx, deltaTime);
}
}
void ScriptRuntime::unloadAll() {

View File

@@ -2,12 +2,14 @@
#include "Common.h"
#include "SceneObject.h"
#include <unordered_set>
class Engine;
struct ScriptContext {
Engine* engine = nullptr;
SceneObject* object = nullptr;
ScriptComponent* script = nullptr;
// Convenience helpers for scripts
SceneObject* FindObjectByName(const std::string& name);
@@ -15,13 +17,21 @@ struct ScriptContext {
void SetPosition(const glm::vec3& pos);
void SetRotation(const glm::vec3& rot);
void SetScale(const glm::vec3& scl);
void MarkDirty();
};
class ScriptRuntime {
public:
using BeginFn = void(*)(ScriptContext&, float);
using SpecFn = void(*)(ScriptContext&, float);
using TestEditorFn = void(*)(ScriptContext&, float);
using UpdateFn = void(*)(ScriptContext&, float);
using TickUpdateFn = void(*)(ScriptContext&, float);
using InspectorFn = void(*)(ScriptContext&);
InspectorFn getInspector(const fs::path& binaryPath);
void tickModule(const fs::path& binaryPath, ScriptContext& ctx, float deltaTime,
bool runSpec, bool runTest);
void unloadAll();
const std::string& getLastError() const { return lastError; }
@@ -29,7 +39,14 @@ private:
struct Module {
void* handle = nullptr;
InspectorFn inspector = nullptr;
BeginFn begin = nullptr;
SpecFn spec = nullptr;
TestEditorFn testEditor = nullptr;
UpdateFn update = nullptr;
TickUpdateFn tickUpdate = nullptr;
std::unordered_set<int> beginCalledObjects;
};
Module* getModule(const fs::path& binaryPath);
std::unordered_map<std::string, Module> loaded;
std::string lastError;
};