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
## 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.

View file

@ -61,12 +61,16 @@ 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.*
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.

View file

@ -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.

View file

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

View file

@ -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_entities: bool = false;
[==[ additional parameters (blocks) ]==]
-- id of the block types used for filtering

View file

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

View file

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

View file

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

View file

@ -142,4 +142,24 @@ app.set_content_sources(sources: table<string>)
-- Сбрасывает список источников контента.
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 определяет список игнорируемых
entities_exclusion: bool = false,
-- учитывать сущности с selectable=false
nonselect_entities: bool = false;
[==[ дополнительные параметры (блоки) ]==]
-- id типов блоков используемых для фильтрации

View file

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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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,
}

View file

@ -16,6 +16,9 @@ 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
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));
}
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;
}
if (def.material.empty()) {
defaults.at("block-material").get(def.material);
}
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("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);

View file

@ -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"

View file

@ -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<scripting::IClientProjectScript> clientScript;
std::unique_ptr<Process> scriptCoroutine;
};

View file

@ -1,6 +1,7 @@
#pragma once
#include "interfaces/Serializable.hpp"
#include "io/path.hpp"
#include <set>
#include <string>
@ -12,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<std::string> permissions;
@ -21,6 +23,7 @@ struct Permissions {
struct Project : Serializable {
std::string name;
std::string title;
io::path path;
std::vector<std::string> basePacks;
Permissions permissions;

View file

@ -14,5 +14,6 @@ struct CoreParameters {
std::filesystem::path projectFolder;
std::string debugServerString;
int tps = 20;
int subProcessDepth = 0;
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);
}));
appScripts = std::make_unique<AppScriptsControl>(params, *project);
appScripts = std::make_unique<AppScriptsControl>(params);
if (params.stdinCommands) {
cmd::start_stdin_cmd_reader(*this);
@ -380,6 +380,7 @@ void Engine::loadAssets() {
void Engine::loadProject() {
io::path projectFile = "project:project.toml";
project = std::make_unique<Project>();
project->path = "project:";
project->deserialize(io::read_object(projectFile));
logger.info() << "loaded project " << util::quote(project->name);
}
@ -412,6 +413,7 @@ void Engine::onWorldClosed() {
}
void Engine::quit() {
logger.info() << "quitSignal set to true";
quitSignal = true;
if (!isHeadless()) {
window->setShouldClose(true);

View file

@ -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();

View file

@ -30,6 +30,7 @@ void ContentGfxCache::refreshVariant(
uint8_t variantIndex,
const Atlas& atlas
) {
std::unordered_map<std::string, std::string> 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);
}

View file

@ -514,6 +514,7 @@ void BlocksRenderer::render(
int x = i % CHUNK_W;
int y = i / (CHUNK_D * CHUNK_W);
int z = (i / CHUNK_D) % CHUNK_W;
switch (def.getModel(state.userbits).type) {
case BlockModelType::BLOCK:
blockCube({x, y, z}, texfaces, def, vox.state, !def.shadeless,
@ -532,7 +533,6 @@ void BlocksRenderer::render(
break;
}
case BlockModelType::CUSTOM: {
if (!denseRender)
blockCustomModel(
{x, y, z},
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)) {}
bool operator==(const std::string& other) const {

View file

@ -20,8 +20,14 @@
#include "window/Window.hpp"
#include "world/Level.hpp"
#include <array>
using namespace scripting;
namespace {
static std::array<std::unique_ptr<Process>, MAX_SUBPROCESSES> processes;
}
/// @brief Check if content is loaded
static int l_is_content_loaded(lua::State* L) {
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)) {
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) {
@ -357,6 +367,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");
@ -365,11 +377,77 @@ 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)),
false
);
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[] = {
/// content
{"is_content_loaded", lua::wrap<l_is_content_loaded>},
@ -402,5 +480,8 @@ const luaL_Reg applib[] = {
{"get_version", lua::wrap<l_get_version>},
{"create_memory_device", lua::wrap<l_create_memory_device>},
{"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}
};

View file

@ -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;

View file

@ -10,6 +10,7 @@
static debug::Logger logger("main");
static void sigterm_handler(int signum) {
logger.info() << (signum == SIGTERM ? "SIGTERM" : "SIGINT") << " received";
Engine::getInstance().quit();
}
@ -25,12 +26,16 @@ 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;
}
std::signal(SIGTERM, sigterm_handler);
#ifdef NDEBUG
std::signal(SIGINT, sigterm_handler);
#endif
debug::Logger::init(coreParameters.userFolder.string() + "/latest.log");
platform::configure_encoding();

View file

@ -176,7 +176,8 @@ std::optional<Entities::RaycastResult> 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 && !settings.includeNonSelectable)) {
continue;
}
if (settings.entitiesFilter) {

View file

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

View file

@ -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;

View file

@ -13,22 +13,22 @@
namespace fs = std::filesystem;
class ArgC {
public:
std::string keyword;
std::function<bool()> execute;
std::string args;
std::string help;
ArgC(
const std::string& keyword,
std::function<bool()> 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<bool(CoreParameters&, util::ArgsReader&)> execute;
std::string args;
std::string help;
ArgC(
const std::string& keyword,
std::function<bool(CoreParameters&, util::ArgsReader&)> execute,
const std::string& args,
const std::string& help
) {
this->keyword = keyword;
this->execute = execute;
this->args = args;
this->help = help;
}
};
@ -36,54 +36,62 @@ static bool perform_keyword(
util::ArgsReader& reader, const std::string& keyword, CoreParameters& params
) {
static const std::vector<ArgC> argumentsCommandline = {
ArgC("--res", [&params, &reader]() -> bool {
ArgC("--res", [](auto& params, auto& reader) -> bool {
params.resFolder = reader.next();
return true;
}, "<path>", "set resources directory."),
ArgC("--dir", [&params, &reader]() -> bool {
ArgC("--dir", [](auto& params, auto& reader) -> bool {
params.userFolder = reader.next();
return true;
}, "<path>", "set userfiles directory."),
ArgC("--project", [&params, &reader]() -> bool {
ArgC("--project", [](auto& params, auto& reader) -> bool {
params.projectFolder = reader.next();
return true;
}, "<path>", "set project directory."),
ArgC("--test", [&params, &reader]() -> bool {
ArgC("--test", [](auto& params, auto& reader) -> bool {
params.testMode = true;
params.scriptFile = reader.next();
return true;
}, "<path>", "test script file."),
ArgC("--script", [&params, &reader]() -> bool {
ArgC("--script", [](auto& params, auto& reader) -> bool {
params.testMode = false;
params.scriptFile = reader.next();
return true;
}, "<path>", "main script file."),
ArgC("--headless", [&params]() -> bool {
ArgC("--headless", [](auto& params, auto& reader) -> bool {
params.headless = true;
return true;
}, "", "run in headless mode."),
ArgC("--stdin-cmd", [&params]() -> bool {
ArgC("--stdin-cmd", [](auto& params, auto& reader) -> bool {
params.stdinCommands = true;
return true;
}, "", "run commands from stdin."),
ArgC("--tps", [&params, &reader]() -> bool {
ArgC("--tps", [](auto& params, auto& reader) -> bool {
params.tps = reader.nextInt();
return true;
}, "<tps>", "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", [&params, &reader]() -> bool {
ArgC("--dbg-server", [](auto& params, auto& reader) -> bool {
params.debugServerString = reader.next();
return true;
}, "<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 << "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;
@ -91,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);

View file

@ -19,6 +19,13 @@
#else
#include <sys/poll.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
namespace platform::internal {
@ -215,8 +222,102 @@ std::filesystem::path platform::get_executable_path() {
#endif
}
void platform::new_engine_instance(
const std::vector<std::string>& args, std::filesystem::path outputFile
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__
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();
@ -285,6 +386,38 @@ 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())
);
}
return std::make_unique<SystemProcess>(pi);
} catch (const std::exception& err) {
CloseHandle(job);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return nullptr;
}
}
if (success) {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
@ -295,6 +428,46 @@ void platform::new_engine_instance(
);
}
#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;
ss << executable;
for (int i = 0; i < args.size(); i++) {
@ -315,6 +488,7 @@ void platform::new_engine_instance(
);
}
#endif
return nullptr;
}
bool platform::stdin_has_data() {

View file

@ -2,8 +2,11 @@
#include <string>
#include <vector>
#include <memory>
#include <filesystem>
#include "interfaces/Process.hpp"
namespace platform {
void configure_encoding();
/// @brief Get Environment locale in ISO format ll_CC
@ -18,8 +21,10 @@ 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(
const std::vector<std::string>& args, std::filesystem::path outputFile
std::unique_ptr<Process> new_engine_instance(
const std::vector<std::string>& args,
std::filesystem::path outputFile,
bool subProcess
);
/// @brief Open URL in web browser
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 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)