From e0be7a8d8c9f8d54d7af1b6b976fefc941ebba1f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 21:46:03 +0300 Subject: [PATCH 01/32] fix custom model blocks render (with some conditions) --- src/content/ContentBuilder.cpp | 13 +++++++------ src/frontend/ContentGfxCache.cpp | 9 ++++++++- src/graphics/render/BlocksRenderer.cpp | 1 - 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index ce77434ed..25029963b 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -26,17 +26,18 @@ std::unique_ptr ContentBuilder::build() { def.rt.tags.insert(tags.add(tag)); } + auto isSolid = [](const Variant& variant) { + return variant.model.type == BlockModelType::BLOCK || + variant.culling == CullingMode::OPTIONAL; + }; + if (def.variants) { for (auto& variant : def.variants->variants) { - variant.rt.solid = - variant.model.type == BlockModelType::BLOCK || - def.explictlySolid; + variant.rt.solid = isSolid(variant) || def.explictlySolid; } def.defaults = def.variants->variants.at(0); } else { - def.defaults.rt.solid = - def.defaults.model.type == BlockModelType::BLOCK || - def.explictlySolid; + def.defaults.rt.solid = isSolid(def.defaults) || def.explictlySolid; } const float EPSILON = 0.01f; diff --git a/src/frontend/ContentGfxCache.cpp b/src/frontend/ContentGfxCache.cpp index b8ce69a5f..9cdcdf70e 100644 --- a/src/frontend/ContentGfxCache.cpp +++ b/src/frontend/ContentGfxCache.cpp @@ -30,6 +30,7 @@ void ContentGfxCache::refreshVariant( uint8_t variantIndex, const Atlas& atlas ) { + std::unordered_map optionalTextures; bool denseRender = settings.denseRender.get(); for (uint side = 0; side < 6; side++) { std::string tex = variant.textureFaces[side]; @@ -43,6 +44,7 @@ void ContentGfxCache::refreshVariant( texOpaque = tex; } else if (variant.culling == CullingMode::OPTIONAL && !denseRender) { tex = texOpaque; + optionalTextures[variant.textureFaces[side]] = texOpaque; } size_t index = getRegionIndex(def.rt.id, variantIndex, side, false); sideregions[index] = atlas.get(tex); @@ -56,7 +58,12 @@ void ContentGfxCache::refreshVariant( if (pos == std::string::npos) { continue; } - if (auto region = atlas.getIf(mesh.texture.substr(pos+1))) { + std::string texture = mesh.texture.substr(pos+1); + const auto& found = optionalTextures.find(texture); + if (found != optionalTextures.end()) { + texture = found->second; + } + if (auto region = atlas.getIf(texture)) { for (auto& vertex : mesh.vertices) { vertex.uv = region->apply(vertex.uv); } diff --git a/src/graphics/render/BlocksRenderer.cpp b/src/graphics/render/BlocksRenderer.cpp index 0aae42010..33ecc8e17 100644 --- a/src/graphics/render/BlocksRenderer.cpp +++ b/src/graphics/render/BlocksRenderer.cpp @@ -538,7 +538,6 @@ void BlocksRenderer::render( break; } case BlockModelType::CUSTOM: { - if (!denseRender) blockCustomModel( {x, y, z}, def, From 0e31bdb83faba49112c4b6f536e7b22402aba6f5 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 20:06:40 +0300 Subject: [PATCH 02/32] fix base:drop hitbox size (revert part of previous change) --- res/content/base/scripts/components/drop.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/content/base/scripts/components/drop.lua b/res/content/base/scripts/components/drop.lua index 39bd03ceb..335b81d89 100644 --- a/res/content/base/scripts/components/drop.lua +++ b/res/content/base/scripts/components/drop.lua @@ -10,7 +10,7 @@ dropitem = ARGS if dropitem.item then dropitem.id = item.index(dropitem.item) end -if dropitem then +if dropitem then timer = dropitem.pickup_delay or timer end if SAVED_DATA.item then @@ -26,6 +26,7 @@ function on_save() end if vc.is_client() then + local SCALE = 0.3 local scale = {1, 1, 1} local rotation = mat4.rotate({ math.random(), math.random(), math.random() @@ -35,7 +36,7 @@ if vc.is_client() then local matrix = mat4.idt() rig:set_model(0, item.model_name(dropitem.id)) local bodysize = math.min(scale[1], scale[2], scale[3]) - body:set_size({scale[1], bodysize, scale[3]}) + body:set_size(vec3.mul({scale[1], bodysize, scale[3]}, SCALE)) mat4.mul(matrix, rotation, matrix) mat4.scale(matrix, scale, matrix) rig:set_matrix(0, matrix) From 17095011e66be4ccc044dfa2c1b8fd449e2ab0cc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 21:10:38 +0300 Subject: [PATCH 03/32] update doc/*/entity-properties.md --- doc/en/entity-properties.md | 2 +- doc/ru/entity-properties.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/entity-properties.md b/doc/en/entity-properties.md index 8bc4d5d37..4934e4264 100644 --- a/doc/en/entity-properties.md +++ b/doc/en/entity-properties.md @@ -61,7 +61,7 @@ Determines how the physics engine will work with it. ### *blocking* -Determines whether the entity blocks installation of blocks. +If true - the entity blocks the placement of a block that intersects with the entity's hitbox. *In the future will also block other entities movement.* diff --git a/doc/ru/entity-properties.md b/doc/ru/entity-properties.md index de10cfd32..47126a258 100644 --- a/doc/ru/entity-properties.md +++ b/doc/ru/entity-properties.md @@ -61,7 +61,7 @@ ### Блокирование - *blocking* -Определяет блокирует ли сущность установку блоков. +Определяет блокирует ли сущность установку блока, пересекающегося с хитбоксом сущности. *В будущем будет также блокировать движение других сущностей.* From 439d95355d954c06fcb3aab09c9ed928afdd4f8a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 21:24:40 +0300 Subject: [PATCH 04/32] add 'selectable' entity property --- src/content/loading/EntityLoader.cpp | 1 + src/objects/Entities.cpp | 2 +- src/objects/EntityDef.hpp | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/content/loading/EntityLoader.cpp b/src/content/loading/EntityLoader.cpp index 5741b2079..e785fb0fe 100644 --- a/src/content/loading/EntityLoader.cpp +++ b/src/content/loading/EntityLoader.cpp @@ -89,6 +89,7 @@ template<> void ContentUnitLoader::loadUnit( root.at("skeleton-name").get(def.skeletonName); root.at("material").get(def.material); root.at("blocking").get(def.blocking); + root.at("selectable").get(def.selectable); root.at("solid").get(def.solid); root.at("mass").get(def.mass); root.at("elasticity").get(def.elasticity); diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index fafd99fe5..f0e3e5340 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -176,7 +176,7 @@ std::optional Entities::rayCast( for (auto [entity, eid, transform, body] : view.each()) { if (eid.uid == settings.ignoredUid || !body.enabled || - (settings.solidEntitiesOnly && !eid.def.solid)) { + (settings.solidEntitiesOnly && !eid.def.solid) || !eid.def.selectable) { continue; } if (settings.entitiesFilter) { diff --git a/src/objects/EntityDef.hpp b/src/objects/EntityDef.hpp index 6ecb1c917..59aed2d7e 100644 --- a/src/objects/EntityDef.hpp +++ b/src/objects/EntityDef.hpp @@ -42,6 +42,9 @@ struct EntityDef { /// @brief Solid entity material material std::string material; + /// @brief Can the entity be interacted with cursor (on_attack, on_use) + bool selectable = true; + /// @brief Does entity prevent blocks setup bool blocking = true; From 35113777de3e9f250e15b37067f3cc4945d8969f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 21:31:48 +0300 Subject: [PATCH 05/32] add 'nonselect_entities' raycast parameter --- src/logic/scripting/lua/libs/libentity.cpp | 4 ++++ src/objects/Entities.cpp | 3 ++- src/objects/Entities.hpp | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 110aee123..4434136ba 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -311,6 +311,10 @@ static int l_world_raycast(lua::State* L) { entityRaycast.entityFilterExcludeMode = lua::toboolean(L, -1); lua::pop(L); } + if (lua::getfield(L, "nonselect_entities", 1)) { + entityRaycast.includeNonSelectable = lua::toboolean(L, -1); + lua::pop(L); + } if (lua::getfield(L, "filter_blocks", 1)) { load_blocks_filter(L, -1, filteredBlocks); blocksRaycast.filter = &filteredBlocks; diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index f0e3e5340..0902ae85a 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -176,7 +176,8 @@ std::optional Entities::rayCast( for (auto [entity, eid, transform, body] : view.each()) { if (eid.uid == settings.ignoredUid || !body.enabled || - (settings.solidEntitiesOnly && !eid.def.solid) || !eid.def.selectable) { + (settings.solidEntitiesOnly && !eid.def.solid) || + (!eid.def.selectable && !settings.includeNonSelectable)) { continue; } if (settings.entitiesFilter) { diff --git a/src/objects/Entities.hpp b/src/objects/Entities.hpp index 0d003ffb1..382790b95 100644 --- a/src/objects/Entities.hpp +++ b/src/objects/Entities.hpp @@ -85,6 +85,7 @@ public: bool solidEntitiesOnly = false; bool entityFilterExcludeMode = false; const std::set* entitiesFilter = nullptr; + bool includeNonSelectable = false; }; struct RaycastResult { From 2b5acfde6279526f476ca58e40407bba415779c0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 21:38:27 +0300 Subject: [PATCH 06/32] update doc/*/libworld.md & update doc/*/entity-properties.md --- doc/en/entity-properties.md | 4 ++++ doc/en/scripting/builtins/libworld.md | 2 ++ doc/ru/entity-properties.md | 6 +++++- doc/ru/scripting/builtins/libworld.md | 2 ++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/doc/en/entity-properties.md b/doc/en/entity-properties.md index 4934e4264..b0aee9aea 100644 --- a/doc/en/entity-properties.md +++ b/doc/en/entity-properties.md @@ -67,6 +67,10 @@ If true - the entity blocks the placement of a block that intersects with the en Default value: *true*. +### *selectable* + +If set to `false` the cursor will ignore the entity, passing the ray. + ### *sensors* A sensor is an area attached to a physical body that detects the entry of other bodies into it. diff --git a/doc/en/scripting/builtins/libworld.md b/doc/en/scripting/builtins/libworld.md index 54b44fe15..5ff749cb8 100644 --- a/doc/en/scripting/builtins/libworld.md +++ b/doc/en/scripting/builtins/libworld.md @@ -92,6 +92,8 @@ world.raycast( -- exclusion filtering mode: -- if true, filter_entities defines the list of ignored entity types entities_exclusion: bool = false, + -- include entities with selectable=false + nonselect_blocks: bool = false; [==[ additional parameters (blocks) ]==] -- id of the block types used for filtering diff --git a/doc/ru/entity-properties.md b/doc/ru/entity-properties.md index 47126a258..2f5ca3791 100644 --- a/doc/ru/entity-properties.md +++ b/doc/ru/entity-properties.md @@ -61,12 +61,16 @@ ### Блокирование - *blocking* -Определяет блокирует ли сущность установку блока, пересекающегося с хитбоксом сущности. +Определяет, блокирует ли сущность установку блока, пересекающегося с хитбоксом сущности. *В будущем будет также блокировать движение других сущностей.* Значение по-умолчанию: *true*. +### Выделяемость - *selectable* + +При значении в `false` курсор будет игнорировать cущность, пропуская луч далее. + ### Список сенсоров - *sensors* Сенсор - область пространства, привязанная к физическому телу, детектирующее попадание в него других тел. diff --git a/doc/ru/scripting/builtins/libworld.md b/doc/ru/scripting/builtins/libworld.md index 6b945aa76..52812f295 100644 --- a/doc/ru/scripting/builtins/libworld.md +++ b/doc/ru/scripting/builtins/libworld.md @@ -91,6 +91,8 @@ world.raycast( -- исключающий режим фильтрации: -- при значении (true) filter_entities определяет список игнорируемых entities_exclusion: bool = false, + -- учитывать сущности с selectable=false + nonselect_entities: bool = false; [==[ дополнительные параметры (блоки) ]==] -- id типов блоков используемых для фильтрации From 2f90083ac62db210d08b5893fc0a1e66c9811528 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 22:17:18 +0300 Subject: [PATCH 07/32] fix doc/en/scripting/buiiltins/libworld.md --- doc/en/scripting/builtins/libworld.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/scripting/builtins/libworld.md b/doc/en/scripting/builtins/libworld.md index 5ff749cb8..72bfaf8e4 100644 --- a/doc/en/scripting/builtins/libworld.md +++ b/doc/en/scripting/builtins/libworld.md @@ -93,7 +93,7 @@ world.raycast( -- if true, filter_entities defines the list of ignored entity types entities_exclusion: bool = false, -- include entities with selectable=false - nonselect_blocks: bool = false; + nonselect_entities: bool = false; [==[ additional parameters (blocks) ]==] -- id of the block types used for filtering From 8a40bf4fcd1c9696f91b65cea25216db1e942257 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 28 Jul 2026 23:10:47 +0300 Subject: [PATCH 08/32] feat: auto-focus search textbox in content menu --- res/layouts/pages/content.xml.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/res/layouts/pages/content.xml.lua b/res/layouts/pages/content.xml.lua index 2937542f4..cb63a96c7 100644 --- a/res/layouts/pages/content.xml.lua +++ b/res/layouts/pages/content.xml.lua @@ -3,6 +3,7 @@ function on_open(params) mode = params.mode end refresh() + document.search_textbox.focused = true end -- add - packs to be added to the world (after apply) From 383dcde2e77cdb7ca672203c8bb21b5806a6d155 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 28 Jul 2026 23:18:41 +0300 Subject: [PATCH 09/32] move default block material name to defaults.toml --- res/content/base/config/defaults.toml | 1 + src/content/ContentBuilder.cpp | 3 +++ src/voxels/Block.hpp | 4 +--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/res/content/base/config/defaults.toml b/res/content/base/config/defaults.toml index 650c24d74..d2c5142cc 100644 --- a/res/content/base/config/defaults.toml +++ b/res/content/base/config/defaults.toml @@ -1,3 +1,4 @@ generator = "base:demo" player-entity = "base:player" hand-skeleton = "base:hand" +block-material = "base:stone" diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index 25029963b..97498b497 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -39,6 +39,9 @@ std::unique_ptr ContentBuilder::build() { } else { def.defaults.rt.solid = isSolid(def.defaults) || def.explictlySolid; } + if (def.material.empty()) { + defaults.at("block-material").get(def.material); + } const float EPSILON = 0.01f; def.rt.solid = diff --git a/src/voxels/Block.hpp b/src/voxels/Block.hpp index 5ba3bb267..ded51244e 100644 --- a/src/voxels/Block.hpp +++ b/src/voxels/Block.hpp @@ -37,8 +37,6 @@ inline constexpr size_t MAX_USER_BLOCK_FIELDS_SIZE = 240; inline constexpr int BLOCK_MAX_VARIANTS = 16; -inline std::string DEFAULT_MATERIAL = "base:stone"; - struct BlockFuncsSet { bool init : 1; bool update : 1; @@ -206,7 +204,7 @@ public: dv::value properties = nullptr; /// @brief id of used BlockMaterial, may specify non-existing material - std::string material = DEFAULT_MATERIAL; + std::string material; /// @brief Light emission R, G, B, S (sky lights: sun, moon, radioactive /// clouds) From d9631467fc3c8006ebdadedf6f2bd724287591d6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 28 Jul 2026 23:28:01 +0300 Subject: [PATCH 10/32] update doc/*/block-properties.md --- doc/en/block-properties.md | 14 +++++++++++++- doc/ru/block-properties.md | 7 ++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/en/block-properties.md b/doc/en/block-properties.md index f3137dd60..d98a8cac5 100644 --- a/doc/en/block-properties.md +++ b/doc/en/block-properties.md @@ -1,6 +1,6 @@ # Block properties -## Visual +## Visual & Audio ### *texture* @@ -70,6 +70,18 @@ Rotation profile (set of available block rotations and behaviour of placing bloc - "pane" - panels, doors, signs - "stairs" - "pane" + flipped variants +### *particles* + +Particles are specified as a JSON object. Property names can be found [in the particles section](particles.md). + +When camera is near to the block, the engine will create an emitter that will run +until the block is destroyed or the camera moves away a certain distance. + +### Material - *material* + +Defines the name of the block's material in the format `pack:material_name`, which affects the selection of block interaction sounds. +Material definitions are located in /block_materials. + ## Variants Some properties can vary dynamically, depending on the variant number stored in the user bits of the block. diff --git a/doc/ru/block-properties.md b/doc/ru/block-properties.md index 50bd7f023..e139f635d 100644 --- a/doc/ru/block-properties.md +++ b/doc/ru/block-properties.md @@ -1,6 +1,6 @@ # Свойства блоков -## Вид +## Вид & Звук ### Текстура - *texture* @@ -78,6 +78,11 @@ При приближении к блоку движок создаст эмиттер, который будет работать до разрушения блока или отдаления камеры на некоторое расстояние. +### Материал - *material* + +Определяет имя материала блока в формате `пак:имя_материала`, что влияет на выбор звуков взаимодействия с блоком. +Определения материалов расположены в /block_materials. + ## Варианты Некоторые свойства могут варьироваться динамически, в зависимости от номера варианта, хранимого в пользовательских битах блока. From 81af4b9e2acba5d67b5bacdb343618c686dd9d5f Mon Sep 17 00:00:00 2001 From: Xertis <118364459+Xertis@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:47:07 +0300 Subject: [PATCH 11/32] fixing errors in rules.lua --- res/modules/internal/rules.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/modules/internal/rules.lua b/res/modules/internal/rules.lua index a1a5a8c32..4d739cf35 100644 --- a/res/modules/internal/rules.lua +++ b/res/modules/internal/rules.lua @@ -1,4 +1,4 @@ -local rules = {nexid = 1, rules = {}} +local rules = {nextid = 1, rules = {}} function rules.get_rule(name) local rule = rules.rules[name] @@ -27,13 +27,13 @@ end function rules.reset(name) local rule = rules.get_rule(name) - rules.set(rule.default) + rules.set(name, rule.default) end function rules.listen(name, handler) local rule = rules.get_rule(name) - local id = rules.nexid - rules.nextid = rules.nexid + 1 + local id = rules.nextid + rules.nextid = rules.nextid + 1 rule.listeners[utf8.encode(id)] = handler return id end From 513abd332c3256774314bba239b956ef03b3f16e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 20:02:30 +0300 Subject: [PATCH 12/32] 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 13/32] 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 14/32] 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 15/32] 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 16/32] 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 2088b5dec1fd82d6d41619107fafd6151e2e1442 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 30 Jul 2026 21:03:58 +0300 Subject: [PATCH 17/32] disable blocks updating caused by blocks.fill command --- res/scripts/stdcmd.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/scripts/stdcmd.lua b/res/scripts/stdcmd.lua index c2b102675..03df9235f 100644 --- a/res/scripts/stdcmd.lua +++ b/res/scripts/stdcmd.lua @@ -140,7 +140,7 @@ console.add_command( for y=y1,y2 do for z=z1,z2 do for x=x1,x2 do - block.set(x, y, z, id) + block.set(x, y, z, id, 0, true) end end end From a5a2bd0c8ac7a1891d45851e5ed7823078c6cc48 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 30 Jul 2026 21:31:33 +0300 Subject: [PATCH 18/32] 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 19/32] 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 20/32] 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 2d4b4b6081d97f5c0591a4184aa221b5a203e65a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Jul 2026 18:06:22 +0300 Subject: [PATCH 21/32] update doc/*/rigging.md --- doc/en/rigging.md | 2 +- doc/ru/rigging.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/rigging.md b/doc/en/rigging.md index 4b69b5769..abb5ad81a 100644 --- a/doc/en/rigging.md +++ b/doc/en/rigging.md @@ -38,7 +38,7 @@ Models are loaded automatically; adding them to preload.json is not required. ## Models -Models should be located in the models folder. Currently only OBJ format is supported. +Models must be located in the models folder. Currently, the following formats are supported: obj, vec3, and [vcm](vcm.md). >[!IMPORTANT] > When loading an obj model, the \*.mtl file is ignored. diff --git a/doc/ru/rigging.md b/doc/ru/rigging.md index 86ec0a09b..388b19ea2 100644 --- a/doc/ru/rigging.md +++ b/doc/ru/rigging.md @@ -38,7 +38,7 @@ ## Модели -Модели должны располагаться в папке models. На данный момент поддерживается только OBJ формат. +Модели должны располагаться в папке models. На данный момент поддерживаются форматы: obj, vec3 и [vcm](vcm.md). >[!IMPORTANT] > При загрузке obj модели игнорируется файл \*.mtl. From 0701f160e61d5f1eeb9984ea6eebc53ddc09ba27 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Jul 2026 18:07:43 +0300 Subject: [PATCH 22/32] update doc/en/scripting/builtins/libblock.md --- doc/en/scripting/builtins/libblock.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/en/scripting/builtins/libblock.md b/doc/en/scripting/builtins/libblock.md index 4513d57cf..cc69e47d8 100644 --- a/doc/en/scripting/builtins/libblock.md +++ b/doc/en/scripting/builtins/libblock.md @@ -28,7 +28,8 @@ block.get(x: int, y: int, z: int) -> int block.get_states(x: int, y: int, z: int) -> int -- Set block with given integer ID and state (default - 0) at given position. -block.set(x: int, y: int, z: int, id: int, states: int) +-- If noupdate=true is passed, the `on_update` event will not be called for adjacent blocks. +block.set(x: int, y: int, z: int, id: int, states: int, noupdate: boolean=false) -- Places a block with a given integer id and state (default - 0) at given position. -- on behalf of the player, calling the on_placed event. From ee9b2ab2660390056a66d11946abd6ecf70528f2 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Jul 2026 18:57:43 +0300 Subject: [PATCH 23/32] 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 24/32] 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 25/32] 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 26/32] 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 27/32] 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 28/32] 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 29/32] 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 30/32] 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 31/32] 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 32/32] 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() ```