From 513abd332c3256774314bba239b956ef03b3f16e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 20:02:30 +0300 Subject: [PATCH 01/18] add platform::new_engine_instance subProcess parameter --- src/logic/scripting/lua/libs/libapp.cpp | 3 +- src/util/platform.cpp | 73 ++++++++++++++++++++++++- src/util/platform.hpp | 4 +- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 6442fcbad..12758c724 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -365,7 +365,8 @@ static int l_start_debug_instance(lua::State* L) { platform::new_engine_instance( std::move(args), - outputPath.empty() ? "" : io::resolve(std::string(outputPath)) + outputPath.empty() ? "" : io::resolve(std::string(outputPath)), + true // debug instance may be detached ); return lua::pushinteger(L, port); } diff --git a/src/util/platform.cpp b/src/util/platform.cpp index f113641e5..76ed8a973 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -18,7 +18,11 @@ #pragma comment(lib, "winmm.lib") #else #include +#include #include +#include +#include +#include #endif namespace platform::internal { @@ -216,7 +220,9 @@ std::filesystem::path platform::get_executable_path() { } void platform::new_engine_instance( - const std::vector& args, std::filesystem::path outputFile + const std::vector& args, + std::filesystem::path outputFile, + bool subProcess ) { auto executable = get_executable_path(); @@ -285,6 +291,37 @@ void platform::new_engine_instance( &si, &pi ); + if (subProcess) { + HANDLE job = CreateJobObjectW(nullptr, nullptr); + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; + info.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + try { + if (!SetInformationJobObject( + job, + JobObjectExtendedLimitInformation, + &info, + sizeof(info))) { + throw std::runtime_error( + "SetInformationJobObject failed with code: " + + std::to_string(GetLastError()) + ); + } + if (!AssignProcessToJobObject(job, pi.hProcess)) { + throw std::runtime_error( + "SetInformationJobObject failed with code: " + + std::to_string(GetLastError()) + ); + } + } catch (const std::exception& err) { + CloseHandle(job); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return; + } + } if (success) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); @@ -295,6 +332,40 @@ void platform::new_engine_instance( ); } #else + + if (subProcess) { + pid_t pid = fork(); + + int fd = open( + outputFile.string().c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644 + ); + + if (pid == 0) { + prctl(PR_SET_PDEATHSIG, SIGTERM); + + if (getppid() == 1) { + _exit(1); + } + + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + close(fd); + + std::vector argv(args.size() + 2); + argv[0] = const_cast(executable.c_str()); + for (int i = 0; i < args.size(); i++) { + argv[i + 1] = args[i].c_str(); + } + argv[args.size() + 1] = nullptr; + + execvp(executable.c_str(), const_cast(argv.data())); + + _exit(127); + } else { + close(fd); + } + } + std::stringstream ss; ss << executable; for (int i = 0; i < args.size(); i++) { diff --git a/src/util/platform.hpp b/src/util/platform.hpp index 54c56a0c5..2ff535248 100644 --- a/src/util/platform.hpp +++ b/src/util/platform.hpp @@ -19,7 +19,9 @@ namespace platform { std::filesystem::path get_executable_path(); /// @brief Run a separate engine instance with specified arguments void new_engine_instance( - const std::vector& args, std::filesystem::path outputFile + const std::vector& args, + std::filesystem::path outputFile, + bool subProcess ); /// @brief Open URL in web browser bool open_url(const std::string& url); From 340aac82c341cbb9cd70ea5c1c1536035344a58e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 20:24:27 +0300 Subject: [PATCH 02/18] fix macos --- src/util/platform.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/util/platform.cpp b/src/util/platform.cpp index 76ed8a973..bc53d288f 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -18,12 +18,14 @@ #pragma comment(lib, "winmm.lib") #else #include -#include #include #include #include #include #endif +#ifdef __linux__ + #include +#endif namespace platform::internal { std::filesystem::path get_executable_path(); @@ -333,6 +335,8 @@ void platform::new_engine_instance( } #else +// TODO: implement +#ifndef __APPLE__ if (subProcess) { pid_t pid = fork(); @@ -365,6 +369,7 @@ void platform::new_engine_instance( close(fd); } } +#endif // __APPLE_ std::stringstream ss; ss << executable; From 4c18163f8aea02c8b555c47269c8b609df1ac594 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 22:22:54 +0300 Subject: [PATCH 03/18] add implicit io::path constructor from std::string_view --- src/io/path.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/io/path.hpp b/src/io/path.hpp index 538431b7c..3ab95300e 100644 --- a/src/io/path.hpp +++ b/src/io/path.hpp @@ -33,6 +33,8 @@ namespace io { } } + path(std::string_view str) : path(std::string(str)) {} + path(const char* str) : path(std::string(str)) {} bool operator==(const std::string& other) const { From 052ae41271124ebca8d3662db050ac78e04cc406 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 23:04:54 +0300 Subject: [PATCH 04/18] fix platform::new_engine_instance --- src/util/platform.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/platform.cpp b/src/util/platform.cpp index bc53d288f..88d42d34a 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -362,11 +362,13 @@ void platform::new_engine_instance( } argv[args.size() + 1] = nullptr; + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); execvp(executable.c_str(), const_cast(argv.data())); _exit(127); } else { close(fd); + return; } } #endif // __APPLE_ From 7f813d2eb140d61baee1a345cd113630bb631f0a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 23:06:02 +0300 Subject: [PATCH 05/18] add app.start_background_instance (test) --- src/devtools/Project.hpp | 2 ++ src/engine/Engine.cpp | 1 + src/logic/scripting/lua/libs/libapp.cpp | 25 ++++++++++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/devtools/Project.hpp b/src/devtools/Project.hpp index daeac7ca0..4906b9373 100644 --- a/src/devtools/Project.hpp +++ b/src/devtools/Project.hpp @@ -1,6 +1,7 @@ #pragma once #include "interfaces/Serializable.hpp" +#include "io/path.hpp" #include #include @@ -21,6 +22,7 @@ struct Permissions { struct Project : Serializable { std::string name; std::string title; + io::path path; std::vector basePacks; Permissions permissions; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 10f8cee6d..55ed9fec0 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -380,6 +380,7 @@ void Engine::loadAssets() { void Engine::loadProject() { io::path projectFile = "project:project.toml"; project = std::make_unique(); + project->path = "project:"; project->deserialize(io::read_object(projectFile)); logger.info() << "loaded project " << util::quote(project->name); } diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 12758c724..771567fd4 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -366,11 +366,33 @@ static int l_start_debug_instance(lua::State* L) { platform::new_engine_instance( std::move(args), outputPath.empty() ? "" : io::resolve(std::string(outputPath)), - true // debug instance may be detached + false ); return lua::pushinteger(L, port); } +static int l_start_background_instance(lua::State* L) { + auto scriptPath = lua::require_lstring(L, 1); + io::path outputPath = "user:background.log"; // TODO: + const auto& paths = engine->getPaths(); + + std::vector args { + "--headless", + "--res", paths.getResourcesFolder().u8string(), + "--dir", paths.getUserFilesFolder().u8string(), + "--script", io::resolve(scriptPath).u8string(), + }; + args.emplace_back("--project"); + args.emplace_back(io::resolve(engine->getProject().path).u8string()); + + platform::new_engine_instance( + std::move(args), + outputPath.empty() ? "" : io::resolve(outputPath), + true + ); + return 0; +} + const luaL_Reg applib[] = { /// content {"is_content_loaded", lua::wrap}, @@ -403,5 +425,6 @@ const luaL_Reg applib[] = { {"get_version", lua::wrap}, {"create_memory_device", lua::wrap}, {"start_debug_instance", lua::wrap}, + {"start_background_instance", lua::wrap}, {nullptr, nullptr} }; From a5a2bd0c8ac7a1891d45851e5ed7823078c6cc48 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 30 Jul 2026 21:31:33 +0300 Subject: [PATCH 06/18] cleanup a bit --- src/devtools/AppScriptsControl.cpp | 6 +----- src/devtools/AppScriptsControl.hpp | 3 +-- src/engine/Engine.cpp | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/devtools/AppScriptsControl.cpp b/src/devtools/AppScriptsControl.cpp index bb1a386a2..f565949aa 100644 --- a/src/devtools/AppScriptsControl.cpp +++ b/src/devtools/AppScriptsControl.cpp @@ -8,11 +8,7 @@ static debug::Logger logger("app-scripts"); -AppScriptsControl::AppScriptsControl( - const CoreParameters& params, const Project& project -) - : project(project) { - +AppScriptsControl::AppScriptsControl(const CoreParameters& params) { io::path scriptFile = params.scriptFile.empty() ? "project:start.lua" diff --git a/src/devtools/AppScriptsControl.hpp b/src/devtools/AppScriptsControl.hpp index 4e44eef60..3531dc2f9 100644 --- a/src/devtools/AppScriptsControl.hpp +++ b/src/devtools/AppScriptsControl.hpp @@ -14,7 +14,7 @@ struct Project; class AppScriptsControl { public: - AppScriptsControl(const CoreParameters& params, const Project& project); + AppScriptsControl(const CoreParameters& params); void tick(); void loadProjectClientScript(); @@ -24,7 +24,6 @@ public: bool isFinished() const; private: - const Project& project; std::unique_ptr clientScript; std::unique_ptr scriptCoroutine; }; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 55ed9fec0..0baa102fc 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -192,7 +192,7 @@ void Engine::initialize(CoreParameters coreParameters) { audio::set_input_device(name == "auto" ? "" : name); })); - appScripts = std::make_unique(params, *project); + appScripts = std::make_unique(params); if (params.stdinCommands) { cmd::start_stdin_cmd_reader(*this); From 466095efcb9398f400f27baba045643aede6814c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 30 Jul 2026 23:47:06 +0300 Subject: [PATCH 07/18] add SystemProcess --- src/main.cpp | 1 + src/util/platform.cpp | 100 ++++++++++++++++++++++++++++++++++++++++-- src/util/platform.hpp | 5 ++- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 77175bd8a..098d72a70 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ static debug::Logger logger("main"); static void sigterm_handler(int signum) { + logger.info() << "SIGTERM received"; Engine::getInstance().quit(); } diff --git a/src/util/platform.cpp b/src/util/platform.cpp index 88d42d34a..e0b2587b7 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -25,6 +25,7 @@ #endif #ifdef __linux__ #include + #include #endif namespace platform::internal { @@ -221,7 +222,98 @@ std::filesystem::path platform::get_executable_path() { #endif } -void platform::new_engine_instance( +class SystemProcess final : public Process { +public: +#ifdef __linux__ + SystemProcess(pid_t pid) : pid(pid) { + } + + ~SystemProcess() { + terminate(); + } +#elif defined(_WIN32) + SystemProcess(const PROCESS_INFORMATION& pi) + : processHandle(pi.hProcess), pid(pi.dwProcessId) { + if (pi.hThread != nullptr) { + CloseHandle(pi.hThread); + } + } + + ~SystemProcess() { + terminate(); + } +#endif + SystemProcess(const SystemProcess&) = delete; + SystemProcess(SystemProcess&&) noexcept = default; + SystemProcess& operator=(const SystemProcess&) = delete; + + SystemProcess& operator=(SystemProcess&& other) noexcept { + if (this != &other) { +#ifdef __linux__ + pid = other.pid; +#elif defined(_WIN32) + if (processHandle != nullptr) { + CloseHandle(processHandle); + } + processHandle = other.processHandle; + pid = other.pid; + other.processHandle = nullptr; + other.pid = 0; +#endif + } + return *this; + } + + bool isActive() const override { +#ifdef __linux__ + if (kill(pid, 0) == 0) { + return true; + } + return errno != ESRCH; +#elif defined(_WIN32) + if (processHandle == nullptr) { + return false; + } + DWORD exitCode; + if (GetExitCodeProcess(processHandle, &exitCode)) { + return exitCode == STILL_ACTIVE; + } + return false; +#endif + } + + void update() override {} + + void waitForEnd() override { +#ifdef __linux__ + waitpid(pid, nullptr, 0); +#elif defined(_WIN32) + if (processHandle != nullptr) { + WaitForSingleObject(processHandle, INFINITE); + } +#endif + }; + + void terminate() final override { +#ifdef __linux__ + kill(pid, SIGKILL); +#elif defined(_WIN32) + if (processHandle != nullptr) { + TerminateProcess(processHandle, 1); + WaitForSingleObject(processHandle, INFINITE); + } +#endif + } +private: +#ifdef __linux__ + pid_t pid; +#elif defined(_WIN32) + HANDLE processHandle = nullptr; + DWORD pid = 0; +#endif +}; + +std::unique_ptr platform::new_engine_instance( const std::vector& args, std::filesystem::path outputFile, bool subProcess @@ -317,11 +409,12 @@ void platform::new_engine_instance( std::to_string(GetLastError()) ); } + return std::make_unique(pi); } catch (const std::exception& err) { CloseHandle(job); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); - return; + return nullptr; } } if (success) { @@ -368,7 +461,7 @@ void platform::new_engine_instance( _exit(127); } else { close(fd); - return; + return std::make_unique(pid); } } #endif // __APPLE_ @@ -393,6 +486,7 @@ void platform::new_engine_instance( ); } #endif + return nullptr; } bool platform::stdin_has_data() { diff --git a/src/util/platform.hpp b/src/util/platform.hpp index 2ff535248..18dc052b6 100644 --- a/src/util/platform.hpp +++ b/src/util/platform.hpp @@ -2,8 +2,11 @@ #include #include +#include #include +#include "interfaces/Process.hpp" + namespace platform { void configure_encoding(); /// @brief Get Environment locale in ISO format ll_CC @@ -18,7 +21,7 @@ namespace platform { /// @brief Get current process running executable path std::filesystem::path get_executable_path(); /// @brief Run a separate engine instance with specified arguments - void new_engine_instance( + std::unique_ptr new_engine_instance( const std::vector& args, std::filesystem::path outputFile, bool subProcess From 4a53090610dce86b8f54d59b438a19395e6039be Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Jul 2026 00:06:25 +0300 Subject: [PATCH 08/18] feat: app coroutine error handling --- res/scripts/stdlib.lua | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 9793216a2..2869bba1c 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -140,7 +140,20 @@ local __vc_named_coroutines = {} local __vc_next_coroutine = 1 function __vc_start_coroutine(chunk) - local co = coroutine.create(chunk) + local co = coroutine.create(function() + local _, err = xpcall(chunk, function(msg) + local traceback = debug.get_traceback(0) + local s = string.format("%s:", msg) + for i=1,#traceback - 2 do + local frame = traceback[i] + s = s .. "\n\t"..tb_frame_tostring(frame) + end + return s + end) + if err then + error(err) + end + end) local id = __vc_next_coroutine __vc_next_coroutine = __vc_next_coroutine + 1 __vc_coroutines[id] = co From ee9b2ab2660390056a66d11946abd6ecf70528f2 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Jul 2026 18:57:43 +0300 Subject: [PATCH 09/18] add quit signal check to the client mainloop --- src/engine/Mainloop.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index 3a196f522..aa505a9ba 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -43,7 +43,7 @@ void Mainloop::run() { auto& appScripts = engine.getAppScripts(); logger.info() << "main loop started"; - while (!window.isShouldClose()) { + while (!window.isShouldClose() && !engine.isQuitSignal()) { time.update(window.time()); engine.applicationTick(); engine.updateFrontend(); From 5a8fd3ceab614eab3922d2fd133dbc20248f17bf Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 1 Aug 2026 22:32:49 +0300 Subject: [PATCH 10/18] update sub-instance lifetime --- src/engine/Engine.cpp | 1 + src/logic/scripting/lua/libs/libapp.cpp | 8 ++++++-- src/main.cpp | 2 +- src/util/platform.cpp | 6 ++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 0baa102fc..f0e20f922 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -413,6 +413,7 @@ void Engine::onWorldClosed() { } void Engine::quit() { + logger.info() << "quitSignal set to true"; quitSignal = true; if (!isHeadless()) { window->setShouldClose(true); diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 771567fd4..4d932c272 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -22,6 +22,10 @@ using namespace scripting; +namespace { + static std::unique_ptr sub_instance = nullptr; +} + /// @brief Check if content is loaded static int l_is_content_loaded(lua::State* L) { return lua::pushboolean(L, content != nullptr); @@ -377,7 +381,7 @@ static int l_start_background_instance(lua::State* L) { const auto& paths = engine->getPaths(); std::vector args { - "--headless", + // "--headless", "--res", paths.getResourcesFolder().u8string(), "--dir", paths.getUserFilesFolder().u8string(), "--script", io::resolve(scriptPath).u8string(), @@ -385,7 +389,7 @@ static int l_start_background_instance(lua::State* L) { args.emplace_back("--project"); args.emplace_back(io::resolve(engine->getProject().path).u8string()); - platform::new_engine_instance( + ::sub_instance = platform::new_engine_instance( std::move(args), outputPath.empty() ? "" : io::resolve(outputPath), true diff --git a/src/main.cpp b/src/main.cpp index 098d72a70..2438d5f50 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,7 +10,7 @@ static debug::Logger logger("main"); static void sigterm_handler(int signum) { - logger.info() << "SIGTERM received"; + logger.info() << "SIGTERM received " << signum; Engine::getInstance().quit(); } diff --git a/src/util/platform.cpp b/src/util/platform.cpp index e0b2587b7..7b08639e9 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -296,7 +296,8 @@ public: void terminate() final override { #ifdef __linux__ - kill(pid, SIGKILL); + logger.info() << "terminating " << pid; + kill(pid, SIGTERM); #elif defined(_WIN32) if (processHandle != nullptr) { TerminateProcess(processHandle, 1); @@ -461,10 +462,11 @@ std::unique_ptr platform::new_engine_instance( _exit(127); } else { close(fd); + usleep(1000); // waiting for execvp call return std::make_unique(pid); } } -#endif // __APPLE_ +#endif std::stringstream ss; ss << executable; From 4503455ae94082506671355b566e006fd47e0acf Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 01:19:01 +0300 Subject: [PATCH 11/18] add sub-instance project permission & feat: max sub-process depth check --- res/project.toml | 3 ++- src/constants.hpp | 2 ++ src/devtools/Project.hpp | 1 + src/engine/CoreParameters.hpp | 1 + src/logic/scripting/lua/libs/libapp.cpp | 20 ++++++++++++++++++-- src/main.cpp | 2 ++ src/util/command_line.cpp | 13 ++++++++++--- 7 files changed, 36 insertions(+), 6 deletions(-) diff --git a/res/project.toml b/res/project.toml index b641ad00f..281964857 100644 --- a/res/project.toml +++ b/res/project.toml @@ -3,5 +3,6 @@ name = "default" base_packs = ["base"] permissions = [ "network", - "record-audio" + "record-audio", + "sub-instances", ] diff --git a/src/constants.hpp b/src/constants.hpp index f43314f4a..0396f59a6 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -16,6 +16,8 @@ inline constexpr bool ENGINE_DEBUG_BUILD = true; inline const std::string ENGINE_VERSION_STRING = "0.32"; +inline constexpr uint MAX_SUBPROCESS_DEPTH = 2; + /// @brief world regions format version inline constexpr uint REGION_FORMAT_VERSION = 3; diff --git a/src/devtools/Project.hpp b/src/devtools/Project.hpp index 4906b9373..571a8b2c5 100644 --- a/src/devtools/Project.hpp +++ b/src/devtools/Project.hpp @@ -13,6 +13,7 @@ struct Permissions { static inline std::string NETWORK = "network"; static inline std::string RECORD_AUDIO = "record-audio"; static inline std::string WRITE_TO_USER = "write-to-user"; + static inline std::string SUB_INSTANCES = "sub-instances"; std::set permissions; diff --git a/src/engine/CoreParameters.hpp b/src/engine/CoreParameters.hpp index b9f585f47..b5d8074ce 100644 --- a/src/engine/CoreParameters.hpp +++ b/src/engine/CoreParameters.hpp @@ -14,5 +14,6 @@ struct CoreParameters { std::filesystem::path projectFolder; std::string debugServerString; int tps = 20; + int subProcessDepth = 0; std::unordered_map projectArgs; }; diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 4d932c272..e3d41d8c7 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -341,6 +341,10 @@ static int l_start_debug_instance(lua::State* L) { if (!engine->getProject().permissions.has(Permissions::DEBUGGING)) { throw std::runtime_error("project has no debugging permission"); } + const auto& params = engine->getCoreParameters(); + if (params.subProcessDepth >= MAX_SUBPROCESS_DEPTH) { + throw std::runtime_error("max subprocess depth exceeded"); + } int port = lua::tointeger(L, 1); if (port == 0) { @@ -361,6 +365,8 @@ static int l_start_debug_instance(lua::State* L) { "--res", paths.getResourcesFolder().u8string(), "--dir", paths.getUserFilesFolder().u8string(), "--dbg-server", "tcp:" + std::to_string(port), + "--sub-depth", std::to_string(engine->getCoreParameters() + .subProcessDepth + 1), }; if (!projectPath.empty()) { args.emplace_back("--project"); @@ -376,15 +382,25 @@ static int l_start_debug_instance(lua::State* L) { } static int l_start_background_instance(lua::State* L) { + if (!engine->getProject().permissions.has(Permissions::SUB_INSTANCES)) { + throw std::runtime_error("project has no sub-instances permission"); + } + const auto& params = engine->getCoreParameters(); + if (params.subProcessDepth >= MAX_SUBPROCESS_DEPTH) { + throw std::runtime_error("max subprocess depth exceeded"); + } + auto scriptPath = lua::require_lstring(L, 1); - io::path outputPath = "user:background.log"; // TODO: + io::path outputPath = lua::isstring(L, 2) ? lua::require_lstring(L, 2) : ""; const auto& paths = engine->getPaths(); std::vector args { - // "--headless", + "--headless", "--res", paths.getResourcesFolder().u8string(), "--dir", paths.getUserFilesFolder().u8string(), "--script", io::resolve(scriptPath).u8string(), + "--sub-depth", std::to_string(engine->getCoreParameters() + .subProcessDepth + 1), }; args.emplace_back("--project"); args.emplace_back(io::resolve(engine->getProject().path).u8string()); diff --git a/src/main.cpp b/src/main.cpp index 2438d5f50..83887422a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,8 @@ int main(int argc, char** argv) { if (!parse_cmdline(argc, argv, coreParameters)) { return EXIT_SUCCESS; } + logger.debug() << "sub-process depth: " + << coreParameters.subProcessDepth; } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return EXIT_FAILURE; diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index 7f34bcea4..14f49caf4 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -78,12 +78,19 @@ static bool perform_keyword( params.debugServerString = reader.next(); return true; }, "", "open debugging server where is {transport}:{port}"), + ArgC("--sub-depth", [¶ms, &reader]() -> bool { + params.subProcessDepth = reader.nextInt(); + return true; + }, "", "sub-process depth"), ArgC("--help", []() -> bool { std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n"; std::cout << "Command-line arguments:\n"; - for (auto& a : argumentsCommandline) { - std::cout << std::setw(24) << std::left << (a.keyword + " " + a.args); - std::cout << "- " << a.help << std::endl; + for (auto& arg : argumentsCommandline) { + if (arg.help.empty()) { + continue; + } + std::cout << std::setw(24) << std::left << (arg.keyword + " " + arg.args); + std::cout << "- " << arg.help << std::endl; } std::cout << std::endl; return false; From 46d3e5c7e1e9fcd511bb6fc4d9077b298763b587 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 01:28:02 +0300 Subject: [PATCH 12/18] cleanup command_line.cpp --- src/util/command_line.cpp | 61 ++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index 14f49caf4..f05231821 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -13,22 +13,22 @@ namespace fs = std::filesystem; class ArgC { - public: - std::string keyword; - std::function execute; - std::string args; - std::string help; - ArgC( - const std::string& keyword, - std::function execute, - const std::string& args, - const std::string& help - ) { - this->keyword = keyword; - this->execute = execute; - this->args = args; - this->help = help; - } +public: + std::string keyword; + std::function execute; + std::string args; + std::string help; + ArgC( + const std::string& keyword, + std::function execute, + const std::string& args, + const std::string& help + ) { + this->keyword = keyword; + this->execute = execute; + this->args = args; + this->help = help; + } }; @@ -36,60 +36,61 @@ static bool perform_keyword( util::ArgsReader& reader, const std::string& keyword, CoreParameters& params ) { static const std::vector argumentsCommandline = { - ArgC("--res", [¶ms, &reader]() -> bool { + ArgC("--res", [](auto& params, auto& reader) -> bool { params.resFolder = reader.next(); return true; }, "", "set resources directory."), - ArgC("--dir", [¶ms, &reader]() -> bool { + ArgC("--dir", [](auto& params, auto& reader) -> bool { params.userFolder = reader.next(); return true; }, "", "set userfiles directory."), - ArgC("--project", [¶ms, &reader]() -> bool { + ArgC("--project", [](auto& params, auto& reader) -> bool { params.projectFolder = reader.next(); return true; }, "", "set project directory."), - ArgC("--test", [¶ms, &reader]() -> bool { + ArgC("--test", [](auto& params, auto& reader) -> bool { params.testMode = true; params.scriptFile = reader.next(); return true; }, "", "test script file."), - ArgC("--script", [¶ms, &reader]() -> bool { + ArgC("--script", [](auto& params, auto& reader) -> bool { params.testMode = false; params.scriptFile = reader.next(); return true; }, "", "main script file."), - ArgC("--headless", [¶ms]() -> bool { + ArgC("--headless", [](auto& params, auto& reader) -> bool { params.headless = true; return true; }, "", "run in headless mode."), - ArgC("--stdin-cmd", [¶ms]() -> bool { + ArgC("--stdin-cmd", [](auto& params, auto& reader) -> bool { params.stdinCommands = true; return true; }, "", "run commands from stdin."), - ArgC("--tps", [¶ms, &reader]() -> bool { + ArgC("--tps", [](auto& params, auto& reader) -> bool { params.tps = reader.nextInt(); return true; }, "", "headless mode tick-rate (default - 20)."), - ArgC("--version", []() -> bool { + ArgC("--version", [](auto&, auto&) -> bool { std::cout << ENGINE_VERSION_STRING << std::endl; return false; }, "", "display the engine version."), - ArgC("--dbg-server", [¶ms, &reader]() -> bool { + ArgC("--dbg-server", [](auto& params, auto& reader) -> bool { params.debugServerString = reader.next(); return true; }, "", "open debugging server where is {transport}:{port}"), - ArgC("--sub-depth", [¶ms, &reader]() -> bool { + ArgC("--sub-depth", [](auto& params, auto& reader) -> bool { params.subProcessDepth = reader.nextInt(); return true; }, "", "sub-process depth"), - ArgC("--help", []() -> bool { + ArgC("--help", [](auto&, auto&) -> bool { std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n"; std::cout << "Command-line arguments:\n"; for (auto& arg : argumentsCommandline) { if (arg.help.empty()) { continue; } - std::cout << std::setw(24) << std::left << (arg.keyword + " " + arg.args); + std::cout << std::setw(24) << std::left + << (arg.keyword + " " + arg.args); std::cout << "- " << arg.help << std::endl; } std::cout << std::endl; @@ -98,7 +99,7 @@ static bool perform_keyword( }; for (auto& a : argumentsCommandline) { if (a.keyword == keyword) { - return a.execute(); + return a.execute(params, reader); } } throw std::runtime_error("unknown argument " + keyword); From 8b2094c6283314cce1f4dcf28b543dff6731efc1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 15:02:44 +0300 Subject: [PATCH 13/18] add SIGTERM handler --- src/main.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 83887422a..402b829b3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,7 +10,7 @@ static debug::Logger logger("main"); static void sigterm_handler(int signum) { - logger.info() << "SIGTERM received " << signum; + logger.info() << (signum == SIGTERM ? "SIGTERM" : "SIGINT") << " received"; Engine::getInstance().quit(); } @@ -33,7 +33,9 @@ int main(int argc, char** argv) { return EXIT_FAILURE; } std::signal(SIGTERM, sigterm_handler); - +#ifdef NDEBUG + std::signal(SIGINT, sigterm_handler); +#endif debug::Logger::init(coreParameters.userFolder.string() + "/latest.log"); platform::configure_encoding(); From b1e14a294444e4fde541b6d56291635d7f381206 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 16:20:33 +0300 Subject: [PATCH 14/18] add app.terminate(handle) --- src/constants.hpp | 1 + src/logic/scripting/lua/libs/libapp.cpp | 33 ++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/constants.hpp b/src/constants.hpp index 0396f59a6..5bacfe078 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -16,6 +16,7 @@ inline constexpr bool ENGINE_DEBUG_BUILD = true; inline const std::string ENGINE_VERSION_STRING = "0.32"; +inline constexpr uint MAX_SUBPROCESSES = 1; inline constexpr uint MAX_SUBPROCESS_DEPTH = 2; /// @brief world regions format version diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index e3d41d8c7..e86b6d88e 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -20,10 +20,12 @@ #include "window/Window.hpp" #include "world/Level.hpp" +#include + using namespace scripting; namespace { - static std::unique_ptr sub_instance = nullptr; + static std::array, MAX_SUBPROCESSES> processes; } /// @brief Check if content is loaded @@ -405,12 +407,36 @@ static int l_start_background_instance(lua::State* L) { args.emplace_back("--project"); args.emplace_back(io::resolve(engine->getProject().path).u8string()); - ::sub_instance = platform::new_engine_instance( + int handle = -1; + for (int i = 0; i < ::processes.size(); i++) { + if (!::processes[i] || !::processes[i]->isActive()) { + handle = i; + break; + } + } + if (handle == -1) { + throw std::runtime_error("sub-processes limit exceeded"); + } + ::processes[handle] = platform::new_engine_instance( std::move(args), outputPath.empty() ? "" : io::resolve(outputPath), true ); - return 0; + return lua::pushinteger(L, handle); +} + +static int l_terminate(lua::State* L) { + int handle = lua::tointeger(L, 1); + if (handle < 0 || handle >= ::processes.size()) { + throw std::runtime_error("invalid process handle"); + } + auto& process = ::processes[handle]; + if (process == nullptr) { + return lua::pushboolean(L, false); + } + bool active = process->isActive(); + process.reset(); + return lua::pushboolean(L, active); } const luaL_Reg applib[] = { @@ -446,5 +472,6 @@ const luaL_Reg applib[] = { {"create_memory_device", lua::wrap}, {"start_debug_instance", lua::wrap}, {"start_background_instance", lua::wrap}, + {"terminate", lua::wrap}, {nullptr, nullptr} }; From 8de3c0de57018c12473f2fc9c8a0c67dd0aa9f6e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 17:54:08 +0300 Subject: [PATCH 15/18] update vc library --- res/scripts/stdmin.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 2868dd634..e341f68a1 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -10,8 +10,9 @@ vc = { is_client = function() return not _vc_headless end, + get_version = __vc_app.get_version, get_setting = __vc_app.get_setting, - get_setting_str = __vc_app.get_setting_str, + str_setting = __vc_app.str_setting, get_setting_info = __vc_app.get_setting_info, } From 466f515ea9cf3b23680bc418547a72544ab71f15 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 18:27:19 +0300 Subject: [PATCH 16/18] update doc/*/scripting/builtins/libapp.md --- doc/en/scripting/builtins/libapp.md | 17 +++++++++++++++++ doc/ru/scripting/builtins/libapp.md | 19 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/doc/en/scripting/builtins/libapp.md b/doc/en/scripting/builtins/libapp.md index 0ec755689..35084d9bd 100644 --- a/doc/en/scripting/builtins/libapp.md +++ b/doc/en/scripting/builtins/libapp.md @@ -191,3 +191,20 @@ app.reset_content_sources() ``` Resets content sources. + +## Sub-instances + +```lua +-- Creates a headless engine instance with the current project and the specified application script. +-- Returns the instance ID. The number of active sub-instances is currently limited to one. +app.start_background_instance( + -- script file + app_script: string, + -- log file + output_file: string | nil +) -> int + +-- Stops the engine sub-instance. +-- Returns true if the instance was alive at the time of the call. +app.terminate(handle: int) -> boolean +``` diff --git a/doc/ru/scripting/builtins/libapp.md b/doc/ru/scripting/builtins/libapp.md index 07e358ef4..920283ce3 100644 --- a/doc/ru/scripting/builtins/libapp.md +++ b/doc/ru/scripting/builtins/libapp.md @@ -142,4 +142,21 @@ app.set_content_sources(sources: table) -- Сбрасывает список источников контента. app.reset_content_sources() -``` \ No newline at end of file +``` + +## Под-экземпляры + +```lua +-- Создаёт headless-экземпляр движка с текущим проектом и указанным сценарием. +-- Возвращает id экземпляра. Число живых под-экземпляров, на данный момент, ограничено одним. +app.start_background_instance( + -- файл сценария + app_script: string, + -- файл лога + output_file: string | nil +) -> int + +-- Останавливает под-экземпляр движка. +-- Возвращает true если экземпляр был жив в момент вызова. +app.terminate(handle: int) -> boolean +``` From 6bbc78098dfd15b353b2d1d28d37133f4fe3651d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 18:32:54 +0300 Subject: [PATCH 17/18] add app.is_instance_alive & rename app.terminate to app.terminate_instance --- doc/en/scripting/builtins/libapp.md | 3 +++ doc/ru/scripting/builtins/libapp.md | 5 ++++- src/logic/scripting/lua/libs/libapp.cpp | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/doc/en/scripting/builtins/libapp.md b/doc/en/scripting/builtins/libapp.md index 35084d9bd..61be41d5d 100644 --- a/doc/en/scripting/builtins/libapp.md +++ b/doc/en/scripting/builtins/libapp.md @@ -204,6 +204,9 @@ app.start_background_instance( output_file: string | nil ) -> int +-- Checks if the engine sub-instance is alive. +app.is_instance_alive(handle: int) -> boolean + -- Stops the engine sub-instance. -- Returns true if the instance was alive at the time of the call. app.terminate(handle: int) -> boolean diff --git a/doc/ru/scripting/builtins/libapp.md b/doc/ru/scripting/builtins/libapp.md index 920283ce3..bbdf5cd2f 100644 --- a/doc/ru/scripting/builtins/libapp.md +++ b/doc/ru/scripting/builtins/libapp.md @@ -156,7 +156,10 @@ app.start_background_instance( output_file: string | nil ) -> int +-- Проверяет, жив ли под-экземпляр движка. +app.is_instance_alive(handle: int) -> boolean + -- Останавливает под-экземпляр движка. -- Возвращает true если экземпляр был жив в момент вызова. -app.terminate(handle: int) -> boolean +app.terminate_instance(handle: int) -> boolean ``` diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index e86b6d88e..9157b33a6 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -425,7 +425,16 @@ static int l_start_background_instance(lua::State* L) { return lua::pushinteger(L, handle); } -static int l_terminate(lua::State* L) { +static int l_is_instance_alive(lua::State* L) { + int handle = lua::tointeger(L, 1); + if (handle < 0 || handle >= ::processes.size()) { + throw std::runtime_error("invalid process handle"); + } + auto& process = ::processes[handle]; + return lua::pushboolean(L, process && process->isActive()); +} + +static int l_terminate_instance(lua::State* L) { int handle = lua::tointeger(L, 1); if (handle < 0 || handle >= ::processes.size()) { throw std::runtime_error("invalid process handle"); @@ -472,6 +481,7 @@ const luaL_Reg applib[] = { {"create_memory_device", lua::wrap}, {"start_debug_instance", lua::wrap}, {"start_background_instance", lua::wrap}, - {"terminate", lua::wrap}, + {"is_instance_alive", lua::wrap}, + {"terminate_instance", lua::wrap}, {nullptr, nullptr} }; From 4d010e8320964bc87e927ee5089e690c0250dd5c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 18:36:12 +0300 Subject: [PATCH 18/18] fix doc/en/scripting/builtins/libapp.md formatting --- doc/en/scripting/builtins/libapp.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/scripting/builtins/libapp.md b/doc/en/scripting/builtins/libapp.md index 61be41d5d..48d79bf8e 100644 --- a/doc/en/scripting/builtins/libapp.md +++ b/doc/en/scripting/builtins/libapp.md @@ -180,13 +180,13 @@ app.get_content_sources() -> table Returns a list of content sources (paths), in descending priority order. -``lua +```lua app.set_content_sources(sources: table) ``` Sets a list of content sources (paths). Specified in descending priority order. -``lua +```lua app.reset_content_sources() ```