Merge branch 'main' into refactor-world-renderer

This commit is contained in:
MihailRis 2026-08-02 20:05:42 +03:00
commit 61b601f95a
41 changed files with 464 additions and 78 deletions

View file

@ -1,6 +1,6 @@
# Block properties # Block properties
## Visual ## Visual & Audio
### *texture* ### *texture*
@ -70,6 +70,18 @@ Rotation profile (set of available block rotations and behaviour of placing bloc
- "pane" - panels, doors, signs - "pane" - panels, doors, signs
- "stairs" - "pane" + flipped variants - "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 ## Variants
Some properties can vary dynamically, depending on the variant number stored in the user bits of the block. Some properties can vary dynamically, depending on the variant number stored in the user bits of the block.

View file

@ -61,12 +61,16 @@ Determines how the physics engine will work with it.
### *blocking* ### *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.* *In the future will also block other entities movement.*
Default value: *true*. Default value: *true*.
### *selectable*
If set to `false` the cursor will ignore the entity, passing the ray.
### *sensors* ### *sensors*
A sensor is an area attached to a physical body that detects the entry of other bodies into it. A sensor is an area attached to a physical body that detects the entry of other bodies into it.

View file

@ -38,7 +38,7 @@ Models are loaded automatically; adding them to preload.json is not required.
## Models ## 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] >[!IMPORTANT]
> When loading an obj model, the \*.mtl file is ignored. > When loading an obj model, the \*.mtl file is ignored.

View file

@ -180,14 +180,34 @@ app.get_content_sources() -> table<string>
Returns a list of content sources (paths), in descending priority order. Returns a list of content sources (paths), in descending priority order.
``lua ```lua
app.set_content_sources(sources: table<string>) app.set_content_sources(sources: table<string>)
``` ```
Sets a list of content sources (paths). Specified in descending priority order. Sets a list of content sources (paths). Specified in descending priority order.
``lua ```lua
app.reset_content_sources() app.reset_content_sources()
``` ```
Resets 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
-- 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
```

View file

@ -28,7 +28,8 @@ block.get(x: int, y: int, z: int) -> int
block.get_states(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. -- 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. -- 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. -- on behalf of the player, calling the on_placed event.

View file

@ -92,6 +92,8 @@ world.raycast(
-- exclusion filtering mode: -- exclusion filtering mode:
-- if true, filter_entities defines the list of ignored entity types -- if true, filter_entities defines the list of ignored entity types
entities_exclusion: bool = false, entities_exclusion: bool = false,
-- include entities with selectable=false
nonselect_entities: bool = false;
[==[ additional parameters (blocks) ]==] [==[ additional parameters (blocks) ]==]
-- id of the block types used for filtering -- id of the block types used for filtering

View file

@ -1,6 +1,6 @@
# Свойства блоков # Свойства блоков
## Вид ## Вид & Звук
### Текстура - *texture* ### Текстура - *texture*
@ -78,6 +78,11 @@
При приближении к блоку движок создаст эмиттер, который будет работать При приближении к блоку движок создаст эмиттер, который будет работать
до разрушения блока или отдаления камеры на некоторое расстояние. до разрушения блока или отдаления камеры на некоторое расстояние.
### Материал - *material*
Определяет имя материала блока в формате `пак:имя_материала`, что влияет на выбор звуков взаимодействия с блоком.
Определения материалов расположены в /block_materials.
## Варианты ## Варианты
Некоторые свойства могут варьироваться динамически, в зависимости от номера варианта, хранимого в пользовательских битах блока. Некоторые свойства могут варьироваться динамически, в зависимости от номера варианта, хранимого в пользовательских битах блока.

View file

@ -61,12 +61,16 @@
### Блокирование - *blocking* ### Блокирование - *blocking*
Определяет блокирует ли сущность установку блоков. Определяет, блокирует ли сущность установку блока, пересекающегося с хитбоксом сущности.
*В будущем будет также блокировать движение других сущностей.* *В будущем будет также блокировать движение других сущностей.*
Значение по-умолчанию: *true*. Значение по-умолчанию: *true*.
### Выделяемость - *selectable*
При значении в `false` курсор будет игнорировать cущность, пропуская луч далее.
### Список сенсоров - *sensors* ### Список сенсоров - *sensors*
Сенсор - область пространства, привязанная к физическому телу, детектирующее попадание в него других тел. Сенсор - область пространства, привязанная к физическому телу, детектирующее попадание в него других тел.

View file

@ -38,7 +38,7 @@
## Модели ## Модели
Модели должны располагаться в папке models. На данный момент поддерживается только OBJ формат. Модели должны располагаться в папке models. На данный момент поддерживаются форматы: obj, vec3 и [vcm](vcm.md).
>[!IMPORTANT] >[!IMPORTANT]
> При загрузке obj модели игнорируется файл \*.mtl. > При загрузке obj модели игнорируется файл \*.mtl.

View file

@ -143,3 +143,23 @@ app.set_content_sources(sources: table<string>)
-- Сбрасывает список источников контента. -- Сбрасывает список источников контента.
app.reset_content_sources() app.reset_content_sources()
``` ```
## Под-экземпляры
```lua
-- Создаёт headless-экземпляр движка с текущим проектом и указанным сценарием.
-- Возвращает id экземпляра. Число живых под-экземпляров, на данный момент, ограничено одним.
app.start_background_instance(
-- файл сценария
app_script: string,
-- файл лога
output_file: string | nil
) -> int
-- Проверяет, жив ли под-экземпляр движка.
app.is_instance_alive(handle: int) -> boolean
-- Останавливает под-экземпляр движка.
-- Возвращает true если экземпляр был жив в момент вызова.
app.terminate_instance(handle: int) -> boolean
```

View file

@ -91,6 +91,8 @@ world.raycast(
-- исключающий режим фильтрации: -- исключающий режим фильтрации:
-- при значении (true) filter_entities определяет список игнорируемых -- при значении (true) filter_entities определяет список игнорируемых
entities_exclusion: bool = false, entities_exclusion: bool = false,
-- учитывать сущности с selectable=false
nonselect_entities: bool = false;
[==[ дополнительные параметры (блоки) ]==] [==[ дополнительные параметры (блоки) ]==]
-- id типов блоков используемых для фильтрации -- id типов блоков используемых для фильтрации

View file

@ -1,3 +1,4 @@
generator = "base:demo" generator = "base:demo"
player-entity = "base:player" player-entity = "base:player"
hand-skeleton = "base:hand" hand-skeleton = "base:hand"
block-material = "base:stone"

View file

@ -26,6 +26,7 @@ function on_save()
end end
if vc.is_client() then if vc.is_client() then
local SCALE = 0.3
local scale = {1, 1, 1} local scale = {1, 1, 1}
local rotation = mat4.rotate({ local rotation = mat4.rotate({
math.random(), math.random(), math.random() math.random(), math.random(), math.random()
@ -35,7 +36,7 @@ if vc.is_client() then
local matrix = mat4.idt() local matrix = mat4.idt()
rig:set_model(0, item.model_name(dropitem.id)) rig:set_model(0, item.model_name(dropitem.id))
local bodysize = math.min(scale[1], scale[2], scale[3]) 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.mul(matrix, rotation, matrix)
mat4.scale(matrix, scale, matrix) mat4.scale(matrix, scale, matrix)
rig:set_matrix(0, matrix) rig:set_matrix(0, matrix)

View file

@ -3,6 +3,7 @@ function on_open(params)
mode = params.mode mode = params.mode
end end
refresh() refresh()
document.search_textbox.focused = true
end end
-- add - packs to be added to the world (after apply) -- add - packs to be added to the world (after apply)

View file

@ -1,4 +1,4 @@
local rules = {nexid = 1, rules = {}} local rules = {nextid = 1, rules = {}}
function rules.get_rule(name) function rules.get_rule(name)
local rule = rules.rules[name] local rule = rules.rules[name]
@ -27,13 +27,13 @@ end
function rules.reset(name) function rules.reset(name)
local rule = rules.get_rule(name) local rule = rules.get_rule(name)
rules.set(rule.default) rules.set(name, rule.default)
end end
function rules.listen(name, handler) function rules.listen(name, handler)
local rule = rules.get_rule(name) local rule = rules.get_rule(name)
local id = rules.nexid local id = rules.nextid
rules.nextid = rules.nexid + 1 rules.nextid = rules.nextid + 1
rule.listeners[utf8.encode(id)] = handler rule.listeners[utf8.encode(id)] = handler
return id return id
end end

View file

@ -3,5 +3,6 @@ name = "default"
base_packs = ["base"] base_packs = ["base"]
permissions = [ permissions = [
"network", "network",
"record-audio" "record-audio",
"sub-instances",
] ]

View file

@ -140,7 +140,7 @@ console.add_command(
for y=y1,y2 do for y=y1,y2 do
for z=z1,z2 do for z=z1,z2 do
for x=x1,x2 do for x=x1,x2 do
block.set(x, y, z, id) block.set(x, y, z, id, 0, true)
end end
end end
end end

View file

@ -140,7 +140,20 @@ local __vc_named_coroutines = {}
local __vc_next_coroutine = 1 local __vc_next_coroutine = 1
function __vc_start_coroutine(chunk) 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 local id = __vc_next_coroutine
__vc_next_coroutine = __vc_next_coroutine + 1 __vc_next_coroutine = __vc_next_coroutine + 1
__vc_coroutines[id] = co __vc_coroutines[id] = co

View file

@ -10,8 +10,9 @@ vc = {
is_client = function() is_client = function()
return not _vc_headless return not _vc_headless
end, end,
get_version = __vc_app.get_version,
get_setting = __vc_app.get_setting, 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, get_setting_info = __vc_app.get_setting_info,
} }

View file

@ -16,6 +16,9 @@ inline constexpr bool ENGINE_DEBUG_BUILD = true;
inline const std::string ENGINE_VERSION_STRING = "0.32"; 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 /// @brief world regions format version
inline constexpr uint REGION_FORMAT_VERSION = 3; inline constexpr uint REGION_FORMAT_VERSION = 3;

View file

@ -26,17 +26,21 @@ std::unique_ptr<Content> ContentBuilder::build() {
def.rt.tags.insert(tags.add(tag)); 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) { if (def.variants) {
for (auto& variant : def.variants->variants) { for (auto& variant : def.variants->variants) {
variant.rt.solid = variant.rt.solid = isSolid(variant) || def.explictlySolid;
variant.model.type == BlockModelType::BLOCK ||
def.explictlySolid;
} }
def.defaults = def.variants->variants.at(0); def.defaults = def.variants->variants.at(0);
} else { } else {
def.defaults.rt.solid = def.defaults.rt.solid = isSolid(def.defaults) || def.explictlySolid;
def.defaults.model.type == BlockModelType::BLOCK || }
def.explictlySolid; if (def.material.empty()) {
defaults.at("block-material").get(def.material);
} }
const float EPSILON = 0.01f; const float EPSILON = 0.01f;

View file

@ -89,6 +89,7 @@ template<> void ContentUnitLoader<EntityDef>::loadUnit(
root.at("skeleton-name").get(def.skeletonName); root.at("skeleton-name").get(def.skeletonName);
root.at("material").get(def.material); root.at("material").get(def.material);
root.at("blocking").get(def.blocking); root.at("blocking").get(def.blocking);
root.at("selectable").get(def.selectable);
root.at("solid").get(def.solid); root.at("solid").get(def.solid);
root.at("mass").get(def.mass); root.at("mass").get(def.mass);
root.at("elasticity").get(def.elasticity); root.at("elasticity").get(def.elasticity);

View file

@ -8,11 +8,7 @@
static debug::Logger logger("app-scripts"); static debug::Logger logger("app-scripts");
AppScriptsControl::AppScriptsControl( AppScriptsControl::AppScriptsControl(const CoreParameters& params) {
const CoreParameters& params, const Project& project
)
: project(project) {
io::path scriptFile = io::path scriptFile =
params.scriptFile.empty() params.scriptFile.empty()
? "project:start.lua" ? "project:start.lua"

View file

@ -14,7 +14,7 @@ struct Project;
class AppScriptsControl { class AppScriptsControl {
public: public:
AppScriptsControl(const CoreParameters& params, const Project& project); AppScriptsControl(const CoreParameters& params);
void tick(); void tick();
void loadProjectClientScript(); void loadProjectClientScript();
@ -24,7 +24,6 @@ public:
bool isFinished() const; bool isFinished() const;
private: private:
const Project& project;
std::unique_ptr<scripting::IClientProjectScript> clientScript; std::unique_ptr<scripting::IClientProjectScript> clientScript;
std::unique_ptr<Process> scriptCoroutine; std::unique_ptr<Process> scriptCoroutine;
}; };

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "interfaces/Serializable.hpp" #include "interfaces/Serializable.hpp"
#include "io/path.hpp"
#include <set> #include <set>
#include <string> #include <string>
@ -12,6 +13,7 @@ struct Permissions {
static inline std::string NETWORK = "network"; static inline std::string NETWORK = "network";
static inline std::string RECORD_AUDIO = "record-audio"; static inline std::string RECORD_AUDIO = "record-audio";
static inline std::string WRITE_TO_USER = "write-to-user"; static inline std::string WRITE_TO_USER = "write-to-user";
static inline std::string SUB_INSTANCES = "sub-instances";
std::set<std::string> permissions; std::set<std::string> permissions;
@ -21,6 +23,7 @@ struct Permissions {
struct Project : Serializable { struct Project : Serializable {
std::string name; std::string name;
std::string title; std::string title;
io::path path;
std::vector<std::string> basePacks; std::vector<std::string> basePacks;
Permissions permissions; Permissions permissions;

View file

@ -14,5 +14,6 @@ struct CoreParameters {
std::filesystem::path projectFolder; std::filesystem::path projectFolder;
std::string debugServerString; std::string debugServerString;
int tps = 20; int tps = 20;
int subProcessDepth = 0;
std::unordered_map<std::string, std::string> projectArgs; std::unordered_map<std::string, std::string> projectArgs;
}; };

View file

@ -192,7 +192,7 @@ void Engine::initialize(CoreParameters coreParameters) {
audio::set_input_device(name == "auto" ? "" : name); audio::set_input_device(name == "auto" ? "" : name);
})); }));
appScripts = std::make_unique<AppScriptsControl>(params, *project); appScripts = std::make_unique<AppScriptsControl>(params);
if (params.stdinCommands) { if (params.stdinCommands) {
cmd::start_stdin_cmd_reader(*this); cmd::start_stdin_cmd_reader(*this);
@ -380,6 +380,7 @@ void Engine::loadAssets() {
void Engine::loadProject() { void Engine::loadProject() {
io::path projectFile = "project:project.toml"; io::path projectFile = "project:project.toml";
project = std::make_unique<Project>(); project = std::make_unique<Project>();
project->path = "project:";
project->deserialize(io::read_object(projectFile)); project->deserialize(io::read_object(projectFile));
logger.info() << "loaded project " << util::quote(project->name); logger.info() << "loaded project " << util::quote(project->name);
} }
@ -412,6 +413,7 @@ void Engine::onWorldClosed() {
} }
void Engine::quit() { void Engine::quit() {
logger.info() << "quitSignal set to true";
quitSignal = true; quitSignal = true;
if (!isHeadless()) { if (!isHeadless()) {
window->setShouldClose(true); window->setShouldClose(true);

View file

@ -43,7 +43,7 @@ void Mainloop::run() {
auto& appScripts = engine.getAppScripts(); auto& appScripts = engine.getAppScripts();
logger.info() << "main loop started"; logger.info() << "main loop started";
while (!window.isShouldClose()) { while (!window.isShouldClose() && !engine.isQuitSignal()) {
time.update(window.time()); time.update(window.time());
engine.applicationTick(); engine.applicationTick();
engine.updateFrontend(); engine.updateFrontend();

View file

@ -30,6 +30,7 @@ void ContentGfxCache::refreshVariant(
uint8_t variantIndex, uint8_t variantIndex,
const Atlas& atlas const Atlas& atlas
) { ) {
std::unordered_map<std::string, std::string> optionalTextures;
bool denseRender = settings.denseRender.get(); bool denseRender = settings.denseRender.get();
for (uint side = 0; side < 6; side++) { for (uint side = 0; side < 6; side++) {
std::string tex = variant.textureFaces[side]; std::string tex = variant.textureFaces[side];
@ -43,6 +44,7 @@ void ContentGfxCache::refreshVariant(
texOpaque = tex; texOpaque = tex;
} else if (variant.culling == CullingMode::OPTIONAL && !denseRender) { } else if (variant.culling == CullingMode::OPTIONAL && !denseRender) {
tex = texOpaque; tex = texOpaque;
optionalTextures[variant.textureFaces[side]] = texOpaque;
} }
size_t index = getRegionIndex(def.rt.id, variantIndex, side, false); size_t index = getRegionIndex(def.rt.id, variantIndex, side, false);
sideregions[index] = atlas.get(tex); sideregions[index] = atlas.get(tex);
@ -56,7 +58,12 @@ void ContentGfxCache::refreshVariant(
if (pos == std::string::npos) { if (pos == std::string::npos) {
continue; 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) { for (auto& vertex : mesh.vertices) {
vertex.uv = region->apply(vertex.uv); vertex.uv = region->apply(vertex.uv);
} }

View file

@ -514,6 +514,7 @@ void BlocksRenderer::render(
int x = i % CHUNK_W; int x = i % CHUNK_W;
int y = i / (CHUNK_D * CHUNK_W); int y = i / (CHUNK_D * CHUNK_W);
int z = (i / CHUNK_D) % CHUNK_W; int z = (i / CHUNK_D) % CHUNK_W;
switch (def.getModel(state.userbits).type) { switch (def.getModel(state.userbits).type) {
case BlockModelType::BLOCK: case BlockModelType::BLOCK:
blockCube({x, y, z}, texfaces, def, vox.state, !def.shadeless, blockCube({x, y, z}, texfaces, def, vox.state, !def.shadeless,
@ -532,7 +533,6 @@ void BlocksRenderer::render(
break; break;
} }
case BlockModelType::CUSTOM: { case BlockModelType::CUSTOM: {
if (!denseRender)
blockCustomModel( blockCustomModel(
{x, y, z}, {x, y, z},
def, def,

View file

@ -33,6 +33,8 @@ namespace io {
} }
} }
path(std::string_view str) : path(std::string(str)) {}
path(const char* str) : path(std::string(str)) {} path(const char* str) : path(std::string(str)) {}
bool operator==(const std::string& other) const { bool operator==(const std::string& other) const {

View file

@ -20,8 +20,14 @@
#include "window/Window.hpp" #include "window/Window.hpp"
#include "world/Level.hpp" #include "world/Level.hpp"
#include <array>
using namespace scripting; using namespace scripting;
namespace {
static std::array<std::unique_ptr<Process>, MAX_SUBPROCESSES> processes;
}
/// @brief Check if content is loaded /// @brief Check if content is loaded
static int l_is_content_loaded(lua::State* L) { static int l_is_content_loaded(lua::State* L) {
return lua::pushboolean(L, content != nullptr); return lua::pushboolean(L, content != nullptr);
@ -337,6 +343,10 @@ static int l_start_debug_instance(lua::State* L) {
if (!engine->getProject().permissions.has(Permissions::DEBUGGING)) { if (!engine->getProject().permissions.has(Permissions::DEBUGGING)) {
throw std::runtime_error("project has no debugging permission"); 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); int port = lua::tointeger(L, 1);
if (port == 0) { if (port == 0) {
@ -357,6 +367,8 @@ static int l_start_debug_instance(lua::State* L) {
"--res", paths.getResourcesFolder().u8string(), "--res", paths.getResourcesFolder().u8string(),
"--dir", paths.getUserFilesFolder().u8string(), "--dir", paths.getUserFilesFolder().u8string(),
"--dbg-server", "tcp:" + std::to_string(port), "--dbg-server", "tcp:" + std::to_string(port),
"--sub-depth", std::to_string(engine->getCoreParameters()
.subProcessDepth + 1),
}; };
if (!projectPath.empty()) { if (!projectPath.empty()) {
args.emplace_back("--project"); args.emplace_back("--project");
@ -365,11 +377,77 @@ static int l_start_debug_instance(lua::State* L) {
platform::new_engine_instance( platform::new_engine_instance(
std::move(args), std::move(args),
outputPath.empty() ? "" : io::resolve(std::string(outputPath)) outputPath.empty() ? "" : io::resolve(std::string(outputPath)),
false
); );
return lua::pushinteger(L, port); return lua::pushinteger(L, port);
} }
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 = lua::isstring(L, 2) ? lua::require_lstring(L, 2) : "";
const auto& paths = engine->getPaths();
std::vector<std::string> args {
"--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());
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 lua::pushinteger(L, handle);
}
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");
}
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[] = { const luaL_Reg applib[] = {
/// content /// content
{"is_content_loaded", lua::wrap<l_is_content_loaded>}, {"is_content_loaded", lua::wrap<l_is_content_loaded>},
@ -402,5 +480,8 @@ const luaL_Reg applib[] = {
{"get_version", lua::wrap<l_get_version>}, {"get_version", lua::wrap<l_get_version>},
{"create_memory_device", lua::wrap<l_create_memory_device>}, {"create_memory_device", lua::wrap<l_create_memory_device>},
{"start_debug_instance", lua::wrap<l_start_debug_instance>}, {"start_debug_instance", lua::wrap<l_start_debug_instance>},
{"start_background_instance", lua::wrap<l_start_background_instance>},
{"is_instance_alive", lua::wrap<l_is_instance_alive>},
{"terminate_instance", lua::wrap<l_terminate_instance>},
{nullptr, nullptr} {nullptr, nullptr}
}; };

View file

@ -311,6 +311,10 @@ static int l_world_raycast(lua::State* L) {
entityRaycast.entityFilterExcludeMode = lua::toboolean(L, -1); entityRaycast.entityFilterExcludeMode = lua::toboolean(L, -1);
lua::pop(L); 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)) { if (lua::getfield(L, "filter_blocks", 1)) {
load_blocks_filter(L, -1, filteredBlocks); load_blocks_filter(L, -1, filteredBlocks);
blocksRaycast.filter = &filteredBlocks; blocksRaycast.filter = &filteredBlocks;

View file

@ -10,6 +10,7 @@
static debug::Logger logger("main"); static debug::Logger logger("main");
static void sigterm_handler(int signum) { static void sigterm_handler(int signum) {
logger.info() << (signum == SIGTERM ? "SIGTERM" : "SIGINT") << " received";
Engine::getInstance().quit(); Engine::getInstance().quit();
} }
@ -25,12 +26,16 @@ int main(int argc, char** argv) {
if (!parse_cmdline(argc, argv, coreParameters)) { if (!parse_cmdline(argc, argv, coreParameters)) {
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
logger.debug() << "sub-process depth: "
<< coreParameters.subProcessDepth;
} catch (const std::runtime_error& err) { } catch (const std::runtime_error& err) {
std::cerr << err.what() << std::endl; std::cerr << err.what() << std::endl;
return EXIT_FAILURE; return EXIT_FAILURE;
} }
std::signal(SIGTERM, sigterm_handler); std::signal(SIGTERM, sigterm_handler);
#ifdef NDEBUG
std::signal(SIGINT, sigterm_handler);
#endif
debug::Logger::init(coreParameters.userFolder.string() + "/latest.log"); debug::Logger::init(coreParameters.userFolder.string() + "/latest.log");
platform::configure_encoding(); platform::configure_encoding();

View file

@ -176,7 +176,8 @@ std::optional<Entities::RaycastResult> Entities::rayCast(
for (auto [entity, eid, transform, body] : view.each()) { for (auto [entity, eid, transform, body] : view.each()) {
if (eid.uid == settings.ignoredUid || !body.enabled || if (eid.uid == settings.ignoredUid || !body.enabled ||
(settings.solidEntitiesOnly && !eid.def.solid)) { (settings.solidEntitiesOnly && !eid.def.solid) ||
(!eid.def.selectable && !settings.includeNonSelectable)) {
continue; continue;
} }
if (settings.entitiesFilter) { if (settings.entitiesFilter) {

View file

@ -85,6 +85,7 @@ public:
bool solidEntitiesOnly = false; bool solidEntitiesOnly = false;
bool entityFilterExcludeMode = false; bool entityFilterExcludeMode = false;
const std::set<entitydefid_t>* entitiesFilter = nullptr; const std::set<entitydefid_t>* entitiesFilter = nullptr;
bool includeNonSelectable = false;
}; };
struct RaycastResult { struct RaycastResult {

View file

@ -42,6 +42,9 @@ struct EntityDef {
/// @brief Solid entity material material /// @brief Solid entity material material
std::string 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 /// @brief Does entity prevent blocks setup
bool blocking = true; bool blocking = true;

View file

@ -13,22 +13,22 @@
namespace fs = std::filesystem; namespace fs = std::filesystem;
class ArgC { class ArgC {
public: public:
std::string keyword; std::string keyword;
std::function<bool()> execute; std::function<bool(CoreParameters&, util::ArgsReader&)> execute;
std::string args; std::string args;
std::string help; std::string help;
ArgC( ArgC(
const std::string& keyword, const std::string& keyword,
std::function<bool()> execute, std::function<bool(CoreParameters&, util::ArgsReader&)> execute,
const std::string& args, const std::string& args,
const std::string& help const std::string& help
) { ) {
this->keyword = keyword; this->keyword = keyword;
this->execute = execute; this->execute = execute;
this->args = args; this->args = args;
this->help = help; this->help = help;
} }
}; };
@ -36,54 +36,62 @@ static bool perform_keyword(
util::ArgsReader& reader, const std::string& keyword, CoreParameters& params util::ArgsReader& reader, const std::string& keyword, CoreParameters& params
) { ) {
static const std::vector<ArgC> argumentsCommandline = { static const std::vector<ArgC> argumentsCommandline = {
ArgC("--res", [&params, &reader]() -> bool { ArgC("--res", [](auto& params, auto& reader) -> bool {
params.resFolder = reader.next(); params.resFolder = reader.next();
return true; return true;
}, "<path>", "set resources directory."), }, "<path>", "set resources directory."),
ArgC("--dir", [&params, &reader]() -> bool { ArgC("--dir", [](auto& params, auto& reader) -> bool {
params.userFolder = reader.next(); params.userFolder = reader.next();
return true; return true;
}, "<path>", "set userfiles directory."), }, "<path>", "set userfiles directory."),
ArgC("--project", [&params, &reader]() -> bool { ArgC("--project", [](auto& params, auto& reader) -> bool {
params.projectFolder = reader.next(); params.projectFolder = reader.next();
return true; return true;
}, "<path>", "set project directory."), }, "<path>", "set project directory."),
ArgC("--test", [&params, &reader]() -> bool { ArgC("--test", [](auto& params, auto& reader) -> bool {
params.testMode = true; params.testMode = true;
params.scriptFile = reader.next(); params.scriptFile = reader.next();
return true; return true;
}, "<path>", "test script file."), }, "<path>", "test script file."),
ArgC("--script", [&params, &reader]() -> bool { ArgC("--script", [](auto& params, auto& reader) -> bool {
params.testMode = false; params.testMode = false;
params.scriptFile = reader.next(); params.scriptFile = reader.next();
return true; return true;
}, "<path>", "main script file."), }, "<path>", "main script file."),
ArgC("--headless", [&params]() -> bool { ArgC("--headless", [](auto& params, auto& reader) -> bool {
params.headless = true; params.headless = true;
return true; return true;
}, "", "run in headless mode."), }, "", "run in headless mode."),
ArgC("--stdin-cmd", [&params]() -> bool { ArgC("--stdin-cmd", [](auto& params, auto& reader) -> bool {
params.stdinCommands = true; params.stdinCommands = true;
return true; return true;
}, "", "run commands from stdin."), }, "", "run commands from stdin."),
ArgC("--tps", [&params, &reader]() -> bool { ArgC("--tps", [](auto& params, auto& reader) -> bool {
params.tps = reader.nextInt(); params.tps = reader.nextInt();
return true; return true;
}, "<tps>", "headless mode tick-rate (default - 20)."), }, "<tps>", "headless mode tick-rate (default - 20)."),
ArgC("--version", []() -> bool { ArgC("--version", [](auto&, auto&) -> bool {
std::cout << ENGINE_VERSION_STRING << std::endl; std::cout << ENGINE_VERSION_STRING << std::endl;
return false; return false;
}, "", "display the engine version."), }, "", "display the engine version."),
ArgC("--dbg-server", [&params, &reader]() -> bool { ArgC("--dbg-server", [](auto& params, auto& reader) -> bool {
params.debugServerString = reader.next(); params.debugServerString = reader.next();
return true; return true;
}, "<serv>", "open debugging server where <serv> is {transport}:{port}"), }, "<serv>", "open debugging server where <serv> is {transport}:{port}"),
ArgC("--help", []() -> bool { ArgC("--sub-depth", [](auto& params, auto& reader) -> bool {
params.subProcessDepth = reader.nextInt();
return true;
}, "<depth>", "sub-process depth"),
ArgC("--help", [](auto&, auto&) -> bool {
std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n"; std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n";
std::cout << "Command-line arguments:\n"; std::cout << "Command-line arguments:\n";
for (auto& a : argumentsCommandline) { for (auto& arg : argumentsCommandline) {
std::cout << std::setw(24) << std::left << (a.keyword + " " + a.args); if (arg.help.empty()) {
std::cout << "- " << a.help << std::endl; continue;
}
std::cout << std::setw(24) << std::left
<< (arg.keyword + " " + arg.args);
std::cout << "- " << arg.help << std::endl;
} }
std::cout << std::endl; std::cout << std::endl;
return false; return false;
@ -91,7 +99,7 @@ static bool perform_keyword(
}; };
for (auto& a : argumentsCommandline) { for (auto& a : argumentsCommandline) {
if (a.keyword == keyword) { if (a.keyword == keyword) {
return a.execute(); return a.execute(params, reader);
} }
} }
throw std::runtime_error("unknown argument " + keyword); throw std::runtime_error("unknown argument " + keyword);

View file

@ -19,6 +19,13 @@
#else #else
#include <sys/poll.h> #include <sys/poll.h>
#include <unistd.h> #include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#endif
#ifdef __linux__
#include <sys/prctl.h>
#include <sys/wait.h>
#endif #endif
namespace platform::internal { namespace platform::internal {
@ -215,8 +222,102 @@ std::filesystem::path platform::get_executable_path() {
#endif #endif
} }
void platform::new_engine_instance( class SystemProcess final : public Process {
const std::vector<std::string>& args, std::filesystem::path outputFile 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__
logger.info() << "terminating " << pid;
kill(pid, SIGTERM);
#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<Process> platform::new_engine_instance(
const std::vector<std::string>& args,
std::filesystem::path outputFile,
bool subProcess
) { ) {
auto executable = get_executable_path(); auto executable = get_executable_path();
@ -285,6 +386,38 @@ void platform::new_engine_instance(
&si, &si,
&pi &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())
);
}
return std::make_unique<SystemProcess>(pi);
} catch (const std::exception& err) {
CloseHandle(job);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return nullptr;
}
}
if (success) { if (success) {
CloseHandle(pi.hProcess); CloseHandle(pi.hProcess);
CloseHandle(pi.hThread); CloseHandle(pi.hThread);
@ -295,6 +428,46 @@ void platform::new_engine_instance(
); );
} }
#else #else
// TODO: implement
#ifndef __APPLE__
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<const char*> argv(args.size() + 2);
argv[0] = const_cast<char*>(executable.c_str());
for (int i = 0; i < args.size(); i++) {
argv[i + 1] = args[i].c_str();
}
argv[args.size() + 1] = nullptr;
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
execvp(executable.c_str(), const_cast<char**>(argv.data()));
_exit(127);
} else {
close(fd);
usleep(1000); // waiting for execvp call
return std::make_unique<SystemProcess>(pid);
}
}
#endif
std::stringstream ss; std::stringstream ss;
ss << executable; ss << executable;
for (int i = 0; i < args.size(); i++) { for (int i = 0; i < args.size(); i++) {
@ -315,6 +488,7 @@ void platform::new_engine_instance(
); );
} }
#endif #endif
return nullptr;
} }
bool platform::stdin_has_data() { bool platform::stdin_has_data() {

View file

@ -2,8 +2,11 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <memory>
#include <filesystem> #include <filesystem>
#include "interfaces/Process.hpp"
namespace platform { namespace platform {
void configure_encoding(); void configure_encoding();
/// @brief Get Environment locale in ISO format ll_CC /// @brief Get Environment locale in ISO format ll_CC
@ -18,8 +21,10 @@ namespace platform {
/// @brief Get current process running executable path /// @brief Get current process running executable path
std::filesystem::path get_executable_path(); std::filesystem::path get_executable_path();
/// @brief Run a separate engine instance with specified arguments /// @brief Run a separate engine instance with specified arguments
void new_engine_instance( std::unique_ptr<Process> new_engine_instance(
const std::vector<std::string>& args, std::filesystem::path outputFile const std::vector<std::string>& args,
std::filesystem::path outputFile,
bool subProcess
); );
/// @brief Open URL in web browser /// @brief Open URL in web browser
bool open_url(const std::string& url); bool open_url(const std::string& url);

View file

@ -37,8 +37,6 @@ inline constexpr size_t MAX_USER_BLOCK_FIELDS_SIZE = 240;
inline constexpr int BLOCK_MAX_VARIANTS = 16; inline constexpr int BLOCK_MAX_VARIANTS = 16;
inline std::string DEFAULT_MATERIAL = "base:stone";
struct BlockFuncsSet { struct BlockFuncsSet {
bool init : 1; bool init : 1;
bool update : 1; bool update : 1;
@ -206,7 +204,7 @@ public:
dv::value properties = nullptr; dv::value properties = nullptr;
/// @brief id of used BlockMaterial, may specify non-existing material /// @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 /// @brief Light emission R, G, B, S (sky lights: sun, moon, radioactive
/// clouds) /// clouds)