From 6ee17f52eeec247072fcd37f75b866c66e8e4e48 Mon Sep 17 00:00:00 2001 From: Anemunt <69436164+darkresident55@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:36:40 -0500 Subject: [PATCH] More Compilation stuff lol. --- Scripts/SampleInspector.cpp | 117 +++++++++++++++++++++++++++++++---- src/Engine.cpp | 27 ++++++++ src/Engine.h | 4 ++ src/EnginePanels.cpp | 6 ++ src/ProjectManager.cpp | 34 +++++++---- src/ScriptCompiler.cpp | 119 +++++++++++++++++++++++++++++++++++- src/ScriptCompiler.h | 2 + src/ScriptRuntime.cpp | 61 ++++++++++++++++-- src/ScriptRuntime.h | 17 ++++++ 9 files changed, 358 insertions(+), 29 deletions(-) diff --git a/Scripts/SampleInspector.cpp b/Scripts/SampleInspector.cpp index b6a373a..1e08b05 100644 --- a/Scripts/SampleInspector.cpp +++ b/Scripts/SampleInspector.cpp @@ -9,21 +9,100 @@ #include "SceneObject.h" #include "ThirdParty/imgui/imgui.h" #include +#include +#include + +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); } diff --git a/src/Engine.cpp b/src/Engine.cpp index f777cec..7ee851e 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -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(viewportWidth) / static_cast(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 = ≻ + + 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); diff --git a/src/Engine.h b/src/Engine.h index b65f8ff..043c8c4 100644 --- a/src/Engine.h +++ b/src/Engine.h @@ -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(); }; diff --git a/src/EnginePanels.cpp b/src/EnginePanels.cpp index d63e33c..180c742 100644 --- a/src/EnginePanels.cpp +++ b/src/EnginePanels.cpp @@ -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"); diff --git a/src/ProjectManager.cpp b/src/ProjectManager.cpp index 8e4427b..f1d1b93 100644 --- a/src/ProjectManager.cpp +++ b/src/ProjectManager.cpp @@ -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,18 +459,21 @@ 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)); - 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; + 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) { + sc.settings[sIdx].key = value.substr(0, sep); + sc.settings[sIdx].value = value.substr(sep + 1); + } else { + sc.settings[sIdx].value = value; + } } } } diff --git a/src/ScriptCompiler.cpp b/src/ScriptCompiler.cpp index db9bfbb..08bd16d 100644 --- a/src/ScriptCompiler.cpp +++ b/src/ScriptCompiler.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #if defined(_WIN32) #include #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; } diff --git a/src/ScriptCompiler.h b/src/ScriptCompiler.h index 70e49fa..fff6b4b 100644 --- a/src/ScriptCompiler.h +++ b/src/ScriptCompiler.h @@ -17,6 +17,8 @@ struct ScriptBuildCommands { std::string link; fs::path objectPath; fs::path binaryPath; + fs::path wrapperPath; + bool usedWrapper = false; }; struct ScriptCompileOutput { diff --git a/src/ScriptRuntime.cpp b/src/ScriptRuntime.cpp index 6adaebc..eff68ec 100644 --- a/src/ScriptRuntime.cpp +++ b/src/ScriptRuntime.cpp @@ -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(it->second.handle)); @@ -54,6 +66,11 @@ ScriptRuntime::InspectorFn ScriptRuntime::getInspector(const fs::path& binaryPat return nullptr; } mod.inspector = reinterpret_cast(GetProcAddress(static_cast(mod.handle), "Script_OnInspector")); + mod.begin = reinterpret_cast(GetProcAddress(static_cast(mod.handle), "Script_Begin")); + mod.spec = reinterpret_cast(GetProcAddress(static_cast(mod.handle), "Script_Spec")); + mod.testEditor = reinterpret_cast(GetProcAddress(static_cast(mod.handle), "Script_TestEditor")); + mod.update = reinterpret_cast(GetProcAddress(static_cast(mod.handle), "Script_Update")); + mod.tickUpdate = reinterpret_cast(GetProcAddress(static_cast(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(dlsym(mod.handle, "Script_OnInspector")); + mod.begin = reinterpret_cast(dlsym(mod.handle, "Script_Begin")); + mod.spec = reinterpret_cast(dlsym(mod.handle, "Script_Spec")); + mod.testEditor = reinterpret_cast(dlsym(mod.handle, "Script_TestEditor")); + mod.update = reinterpret_cast(dlsym(mod.handle, "Script_Update")); + mod.tickUpdate = reinterpret_cast(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(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() { diff --git a/src/ScriptRuntime.h b/src/ScriptRuntime.h index 9da7570..b9e1b80 100644 --- a/src/ScriptRuntime.h +++ b/src/ScriptRuntime.h @@ -2,12 +2,14 @@ #include "Common.h" #include "SceneObject.h" +#include 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 beginCalledObjects; }; + Module* getModule(const fs::path& binaryPath); std::unordered_map loaded; std::string lastError; };