From 68f4c3c55ef03276ed6280b84eccdc616ab53337 Mon Sep 17 00:00:00 2001 From: NigthLier Date: Tue, 5 May 2026 12:58:11 +0300 Subject: [PATCH 01/92] Added Clion build section --- CMakePresets.json | 21 +++++++++++++++++++++ README.md | 20 ++++++++++++++++++++ src/CMakeLists.txt | 11 +++++++++-- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 47ba71e34..508719628 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -15,6 +15,22 @@ "VOXELENGINE_BUILD_TESTS": "ON" } }, + { + "name": "default-ninja-mingw-windows", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "generator": "Ninja Multi-Config", + "binaryDir": "${sourceDir}/build-mingw", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "VCPKG_HOST_TRIPLET": "x64-mingw-static", + "VCPKG_TARGET_TRIPLET": "x64-mingw-static", + "VOXELENGINE_BUILD_TESTS": "ON" + } + }, { "name": "default-ninja-gnu-linux", "condition": { @@ -60,6 +76,11 @@ "configurePreset": "default-vs-msvc-windows", "configuration": "Debug" }, + { + "name": "default-ninja-mingw-windows", + "configurePreset": "default-ninja-mingw-windows", + "configuration": "Debug" + }, { "name": "default-ninja-gnu-linux", "configurePreset": "default-ninja-gnu-linux", diff --git a/README.md b/README.md index 1d6a462c9..e672ef541 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,26 @@ cmake --build --preset default-vs-msvc-windows > [!NOTE] > Make sure your `CMakeUserPresets.json` (if used) contains the correct `VCPKG_ROOT` path. +--- +### Building with CLion (MinGW / Ninja) + +> [!NOTE] +> Requirements: **vcpkg**, **Git**, and **CLion** (bundled with CMake, Ninja and MinGW toolchain). + +> [!WARNING] +> Avoid installing tools in paths with spaces (e.g. `Program Files`). +> This may break MinGW builds (especially `windres`). + +Install **vcpkg** manually as described in the Visual Studio section. + + +#### Configure CLion +Open the project in CLion and configure CMake profile: +```text +Settings → Build, Execution, Deployment → CMake +``` +Reload CMake project and build the engine. + --- ## Build using Docker diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d6f4c5b74..12ece0f7f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,14 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows") "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/" ) else() + set(LUAJIT_PACKAGE_DIR "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}") + + if(EXISTS "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit-2.1") + set(LUAJIT_INCLUDE_DIR "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit-2.1") + else() + set(LUAJIT_INCLUDE_DIR "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit") + endif() + add_library(luajit SHARED IMPORTED) set_target_properties( luajit @@ -45,8 +53,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows") "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/bin/lua51.dll" IMPORTED_IMPLIB "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/lib/lua51.lib" - INTERFACE_INCLUDE_DIRECTORIES - "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit" + INTERFACE_INCLUDE_DIRECTORIES "${LUAJIT_INCLUDE_DIR}" ) endif() From b2ed4b472f9b93f796381300859f58dcd12aac4f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 1 Jun 2026 01:14:30 +0300 Subject: [PATCH 02/92] feat: client-side '--script' and '--test' basic support --- src/devtools/AppScriptsControl.cpp | 59 ++++++++++++++++++++++++++++++ src/devtools/AppScriptsControl.hpp | 28 ++++++++++++++ src/devtools/Project.cpp | 21 +---------- src/devtools/Project.hpp | 14 +------ src/engine/Engine.cpp | 20 +++++----- src/engine/Engine.hpp | 2 + src/engine/Mainloop.cpp | 11 +++++- src/engine/ServerMainloop.cpp | 14 +------ 8 files changed, 112 insertions(+), 57 deletions(-) create mode 100644 src/devtools/AppScriptsControl.cpp create mode 100644 src/devtools/AppScriptsControl.hpp diff --git a/src/devtools/AppScriptsControl.cpp b/src/devtools/AppScriptsControl.cpp new file mode 100644 index 000000000..16c3475d5 --- /dev/null +++ b/src/devtools/AppScriptsControl.cpp @@ -0,0 +1,59 @@ +#include "AppScriptsControl.hpp" + +#include "debug/Logger.hpp" +#include "engine/CoreParameters.hpp" +#include "io/io.hpp" +#include "Project.hpp" +#include "logic/scripting/scripting.hpp" + +static debug::Logger logger("app-scripts"); + +AppScriptsControl::AppScriptsControl( + const CoreParameters& params, const Project& project +) + : project(project) { + + io::path scriptFile = + params.scriptFile.empty() + ? "project:start.lua" + : std::string("script:") + params.scriptFile.filename().u8string(); + if (io::exists(scriptFile)) { + logger.info() << "starting script: " << params.scriptFile.u8string(); + scriptCoroutine = scripting::start_app_script(scriptFile); + } else { + logger.warning() << "script does not exists: " << params.scriptFile.u8string(); + } + + if (!params.headless) { + loadProjectClientScript(); + } +} + +void AppScriptsControl::loadProjectClientScript() { + io::path scriptFile = "project:project_client.lua"; + if (io::exists(scriptFile)) { + logger.info() << "starting project client script: " << scriptFile.string(); + clientScript = scripting::load_client_project_script(scriptFile); + } else { + logger.warning() << "project client script does not exists: " << scriptFile.string(); + } +} + +void AppScriptsControl::onScreenChange(const std::string& name, bool show) { + if (clientScript) { + clientScript->onScreenChange(name, show); + } +} + +void AppScriptsControl::tick() { + if (scriptCoroutine && scriptCoroutine->isActive()) { + scriptCoroutine->update(); + } +} + +void AppScriptsControl::terminate(std::string_view reason) { + if (scriptCoroutine->isActive()) { + scriptCoroutine->terminate(); + logger.info() << "script has been terminated due to " << reason; + } +} diff --git a/src/devtools/AppScriptsControl.hpp b/src/devtools/AppScriptsControl.hpp new file mode 100644 index 000000000..fb081a986 --- /dev/null +++ b/src/devtools/AppScriptsControl.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "interfaces/Process.hpp" + +#include +#include + +namespace scripting { + class IClientProjectScript; +} + +struct CoreParameters; +struct Project; + +class AppScriptsControl { +public: + AppScriptsControl(const CoreParameters& params, const Project& project); + + void tick(); + void loadProjectClientScript(); + void terminate(std::string_view reason); + + void onScreenChange(const std::string& name, bool show); +private: + const Project& project; + std::unique_ptr clientScript; + std::unique_ptr scriptCoroutine; +}; diff --git a/src/devtools/Project.cpp b/src/devtools/Project.cpp index 86c73640e..12a85dedc 100644 --- a/src/devtools/Project.cpp +++ b/src/devtools/Project.cpp @@ -2,6 +2,7 @@ #include "data/dv_util.hpp" #include "debug/Logger.hpp" +#include "engine/CoreParameters.hpp" #include "io/io.hpp" #include "io/path.hpp" #include "logic/scripting/scripting.hpp" @@ -40,26 +41,6 @@ void Project::deserialize(const dv::value& src) { } } -void Project::loadProjectClientScript() { - io::path scriptFile = "project:project_client.lua"; - if (io::exists(scriptFile)) { - logger.info() << "starting project client script"; - clientScript = scripting::load_client_project_script(scriptFile); - } else { - logger.warning() << "project client script does not exists"; - } -} - -void Project::loadProjectStartScript() { - io::path scriptFile = "project:start.lua"; - if (io::exists(scriptFile)) { - logger.info() << "starting project start script"; - setupCoroutine = scripting::start_app_script(scriptFile); - } else { - logger.warning() << "project start script does not exists"; - } -} - bool Permissions::has(const std::string& name) const { return permissions.find(name) != permissions.end(); } diff --git a/src/devtools/Project.hpp b/src/devtools/Project.hpp index 23aa11ca5..daeac7ca0 100644 --- a/src/devtools/Project.hpp +++ b/src/devtools/Project.hpp @@ -1,17 +1,12 @@ #pragma once +#include "interfaces/Serializable.hpp" + #include #include #include #include -#include "interfaces/Process.hpp" -#include "interfaces/Serializable.hpp" - -namespace scripting { - class IClientProjectScript; -} - struct Permissions { static inline std::string DEBUGGING = "debugging"; static inline std::string NETWORK = "network"; @@ -27,15 +22,10 @@ struct Project : Serializable { std::string name; std::string title; std::vector basePacks; - std::unique_ptr clientScript; - std::unique_ptr setupCoroutine; Permissions permissions; ~Project(); dv::value serialize() const override; void deserialize(const dv::value& src) override; - - void loadProjectClientScript(); - void loadProjectStartScript(); }; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 879d95f0b..10f8cee6d 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -13,6 +13,7 @@ #include "content/ContentControl.hpp" #include "core_defs.hpp" #include "debug/Logger.hpp" +#include "devtools/AppScriptsControl.hpp" #include "devtools/DebuggingServer.hpp" #include "devtools/Editor.hpp" #include "devtools/Project.hpp" @@ -191,10 +192,8 @@ void Engine::initialize(CoreParameters coreParameters) { audio::set_input_device(name == "auto" ? "" : name); })); - project->loadProjectStartScript(); - if (!params.headless) { - project->loadProjectClientScript(); - } + appScripts = std::make_unique(params, *project); + if (params.stdinCommands) { cmd::start_stdin_cmd_reader(*this); } @@ -263,9 +262,7 @@ void Engine::detachDebugger() { } void Engine::applicationTick() { - if (project->setupCoroutine && project->setupCoroutine->isActive()) { - project->setupCoroutine->update(); - } + appScripts->tick(); } void Engine::updateFrontend() { @@ -353,6 +350,7 @@ void Engine::close() { network.reset(); clearKeepedObjects(); project.reset(); + appScripts.reset(); scripting::close(); logger.info() << "scripting finished"; if (!params.headless) { @@ -387,8 +385,8 @@ void Engine::loadProject() { } void Engine::setScreen(std::shared_ptr screen) { - if (project->clientScript && this->screen) { - project->clientScript->onScreenChange(this->screen->getName(), false); + if (this->screen) { + appScripts->onScreenChange(this->screen->getName(), false); } // reset audio channels (stop all sources) audio::reset_channel(audio::get_channel_index("regular")); @@ -397,8 +395,8 @@ void Engine::setScreen(std::shared_ptr screen) { if (this->screen) { this->screen->onOpen(); } - if (project->clientScript && this->screen) { - project->clientScript->onScreenChange(this->screen->getName(), true); + if (this->screen) { + appScripts->onScreenChange(this->screen->getName(), true); window->setShouldRefresh(); } } diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 4f726b700..169631214 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -12,6 +12,7 @@ class Assets; class AssetsLoader; class AssetsManagement; +class AppScriptsControl; class ContentControl; class EngineController; class EnginePaths; @@ -55,6 +56,7 @@ class Engine : public util::ObjectsKeeper { std::unique_ptr project; std::unique_ptr settingsHandler; std::unique_ptr assets; + std::unique_ptr appScripts; std::shared_ptr screen; std::unique_ptr content; std::unique_ptr controller; diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index d6ca17cb9..342f81aaf 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -9,6 +9,8 @@ #include "world/Level.hpp" #include "graphics/ui/GUI.hpp" #include "graphics/ui/elements/Container.hpp" +#include "logic/scripting/scripting.hpp" +#include "io/path.hpp" static debug::Logger logger("mainloop"); @@ -16,9 +18,11 @@ Mainloop::Mainloop(Engine& engine) : engine(engine) { } void Mainloop::run() { + const auto& coreParams = engine.getCoreParameters(); auto& time = engine.getTime(); auto& window = engine.getWindow(); auto& settings = engine.getSettings(); + double targetDelta = 1.0 / static_cast(coreParams.tps); engine.setLevelConsumer([this](auto level, int64_t localPlayer) { if (level == nullptr) { @@ -35,10 +39,13 @@ void Mainloop::run() { logger.info() << "starting menu screen"; engine.setScreen(std::make_shared(engine)); + + double testTimer = 0.0; logger.info() << "main loop started"; - while (!window.isShouldClose()){ - time.update(window.time()); + while (!window.isShouldClose()) { + testTimer += targetDelta; + time.update(coreParams.testMode ? testTimer : window.time()); engine.applicationTick(); engine.updateFrontend(); diff --git a/src/engine/ServerMainloop.cpp b/src/engine/ServerMainloop.cpp index 54698194e..7c9970e5e 100644 --- a/src/engine/ServerMainloop.cpp +++ b/src/engine/ServerMainloop.cpp @@ -1,13 +1,13 @@ #include "ServerMainloop.hpp" #include "Engine.hpp" -#include "logic/scripting/scripting.hpp" #include "logic/LevelController.hpp" #include "interfaces/Process.hpp" #include "debug/Logger.hpp" #include "world/Level.hpp" #include "world/World.hpp" #include "util/platform.hpp" +#include "devtools/Project.hpp" #include @@ -32,21 +32,12 @@ void ServerMainloop::run() { setLevel(std::move(level)); }); - auto process = scripting::start_app_script( - "script:" + coreParams.scriptFile.filename().u8string() - ); - double targetDelta = 1.0 / static_cast(coreParams.tps); double delta = targetDelta; auto begin = system_clock::now(); auto startupTime = begin; - while (process->isActive()) { - if (engine.isQuitSignal()) { - process->terminate(); - logger.info() << "script has been terminated due to quit signal"; - break; - } + while (engine.isQuitSignal()) { if (coreParams.testMode) { time.step(delta); } else { @@ -55,7 +46,6 @@ void ServerMainloop::run() { duration_cast(now - startupTime).count() / 1e6); delta = time.getDelta(); } - process->update(); if (controller) { controller->getLevel()->getWorld()->updateTimers(delta); controller->update(glm::min(delta, 0.2), false); From 0185ef68167a303c42a4015f2ab6fbaeee467f48 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 1 Jun 2026 18:01:44 +0300 Subject: [PATCH 03/92] fix audio input device --- src/audio/audio.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index d6e32d089..b037addcb 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -173,6 +173,12 @@ void audio::initialize( logger.info() << "initializing NoAudio backend"; backend = NoAudio::create().release(); } + if (settings.inputDevice.get() == "auto") { + auto inputDevices = audio::get_input_devices_names(); + if (!inputDevices.empty()) { + settings.inputDevice.set(inputDevices.at(0)); + } + } struct { std::string name; NumberSetting* setting; @@ -210,7 +216,9 @@ void audio::initialize( ::input_device = backend->openInputDevice( settings.inputDevice.get(), 44100, 1, 16 ); - ::input_device->startCapture(); + if (::input_device != nullptr) { + ::input_device->startCapture(); + } } else { if (::input_device) { ::input_device->stopCapture(); From 5930ef8285d46c2f1c0c81ebb9207c139e6f48a8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 2 Jun 2026 23:51:28 +0300 Subject: [PATCH 04/92] add events.remove --- res/modules/internal/events.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/res/modules/internal/events.lua b/res/modules/internal/events.lua index 98ff780f0..1243034c8 100644 --- a/res/modules/internal/events.lua +++ b/res/modules/internal/events.lua @@ -14,6 +14,7 @@ function events.on(event, func) events.handlers[event] = {} end table.insert(events.handlers[event], func) + return func end function events.reset(event, func) @@ -24,6 +25,14 @@ function events.reset(event, func) end end +function events.remove(event, handler) + local handlers = events.handlers[event] + if not handlers then + return + end + table.remove_value(handlers, handler) +end + function events.remove_by_prefix(prefix) for name, handlers in pairs(events.handlers) do local actualname = name From bb135d33b5ab8dde0d71342a323d64424befc357 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 2 Jun 2026 23:57:22 +0300 Subject: [PATCH 05/92] add on_destroy layout event --- src/frontend/UiDocument.cpp | 16 ++++++++++++++++ src/frontend/UiDocument.hpp | 3 +++ src/logic/scripting/scripting.cpp | 18 ++++++++++++++++++ src/logic/scripting/scripting.hpp | 7 +++++-- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/frontend/UiDocument.cpp b/src/frontend/UiDocument.cpp index 7e0c9bfc6..d0a158baf 100644 --- a/src/frontend/UiDocument.cpp +++ b/src/frontend/UiDocument.cpp @@ -7,6 +7,9 @@ #include "graphics/ui/elements/InventoryView.hpp" #include "graphics/ui/gui_xml.hpp" #include "logic/scripting/scripting.hpp" +#include "debug/Logger.hpp" + +static debug::Logger logger("ui-document"); UiDocument::UiDocument( std::string id, @@ -17,6 +20,19 @@ UiDocument::UiDocument( rebuildIndices(); } +UiDocument::~UiDocument() { + try { + scripting::on_ui_destroy(this); + } catch (const std::exception& err) { + logger.error() << "an error occurred on calling on_destroy event for document '" + << id << "': " << err.what(); + } catch (...) { + logger.error() << "unknown exception caught on calling on_destroy " + "event for document '" + << id << "'"; + } +} + void UiDocument::rebuildIndices() { map.clear(); gui::UINode::getIndices(root, map); diff --git a/src/frontend/UiDocument.hpp b/src/frontend/UiDocument.hpp index 5b40ca8ca..b2f0c9761 100644 --- a/src/frontend/UiDocument.hpp +++ b/src/frontend/UiDocument.hpp @@ -17,6 +17,7 @@ struct UiDocScript { bool onopen : 1; bool onprogress : 1; bool onclose : 1; + bool ondestroy : 1; }; using UINodesMap = std::unordered_map>; @@ -35,6 +36,8 @@ public: scriptenv env ); + ~UiDocument(); + void rebuildIndices(); void pushIndices(const std::shared_ptr& node); diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 37597b058..67f8989c8 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -594,6 +594,9 @@ bool scripting::on_item_break_block( void scripting::on_ui_open( UiDocument* layout, std::vector args ) { + if (!layout->getScript().onopen) { + return; + } auto argsptr = std::make_shared>(std::move(args)); std::string name = layout->getId() + ".open"; @@ -608,6 +611,9 @@ void scripting::on_ui_open( void scripting::on_ui_progress( UiDocument* layout, int workDone, int workTotal ) { + if (!layout->getScript().onprogress){ + return; + } std::string name = layout->getId() + ".progress"; lua::emit_event(lua::get_main_state(), name, [=](auto L) { lua::pushinteger(L, workDone); @@ -617,12 +623,23 @@ void scripting::on_ui_progress( } void scripting::on_ui_close(UiDocument* layout, Inventory* inventory) { + if (!layout->getScript().onclose) { + return; + } std::string name = layout->getId() + ".close"; lua::emit_event(lua::get_main_state(), name, [inventory](auto L) { return lua::pushinteger(L, inventory ? inventory->getId() : 0); }); } +void scripting::on_ui_destroy(UiDocument* layout) { + if (!layout->getScript().ondestroy) { + return; + } + std::string name = layout->getId() + ".destroy"; + lua::emit_event(lua::get_main_state(), name, [](auto L) { return 0; }); +} + void scripting::on_scripts_loading() { auto L = lua::get_main_state(); @@ -810,6 +827,7 @@ void scripting::load_layout_script( script.onprogress = register_event(env, "on_progress", prefix + ".progress"); script.onclose = register_event(env, "on_close", prefix + ".close"); + script.ondestroy = register_event(env, "on_destroy", prefix + ".destroy"); } void scripting::close() { diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index 9d29528b8..68ac57486 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -152,14 +152,17 @@ namespace scripting { void on_attacked(const Entity& entity, Player* player, entityid_t attacker); void on_entity_used(const Entity& entity, Player* player); - /// @brief Called on UI view show + /// @brief Called on UI document show void on_ui_open(UiDocument* layout, std::vector args); void on_ui_progress(UiDocument* layout, int workDone, int totalWork); - /// @brief Called on UI view close + /// @brief Called on UI document close void on_ui_close(UiDocument* layout, Inventory* inventory); + /// @brief Called on UI document destroy + void on_ui_destroy(UiDocument* layout); + /// @brief Called on Content loading void on_scripts_loading(); From 52829023c2718876ce97bc91345b3cadfdd7d10e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 2 Jun 2026 23:58:22 +0300 Subject: [PATCH 06/92] fix: duplicating core:chat event handlers in in-game chat --- res/layouts/ingame_chat.xml.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/res/layouts/ingame_chat.xml.lua b/res/layouts/ingame_chat.xml.lua index dce629887..fdd535045 100644 --- a/res/layouts/ingame_chat.xml.lua +++ b/res/layouts/ingame_chat.xml.lua @@ -19,7 +19,7 @@ local function update_line(line, uptime) end end -events.on("core:chat", function(message) +local core_chat_handler = events.on("core:chat", function(message) while #lines >= max_lines do document[lines[1][1]]:destruct() table.remove(lines, 1) @@ -36,7 +36,7 @@ end) function on_open() if not initialized then initialized = true - + document.root:setInterval(1/animation_fps * 1000, function () local uptime = time.uptime() for _, line in ipairs(lines) do @@ -52,3 +52,7 @@ function on_open() end) end end + +function on_destroy() + events.remove("core:chat", core_chat_handler) +end From 089bcbbb64b8009db03fb9b6ed04c491e46d6933 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 3 Jun 2026 21:50:36 +0300 Subject: [PATCH 07/92] fix document events & breaking: document events removed from 'events' library temporary --- src/frontend/UiDocument.cpp | 2 +- src/frontend/hud.cpp | 4 +- src/frontend/menu.cpp | 8 +-- src/graphics/ui/elements/InlineFrame.cpp | 4 +- src/logic/scripting/lua/libs/libgui.cpp | 2 +- src/logic/scripting/scripting.cpp | 77 +++++++++++------------- src/logic/scripting/scripting.hpp | 8 +-- 7 files changed, 50 insertions(+), 55 deletions(-) diff --git a/src/frontend/UiDocument.cpp b/src/frontend/UiDocument.cpp index d0a158baf..4e8dbe72a 100644 --- a/src/frontend/UiDocument.cpp +++ b/src/frontend/UiDocument.cpp @@ -22,7 +22,7 @@ UiDocument::UiDocument( UiDocument::~UiDocument() { try { - scripting::on_ui_destroy(this); + scripting::on_ui_destroy(*this); } catch (const std::exception& err) { logger.error() << "an error occurred on calling on_destroy event for document '" << id << "': " << err.what(); diff --git a/src/frontend/hud.cpp b/src/frontend/hud.cpp index 4366d8c6e..1aa03436b 100644 --- a/src/frontend/hud.cpp +++ b/src/frontend/hud.cpp @@ -563,7 +563,7 @@ void Hud::add(const HudElement& element, const dv::value& argsArray) { args.emplace_back(static_cast(blockPos[i])); } scripting::on_ui_open( - element.getDocument(), + *element.getDocument(), std::move(args) ); } @@ -578,7 +578,7 @@ void Hud::onRemove(const HudElement& element) { if (invview) { inventory = invview->getInventory().get(); } - scripting::on_ui_close(document, inventory); + scripting::on_ui_close(*document, inventory); if (invview) { invview->unbind(); } diff --git a/src/frontend/menu.cpp b/src/frontend/menu.cpp index 372264a59..ab06b42dc 100644 --- a/src/frontend/menu.cpp +++ b/src/frontend/menu.cpp @@ -86,7 +86,7 @@ UiDocument* menus::show( ); auto document = documentPtr.get(); engine.getAssets()->store(std::move(documentPtr), fullname); - scripting::on_ui_open(document, std::move(args)); + scripting::on_ui_open(*document, std::move(args)); menu->addPage(name, document->getRoot()); menu->setPage(name); return document; @@ -99,13 +99,13 @@ void menus::show_process_panel( auto menu = engine.getGUI().getMenu(); menu->reset(); - auto doc = + auto document = menus::show(engine, "process", {util::wstr2str_utf8(langs::get(text))}); - std::dynamic_pointer_cast(doc->getRoot()) + std::dynamic_pointer_cast(document->getRoot()) ->listenInterval(0.01f, [=]() { task->update(); uint tasksDone = task->getWorkDone(); - scripting::on_ui_progress(doc, tasksDone, initialWork); + scripting::on_ui_progress(*document, tasksDone, initialWork); }); } diff --git a/src/graphics/ui/elements/InlineFrame.cpp b/src/graphics/ui/elements/InlineFrame.cpp index 50fc79762..319a16b1b 100644 --- a/src/graphics/ui/elements/InlineFrame.cpp +++ b/src/graphics/ui/elements/InlineFrame.cpp @@ -13,7 +13,7 @@ InlineFrame::~InlineFrame() = default; void InlineFrame::setSrc(const std::string& src) { this->src = src; if (document) { - scripting::on_ui_close(document.get(), nullptr); + scripting::on_ui_close(*document, nullptr); document = nullptr; root = nullptr; } @@ -31,7 +31,7 @@ void InlineFrame::setDocument(const std::shared_ptr& document) { root->setSize(size); gui.postRunnable([this]() { - scripting::on_ui_open(this->document.get(), {}); + scripting::on_ui_open(*this->document, {}); }); } diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index cdd3dad28..5ede0694c 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -1144,7 +1144,7 @@ static int l_gui_load_document(lua::State* L) { auto document = documentPtr.get(); engine->requireAssets().store(std::move(documentPtr), alias); - scripting::on_ui_open(document, {args}); + scripting::on_ui_open(*document, {args}); return 0; } diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 67f8989c8..6954ee6b8 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -591,53 +591,46 @@ bool scripting::on_item_break_block( ); } -void scripting::on_ui_open( - UiDocument* layout, std::vector args -) { - if (!layout->getScript().onopen) { - return; - } - auto argsptr = - std::make_shared>(std::move(args)); - std::string name = layout->getId() + ".open"; - lua::emit_event(lua::get_main_state(), name, [=](auto L) { - for (const auto& value : *argsptr) { - lua::pushvalue(L, value); +static void call_layout_event(const UiDocument& layout, const std::vector& args, const std::string& eventName) { + auto L = lua::get_main_state(); + lua::pushenv(L, *layout.getEnvironment()); + if (lua::getfield(L, eventName)) { + for (const auto& arg : args) { + lua::pushvalue(L, arg); } - return argsptr->size(); - }); + lua::call_nothrow(L, args.size(), 0); + } + lua::pop(L); +} + +void scripting::on_ui_open( + const UiDocument& layout, std::vector args +) { + if (layout.getScript().onopen) { + call_layout_event(layout, args, "on_open"); + } } void scripting::on_ui_progress( - UiDocument* layout, int workDone, int workTotal + const UiDocument& layout, int workDone, int workTotal ) { - if (!layout->getScript().onprogress){ - return; + if (layout.getScript().onprogress){ + call_layout_event(layout, {workDone, workTotal}, "on_progress"); } - std::string name = layout->getId() + ".progress"; - lua::emit_event(lua::get_main_state(), name, [=](auto L) { - lua::pushinteger(L, workDone); - lua::pushinteger(L, workTotal); - return 2; - }); } -void scripting::on_ui_close(UiDocument* layout, Inventory* inventory) { - if (!layout->getScript().onclose) { - return; +void scripting::on_ui_close(const UiDocument& layout, Inventory* inventory) { + if (layout.getScript().onclose) { + call_layout_event( + layout, {inventory ? inventory->getId() : 0}, "on_close" + ); } - std::string name = layout->getId() + ".close"; - lua::emit_event(lua::get_main_state(), name, [inventory](auto L) { - return lua::pushinteger(L, inventory ? inventory->getId() : 0); - }); } -void scripting::on_ui_destroy(UiDocument* layout) { - if (!layout->getScript().ondestroy) { - return; +void scripting::on_ui_destroy(const UiDocument& layout) { + if (layout.getScript().ondestroy) { + call_layout_event(layout, {}, "on_destroy"); } - std::string name = layout->getId() + ".destroy"; - lua::emit_event(lua::get_main_state(), name, [](auto L) { return 0; }); } void scripting::on_scripts_loading() { @@ -822,12 +815,14 @@ void scripting::load_layout_script( ) { int env = *senv; - lua::pop(lua::get_main_state(), load_script(env, "layout", file, fileName)); - script.onopen = register_event(env, "on_open", prefix + ".open"); - script.onprogress = - register_event(env, "on_progress", prefix + ".progress"); - script.onclose = register_event(env, "on_close", prefix + ".close"); - script.ondestroy = register_event(env, "on_destroy", prefix + ".destroy"); + auto L = lua::get_main_state(); + lua::pop(L, load_script(env, "layout", file, fileName)); + lua::pushenv(L, env); + script.onopen = lua::hasfield(L, "on_open"); + script.onprogress = lua::hasfield(L, "on_progress"); + script.onclose = lua::hasfield(L, "on_close"); + script.ondestroy = lua::hasfield(L, "on_destroy"); + lua::pop(L); } void scripting::close() { diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index 68ac57486..ff2306589 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -153,15 +153,15 @@ namespace scripting { void on_entity_used(const Entity& entity, Player* player); /// @brief Called on UI document show - void on_ui_open(UiDocument* layout, std::vector args); + void on_ui_open(const UiDocument& layout, std::vector args); - void on_ui_progress(UiDocument* layout, int workDone, int totalWork); + void on_ui_progress(const UiDocument& layout, int workDone, int totalWork); /// @brief Called on UI document close - void on_ui_close(UiDocument* layout, Inventory* inventory); + void on_ui_close(const UiDocument& layout, Inventory* inventory); /// @brief Called on UI document destroy - void on_ui_destroy(UiDocument* layout); + void on_ui_destroy(const UiDocument& layout); /// @brief Called on Content loading void on_scripts_loading(); From 841c2a5d543fc53a8c0539c60420287c3d7d9bd6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 4 Jun 2026 20:40:42 +0300 Subject: [PATCH 08/92] add file.remove_ext --- res/modules/internal/extensions/file.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/modules/internal/extensions/file.lua b/res/modules/internal/extensions/file.lua index 0f1040128..d308eda9c 100644 --- a/res/modules/internal/extensions/file.lua +++ b/res/modules/internal/extensions/file.lua @@ -23,6 +23,10 @@ function file.parent(path) return dir end +function file.remove_ext(path) + return string.format("%s/%s", file.parent(path), file.stem(path)) +end + function file.path(path) local pos = path:find(':') return path:sub(pos + 1) From 0bfc63d51fe55e36578f85af0e527bb4169f8764 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 4 Jun 2026 20:45:43 +0300 Subject: [PATCH 09/92] add debug.Logger lua class --- res/modules/internal/debugging.lua | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/res/modules/internal/debugging.lua b/res/modules/internal/debugging.lua index 1b56ee4b1..dd98a5c26 100644 --- a/res/modules/internal/debugging.lua +++ b/res/modules/internal/debugging.lua @@ -148,3 +148,29 @@ function error(message, level) end __error(message, level) end + +local LoggerClass = { + __index = { + info = function (self, text) + debug.log(string.format("[%s] %s", self.name, text)) + end, + warning = function (self, text) + debug.warning(string.format("[%s] %s", self.name, text)) + end, + error = function (self, text) + debug.error(string.format("[%s] %s", self.name, text)) + end + } +} + +local _getinfo = debug.getinfo + +function debug.Logger(name) + if not name then + local source = _getinfo(2, "S").source + name = file.remove_ext(source) + else + name = string.format("%s:%s", file.prefix(_getinfo(2, "S").source), name) + end + return setmetatable({name=name}, LoggerClass) +end From dd62393eb6b9ca8afdc1e5082c285a091d658532 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 4 Jun 2026 23:02:06 +0300 Subject: [PATCH 10/92] use real delta in client-side tests --- src/engine/Mainloop.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index 342f81aaf..241fd6c2e 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -18,11 +18,9 @@ Mainloop::Mainloop(Engine& engine) : engine(engine) { } void Mainloop::run() { - const auto& coreParams = engine.getCoreParameters(); auto& time = engine.getTime(); auto& window = engine.getWindow(); auto& settings = engine.getSettings(); - double targetDelta = 1.0 / static_cast(coreParams.tps); engine.setLevelConsumer([this](auto level, int64_t localPlayer) { if (level == nullptr) { @@ -39,13 +37,10 @@ void Mainloop::run() { logger.info() << "starting menu screen"; engine.setScreen(std::make_shared(engine)); - - double testTimer = 0.0; logger.info() << "main loop started"; while (!window.isShouldClose()) { - testTimer += targetDelta; - time.update(coreParams.testMode ? testTimer : window.time()); + time.update(window.time()); engine.applicationTick(); engine.updateFrontend(); From b1311fd03cde0569c87f533863acff6976256f59 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 4 Jun 2026 23:21:58 +0300 Subject: [PATCH 11/92] fix test mode --- src/devtools/AppScriptsControl.cpp | 4 ++++ src/devtools/AppScriptsControl.hpp | 2 ++ src/engine/Engine.hpp | 4 ++++ src/engine/Mainloop.cpp | 20 ++++++++++++++------ src/engine/ServerMainloop.cpp | 3 ++- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/devtools/AppScriptsControl.cpp b/src/devtools/AppScriptsControl.cpp index 16c3475d5..bb1a386a2 100644 --- a/src/devtools/AppScriptsControl.cpp +++ b/src/devtools/AppScriptsControl.cpp @@ -57,3 +57,7 @@ void AppScriptsControl::terminate(std::string_view reason) { logger.info() << "script has been terminated due to " << reason; } } + +bool AppScriptsControl::isFinished() const { + return scriptCoroutine == nullptr || !scriptCoroutine->isActive(); +} diff --git a/src/devtools/AppScriptsControl.hpp b/src/devtools/AppScriptsControl.hpp index fb081a986..4e44eef60 100644 --- a/src/devtools/AppScriptsControl.hpp +++ b/src/devtools/AppScriptsControl.hpp @@ -21,6 +21,8 @@ public: void terminate(std::string_view reason); void onScreenChange(const std::string& name, bool show); + + bool isFinished() const; private: const Project& project; std::unique_ptr clientScript; diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 169631214..153acf4df 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -186,5 +186,9 @@ public: return debuggingServer.get(); } + AppScriptsControl& getAppScripts() { + return *appScripts; + } + void detachDebugger(); }; diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index 241fd6c2e..3a196f522 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -1,16 +1,17 @@ #include "Mainloop.hpp" -#include "Engine.hpp" #include "debug/Logger.hpp" +#include "devtools/AppScriptsControl.hpp" #include "devtools/Project.hpp" -#include "frontend/screens/MenuScreen.hpp" +#include "Engine.hpp" #include "frontend/screens/LevelScreen.hpp" +#include "frontend/screens/MenuScreen.hpp" +#include "graphics/ui/elements/Container.hpp" +#include "graphics/ui/GUI.hpp" +#include "io/path.hpp" +#include "logic/scripting/scripting.hpp" #include "window/Window.hpp" #include "world/Level.hpp" -#include "graphics/ui/GUI.hpp" -#include "graphics/ui/elements/Container.hpp" -#include "logic/scripting/scripting.hpp" -#include "io/path.hpp" static debug::Logger logger("mainloop"); @@ -21,6 +22,7 @@ void Mainloop::run() { auto& time = engine.getTime(); auto& window = engine.getWindow(); auto& settings = engine.getSettings(); + const auto& coreParams = engine.getCoreParameters(); engine.setLevelConsumer([this](auto level, int64_t localPlayer) { if (level == nullptr) { @@ -37,6 +39,8 @@ void Mainloop::run() { logger.info() << "starting menu screen"; engine.setScreen(std::make_shared(engine)); + + auto& appScripts = engine.getAppScripts(); logger.info() << "main loop started"; while (!window.isShouldClose()) { @@ -52,6 +56,10 @@ void Mainloop::run() { settings.display.adaptiveFpsInMenu.get() && dynamic_cast(engine.getScreen().get()) != nullptr ); + if (coreParams.testMode && appScripts.isFinished()) { + logger.info() << "test finished"; + engine.quit(); + } } logger.info() << "main loop stopped"; } diff --git a/src/engine/ServerMainloop.cpp b/src/engine/ServerMainloop.cpp index 7c9970e5e..c1a95330d 100644 --- a/src/engine/ServerMainloop.cpp +++ b/src/engine/ServerMainloop.cpp @@ -1,6 +1,7 @@ #include "ServerMainloop.hpp" #include "Engine.hpp" +#include "devtools/AppScriptsControl.hpp" #include "logic/LevelController.hpp" #include "interfaces/Process.hpp" #include "debug/Logger.hpp" @@ -37,7 +38,7 @@ void ServerMainloop::run() { auto begin = system_clock::now(); auto startupTime = begin; - while (engine.isQuitSignal()) { + while (!engine.isQuitSignal() && !engine.getAppScripts().isFinished()) { if (coreParams.testMode) { time.step(delta); } else { From 902c885fe0d72cbe5bd2ea9a95d4551516197d4a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 5 Jun 2026 23:19:21 +0300 Subject: [PATCH 12/92] fix uinode.parent property --- src/logic/scripting/lua/libs/libgui.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 5ede0694c..057dc205e 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -420,15 +420,19 @@ static int p_get_data(UINode* node, lua::State* L) { return 0; } -static const std::string& request_node_id(const DocumentNode& docnode) { - std::string id = docnode.node->getId(); +static const std::string& request_node_id(UiDocument& document, UINode& node) { + const std::string& id = node.getId(); if (id.empty()) { - id = "#" + std::to_string( - reinterpret_cast(docnode.node.get())); + node.setId( "#" + std::to_string( + reinterpret_cast(&node))); + document.pushIndices(node.shared_from_this()); + return node.getId(); } - docnode.node->setId(std::move(id)); - docnode.document->pushIndices(docnode.node); - return docnode.node->getId(); + return id; +} + +static const std::string& request_node_id(const DocumentNode& docnode) { + return request_node_id(*docnode.document, *docnode.node); } /// @brief Push UI-document node object to stack @@ -443,15 +447,14 @@ static int push_document_node(lua::State* L, const std::string& id) { static int p_get_parent(UINode* node, lua::State* L) { auto parent = node->getParent(); - if (!parent) { + if (parent == nullptr) { return 0; } auto docname = lua::require_string(L, 1); auto element = lua::require_string(L, 2); auto docnode = get_document_node_impl(L, docname, element); - const auto& id = request_node_id(docnode); - + const auto& id = request_node_id(*docnode.document, *parent); return push_document_node(L, id); } From d2f272e403cf000810396e4760e9ee39c6e8b8b3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 5 Jun 2026 23:22:44 +0300 Subject: [PATCH 13/92] add table.keys(t) --- res/modules/internal/extensions/table.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/res/modules/internal/extensions/table.lua b/res/modules/internal/extensions/table.lua index d2d665755..f47eb643b 100644 --- a/res/modules/internal/extensions/table.lua +++ b/res/modules/internal/extensions/table.lua @@ -201,3 +201,11 @@ function table.tostring(t) end return s..']' end + +function table.keys(t) + local keys = {} + for key, _ in pairs(t) do + table.insert(keys, key) + end + return keys +end From 3429435ff509929a523ca2ea618dab04b50c682d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 5 Jun 2026 23:25:10 +0300 Subject: [PATCH 14/92] add 'test' library for e2e tests purposes --- res/modules/internal/test.lua | 136 +++++++++++++++ res/scripts/stdlib.lua | 4 + src/logic/scripting/lua/libs/api_lua.hpp | 1 + src/logic/scripting/lua/libs/libtest.cpp | 45 +++++ src/logic/scripting/lua/lua_engine.cpp | 14 +- src/window/detail/GLFWWindow.cpp | 204 +++++++++++++---------- src/window/input.hpp | 5 + 7 files changed, 313 insertions(+), 96 deletions(-) create mode 100644 res/modules/internal/test.lua create mode 100644 src/logic/scripting/lua/libs/libtest.cpp diff --git a/res/modules/internal/test.lua b/res/modules/internal/test.lua new file mode 100644 index 000000000..a6ca7d096 --- /dev/null +++ b/res/modules/internal/test.lua @@ -0,0 +1,136 @@ +local __app = __vc_app + +local function get_node_center(node) + local pos = node.wpos + local size = node.size + return pos[1] + size[1] / 2, pos[2] + size[2] / 2 +end + +local function types_check_enum(value, enum) + if value == nil then + return true, nil + end + if type(value) ~= "string" then + return false, string.format("string expected [%s]", table.concat(table.keys(enum), "|")) + end + return true, nil +end + +local function types_check_number(value) + if value == nil then + return true, nil + end + local value_type = type(value) + if value_type ~= "number" then + return false, string.format("number expected, got %s", value_type) + end +end + +local function check_option(t, key, func, ...) + local value = t[key] + if value == nil then + return nil + end + local success, message = func(value, ...) + if not success then + error(string.format("invalid option '%s': %s", key, message)) + end +end + +local buttons_enum = { + left = 0, + right = 1, + middle = 2, +} +local no_options = {} + +local _tick = __app.tick +local _set_button_pressed = test.set_button_pressed +local _set_key_pressed = test.set_key_pressed +local _enter_text = test.enter_text +test.set_button_pressed = nil +test.set_key_pressed = nil +test.enter_text = nil + +function test.click(node, options) + options = options or no_options + + check_option(options, 'button', types_check_enum, buttons_enum) + check_option(options, 'timeout', types_check_number) + + local button = buttons_enum[options.button or 'left'] + local timeout = options.timeout or 1000 + + local tm = time.precise_time() + while (time.precise_time() - tm) * 1000 < timeout do + if node.exists then + break + end + _tick() + end + if not node.exists then + error(string.format("timeout %s ms exceeded: element %s does not exists", + math.floor(timeout), string.escape(node.id))) + end + + local center_x, center_y = get_node_center(node) + _set_button_pressed(button, center_x, center_y, true) + _tick() + _set_button_pressed(button, center_x, center_y, false) + _tick() +end + +-- TODO: replace with resolving in C++ side +function test.find_by_attr(node, attr, value) + if node[attr] == value then + return node + end + local i = 1 + while true do + local subnode = node[i] + if subnode == nil then + break + end + local found = test.find_by_attr(subnode, attr, value) + if found then + return found + end + i = i + 1 + end +end + +function test.find_by_text(node, text) + return test.find_by_attr(node, "text", text) +end + +function test.find_by_attr_presence(node, attr) + if node[attr] then + return node + end + local i = 1 + while true do + local subnode = node[i] + if subnode == nil then + break + end + local found = test.find_by_attr_presence(subnode, attr) + if found then + return found + end + i = i + 1 + end +end + +function test.fill(node, text, options) + test.click(node, options) + __app.tick() + _enter_text(text) + test.press("enter") +end + +function test.press(key) + _set_key_pressed(key, true) + __app.tick() + _set_key_pressed(key, false) + __app.tick() +end diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 093501d75..a1949c1e6 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -127,6 +127,10 @@ require "core:internal/extensions/inventory" asserts = require "core:internal/asserts" events = require "core:internal/events" +if test then + require "core:internal/test" +end + function pack.unload(prefix) events.remove_by_prefix(prefix) end diff --git a/src/logic/scripting/lua/libs/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp index e304d6dc1..123e1e532 100644 --- a/src/logic/scripting/lua/libs/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -44,6 +44,7 @@ extern const luaL_Reg posteffectslib[]; // gfx.posteffects extern const luaL_Reg quatlib[]; extern const luaL_Reg randomlib[]; extern const luaL_Reg compressionlib[]; +extern const luaL_Reg testlib[]; extern const luaL_Reg text3dlib[]; // gfx.text3d extern const luaL_Reg timelib[]; extern const luaL_Reg tomllib[]; diff --git a/src/logic/scripting/lua/libs/libtest.cpp b/src/logic/scripting/lua/libs/libtest.cpp new file mode 100644 index 000000000..d6675deeb --- /dev/null +++ b/src/logic/scripting/lua/libs/libtest.cpp @@ -0,0 +1,45 @@ +#include "api_lua.hpp" +#include "engine/Engine.hpp" +#include "window/input.hpp" + +#include + +using namespace scripting; + +static int l_set_button_pressed(lua::State* L) { + int button = lua::touinteger(L, 1); + int x = lua::tointeger(L, 2); + int y = lua::tointeger(L, 3); + bool pressed = lua::toboolean(L, 4); + + auto& input = engine->getInput(); + input.simulateCursorPos(x, y); + input.simulateClick(button, pressed); + return 0; +} + +static int l_set_key_pressed(lua::State* L) { + auto key = lua::require_string(L, 1); + bool pressed = lua::toboolean(L, 2); + + auto& input = engine->getInput(); + input.simulateKey(input_util::keycode_from(key), pressed); + return 0; +} + +static int l_enter_text(lua::State* L) { + auto string = lua::require_wstring(L, 1); + + auto& input = engine->getInput(); + for (auto chr : string) { + input.simulateCodepoint(chr); + } + return 0; +} + +const luaL_Reg testlib[] = { + {"set_button_pressed", lua::wrap}, + {"set_key_pressed", lua::wrap}, + {"enter_text", lua::wrap}, + {nullptr, nullptr} +}; diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index afb2f42eb..38b6a4b83 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -19,6 +19,7 @@ namespace { debug::Logger logger("lua-state"); lua::State* main_thread = nullptr; bool headless_mode = false; + bool test_mode = false; const std::unordered_map* project_args; } @@ -69,12 +70,12 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "yaml", yamllib); openlib(L, "__vc_app", applib); - lua::getglobal(L, "__vc_app"); - lua::setregistry(L, "app"); + getglobal(L, "__vc_app"); + setregistry(L, "app"); if (stateType == StateType::SCRIPT) { - lua::getregistry(L, "app"); - lua::setglobal(L, "app"); + getregistry(L, "app"); + setglobal(L, "app"); } if (stateType == StateType::BASE || stateType == StateType::SCRIPT) { openlib(L, "assets", assetslib); @@ -99,6 +100,10 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "__transform", transformlib); } + if (::test_mode) { + openlib(L, "test", testlib); + } + addfunc(L, "print", lua::wrap); addfunc(L, "crc32", lua::wrap); } @@ -191,6 +196,7 @@ void lua::initialize(const EnginePaths& paths, const CoreParameters& params) { logger.info() << LUAJIT_VERSION; headless_mode = params.headless; + test_mode = params.testMode; project_args = ¶ms.projectArgs; main_thread = create_state( paths, params.headless ? StateType::SCRIPT : StateType::BASE diff --git a/src/window/detail/GLFWWindow.cpp b/src/window/detail/GLFWWindow.cpp index db3d67241..6f42258d8 100644 --- a/src/window/detail/GLFWWindow.cpp +++ b/src/window/detail/GLFWWindow.cpp @@ -165,17 +165,127 @@ inline constexpr short MOUSE_KEYS_OFFSET = 1024; static GLFWcursor* standard_cursors[static_cast(CursorShape::LAST) + 1] = {}; -class GLFWInput : public Input { +class BaseInput : public Input { public: + std::vector codepoints; int scroll = 0; + + void onKeyCallback(int key, bool pressed) { + bool prevPressed = keys[key]; + keys[key] = pressed; + frames[key] = currentFrame; + if (pressed && !prevPressed) { + const auto& callbacks = keyCallbacks.find(static_cast(key)); + if (callbacks != keyCallbacks.end()) { + callbacks->second.notify(); + } + } + if (pressed && key < MOUSE_KEYS_OFFSET) { + pressedKeys.push_back(static_cast(key)); + } + } + + void onMouseCallback(int button, bool pressed) { + int key = button + MOUSE_KEYS_OFFSET; + onKeyCallback(key, pressed); + } + + bool isCursorLocked() const override { + return cursorLocked; + } + + void setCursorPosition(double xpos, double ypos) { + if (cursorDrag) { + delta.x += xpos - cursor.x; + delta.y += ypos - cursor.y; + } else { + cursorDrag = true; + } + cursor.x = xpos; + cursor.y = ypos; + } + + Bindings& getBindings() override { + return bindings; + } + + const Bindings& getBindings() const override { + return bindings; + } + + ObserverHandler addKeyCallback(Keycode key, KeyCallback callback) override { + return keyCallbacks[key].add(std::move(callback)); + } + + const std::vector& getPressedKeys() const override { + return pressedKeys; + } + + const std::vector& getCodepoints() const override { + return codepoints; + } + + CursorState getCursor() const override { + return {isCursorLocked(), cursor, delta}; + } + + int getScroll() override { + return scroll; + } + + bool pressed(Keycode key) const override { + int keycode = static_cast(key); + if (keycode < 0 || keycode >= KEYS_BUFFER_SIZE) { + return false; + } + return keys[keycode]; + } + bool jpressed(Keycode keycode) const override { + return pressed(keycode) && + frames[static_cast(keycode)] == currentFrame; + } + + bool clicked(Mousecode code) const override { + return pressed( + static_cast(MOUSE_KEYS_OFFSET + static_cast(code)) + ); + } + bool jclicked(Mousecode code) const override { + return clicked(code) && + frames[static_cast(code) + MOUSE_KEYS_OFFSET] == + currentFrame; + } + + void simulateKey(Keycode key, bool pressed) override { + onKeyCallback(static_cast(key), pressed); + } + + void simulateClick(int button, bool pressed) override { + onMouseCallback(static_cast(button), pressed); + } + + void simulateCursorPos(double xpos, double ypos) override { + setCursorPosition(xpos, ypos); + } + + void simulateCodepoint(uint codepoint) override { + codepoints.push_back(codepoint); + } +protected: uint currentFrame = 0; uint frames[KEYS_BUFFER_SIZE] {}; - std::vector codepoints; std::vector pressedKeys; Bindings bindings; bool keys[KEYS_BUFFER_SIZE] {}; std::unordered_map> keyCallbacks; + bool cursorLocked = false; + bool cursorDrag = false; + glm::vec2 delta {}; + glm::vec2 cursor {}; +}; +class GLFWInput : public BaseInput { +public: GLFWInput(GLFWwindow* window) : window(window) { } @@ -225,26 +335,6 @@ public: } } - void onKeyCallback(int key, bool pressed) { - bool prevPressed = keys[key]; - keys[key] = pressed; - frames[key] = currentFrame; - if (pressed && !prevPressed) { - const auto& callbacks = keyCallbacks.find(static_cast(key)); - if (callbacks != keyCallbacks.end()) { - callbacks->second.notify(); - } - } - if (pressed && key < MOUSE_KEYS_OFFSET) { - pressedKeys.push_back(static_cast(key)); - } - } - - void onMouseCallback(int button, bool pressed) { - int key = button + MOUSE_KEYS_OFFSET; - onKeyCallback(key, pressed); - } - const char* getClipboardText() const override { return glfwGetClipboardString(window); } @@ -253,41 +343,6 @@ public: glfwSetClipboardString(window, text); } - int getScroll() override { - return scroll; - } - - bool pressed(Keycode key) const override { - int keycode = static_cast(key); - if (keycode < 0 || keycode >= KEYS_BUFFER_SIZE) { - return false; - } - return keys[keycode]; - } - bool jpressed(Keycode keycode) const override { - return pressed(keycode) && - frames[static_cast(keycode)] == currentFrame; - } - - bool clicked(Mousecode code) const override { - return pressed( - static_cast(MOUSE_KEYS_OFFSET + static_cast(code)) - ); - } - bool jclicked(Mousecode code) const override { - return clicked(code) && - frames[static_cast(code) + MOUSE_KEYS_OFFSET] == - currentFrame; - } - - CursorState getCursor() const override { - return {isCursorLocked(), cursor, delta}; - } - - bool isCursorLocked() const override { - return cursorLocked; - } - void toggleCursor() override { cursorDrag = false; if (cursorLocked) { @@ -298,43 +353,8 @@ public: } cursorLocked = !cursorLocked; } - - void setCursorPosition(double xpos, double ypos) { - if (cursorDrag) { - delta.x += xpos - cursor.x; - delta.y += ypos - cursor.y; - } else { - cursorDrag = true; - } - cursor.x = xpos; - cursor.y = ypos; - } - - Bindings& getBindings() override { - return bindings; - } - - const Bindings& getBindings() const override { - return bindings; - } - - ObserverHandler addKeyCallback(Keycode key, KeyCallback callback) override { - return keyCallbacks[key].add(std::move(callback)); - } - - const std::vector& getPressedKeys() const override { - return pressedKeys; - } - - const std::vector& getCodepoints() const override { - return codepoints; - } private: GLFWwindow* window; - bool cursorLocked = false; - bool cursorDrag = false; - glm::vec2 delta {}; - glm::vec2 cursor {}; }; static_assert(!std::is_abstract()); diff --git a/src/window/input.hpp b/src/window/input.hpp index 84c120409..59d663672 100644 --- a/src/window/input.hpp +++ b/src/window/input.hpp @@ -274,6 +274,11 @@ public: virtual bool clicked(Mousecode mousecode) const = 0; virtual bool jclicked(Mousecode mousecode) const = 0; + virtual void simulateKey(Keycode key, bool pressed) = 0; + virtual void simulateClick(int button, bool pressed) = 0; + virtual void simulateCursorPos(double xpos, double ypos) = 0; + virtual void simulateCodepoint(uint codepoint) = 0; + virtual CursorState getCursor() const = 0; virtual bool isCursorLocked() const = 0; From 84b2aef81f82d46b07d9bd059540d7ac90cae3ec Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 8 Jun 2026 22:21:15 +0300 Subject: [PATCH 15/92] fix ui elements related things --- res/modules/internal/gui_util.lua | 19 +++++++++++-------- res/modules/internal/test.lua | 5 ++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/res/modules/internal/gui_util.lua b/res/modules/internal/gui_util.lua index 0ec47291e..656a0dfc8 100644 --- a/res/modules/internal/gui_util.lua +++ b/res/modules/internal/gui_util.lua @@ -77,15 +77,18 @@ function Element.new(docname, name) end -- the engine automatically creates an instance for every ui document (layout) -local Document = {} -function Document.new(docname) - return setmetatable({name=docname}, { - __index=function(self, k) - local elem = Element.new(self.name, k) - rawset(self, k, elem) - return elem +local Document = { + __index=function(self, k) + if type(k) ~= "string" then + error("element id is not a string") end - }) + local elem = Element.new(self.name, k) + rawset(self, k, elem) + return elem + end +} +function Document.new(docname) + return setmetatable({name=docname}, Document) end local RadioGroup = {} diff --git a/res/modules/internal/test.lua b/res/modules/internal/test.lua index a6ca7d096..ccabf3add 100644 --- a/res/modules/internal/test.lua +++ b/res/modules/internal/test.lua @@ -70,7 +70,7 @@ function test.click(node, options) end if not node.exists then error(string.format("timeout %s ms exceeded: element %s does not exists", - math.floor(timeout), string.escape(node.id))) + math.floor(timeout), string.escape(rawget(node, 'name')))) end local center_x, center_y = get_node_center(node) @@ -82,6 +82,9 @@ end -- TODO: replace with resolving in C++ side function test.find_by_attr(node, attr, value) + if getmetatable(node) == Document then + return test.find_by_attr(node.root, attr, value) + end if node[attr] == value then return node end From 958d0c7072988f0f3e772896ed17ae243f56377e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 9 Jun 2026 19:13:07 +0300 Subject: [PATCH 16/92] update doc/ru/scripting/extensions.md --- doc/ru/scripting/extensions.md | 8 +++++++- res/modules/internal/extensions/table.lua | 6 +----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index 868210056..922d02404 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -62,6 +62,12 @@ table.sub(arr: table, start: number | nil, stop: number | nil) -> table table.insert_unique(t: table, val: any) table.insert_unique(t: table, pos: int, val: any) +-- Возвращает таблицу, содержащую все ключи переданной таблицы, включая числовые. +table.keys(t: table) -> table + +-- Добавляет в таблицу t все пары ключ-значение из таблицы extension, при этом если в extension присутствует ключ из t, то его значение будет перезаписано. +table.extend(t: table, extension: table) -> table + -- Конвертирует переданную таблицу в строку. table.tostring(t: table) -> string ``` @@ -200,4 +206,4 @@ await(co: coroutine) -> result, error -- Константа, в которой хранится PID текущего инстанса движка. os.pid -> number -``` \ No newline at end of file +``` diff --git a/res/modules/internal/extensions/table.lua b/res/modules/internal/extensions/table.lua index f47eb643b..a96d78fe8 100644 --- a/res/modules/internal/extensions/table.lua +++ b/res/modules/internal/extensions/table.lua @@ -59,11 +59,7 @@ end function table.extend(t1, t2) for i, v in pairs(t2) do - if type(i) == "number" then - t1[#t1 + 1] = v - else - t1[i] = v - end + t1[i] = v end return t1 From 5a040893f1763e495456e2600d7bd580094bb8c8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 9 Jun 2026 19:42:51 +0300 Subject: [PATCH 17/92] add doc/en/scripting/extensions.md --- doc/en/scripting.md | 1 + doc/en/scripting/extensions.md | 204 +++++++++++++++++++++++++++++++++ doc/ru/scripting/extensions.md | 2 +- 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 doc/en/scripting/extensions.md diff --git a/doc/en/scripting.md b/doc/en/scripting.md index a21b959ed..17e7f7a7a 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -42,6 +42,7 @@ Subsections: - [utf8](scripting/builtins/libutf8.md) - [vec2, vec3, vec4](scripting/builtins/libvecn.md) - [world](scripting/builtins/libworld.md) +- [Extensions for standard libraries](scripting/extensions.md) - [Module core:bit_converter](scripting/modules/core_bit_converter.md) - [Module core:data_buffer](scripting/modules/core_data_buffer.md) - [Module core:vector2, core:vector3](scripting/modules/core_vector2_vector3.md) diff --git a/doc/en/scripting/extensions.md b/doc/en/scripting/extensions.md new file mode 100644 index 000000000..94a55ef00 --- /dev/null +++ b/doc/en/scripting/extensions.md @@ -0,0 +1,204 @@ +# Standard Library Extensions + +The **stdmin.lua** kernel script defines functions that extend and complement some of the standard **Lua** libraries. + +## Contents: +- [table extensions](#table-extensions) +- [string extensions](#string-extensions) +- [math extensions](#math-extensions) +- [bit extensions](#bit-extensions) +- [additional global functions](#additional-global-functions) + +## Table Extensions +```lua +-- Creates and returns a copy of the given table by creating a new one and copying all elements from the given table into it. +table.copy(t: table) -> table + +-- The deep copy function creates a full copy of the source table, including all its subtables. +table.deep_copy(t: table) -> table + +-- Returns the number of pairs in the given table. +table.count_pairs(t: table) -> int + +-- Returns one element from the given table at a random position. +table.random(t: table) -> any + +-- Returns true if x is contained in t. +table.has(t: table, x: any) -> boolean + +-- Returns the index of x in t. If the given object is not contained in the table, the function returns -1. +table.index(t: table, x: any) -> int + +-- Removes element x from t. +table.remove_value(t: table, x: any) + +-- Shuffles values ​​in the table. +table.shuffle(t: table) -> table + +-- Adds values ​​from table t2 to table t1. If table t2 contains a key from t1, the key's value will not be changed. +table.merge(t1: table, t2: table) -> table + +-- Iterates through the table and applies a func to all its elements, returning the new value of the element. +table.map(t: table, func: function(indx, value)) -> table + +-- Iterates through the table using a func that returns true if the element should be kept and false if it should be deleted. +table.filter(t: table, func: function(indx, value)) -> table + +-- Allows you to safely retrieve the value for the specified key. If the key exists in the table, the method will return its value. +-- If the key is missing, the method will set it to the default value and return it. +table.set_default(t: table, key: int | string, default: any) -> any + +-- Returns a "flattened" version of the original table. +table.flat(t: table) -> table + +-- Returns a deep "flat" version of the original table. +table.deep_flat(t: table) -> table + +-- Returns a truncated version of the table from index start to index stop, inclusive. Key-value pairs +-- are not preserved in the new table. For nil values, the value starts at 1 and ends at #arr, respectively. +table.sub(arr: table, start: number | nil, stop: number | nil) -> table + +-- Adds a value to the table only if it was not originally there. +table.insert_unique(t: table, val: any) +table.insert_unique(t: table, pos: int, val: any) + +-- Returns a table containing all keys of the given table, including numeric ones. table.keys(t: table) -> table + +-- Adds all key-value pairs from table extension to table t. If a key from t is present in extension, its value will be overwritten. +table.extend(t: table, extension: table) -> table + +-- Converts the passed table to a string. +table.tostring(t: table) -> string +``` + +## String extensions + +It's important to note that all of the functions listed below that extend **string** can be used as meta-methods on string instances, i.e.: + +```lua +local str = "ABA str BAB" + +if str:starts_with("ABA") and str:ends_with("BAB") then +print(str:replace("BA", "DC")) +end +``` + +```lua +-- Splits the string into parts based on the specified separator/expression and returns the result as a table of strings. If withpattern is true, the separator parameter will be evaluated as a regular expression. string.explode(separator: string, str: string, withpattern: boolean) -> table + +-- Splits the string into parts based on the specified delimiter and returns the result as a table of strings. +string.split(str: string, delimiter: string) -> table + +-- Escapes special characters in the string, such as `()[]+-.$%^?*`, into `%character` format. The `NUL` character (`\0`) will be converted to `%z`. +string.pattern_safe(str: string) -> string + +-- Splits seconds into hours, minutes, and milliseconds and formats them using the following parameter order: `minutes, seconds, milliseconds`, and then returns the result. If format is not specified, it returns a table where: +-- h - hours, +-- m - minutes, +-- s - seconds, +-- ms - milliseconds. +string.formatted_time(seconds: number, format: string) -> string | table + +-- Replaces all substrings in str equal to tofind with toreplace and returns a string with all the modified substrings. +string.replace(str: string, tofind: string, toreplace: string) -> string + +-- Removes all characters equal to char from the string from the left and right ends and returns the result. +-- If the char parameter is undefined, all empty characters will be selected. +string.trim(str: string, char: string) -> string + +-- Removes all characters equal to char from the string from the left end and returns the result. +-- If the char parameter is undefined, all empty characters will be selected. +string.trim_left(str: string, char: string) -> string + +-- Removes all characters equal to char from the right end of the string and returns the result. +-- If the char parameter is undefined, all empty characters will be selected. +string.trim_right(str: string, char: string) -> string + +-- Returns true if the string begins with the substring start. +string.starts_with(str: string, start: string) -> boolean + +-- Returns true if the string ends with the substring endStr. +string.ends_with(str: string, endStr: string) -> boolean + +-- The string.lower and string.upper functions are also overridden by utf8.lower and utf8.upper. + +-- Escapes a string. It is an alias for utf8.escape. +string.escape(str: string) -> string + +-- Escapes special XML characters. An alias for utf8.escape_xml. +string.escape_xml(text: string) -> string + +-- Adds a char to the left and right of the string until its size equals size. +-- By default, char is equal to the space character. +string.pad(str: string, size: int, char: string) -> string + +-- Adds a char to the left of the string until its size equals size. +-- By default, char is equal to the space character. +string.left_pad(str: string, size: int, char: string) -> string + +-- Adds a char to the right of the string until its size equals size. +-- By default, char is equal to the space character. +string.right_pad(str: string, size: int, char: string) -> string +``` + +## Math extensions + +```lua +-- Returns _in if it is in the range low <= _in <= high +-- Otherwise, returns the boundary to which _in is closest. +math.clamp(_in: number, low: number, high: number) -> number + +-- Returns a random fractional number in the range low to high. +math.rand(low: number, high: number) -> number + +-- Returns the normalized value of num relative to conf. +math.normalize(num: number, [optional] conf: number) -> number + +-- Returns the rounded value of num to the specified number of decimal places. +math.round(num: number, [optional] places: number) -> number + +-- Returns the sum of all received arguments. If a table was passed as an argument, the method will return the sum of all its elements. +math.sum(x: number, ... | t: table) -> number + +``` + +## Bit extensions +```lua + +-- Common arguments: +-- * expr: A string containing a bitwise expression, conforming to the Lua 5.3 bitwise operations format +-- * args: A list of names of the expression arguments. If nil, the list is automatically generated based on the detected identifiers. + +- Compiles the function to perform bitwise operations +-- * asFunction: If true, returns the function; otherwise, returns a string of function code +bit.compile(expr: string, args: table | nil, asFunction: boolean=true) -> function | string + +-- Compiles the function to perform bitwise operations and executes it in place +-- * ...: Values ​​to be passed to the compiled function. bit.execute(expr: string, args: table | nil, ...) -> number +``` +## Additional Global Functions + +This script also defines other global functions that are available for use. Their list is below. + +```lua +-- Returns true if the passed table is an array, that is, if each key is an integer greater than or equal to one +-- and if each key follows the previous one. +is_array(x: table) -> boolean + +-- Splits the path into two parts and returns them: the entry point and the file path. +parse_path(path: string) -> string, string + +-- Calls the function func iters times, passing it the arguments ..., and then prints to the console the time in microseconds that has elapsed +-- since the call to timeit. timeit(iters: int, func: function, ...) + +-- Causes the coroutine to pause until the number of seconds specified in timesec has elapsed. +-- The function can only be used inside a coroutine. +sleep(timesec: number) + +-- Waits for the passed coroutine to complete, returning the control flow. The function can only be used inside a coroutine. +-- Returns values ​​similar to those returned by pcall. +await(co: coroutine) -> result, error + +-- A constant storing the PID of the current engine instance. +os.pid -> number +``` diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index 922d02404..54a19f723 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -7,7 +7,7 @@ - [расширения для string](#расширения-для-string) - [расширения для math](#расширения-для-math) - [расширения для bit](#расширения-для-bit) -- [Дополнительные глобальные функции](#дополнительные-глобальные-функции) +- [дополнительные глобальные функции](#дополнительные-глобальные-функции) ## Расширения для table ```lua From 4a11dd28ec9b7a0344c21a6fc4c3ab80c429eb50 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 9 Jun 2026 23:51:26 +0300 Subject: [PATCH 18/92] fix segfault on world.get_chunk_data if no world open --- src/logic/scripting/lua/libs/libworld.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/logic/scripting/lua/libs/libworld.cpp b/src/logic/scripting/lua/libs/libworld.cpp index c2d6be43c..3b3b4af59 100644 --- a/src/logic/scripting/lua/libs/libworld.cpp +++ b/src/logic/scripting/lua/libs/libworld.cpp @@ -129,6 +129,9 @@ static int l_get_generator(lua::State* L) { } static int l_get_chunk_data(lua::State* L) { + if (level == nullptr) { + throw std::runtime_error("world is not open"); + } int x = static_cast(lua::tointeger(L, 1)); int z = static_cast(lua::tointeger(L, 2)); const auto& chunk = level->chunks->getChunk(x, z); @@ -175,7 +178,7 @@ static void integrate_chunk_client(Chunk& chunk) { static int l_set_chunk_data(lua::State* L) { if (level == nullptr) { - throw std::runtime_error("no open world"); + throw std::runtime_error("world is not open"); } int x = static_cast(lua::tointeger(L, 1)); @@ -201,7 +204,7 @@ static int l_set_chunk_data(lua::State* L) { static int l_save_chunk_data(lua::State* L) { if (level == nullptr) { - throw std::runtime_error("no open world"); + throw std::runtime_error("world is not open"); } int x = static_cast(lua::tointeger(L, 1)); From 4cc6a8387242d1edfc47b6dae4e74a1ff15c1006 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 10 Jun 2026 01:28:23 +0300 Subject: [PATCH 19/92] speed up monospace fonts --- src/coders/vector_fonts.cpp | 1 + src/coders/vector_fonts.hpp | 6 ++++++ src/graphics/core/Font.cpp | 11 ++++++++++- src/graphics/core/Font.hpp | 2 ++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/coders/vector_fonts.cpp b/src/coders/vector_fonts.cpp index ad7706e6b..a2b6b55d0 100644 --- a/src/coders/vector_fonts.cpp +++ b/src/coders/vector_fonts.cpp @@ -30,6 +30,7 @@ namespace { public: FTFontFile(FT_Face face, util::Buffer buffer) : face(std::move(face)), buffer(std::move(buffer)) { + monospace = this->face->face_flags & FT_FACE_FLAG_FIXED_WIDTH; } ~FTFontFile() { diff --git a/src/coders/vector_fonts.hpp b/src/coders/vector_fonts.hpp index a28f3c5bf..689eb17de 100644 --- a/src/coders/vector_fonts.hpp +++ b/src/coders/vector_fonts.hpp @@ -19,6 +19,12 @@ namespace vector_fonts { virtual std::unique_ptr renderPage( int pageid, std::vector& glyphs, int size ) = 0; + + bool isMonospace() const { + return monospace; + } + protected: + bool monospace = true; }; void initialize(); diff --git a/src/graphics/core/Font.cpp b/src/graphics/core/Font.cpp index 3f0b4b69b..7aaa67e80 100644 --- a/src/graphics/core/Font.cpp +++ b/src/graphics/core/Font.cpp @@ -26,6 +26,11 @@ Font::Font( pages(std::move(pages)), glyphs(std::move(glyphs)), fontFile(std::move(fontFile)) { + if (this->fontFile.has_value()) { + if (auto fontPtr = this->fontFile->lock()) { + monospace = fontPtr->isMonospace(); + } + } } Font::~Font() = default; @@ -34,6 +39,10 @@ int Font::getYOffset() const { return yoffset; } +bool Font::isMonospace() const { + return monospace; +} + int Font::getLineHeight() const { return lineHeight; } @@ -53,7 +62,7 @@ bool Font::isPrintableChar(uint codepoint) const { int FontMetrics::calcWidth(std::wstring_view text, size_t offset, size_t length) const { auto font = this->font.has_value() ? this->font->lock() : nullptr; - if (font == nullptr) { + if (font == nullptr || font->isMonospace()) { return std::min(text.length() - offset, length) * _glyphInterval; } int totalWidth = 0; diff --git a/src/graphics/core/Font.hpp b/src/graphics/core/Font.hpp index e23c45ba8..1f0e3ce8c 100644 --- a/src/graphics/core/Font.hpp +++ b/src/graphics/core/Font.hpp @@ -39,6 +39,7 @@ public: ); ~Font(); + bool isMonospace() const; int getLineHeight() const; int getYOffset() const; @@ -94,6 +95,7 @@ private: int lineHeight; int yoffset; int glyphInterval; + bool monospace = true; std::vector> pages; std::vector glyphs; std::optional> fontFile; From 07702f9e57fd0e2fd2ebcae00e3f5f8c6ae3c885 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 10 Jun 2026 23:08:48 +0300 Subject: [PATCH 20/92] refactor descriptors_manager --- .../internal/stream_providers/file.lua | 13 +- src/logic/scripting/descriptors_manager.cpp | 104 ---------------- src/logic/scripting/descriptors_manager.hpp | 39 ------ src/logic/scripting/io_descriptors.cpp | 111 ++++++++++++++++++ src/logic/scripting/io_descriptors.hpp | 23 ++++ src/logic/scripting/lua/libs/libfile.cpp | 42 +++---- 6 files changed, 166 insertions(+), 166 deletions(-) delete mode 100644 src/logic/scripting/descriptors_manager.cpp delete mode 100644 src/logic/scripting/descriptors_manager.hpp create mode 100644 src/logic/scripting/io_descriptors.cpp create mode 100644 src/logic/scripting/io_descriptors.hpp diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua index ec81a42c0..484e869e4 100644 --- a/res/modules/internal/stream_providers/file.lua +++ b/res/modules/internal/stream_providers/file.lua @@ -9,10 +9,19 @@ local lib = { close = file.__close_descriptor } +local __open_descriptor = file.__open_descriptor +file.__open_descriptor = nil +file.__read_descriptor = nil +file.__write_descriptor = nil +file.__seek_descriptor = nil +file.__flush_descriptor = nil +file.__has_descriptor = nil +file.__close_descriptor = nil + return function(path, mode) return io_stream.new( - file.__open_descriptor(path, mode), + __open_descriptor(path, mode), mode:find('b') ~= nil, lib ) -end \ No newline at end of file +end diff --git a/src/logic/scripting/descriptors_manager.cpp b/src/logic/scripting/descriptors_manager.cpp deleted file mode 100644 index f7458ccc5..000000000 --- a/src/logic/scripting/descriptors_manager.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "logic/scripting/descriptors_manager.hpp" - -#include "debug/Logger.hpp" - -static debug::Logger logger("descriptors-manager"); - -namespace scripting { - - std::vector> descriptors_manager::descriptors; - - std::istream* descriptors_manager::get_input(int descriptor) { - if (!is_readable(descriptor)) - return nullptr; - - return descriptors[descriptor]->in.get(); - } - - std::ostream* descriptors_manager::get_output(int descriptor) { - if (!is_writeable(descriptor)) - return nullptr; - - return descriptors[descriptor]->out.get(); - } - - void descriptors_manager::flush(int descriptor) { - if (is_writeable(descriptor)) { - descriptors[descriptor]->out->flush(); - } - } - - bool descriptors_manager::has_descriptor(int descriptor) { - return is_readable(descriptor) || is_writeable(descriptor); - } - - bool descriptors_manager::is_readable(int descriptor) { - return descriptor >= 0 && descriptor < static_cast(descriptors.size()) - && descriptors[descriptor].has_value() - && descriptors[descriptor]->in != nullptr; - } - - bool descriptors_manager::is_writeable(int descriptor) { - return descriptor >= 0 && descriptor < static_cast(descriptors.size()) - && descriptors[descriptor].has_value() - && descriptors[descriptor]->out != nullptr; - } - - void descriptors_manager::close(int descriptor) { - if (descriptor >= 0 && descriptor < static_cast(descriptors.size())) { - if (descriptors[descriptor].has_value()) { - auto& desc = descriptors[descriptor].value(); - - if (desc.out) - desc.out->flush(); - - desc.in.reset(); - desc.out.reset(); - } - - descriptors[descriptor].reset(); - - descriptors[descriptor] = std::nullopt; - } - } - - int descriptors_manager::open_descriptor(const io::path& path, bool write, bool read) { - std::unique_ptr in; - std::unique_ptr out; - - try { - if (read) - in = io::read(path); - - if (write) - out = io::write(path); - } catch (const std::exception& e) { - logger.error() << "failed to open descriptor for " << path.string() - << ": " << e.what(); - - return -1; - } - - for (int i = 0; i < static_cast(descriptors.size()); ++i) { - if (!descriptors[i].has_value()) { - descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) }; - return i; - } - } - - descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) }); - - return static_cast(descriptors.size() - 1); - } - - - void descriptors_manager::close_all_descriptors() { - for (int i = 0; i < static_cast(descriptors.size()); ++i) { - if (descriptors[i].has_value()) { - close(i); - } - } - - descriptors.clear(); - } -} \ No newline at end of file diff --git a/src/logic/scripting/descriptors_manager.hpp b/src/logic/scripting/descriptors_manager.hpp deleted file mode 100644 index ccd1b67a3..000000000 --- a/src/logic/scripting/descriptors_manager.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "io/io.hpp" - -namespace scripting { - - struct StreamDescriptor { - std::unique_ptr in; - std::unique_ptr out; - }; - - class descriptors_manager { - private: - static std::vector> descriptors; - - public: - static std::istream* get_input(int descriptor); - static std::ostream* get_output(int descriptor); - - static void flush(int descriptor); - - static bool has_descriptor(int descriptor); - - static bool is_readable(int descriptor); - static bool is_writeable(int descriptor); - - static void close(int descriptor); - static int open_descriptor(const io::path& path, bool write, bool read); - - static void close_all_descriptors(); - }; -} \ No newline at end of file diff --git a/src/logic/scripting/io_descriptors.cpp b/src/logic/scripting/io_descriptors.cpp new file mode 100644 index 000000000..e6fda04dc --- /dev/null +++ b/src/logic/scripting/io_descriptors.cpp @@ -0,0 +1,111 @@ +#include "logic/scripting/io_descriptors.hpp" + +#include "debug/Logger.hpp" +#include "io/io.hpp" + +#include +#include +#include +#include + +static debug::Logger logger("descriptors-manager"); + +using namespace scripting; + +namespace { + struct StreamDescriptor { + std::unique_ptr in; + std::unique_ptr out; + }; + std::vector> descriptors; +} + +std::istream* io_descriptors::get_input(int id) { + if (!is_readable(id)) { + return nullptr; + } + return ::descriptors[id]->in.get(); +} + +std::ostream* io_descriptors::get_output(int id) { + if (!is_writeable(id)) { + return nullptr; + } + return ::descriptors[id]->out.get(); +} + +void io_descriptors::flush(int id) { + if (is_writeable(id)) { + ::descriptors[id]->out->flush(); + } +} + +bool io_descriptors::has_descriptor(int id) { + return id >= 0 && id < static_cast(::descriptors.size()) && + ::descriptors[id].has_value() && + (::descriptors[id]->in != nullptr || + ::descriptors[id]->out != nullptr); +} + +bool io_descriptors::is_readable(int id) { + return id >= 0 && id < static_cast(::descriptors.size()) + && ::descriptors[id].has_value() + && ::descriptors[id]->in != nullptr; +} + +bool io_descriptors::is_writeable(int id) { + return id >= 0 && id < static_cast(::descriptors.size()) + && ::descriptors[id].has_value() + && ::descriptors[id]->out != nullptr; +} + +void io_descriptors::close(int id) { + if (!has_descriptor(id)) { + return; + } + auto& desc = ::descriptors[id].value(); + if (desc.out) { + desc.out->flush(); + } + desc.in.reset(); + desc.out.reset(); + ::descriptors[id] = std::nullopt; +} + +int io_descriptors::open_descriptor(const io::path& path, bool write, bool read) { + std::unique_ptr in; + std::unique_ptr out; + + try { + if (read) { + in = io::read(path); + } + if (write) { + out = io::write(path); + } + } catch (const std::exception& e) { + logger.error() << "failed to open descriptor for " << path.string() + << ": " << e.what(); + return -1; + } + + for (int i = 0; i < static_cast(descriptors.size()); ++i) { + if (!descriptors[i].has_value()) { + descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) }; + return i; + } + } + + ::descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) }); + return static_cast(::descriptors.size() - 1); +} + + +void io_descriptors::close_all_descriptors() { + for (int i = 0; i < static_cast(::descriptors.size()); ++i) { + if (::descriptors[i].has_value()) { + close(i); + } + } + ::descriptors.clear(); +} diff --git a/src/logic/scripting/io_descriptors.hpp b/src/logic/scripting/io_descriptors.hpp new file mode 100644 index 000000000..0400a920a --- /dev/null +++ b/src/logic/scripting/io_descriptors.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#include "io/fwd.hpp" + +namespace scripting::io_descriptors { + std::istream* get_input(int id); + std::ostream* get_output(int id); + + void flush(int id); + + bool has_descriptor(int id); + + bool is_readable(int id); + bool is_writeable(int id); + + void close(int id); + int open_descriptor(const io::path& path, bool write, bool read); + + void close_all_descriptors(); +} diff --git a/src/logic/scripting/lua/libs/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp index 26248379c..964c6d487 100644 --- a/src/logic/scripting/lua/libs/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -10,7 +10,7 @@ #include "util/stringutil.hpp" #include "api_lua.hpp" #include "../lua_engine.hpp" -#include "logic/scripting/descriptors_manager.hpp" +#include "logic/scripting/io_descriptors.hpp" namespace fs = std::filesystem; using namespace scripting; @@ -263,13 +263,13 @@ static int l_open_descriptor(lua::State* L) { std::vector buffer; if(wplusMode) { - int temp_descriptor = scripting::descriptors_manager::open_descriptor(path, false, true); + int temp_descriptor = io_descriptors::open_descriptor(path, false, true); if (temp_descriptor == -1) { throw std::runtime_error("failed to open descriptor for initial reading"); } - auto* in_stream = scripting::descriptors_manager::get_input(temp_descriptor); + auto* in_stream = io_descriptors::get_input(temp_descriptor); in_stream->seekg(0, std::ios::end); std::streamsize size = in_stream->tellg(); @@ -278,17 +278,17 @@ static int l_open_descriptor(lua::State* L) { buffer.resize(size); in_stream->read(buffer.data(), size); - scripting::descriptors_manager::close(temp_descriptor); + io_descriptors::close(temp_descriptor); } - int descriptor = scripting::descriptors_manager::open_descriptor(path, write, read); + int descriptor = io_descriptors::open_descriptor(path, write, read); if(descriptor == -1) { throw std::runtime_error("failed to open descriptor"); } if(wplusMode) { - auto* out_stream = scripting::descriptors_manager::get_output(descriptor); + auto* out_stream = io_descriptors::get_output(descriptor); out_stream->write(buffer.data(), buffer.size()); out_stream->flush(); } @@ -297,23 +297,23 @@ static int l_open_descriptor(lua::State* L) { } static int l_has_descriptor(lua::State* L) { - return lua::pushboolean(L, scripting::descriptors_manager::has_descriptor(lua::tointeger(L, 1))); + return lua::pushboolean(L, io_descriptors::has_descriptor(lua::tointeger(L, 1))); } static int l_read_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - if (!scripting::descriptors_manager::is_readable(descriptor)) { + if (!io_descriptors::is_readable(descriptor)) { throw std::runtime_error("descriptor is not readable"); } int maxlen = lua::tointeger(L, 2); - auto* stream = scripting::descriptors_manager::get_input(descriptor); + auto* stream = io_descriptors::get_input(descriptor); util::Buffer buffer(maxlen); @@ -327,17 +327,17 @@ static int l_read_descriptor(lua::State* L) { static int l_write_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - if (!scripting::descriptors_manager::is_writeable(descriptor)) { + if (!io_descriptors::is_writeable(descriptor)) { throw std::runtime_error("descriptor is not writeable"); } auto data = lua::bytearray_as_string(L, 2); - auto* stream = scripting::descriptors_manager::get_output(descriptor); + auto* stream = io_descriptors::get_output(descriptor); stream->write(data.data(), static_cast(data.size())); @@ -350,7 +350,7 @@ static int l_write_descriptor(lua::State* L) { static int l_seek_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } @@ -371,7 +371,7 @@ static int l_seek_descriptor(lua::State* L) { throw std::runtime_error("invalid seek mode"); } - auto* stream = scripting::descriptors_manager::get_output(descriptor); + auto* stream = io_descriptors::get_output(descriptor); stream->seekp(lua::tointeger(L, 3), dir); @@ -385,31 +385,31 @@ static int l_seek_descriptor(lua::State* L) { static int l_flush_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - if (!scripting::descriptors_manager::is_writeable(descriptor)) { + if (!io_descriptors::is_writeable(descriptor)) { throw std::runtime_error("descriptor is not writeable"); } - scripting::descriptors_manager::flush(descriptor); + io_descriptors::flush(descriptor); return 0; } static int l_close_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - scripting::descriptors_manager::close(descriptor); + io_descriptors::close(descriptor); return 0; } static int l_close_all_descriptors(lua::State* L) { - scripting::descriptors_manager::close_all_descriptors(); + io_descriptors::close_all_descriptors(); return 0; } From ce0f90d21e6e8ba2da491e6e9ad18f7d7b679eb9 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 11 Jun 2026 00:17:31 +0300 Subject: [PATCH 21/92] feat: memory_istream seek support --- src/io/memory_istream.hpp | 52 +++++++++++++++++++++++++++++++++++++- test/io/memory_istream.cpp | 11 ++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/io/memory_istream.hpp b/src/io/memory_istream.hpp index 7c0aded64..84a0c703f 100644 --- a/src/io/memory_istream.hpp +++ b/src/io/memory_istream.hpp @@ -15,8 +15,33 @@ public: memory_streambuf(const memory_streambuf&) = delete; memory_streambuf& operator=(const memory_streambuf&) = delete; + pos_type seekoff(off_type off, std::ios_base::seekdir way, + std::ios_base::openmode mode = std::ios_base::in) override { + if (!(mode & std::ios_base::in)) return pos_type(off_type(-1)); + char* base = eback(); + char* end = egptr(); + if (!base || !end) return pos_type(off_type(-1)); + + std::ptrdiff_t current = gptr() - base; + std::ptrdiff_t newpos; + if (way == std::ios_base::beg) newpos = off; + else if (way == std::ios_base::cur) newpos = current + off; + else if (way == std::ios_base::end) newpos = (end - base) + off; + else return pos_type(off_type(-1)); + + if (newpos < 0 || base + newpos > end) return pos_type(off_type(-1)); + setg(base, base + newpos, end); + return pos_type(off_type(newpos)); + } + + pos_type seekpos(pos_type sp, std::ios_base::openmode mode = std::ios_base::in) override { + return seekoff(off_type(sp), std::ios_base::beg, mode); + } protected: int_type underflow() override { + if (gptr() < egptr()) { + return traits_type::to_int_type(*gptr()); + } return traits_type::eof(); } @@ -36,7 +61,7 @@ private: class memory_view_streambuf : public std::streambuf { public: explicit memory_view_streambuf(const util::Buffer& buffer) - : buffer(std::move(buffer)) { + : buffer(buffer) { char* base = const_cast(this->buffer.data()); char* end = base + this->buffer.size(); setg(base, base, end); @@ -45,8 +70,33 @@ public: memory_view_streambuf(const memory_view_streambuf&) = delete; memory_view_streambuf& operator=(const memory_view_streambuf&) = delete; + pos_type seekoff(off_type off, std::ios_base::seekdir way, + std::ios_base::openmode mode = std::ios_base::in) override { + if (!(mode & std::ios_base::in)) return pos_type(off_type(-1)); + char* base = eback(); + char* end = egptr(); + if (!base || !end) return pos_type(off_type(-1)); + + std::ptrdiff_t current = gptr() - base; + std::ptrdiff_t newpos; + if (way == std::ios_base::beg) newpos = off; + else if (way == std::ios_base::cur) newpos = current + off; + else if (way == std::ios_base::end) newpos = (end - base) + off; + else return pos_type(off_type(-1)); + + if (newpos < 0 || base + newpos > end) return pos_type(off_type(-1)); + setg(base, base + newpos, end); + return pos_type(off_type(newpos)); + } + + pos_type seekpos(pos_type sp, std::ios_base::openmode mode = std::ios_base::in) override { + return seekoff(off_type(sp), std::ios_base::beg, mode); + } protected: int_type underflow() override { + if (gptr() < egptr()) { + return traits_type::to_int_type(*gptr()); + } return traits_type::eof(); } diff --git a/test/io/memory_istream.cpp b/test/io/memory_istream.cpp index faabd23cd..674d250fd 100644 --- a/test/io/memory_istream.cpp +++ b/test/io/memory_istream.cpp @@ -16,4 +16,15 @@ TEST(io, memory_istream) { ASSERT_EQ(text, std::string(data)); stream.read(text.data(), 1); ASSERT_TRUE(stream.eof()); + + // seek + stream.clear(); + stream.seekg(0); + ASSERT_TRUE(stream.good()); + stream.read(text.data(), n); + ASSERT_EQ(text, std::string(data)); + stream.seekg(-6, std::ios_base::cur); + ASSERT_TRUE(stream.good()); + stream.read(text.data(), 6); + ASSERT_EQ(text.substr(0, 6), "world!"); } From 0d2a0b012deda064e164bb581f84fef84523714d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 11 Jun 2026 00:27:15 +0300 Subject: [PATCH 22/92] add io_stream:tell & fix io_stream:seek --- .../internal/stream_providers/file.lua | 2 + res/modules/io_stream.lua | 6 +- src/logic/scripting/io_descriptors.cpp | 34 +++++++-- src/logic/scripting/io_descriptors.hpp | 3 + src/logic/scripting/lua/libs/libfile.cpp | 74 ++++++++++--------- 5 files changed, 77 insertions(+), 42 deletions(-) diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua index 484e869e4..840ff93f2 100644 --- a/res/modules/internal/stream_providers/file.lua +++ b/res/modules/internal/stream_providers/file.lua @@ -4,6 +4,7 @@ local lib = { read = file.__read_descriptor, write = file.__write_descriptor, seek = file.__seek_descriptor, + tell = file.__tell_descriptor, flush = file.__flush_descriptor, is_alive = file.__has_descriptor, close = file.__close_descriptor @@ -14,6 +15,7 @@ file.__open_descriptor = nil file.__read_descriptor = nil file.__write_descriptor = nil file.__seek_descriptor = nil +file.__tell_descriptor = nil file.__flush_descriptor = nil file.__has_descriptor = nil file.__close_descriptor = nil diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index b608a22a9..8a4507a3b 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -373,6 +373,10 @@ function io_stream:seek(mode, offset) self.ioLib.seek(self.descriptor, mode, offset) end +function io_stream:tell() + return self.ioLib.tell(self.descriptor) +end + function io_stream:is_alive() return self.ioLib.is_alive(self.descriptor) end @@ -399,4 +403,4 @@ function io_stream:flush() if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then self.ioLib.flush(self.descriptor) end end -return io_stream \ No newline at end of file +return io_stream diff --git a/src/logic/scripting/io_descriptors.cpp b/src/logic/scripting/io_descriptors.cpp index e6fda04dc..132eea0eb 100644 --- a/src/logic/scripting/io_descriptors.cpp +++ b/src/logic/scripting/io_descriptors.cpp @@ -14,6 +14,7 @@ using namespace scripting; namespace { struct StreamDescriptor { + // TODO: std::iostream? std::unique_ptr in; std::unique_ptr out; }; @@ -34,6 +35,31 @@ std::ostream* io_descriptors::get_output(int id) { return ::descriptors[id]->out.get(); } +static StreamDescriptor& require_descriptor(int id) { + if (!io_descriptors::has_descriptor(id)) { + throw std::runtime_error( + "io-descriptor with id " + std::to_string(id) + " does not exists" + ); + } + return *::descriptors[id]; +} + +std::istream& io_descriptors::require_input(int id) { + const auto& descriptor = require_descriptor(id); + if (descriptor.in) { + return *descriptor.in; + } + throw std::runtime_error("io-descriptor is not readable"); +} + +std::ostream& io_descriptors::require_output(int id) { + const auto& descriptor = require_descriptor(id); + if (descriptor.out) { + return *descriptor.out; + } + throw std::runtime_error("io-descriptor is not writeable"); +} + void io_descriptors::flush(int id) { if (is_writeable(id)) { ::descriptors[id]->out->flush(); @@ -42,15 +68,11 @@ void io_descriptors::flush(int id) { bool io_descriptors::has_descriptor(int id) { return id >= 0 && id < static_cast(::descriptors.size()) && - ::descriptors[id].has_value() && - (::descriptors[id]->in != nullptr || - ::descriptors[id]->out != nullptr); + ::descriptors[id].has_value(); } bool io_descriptors::is_readable(int id) { - return id >= 0 && id < static_cast(::descriptors.size()) - && ::descriptors[id].has_value() - && ::descriptors[id]->in != nullptr; + return has_descriptor(id) && ::descriptors[id]->in != nullptr; } bool io_descriptors::is_writeable(int id) { diff --git a/src/logic/scripting/io_descriptors.hpp b/src/logic/scripting/io_descriptors.hpp index 0400a920a..7e6207791 100644 --- a/src/logic/scripting/io_descriptors.hpp +++ b/src/logic/scripting/io_descriptors.hpp @@ -9,6 +9,9 @@ namespace scripting::io_descriptors { std::istream* get_input(int id); std::ostream* get_output(int id); + std::istream& require_input(int id); + std::ostream& require_output(int id); + void flush(int id); bool has_descriptor(int id); diff --git a/src/logic/scripting/lua/libs/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp index 964c6d487..1a09e9962 100644 --- a/src/logic/scripting/lua/libs/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -302,46 +302,26 @@ static int l_has_descriptor(lua::State* L) { static int l_read_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - - if (!io_descriptors::has_descriptor(descriptor)) { - throw std::runtime_error("unknown descriptor"); - } - - if (!io_descriptors::is_readable(descriptor)) { - throw std::runtime_error("descriptor is not readable"); - } - int maxlen = lua::tointeger(L, 2); - auto* stream = io_descriptors::get_input(descriptor); + auto& stream = io_descriptors::require_input(descriptor); + if (stream.eof()) { + stream.clear(); + } util::Buffer buffer(maxlen); - - stream->read(buffer.data(), maxlen); - - std::streamsize read_len = stream->gcount(); - + stream.read(buffer.data(), maxlen); + std::streamsize read_len = stream.gcount(); return lua::create_bytearray(L, buffer.data(), read_len); } static int l_write_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - - if (!io_descriptors::has_descriptor(descriptor)) { - throw std::runtime_error("unknown descriptor"); - } - - if (!io_descriptors::is_writeable(descriptor)) { - throw std::runtime_error("descriptor is not writeable"); - } - auto data = lua::bytearray_as_string(L, 2); - auto* stream = io_descriptors::get_output(descriptor); - - stream->write(data.data(), static_cast(data.size())); - - if (!stream->good()) { + auto& stream = io_descriptors::require_output(descriptor); + stream.write(data.data(), static_cast(data.size())); + if (!stream.good()) { throw std::runtime_error("failed to write to stream"); } return 0; @@ -354,8 +334,9 @@ static int l_seek_descriptor(lua::State* L) { throw std::runtime_error("unknown descriptor"); } - std::string mode = lua::require_string(L, 2); + auto mode = lua::require_string(L, 2); std::ios_base::seekdir dir; + auto position = lua::tointeger(L, 3); switch (mode[0]) { case 'b': @@ -371,15 +352,37 @@ static int l_seek_descriptor(lua::State* L) { throw std::runtime_error("invalid seek mode"); } - auto* stream = io_descriptors::get_output(descriptor); + if (io_descriptors::is_writeable(descriptor)) { + auto& stream = io_descriptors::require_output(descriptor); + stream.seekp(position, dir); + if (!stream.good()) { + throw std::runtime_error("failed to seek stream"); + } + } + if (io_descriptors::is_readable(descriptor)) { + auto& stream = io_descriptors::require_input(descriptor); + stream.seekg(position, dir); + if (!stream.good()) { + throw std::runtime_error("failed to seek stream"); + } + } + return 0; +} - stream->seekp(lua::tointeger(L, 3), dir); +static int l_tell_descriptor(lua::State* L) { + int descriptor = lua::tointeger(L, 1); - if (!stream->good()) { - throw std::runtime_error("failed to seek stream"); + if (!io_descriptors::has_descriptor(descriptor)) { + throw std::runtime_error("unknown descriptor"); } - return 0; + if (io_descriptors::is_writeable(descriptor)) { + auto& stream = io_descriptors::require_output(descriptor); + return lua::pushinteger(L, stream.tellp()); + } else { + auto& stream = io_descriptors::require_input(descriptor); + return lua::pushinteger(L, stream.tellg()); + } } static int l_flush_descriptor(lua::State* L) { @@ -442,6 +445,7 @@ const luaL_Reg filelib[] = { {"__read_descriptor", lua::wrap}, {"__write_descriptor", lua::wrap}, {"__seek_descriptor", lua::wrap}, + {"__tell_descriptor", lua::wrap}, {"__flush_descriptor", lua::wrap}, {"__close_descriptor", lua::wrap}, {"__close_all_descriptors", lua::wrap}, From 34af3523d957b664f0f8800a5cc2fd34df389cba Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 11 Jun 2026 00:29:01 +0300 Subject: [PATCH 23/92] add io_stream lua test --- dev/tests/io_stream.lua | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 dev/tests/io_stream.lua diff --git a/dev/tests/io_stream.lua b/dev/tests/io_stream.lua new file mode 100644 index 000000000..d08c68b0c --- /dev/null +++ b/dev/tests/io_stream.lua @@ -0,0 +1,34 @@ +local device = file.create_memory_device() +file.write(device..":test.txt", "Hello\nWorld") +file.write_bytes(device..":test.bin", Bytearray({20, 30, 100, 200, 255})) + +local stream = file.open(device..":test.txt", 'r') + +local line1 = stream:read_line() +local line2 = stream:read_line() + +assert(line1 == "Hello") +assert(line2 == "World") + +local stream2 = file.open(device..":test.bin", 'rb') +local data = stream2:read(5) +assert(data[1] == 20) +assert(data[2] == 30) +assert(data[3] == 100) +assert(data[4] == 200) +assert(data[5] == 255) + +local stream3 = file.open(device..":test.txt", 'r') +assert(stream3:is_alive()) +stream3:close() +assert(not stream3:is_alive()) + +local stream4 = file.open(device..":test.txt", 'r') +stream4:seek('b', 6) +local line = stream4:read_line() +assert(line == "World") + +local stream5 = file.open(device..":test.txt", 'r') +stream5:seek('e', 0) +local pos = stream5:tell() +assert(pos == 11) From 5424f51f7035dfc4da2f77a9c19821da2f9bae5b Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 19:04:58 +0900 Subject: [PATCH 24/92] added io_stream.wrap_bytearray --- doc/ru/scripting/io_stream.md | 10 ++ .../internal/stream_providers/bytearray.lua | 126 ++++++++++++++++++ res/scripts/stdlib.lua | 2 + 3 files changed, 138 insertions(+) create mode 100644 res/modules/internal/stream_providers/bytearray.lua diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index cc3403eda..2362682ad 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -188,6 +188,16 @@ io_stream:close() -- Вызывает ioLib.flush() в all flush-режиме io_stream:flush() +-- Создаёт новый поток из Bytearray. +-- Может использоваться одновременно как для чтения, так и для записи. +-- Результат записи будет записан в тот же Bytearray, что был передан в функцию. +io_stream.wrap_bytearray( + buffer: Bytearray, + + -- по-умолчанию равен true, поскольку функция из вводных аргументов + -- будет использоваться преимущественно для работы с двоичными данными. + [опционально] binaryMode: boolean = true + ) -> io_stream -- Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua) io_stream.new( diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua new file mode 100644 index 000000000..6909bdf5c --- /dev/null +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -0,0 +1,126 @@ +local io_stream = require "core:io_stream" + +local lib = { } + +local buffers = { } +local positions = { } + +local nextDescriptor = 0 + +local function openDescriptor(buffer) + nextDescriptor = nextDescriptor + 1 + + buffers[nextDescriptor] = buffer + positions[nextDescriptor] = 1 + + return nextDescriptor +end + +local function requireDescriptor(descriptor) + if not buffers[descriptor] then + error("unknown descriptor") + end +end + +function lib.read(descriptor, length) + requireDescriptor(descriptor) + + local buf = buffers[descriptor] + local bufLength = #buf + local pos = positions[descriptor] + + local toRead = math.min(bufLength - pos + 1, length) + + if toRead <= 0 then + return Bytearray() + end + + local segment = buf:slice(pos, toRead) + + positions[descriptor] = pos + toRead + + return segment +end + +function lib.write(descriptor, data) + requireDescriptor(descriptor) + + local buf = buffers[descriptor] + local pos = positions[descriptor] + + local bufLength = #buf + local dataLength = #data + + local endPos = pos + dataLength - 1 + + -- size ensuring + if endPos > bufLength then + for i = bufLength + 1, endPos do + buf[i] = 0 + end + end + + for i = 1, dataLength do + buf[i + pos - 1] = data[i] + end + + positions[descriptor] = pos + dataLength +end + +function lib.seek(descriptor, mode, offset) + requireDescriptor(descriptor) + + local buf = buffers[descriptor] + local bufLength = #buf + + local base + + if mode == 'b' then + base = 1 + elseif mode == 'c' then + base = positions[descriptor] + elseif mode == 'e' then + base = bufLength + 1 + else error('invalid seek mode') end + + local newPos = base + offset + + if newPos < 1 then + error('failed to seek stream') + end + + positions[descriptor] = newPos +end + +function lib.tell(descriptor) + requireDescriptor(descriptor) + + return positions[descriptor] +end + +function lib.flush(descriptor) + requireDescriptor(descriptor) +end + +function lib.is_alive(descriptor) + return buffers[descriptor] ~= nil +end + +function lib.close(descriptor) + requireDescriptor(descriptor) + + buffers[descriptor] = nil + positions[descriptor] = nil +end + +return function(buffer, binaryMode) + if binaryMode == nil then + binaryMode = true + end + + return io_stream.new( + openDescriptor(buffer), + binaryMode, + lib + ) +end \ No newline at end of file diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 093501d75..17da8ba85 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -332,6 +332,8 @@ else os.pid = ffi.C.getpid() end +require("core:io_stream").wrap_bytearray = require "core:internal/stream_providers/bytearray" + math.randomseed(time.uptime() * 1536227939) rules = require "core:internal/rules" From fdec2d722d2b81e7bdbdfec2209a9df28d10dde0 Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 19:20:37 +0900 Subject: [PATCH 25/92] fix of io_stream:read_fully for binary and yield mode --- res/modules/io_stream.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index b608a22a9..5350abb9f 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -145,8 +145,12 @@ function io_stream:__update_read_buffer() end end -function io_stream:__read(length) +function io_stream:__read(length, fromReadFully) if self.mode == YIELD_MODE then + if fromReadFully then + return self.ioLib.read(self.descriptor, length) + end + local buffer = Bytearray() while #buffer < length do @@ -203,7 +207,9 @@ function io_stream:read_fully(useTable) if self.binaryMode then local result = useTable and Bytearray() or { } - readFully(result, function() return self:__read(self.maxBufferSize) end) + readFully(result, function() return self:__read(self.maxBufferSize, true) end) + + return result else if useTable then local lines = { } From bf98091a9ce86e0cfbcbf5bffaf853e77fcf936f Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 20:19:24 +0900 Subject: [PATCH 26/92] added low-level ioLib.available for io_stream's --- doc/ru/scripting/io_stream.md | 18 ++++++------- .../internal/stream_providers/bytearray.lua | 9 +++++++ .../internal/stream_providers/file.lua | 1 + .../stream_providers/named_pipe_unix.lua | 14 ++++++++++ .../stream_providers/named_pipe_windows.lua | 16 +++++++++++ res/modules/io_stream.lua | 7 +++-- src/logic/scripting/descriptors_manager.cpp | 27 +++++++++++++++++++ src/logic/scripting/descriptors_manager.hpp | 2 ++ src/logic/scripting/lua/libs/libfile.cpp | 15 +++++++++++ 9 files changed, 97 insertions(+), 12 deletions(-) diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index 2362682ad..0c853131f 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -154,16 +154,6 @@ io_stream:seek( ## Методы Buffered-режима ```lua ---[[ -Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false - -Если не определён, то возвращает количество байт, которое можно прочитать - ---]] -io_stream:available( - [опционально] length: number -) -> number | boolean - -- Возвращает максимальный размер буферов io_stream:get_max_buffer_size() -> number @@ -174,6 +164,14 @@ io_stream:set_max_buffer_size(max_size: number) ## Методы контроля состояния потока ```lua +--[[ +Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false. + +Если не определён, то возвращает количество байт, которое можно прочитать. +--]] +io_stream:available( + [опционально] length: number +) -> number | boolean -- Возвращает true, если поток открыт на данный момент io_stream:is_alive() -> bool diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua index 6909bdf5c..35a15b654 100644 --- a/res/modules/internal/stream_providers/bytearray.lua +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -102,6 +102,15 @@ function lib.flush(descriptor) requireDescriptor(descriptor) end +function lib.available(descriptor) + requireDescriptor(descriptor) + + local buf = buffers[descriptor] + local pos = positions[descriptor] + + return math.max(#buf - pos + 1, 0) +end + function lib.is_alive(descriptor) return buffers[descriptor] ~= nil end diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua index ec81a42c0..f73961e22 100644 --- a/res/modules/internal/stream_providers/file.lua +++ b/res/modules/internal/stream_providers/file.lua @@ -5,6 +5,7 @@ local lib = { write = file.__write_descriptor, seek = file.__seek_descriptor, flush = file.__flush_descriptor, + available = file.__available_descriptor, is_alive = file.__has_descriptor, close = file.__close_descriptor } diff --git a/res/modules/internal/stream_providers/named_pipe_unix.lua b/res/modules/internal/stream_providers/named_pipe_unix.lua index 8b924f47a..237910ecb 100644 --- a/res/modules/internal/stream_providers/named_pipe_unix.lua +++ b/res/modules/internal/stream_providers/named_pipe_unix.lua @@ -9,6 +9,7 @@ int close(int fd); ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); int fcntl(int fd, int cmd, ...); +int ioctl(int fd, unsigned long request, ...); const char *strerror(int errnum); ]] @@ -20,6 +21,7 @@ local O_WRONLY = 0x1 local O_RDWR = 0x2 local O_NONBLOCK = 0x800 local F_GETFL = 3 +local FIONREAD = 0x541B local function getError() local err = FFI.errno() @@ -66,6 +68,18 @@ function lib.flush(fd) -- no flush on unix end +function lib.available(fd) + if fd == nil or fd < 0 then return 0 end + + local bytes_ready = FFI.new("int[1]", 0) + + if C.ioctl(fd, FIONREAD, bytes_ready) == -1 then + return 0 + end + + return tonumber(bytes_ready[0]) +end + function lib.is_alive(fd) if fd == nil or fd < 0 then return false end diff --git a/res/modules/internal/stream_providers/named_pipe_windows.lua b/res/modules/internal/stream_providers/named_pipe_windows.lua index 3d4b31af1..3a18f4928 100644 --- a/res/modules/internal/stream_providers/named_pipe_windows.lua +++ b/res/modules/internal/stream_providers/named_pipe_windows.lua @@ -105,6 +105,22 @@ function lib.flush(handle) C.FlushFileBuffers(handle) end +function lib.available(handle) + if handle == nil or handle == INVALID_HANDLE_VALUE then + return 0 + end + + local bytes_available = FFI.new("DWORD[1]") + + local success = C.PeekNamedPipe(handle, nil, 0, nil, bytes_available, nil) + + if success == 0 then + return 0 + end + + return tonumber(bytes_available[0]) +end + function lib.is_alive(handle) if handle == nil or handle == INVALID_HANDLE_VALUE then return false diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 5350abb9f..3dff50ce4 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -42,14 +42,17 @@ local function readFully(result, readFunc) end --[[ - descriptor - descriptor of stream for provided I/O library binaryMode - if enabled, most methods will expect bytes instead of strings ioLib - I/O library. Should include the following functions: read(descriptor: int, length: int) -> Bytearray May return bytearray with a smaller size if bytes have not arrived yet or have run out write(descriptor: int, data: Bytearray) + seek(descriptor: int, mode: string, offset: int) + Mode may be 'b' (relative begin), 'c' (relative current), 'e' (relative end + 1) flush(descriptor: int) + available(descriptor: int) -> int + May return 0 if environment is not support available method is_alive(descriptor: int) -> bool close(descriptor: int) --]] @@ -130,7 +133,7 @@ function io_stream:available(length) else return #self.readBuffer >= length end - end + else return self.ioLib.available(self.descriptor) end end function io_stream:__update_read_buffer() diff --git a/src/logic/scripting/descriptors_manager.cpp b/src/logic/scripting/descriptors_manager.cpp index f7458ccc5..0ff2f018b 100644 --- a/src/logic/scripting/descriptors_manager.cpp +++ b/src/logic/scripting/descriptors_manager.cpp @@ -28,6 +28,33 @@ namespace scripting { } } + int descriptors_manager::available(int descriptor) { + if (!is_readable(descriptor)) + return 0; + + auto* stream = descriptors[descriptor]->in.get(); + + auto current_pos = stream->tellg(); + + if (current_pos == -1) { + return 0; + } + + stream->seekg(0, std::ios::end); + + auto end_pos = stream->tellg(); + + stream->seekg(current_pos, std::ios::beg); + + auto remaining = end_pos - current_pos; + + if (remaining > std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + + return static_cast(remaining); + } + bool descriptors_manager::has_descriptor(int descriptor) { return is_readable(descriptor) || is_writeable(descriptor); } diff --git a/src/logic/scripting/descriptors_manager.hpp b/src/logic/scripting/descriptors_manager.hpp index ccd1b67a3..64373c7a4 100644 --- a/src/logic/scripting/descriptors_manager.hpp +++ b/src/logic/scripting/descriptors_manager.hpp @@ -26,6 +26,8 @@ namespace scripting { static void flush(int descriptor); + static int available(int descriptor); + static bool has_descriptor(int descriptor); static bool is_readable(int descriptor); diff --git a/src/logic/scripting/lua/libs/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp index 26248379c..4fd006582 100644 --- a/src/logic/scripting/lua/libs/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -397,6 +397,20 @@ static int l_flush_descriptor(lua::State* L) { return 0; } +static int l_available_descriptor(lua::State* L) { + int descriptor = lua::tointeger(L, 1); + + if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + throw std::runtime_error("unknown descriptor"); + } + + if (!scripting::descriptors_manager::is_readable(descriptor)) { + throw std::runtime_error("descriptor is not readable"); + } + + return lua::pushinteger(L, scripting::descriptors_manager::available(descriptor)); +} + static int l_close_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); @@ -443,6 +457,7 @@ const luaL_Reg filelib[] = { {"__write_descriptor", lua::wrap}, {"__seek_descriptor", lua::wrap}, {"__flush_descriptor", lua::wrap}, + {"__available_descriptor", lua::wrap}, {"__close_descriptor", lua::wrap}, {"__close_all_descriptors", lua::wrap}, {nullptr, nullptr} From dfd67011a40382e0c10edff648e9d82b45eb169a Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 20:51:49 +0900 Subject: [PATCH 27/92] added socket:as_stream() --- doc/ru/scripting/builtins/libnetwork.md | 5 ++ .../internal/stream_providers/socket.lua | 76 +++++++++++++++++++ res/scripts/classes.lua | 1 + res/scripts/stdlib.lua | 2 + 4 files changed, 84 insertions(+) create mode 100644 res/modules/internal/stream_providers/socket.lua diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index 31f59fb25..5bc5eaafd 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -92,6 +92,11 @@ socket:recv_async( [опционально] usetable: boolean=false ) -> nil|table|Bytearray +-- Оборачивает сокет в io_stream (см. ../io_stream.md) +socket:as_stream( + [опционально] binaryMode: boolean=true +) -> io_stream + -- Закрывает соединение socket:close() diff --git a/res/modules/internal/stream_providers/socket.lua b/res/modules/internal/stream_providers/socket.lua new file mode 100644 index 000000000..88684b0ff --- /dev/null +++ b/res/modules/internal/stream_providers/socket.lua @@ -0,0 +1,76 @@ +local io_stream = require "core:io_stream" + +local lib = { } + +local sockets = { } + +local nextDescriptor = 0 + +local function openDescriptor(socket) + nextDescriptor = nextDescriptor + 1 + + sockets[nextDescriptor] = socket + + return nextDescriptor +end + +local function requireDescriptor(descriptor) + if not sockets[descriptor] then + error("unknown descriptor") + end +end + +function lib.read(descriptor, length) + requireDescriptor(descriptor) + + return sockets[descriptor]:recv(length) +end + +function lib.write(descriptor, data) + requireDescriptor(descriptor) + + sockets[descriptor]:send(data) +end + +function lib.seek(descriptor, mode, offset) + error("cannot seek socket") +end + +function lib.tell(descriptor) + error("cannot tell socket") +end + +function lib.flush(descriptor) + requireDescriptor(descriptor) +end + +function lib.available(descriptor) + requireDescriptor(descriptor) + + return sockets[descriptor]:available() +end + +function lib.is_alive(descriptor) + local socket = sockets[descriptor] + + return socket ~= nil and socket:is_alive() +end + +function lib.close(descriptor) + requireDescriptor(descriptor) + + sockets[descriptor]:close() + sockets[descriptor] = nil +end + +return function(socket, binaryMode) + if binaryMode == nil then + binaryMode = true + end + + return io_stream.new( + openDescriptor(socket), + binaryMode, + lib + ) +end \ No newline at end of file diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 664710a30..a057d52e6 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -49,6 +49,7 @@ local Socket = {__index={ end return self:recv(length, usetable) end, + as_stream=network.__as_stream, close=function(self) return network.__close(self.id) end, available=function(self) return network.__available(self.id) or 0 end, is_alive=function(self) return network.__is_alive(self.id) end, diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 17da8ba85..6d25ed86c 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -334,6 +334,8 @@ end require("core:io_stream").wrap_bytearray = require "core:internal/stream_providers/bytearray" +network.__as_stream = require "core:internal/stream_providers/socket" + math.randomseed(time.uptime() * 1536227939) rules = require "core:internal/rules" From f8317e05c413331aab81f1d271477ec9c00b9bc7 Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 21:01:53 +0900 Subject: [PATCH 28/92] available in io_stream docs moved to I/O section --- doc/ru/scripting/io_stream.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index 0c853131f..eab023f67 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -139,7 +139,16 @@ io_stream:read_fully( ) -> Bytearray | table | table | string --[[ -Устанавливает позицию в потоке +Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false. + +Если не определён, то возвращает количество байт, которое можно прочитать. +--]] +io_stream:available( + [опционально] length: number +) -> number | boolean + +--[[ +Устанавливает позицию в потоке (всегда в байтах) Режимы: b - Задаёт позицию относительно начало файла c - Задаёт позицию относительно текущей позиции @@ -164,15 +173,6 @@ io_stream:set_max_buffer_size(max_size: number) ## Методы контроля состояния потока ```lua ---[[ -Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false. - -Если не определён, то возвращает количество байт, которое можно прочитать. ---]] -io_stream:available( - [опционально] length: number -) -> number | boolean - -- Возвращает true, если поток открыт на данный момент io_stream:is_alive() -> bool From cd040cfeb47f91b29c6d138f745b442483f4cf44 Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 21:13:30 +0900 Subject: [PATCH 29/92] fix of io_stream:available for non-buffered streams when "length" arg defined --- res/modules/io_stream.lua | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 3dff50ce4..d2f132802 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -125,15 +125,21 @@ function io_stream:set_max_buffer_size(maxBufferSize) end function io_stream:available(length) + local available + if self.mode == BUFFERED_MODE then self:__update_read_buffer() - if not length then - return #self.readBuffer - else - return #self.readBuffer >= length - end - else return self.ioLib.available(self.descriptor) end + available = #self.readBuffer + else + available = self.ioLib.available(self.descriptor) + end + + if not length then + return available + else + return available >= length + end end function io_stream:__update_read_buffer() From dd9ba26f7b019a9558ea8b62949444bf6ec7bc23 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 00:29:08 +0900 Subject: [PATCH 30/92] conflicts fix --- .../internal/stream_providers/file.lua | 1 + src/logic/scripting/descriptors_manager.cpp | 131 ------------------ src/logic/scripting/descriptors_manager.hpp | 41 ------ src/logic/scripting/io_descriptors.cpp | 27 ++++ src/logic/scripting/io_descriptors.hpp | 1 + src/logic/scripting/lua/libs/libfile.cpp | 6 +- 6 files changed, 32 insertions(+), 175 deletions(-) delete mode 100644 src/logic/scripting/descriptors_manager.cpp delete mode 100644 src/logic/scripting/descriptors_manager.hpp diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua index 178b153d3..5e186ae16 100644 --- a/res/modules/internal/stream_providers/file.lua +++ b/res/modules/internal/stream_providers/file.lua @@ -18,6 +18,7 @@ file.__write_descriptor = nil file.__seek_descriptor = nil file.__tell_descriptor = nil file.__flush_descriptor = nil +file.__available_descriptor = nil file.__has_descriptor = nil file.__close_descriptor = nil diff --git a/src/logic/scripting/descriptors_manager.cpp b/src/logic/scripting/descriptors_manager.cpp deleted file mode 100644 index 0ff2f018b..000000000 --- a/src/logic/scripting/descriptors_manager.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "logic/scripting/descriptors_manager.hpp" - -#include "debug/Logger.hpp" - -static debug::Logger logger("descriptors-manager"); - -namespace scripting { - - std::vector> descriptors_manager::descriptors; - - std::istream* descriptors_manager::get_input(int descriptor) { - if (!is_readable(descriptor)) - return nullptr; - - return descriptors[descriptor]->in.get(); - } - - std::ostream* descriptors_manager::get_output(int descriptor) { - if (!is_writeable(descriptor)) - return nullptr; - - return descriptors[descriptor]->out.get(); - } - - void descriptors_manager::flush(int descriptor) { - if (is_writeable(descriptor)) { - descriptors[descriptor]->out->flush(); - } - } - - int descriptors_manager::available(int descriptor) { - if (!is_readable(descriptor)) - return 0; - - auto* stream = descriptors[descriptor]->in.get(); - - auto current_pos = stream->tellg(); - - if (current_pos == -1) { - return 0; - } - - stream->seekg(0, std::ios::end); - - auto end_pos = stream->tellg(); - - stream->seekg(current_pos, std::ios::beg); - - auto remaining = end_pos - current_pos; - - if (remaining > std::numeric_limits::max()) { - return std::numeric_limits::max(); - } - - return static_cast(remaining); - } - - bool descriptors_manager::has_descriptor(int descriptor) { - return is_readable(descriptor) || is_writeable(descriptor); - } - - bool descriptors_manager::is_readable(int descriptor) { - return descriptor >= 0 && descriptor < static_cast(descriptors.size()) - && descriptors[descriptor].has_value() - && descriptors[descriptor]->in != nullptr; - } - - bool descriptors_manager::is_writeable(int descriptor) { - return descriptor >= 0 && descriptor < static_cast(descriptors.size()) - && descriptors[descriptor].has_value() - && descriptors[descriptor]->out != nullptr; - } - - void descriptors_manager::close(int descriptor) { - if (descriptor >= 0 && descriptor < static_cast(descriptors.size())) { - if (descriptors[descriptor].has_value()) { - auto& desc = descriptors[descriptor].value(); - - if (desc.out) - desc.out->flush(); - - desc.in.reset(); - desc.out.reset(); - } - - descriptors[descriptor].reset(); - - descriptors[descriptor] = std::nullopt; - } - } - - int descriptors_manager::open_descriptor(const io::path& path, bool write, bool read) { - std::unique_ptr in; - std::unique_ptr out; - - try { - if (read) - in = io::read(path); - - if (write) - out = io::write(path); - } catch (const std::exception& e) { - logger.error() << "failed to open descriptor for " << path.string() - << ": " << e.what(); - - return -1; - } - - for (int i = 0; i < static_cast(descriptors.size()); ++i) { - if (!descriptors[i].has_value()) { - descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) }; - return i; - } - } - - descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) }); - - return static_cast(descriptors.size() - 1); - } - - - void descriptors_manager::close_all_descriptors() { - for (int i = 0; i < static_cast(descriptors.size()); ++i) { - if (descriptors[i].has_value()) { - close(i); - } - } - - descriptors.clear(); - } -} \ No newline at end of file diff --git a/src/logic/scripting/descriptors_manager.hpp b/src/logic/scripting/descriptors_manager.hpp deleted file mode 100644 index 64373c7a4..000000000 --- a/src/logic/scripting/descriptors_manager.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "io/io.hpp" - -namespace scripting { - - struct StreamDescriptor { - std::unique_ptr in; - std::unique_ptr out; - }; - - class descriptors_manager { - private: - static std::vector> descriptors; - - public: - static std::istream* get_input(int descriptor); - static std::ostream* get_output(int descriptor); - - static void flush(int descriptor); - - static int available(int descriptor); - - static bool has_descriptor(int descriptor); - - static bool is_readable(int descriptor); - static bool is_writeable(int descriptor); - - static void close(int descriptor); - static int open_descriptor(const io::path& path, bool write, bool read); - - static void close_all_descriptors(); - }; -} \ No newline at end of file diff --git a/src/logic/scripting/io_descriptors.cpp b/src/logic/scripting/io_descriptors.cpp index 132eea0eb..ead8566d1 100644 --- a/src/logic/scripting/io_descriptors.cpp +++ b/src/logic/scripting/io_descriptors.cpp @@ -66,6 +66,33 @@ void io_descriptors::flush(int id) { } } +int io_descriptors::available(int id) { + if (!is_readable(id)) + return 0; + + auto* stream = descriptors[id]->in.get(); + + auto current_pos = stream->tellg(); + + if (current_pos == -1) { + return 0; + } + + stream->seekg(0, std::ios::end); + + auto end_pos = stream->tellg(); + + stream->seekg(current_pos, std::ios::beg); + + auto remaining = end_pos - current_pos; + + if (remaining > std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + + return static_cast(remaining); +} + bool io_descriptors::has_descriptor(int id) { return id >= 0 && id < static_cast(::descriptors.size()) && ::descriptors[id].has_value(); diff --git a/src/logic/scripting/io_descriptors.hpp b/src/logic/scripting/io_descriptors.hpp index 7e6207791..56ed21a7a 100644 --- a/src/logic/scripting/io_descriptors.hpp +++ b/src/logic/scripting/io_descriptors.hpp @@ -13,6 +13,7 @@ namespace scripting::io_descriptors { std::ostream& require_output(int id); void flush(int id); + int available(int id); bool has_descriptor(int id); diff --git a/src/logic/scripting/lua/libs/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp index e51ce51a0..1bcceb6d3 100644 --- a/src/logic/scripting/lua/libs/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -403,15 +403,15 @@ static int l_flush_descriptor(lua::State* L) { static int l_available_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - if (!scripting::descriptors_manager::is_readable(descriptor)) { + if (!io_descriptors::is_readable(descriptor)) { throw std::runtime_error("descriptor is not readable"); } - return lua::pushinteger(L, scripting::descriptors_manager::available(descriptor)); + return lua::pushinteger(L, io_descriptors::available(descriptor)); } static int l_close_descriptor(lua::State* L) { From 490db13498fb8f204d3a7d297540a337a77fed04 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 00:31:58 +0900 Subject: [PATCH 31/92] added tell stub for named pipe stream's --- res/modules/internal/stream_providers/named_pipe_unix.lua | 4 ++++ res/modules/internal/stream_providers/named_pipe_windows.lua | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/res/modules/internal/stream_providers/named_pipe_unix.lua b/res/modules/internal/stream_providers/named_pipe_unix.lua index 237910ecb..614c9aaf4 100644 --- a/res/modules/internal/stream_providers/named_pipe_unix.lua +++ b/res/modules/internal/stream_providers/named_pipe_unix.lua @@ -64,6 +64,10 @@ function lib.seek(fd, mode, offset) error("cannot seek the named pipe") end +function lib.tell(fd) + error("cannot tell the named pipe") +end + function lib.flush(fd) -- no flush on unix end diff --git a/res/modules/internal/stream_providers/named_pipe_windows.lua b/res/modules/internal/stream_providers/named_pipe_windows.lua index 3a18f4928..d79335f7d 100644 --- a/res/modules/internal/stream_providers/named_pipe_windows.lua +++ b/res/modules/internal/stream_providers/named_pipe_windows.lua @@ -101,6 +101,10 @@ function lib.seek(handle, mode, offset) error("cannot seek the named pipe") end +function lib.tell(handle) + error("cannot tell the named pipe") +end + function lib.flush(handle) C.FlushFileBuffers(handle) end From 21019e6737b14a03f8cf7bf7ab0bdac790dfeda0 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 00:57:38 +0900 Subject: [PATCH 32/92] added note about tell to io_stream --- res/modules/io_stream.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 7e5290c1a..3e91daa4e 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -50,6 +50,9 @@ ioLib - I/O library. Should include the following functions: write(descriptor: int, data: Bytearray) seek(descriptor: int, mode: string, offset: int) Mode may be 'b' (relative begin), 'c' (relative current), 'e' (relative end + 1) + May throw error if environment is not support seek + tell(descriptor: int) -> int + May throw error if environment is not support tell flush(descriptor: int) available(descriptor: int) -> int May return 0 if environment is not support available method From 82da7e907d569879d8706f381abccb10371e6c7e Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 14:47:58 +0900 Subject: [PATCH 33/92] removed require to define available, tell, seek in ioLib --- .../internal/stream_providers/bytearray.lua | 4 --- .../stream_providers/named_pipe_unix.lua | 12 -------- .../stream_providers/named_pipe_windows.lua | 8 ----- .../internal/stream_providers/socket.lua | 12 -------- res/modules/io_stream.lua | 29 +++++++++++++------ 5 files changed, 20 insertions(+), 45 deletions(-) diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua index 35a15b654..0f168c851 100644 --- a/res/modules/internal/stream_providers/bytearray.lua +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -98,10 +98,6 @@ function lib.tell(descriptor) return positions[descriptor] end -function lib.flush(descriptor) - requireDescriptor(descriptor) -end - function lib.available(descriptor) requireDescriptor(descriptor) diff --git a/res/modules/internal/stream_providers/named_pipe_unix.lua b/res/modules/internal/stream_providers/named_pipe_unix.lua index 614c9aaf4..933f6292b 100644 --- a/res/modules/internal/stream_providers/named_pipe_unix.lua +++ b/res/modules/internal/stream_providers/named_pipe_unix.lua @@ -60,18 +60,6 @@ function lib.write(fd, bytearray) end end -function lib.seek(fd, mode, offset) - error("cannot seek the named pipe") -end - -function lib.tell(fd) - error("cannot tell the named pipe") -end - -function lib.flush(fd) - -- no flush on unix -end - function lib.available(fd) if fd == nil or fd < 0 then return 0 end diff --git a/res/modules/internal/stream_providers/named_pipe_windows.lua b/res/modules/internal/stream_providers/named_pipe_windows.lua index d79335f7d..817925b08 100644 --- a/res/modules/internal/stream_providers/named_pipe_windows.lua +++ b/res/modules/internal/stream_providers/named_pipe_windows.lua @@ -97,14 +97,6 @@ function lib.write(handle, bytearray) end end -function lib.seek(handle, mode, offset) - error("cannot seek the named pipe") -end - -function lib.tell(handle) - error("cannot tell the named pipe") -end - function lib.flush(handle) C.FlushFileBuffers(handle) end diff --git a/res/modules/internal/stream_providers/socket.lua b/res/modules/internal/stream_providers/socket.lua index 88684b0ff..0faedd695 100644 --- a/res/modules/internal/stream_providers/socket.lua +++ b/res/modules/internal/stream_providers/socket.lua @@ -32,18 +32,6 @@ function lib.write(descriptor, data) sockets[descriptor]:send(data) end -function lib.seek(descriptor, mode, offset) - error("cannot seek socket") -end - -function lib.tell(descriptor) - error("cannot tell socket") -end - -function lib.flush(descriptor) - requireDescriptor(descriptor) -end - function lib.available(descriptor) requireDescriptor(descriptor) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 3e91daa4e..ef1150237 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -47,15 +47,14 @@ binaryMode - if enabled, most methods will expect bytes instead of strings ioLib - I/O library. Should include the following functions: read(descriptor: int, length: int) -> Bytearray May return bytearray with a smaller size if bytes have not arrived yet or have run out + May throw error if descriptor isn't readable write(descriptor: int, data: Bytearray) - seek(descriptor: int, mode: string, offset: int) + May throw error if descriptor isn't writeable + [optional] seek(descriptor: int, mode: string, offset: int) Mode may be 'b' (relative begin), 'c' (relative current), 'e' (relative end + 1) - May throw error if environment is not support seek - tell(descriptor: int) -> int - May throw error if environment is not support tell - flush(descriptor: int) - available(descriptor: int) -> int - May return 0 if environment is not support available method + [optional] tell(descriptor: int) -> int + [optional] flush(descriptor: int) + [optional] available(descriptor: int) -> int is_alive(descriptor: int) -> bool close(descriptor: int) --]] @@ -135,7 +134,7 @@ function io_stream:available(length) available = #self.readBuffer else - available = self.ioLib.available(self.descriptor) + available = self.ioLib.available and self.ioLib.available(self.descriptor) or 0 end if not length then @@ -388,10 +387,18 @@ function io_stream:write(arg, ...) end function io_stream:seek(mode, offset) + if not self.ioLib.seek then + error("cannot seek this stream") + end + self.ioLib.seek(self.descriptor, mode, offset) end function io_stream:tell() + if not self.ioLib.tell then + error("cannot tell this stream") + end + return self.ioLib.tell(self.descriptor) end @@ -418,7 +425,11 @@ function io_stream:flush() self.writeBuffer:clear() end - if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then self.ioLib.flush(self.descriptor) end + if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then + if self.ioLib.flush then + self.ioLib.flush(self.descriptor) + elseif self:is_closed() then error("stream is closed") end + end end return io_stream From 15f17a0bc33598812e00fe073fbafa21bdbdf59a Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 14:49:43 +0900 Subject: [PATCH 34/92] all numbers changed to ints in io_stream docs and documented tell --- doc/ru/scripting/io_stream.md | 51 ++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index eab023f67..57f447643 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -48,13 +48,13 @@ **Независимый флаг** (включается через `set_binary_mode(true)` или при создании потока). Определяет, в каком виде методы `read` и `write` принимают и возвращают данные: -| binary = true | binary = false (по умолчанию) | -|-----------------------------------------|----------------------------------------| +| binary = true | binary = false (по умолчанию) | +|------------------------------------------------------------|----------------------------------------| | Данные — это **байты** (`Bytearray`, таблица чисел 0..255) | Данные — это **текстовые строки** | -| `read(n)` → `Bytearray` или `table`| `read()` → одна строка | -| `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице | -| `write(Bytearray)` → запись байтов | `write("hello")` → строка + `\n` | -| `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n`| +| `read(n)` → `Bytearray` или `table` | `read()` → одна строка | +| `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице | +| `write(Bytearray)` → запись байтов | `write("hello")` → строка + `\n` | +| `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n`| `read_line` / `write_line` — работают как в текстовом режиме @@ -90,25 +90,25 @@ io_stream:set_flush_mode(string) --[[ Читает данные из потока В двоичном режиме: - Если arg - number, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true + Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true Если arg - string, то функция интерпретирует arg как шаблон для byteutil. Прочитает кол-во байт, которое определено шаблоном, передаст их в byteutil.unpack и вернёт результат В текстовом режиме: - Если arg - number, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы + Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы Если arg не определён, то читает одну строку с окончанием CRLF/LF и возвращает её. --]] io_stream:read( - [опционально] arg: number | string, + [опционально] arg: int | string, [опционально] useTable | trimEmptyLines: boolean -) -> Bytearray | table | string | table | ... +) -> Bytearray | table | string | table | ... --[[ Записывает данные в поток В двоичном режиме: Если arg - string, то функция интерпретирует arg как шаблон для byteutil, передаст его и ... в byteutil.pack и результат запишет в поток - Если arg - Bytearray | table, то записывает байты в поток + Если arg - Bytearray | table, то записывает байты в поток В текстовом режиме: Если arg - string, то записывает строку в поток (вместе с окончанием LF) @@ -116,7 +116,7 @@ io_stream:read( Если arg - table, то записывает каждую строку из таблицы отдельно --]] io_stream:write( - arg: Bytearray | table | string | table, + arg: Bytearray | table | string | table, [опционально] ... ) @@ -128,7 +128,7 @@ io_stream:write_line(string) --[[ В двоичном режиме: - Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если useTable = true + Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если useTable = true В текстовом режиме: Читает все доступные строки из потока в table если useTable = true, или в одну строку вместе с окончаниями, если нет @@ -136,16 +136,18 @@ io_stream:write_line(string) --]] io_stream:read_fully( [опционально] useTable: boolean -) -> Bytearray | table | table | string +) -> Bytearray | table | table | string --[[ Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false. Если не определён, то возвращает количество байт, которое можно прочитать. + +В не буферизированном режиме потока может всегда возвращать 0 или false, если поток не поддерживает available. --]] io_stream:available( - [опционально] length: number -) -> number | boolean + [опционально] length: int +) -> int | boolean --[[ Устанавливает позицию в потоке (всегда в байтах) @@ -153,21 +155,27 @@ io_stream:available( b - Задаёт позицию относительно начало файла c - Задаёт позицию относительно текущей позиции e - Задаёт позицию относительно конца файла + +Может бросать ошибку, если поток не поддерживает seek. --]] io_stream:seek( mode: string - offset: number + offset: int ) + +-- Возвращает текущую позицию в потоке от начала. +-- Может бросать ошибку, если поток не поддерживает tell. +io_stream:tell() -> int ``` ## Методы Buffered-режима ```lua -- Возвращает максимальный размер буферов -io_stream:get_max_buffer_size() -> number +io_stream:get_max_buffer_size() -> int -- Задаёт новый максимальный размер буферов -io_stream:set_max_buffer_size(max_size: number) +io_stream:set_max_buffer_size(max_size: int) ``` ## Методы контроля состояния потока @@ -183,7 +191,8 @@ io_stream:is_closed() -> bool io_stream:close() -- Записывает все данные из write-буфера в поток в buffer/all flush-режимах --- Вызывает ioLib.flush() в all flush-режиме +-- Вызывает ioLib.flush() в all flush-режиме, или ничего не делает, если +-- ioLib не поддерживает flush. io_stream:flush() -- Создаёт новый поток из Bytearray. @@ -200,7 +209,7 @@ io_stream.wrap_bytearray( -- Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua) io_stream.new( descriptor: int, - binaryMode: bool, + binaryMode: boolean, ioLib: table, [опционально] mode: string = "default", [опционально] flushMode: string = "all" From 658af207abc31190233e10ab943d8ed17c23e20a Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 15:09:20 +0900 Subject: [PATCH 35/92] minor change in binary read_fully --- res/modules/io_stream.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index ef1150237..7a760fa69 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -218,7 +218,13 @@ function io_stream:read_fully(useTable) if self.binaryMode then local result = useTable and Bytearray() or { } - readFully(result, function() return self:__read(self.maxBufferSize, true) end) + local avail = self:available() + + if avail == 0 then + avail = self.maxBufferSize + end + + readFully(result, function() return self:__read(avail, true) end) return result else From e7bc0bdbda15a31ac1f6fb3efefecc4c4f8a7049 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 15:59:45 +0900 Subject: [PATCH 36/92] major fixes of buffered mode in io_stream - fixed underflow and overflow errors: now stream automatically does flush or read --- res/modules/io_stream.lua | 61 +++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 7a760fa69..10f45d6a4 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -127,14 +127,10 @@ function io_stream:set_max_buffer_size(maxBufferSize) end function io_stream:available(length) - local available + local available = self.ioLib.available and self.ioLib.available(self.descriptor) or 0 if self.mode == BUFFERED_MODE then - self:__update_read_buffer() - - available = #self.readBuffer - else - available = self.ioLib.available and self.ioLib.available(self.descriptor) or 0 + available = available + #self.readBuffer end if not length then @@ -144,18 +140,6 @@ function io_stream:available(length) end end -function io_stream:__update_read_buffer() - local readed = Bytearray() - - readFully(readed, function(length) return self.ioLib.read(self.descriptor, length) end) - - self.readBuffer:append(readed) - - if #self.readBuffer > self.maxBufferSize then - error "buffer overflow" - end -end - function io_stream:__read(length, fromReadFully) if self.mode == YIELD_MODE then if fromReadFully then @@ -172,27 +156,23 @@ function io_stream:__read(length, fromReadFully) return buffer elseif self.mode == BUFFERED_MODE then - self:__update_read_buffer() + local bufLen = #self.readBuffer - if #self.readBuffer < length then - error "buffer underflow" + if bufLen < length then + self.readBuffer:append( + self.ioLib.read(self.descriptor, self.maxBufferSize - bufLen) + ) end - local copy + bufLen = #self.readBuffer - if #self.readBuffer == length then - copy = Bytearray() - - copy:append(self.readBuffer) + length = math.min(bufLen, length) + local copy = self.readBuffer:slice(1, length) + + if bufLen == length then self.readBuffer:clear() else - copy = Bytearray() - - for i = 1, length do - copy[i] = self.readBuffer[i] - end - self.readBuffer:remove(1, length) end @@ -204,11 +184,22 @@ end function io_stream:__write(data) if self.mode == BUFFERED_MODE then - self.writeBuffer:append(data) + local dataLength = #data - if #self.writeBuffer > self.maxBufferSize then - error "buffer overflow" + if #self.writeBuffer + dataLength > self.maxBufferSize then + self:flush() end + + if dataLength > self.maxBufferSize then + local toWrite = math.floor(dataLength / self.maxBufferSize) * self.maxBufferSize + local toSave = dataLength - toWrite + + self.ioLib.write(self.descriptor, data:slice(1, toWrite)) + + self:flush() + + self.writeBuffer = data:slice(toWrite + 1, toSave) + else self.writeBuffer:append(data) end elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then return self.ioLib.write(self.descriptor, data) end From 36509fb7bca43d53a1946f51991b094cdb716190 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 16:03:52 +0900 Subject: [PATCH 37/92] added bytes reserve when creating buffers for stream --- res/modules/io_stream.lua | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 10f45d6a4..98a37529c 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -93,14 +93,19 @@ function io_stream:set_mode(mode) error("invalid stream mode: "..mode) end - if self.mode == BUFFERED_MODE then + if self.writeBuffer then self.writeBuffer:clear() - self.readBuffer:clear() + self.writeBuffer = nil end - if mode == BUFFERED_MODE and not self.writeBuffer then - self.writeBuffer = Bytearray() - self.readBuffer = Bytearray() + if self.readBuffer then + self.readBuffer:clear() + self.readBuffer = nil + end + + if mode == BUFFERED_MODE then + self.writeBuffer = Bytearray(self.maxBufferSize) + self.readBuffer = Bytearray(self.maxBufferSize) end self.mode = mode @@ -124,6 +129,9 @@ end function io_stream:set_max_buffer_size(maxBufferSize) self.maxBufferSize = maxBufferSize + + self.writeBuffer = Bytearray(self.maxBufferSize) + self.readBuffer = Bytearray(self.maxBufferSize) end function io_stream:available(length) From c45d994216e57811b36d9463fab7ad2c7cc051c0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 6 Jul 2026 12:44:09 +0300 Subject: [PATCH 38/92] fix BasicParser power function --- src/coders/BasicParser.inl | 21 +++++++++++++++++++-- test/coders/json.cpp | 6 ++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/coders/BasicParser.inl b/src/coders/BasicParser.inl index 796205b2a..5b249bfcf 100644 --- a/src/coders/BasicParser.inl +++ b/src/coders/BasicParser.inl @@ -22,9 +22,26 @@ namespace { } inline double power(double base, int64_t power) { + if (power == 0) { + return 1.0; + } + + uint64_t exp; + if (power < 0) { + base = 1.0 / base; + exp = static_cast(-(power + 1)) + 1; + } else { + exp = static_cast(power); + } + double result = 1.0; - for (int64_t i = 0; i < power; i++) { - result *= base; + + while (exp > 0) { + if (exp & 1) + result *= base; + + base *= base; + exp >>= 1; } return result; } diff --git a/test/coders/json.cpp b/test/coders/json.cpp index be9f0534d..bf3d53a91 100644 --- a/test/coders/json.cpp +++ b/test/coders/json.cpp @@ -8,6 +8,8 @@ TEST(JSON, EncodeDecode) { const int bytesSize = 20; const int year = 2019; const float score = 3.141592; + const double distance = 1.23e45; + const double epsilon = 1e-6; const bool visible = true; dv::objects::Bytes srcBytes(bytesSize); for (int i = 0; i < bytesSize; i ++) { @@ -20,6 +22,8 @@ TEST(JSON, EncodeDecode) { object["name"] = name; object["year"] = year; object["score"] = score; + object["distance"] = distance; + object["epsilon"] = epsilon; object["visible"] = visible; object["data"] = srcBytes; @@ -30,6 +34,8 @@ TEST(JSON, EncodeDecode) { EXPECT_EQ(object["name"].asString(), name); EXPECT_EQ(object["year"].asInteger(), year); EXPECT_FLOAT_EQ(object["score"].asNumber(), score); + EXPECT_DOUBLE_EQ(object["distance"].asNumber(), distance); + EXPECT_DOUBLE_EQ(object["epsilon"].asNumber(), epsilon); EXPECT_EQ(object["visible"].asBoolean(), visible); auto b64string = object["data"].asString(); From 2d8b50e1986ae047a75c6a2b359249f32c61457a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 6 Jul 2026 20:33:01 +0300 Subject: [PATCH 39/92] fix small heap alignment violations --- src/util/SmallHeap.hpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/util/SmallHeap.hpp b/src/util/SmallHeap.hpp index 8a6ff9290..213d36958 100644 --- a/src/util/SmallHeap.hpp +++ b/src/util/SmallHeap.hpp @@ -11,8 +11,10 @@ namespace util { template - inline T read_int_le(const uint8_t* src, size_t offset=0) { - return dataio::le2h(*(reinterpret_cast(src) + offset)); + inline T read_int_le(const uint8_t* src, ptrdiff_t offset=0) { + T tmp; + std::memcpy(&tmp, src + offset * sizeof(T), sizeof(T)); + return dataio::le2h(tmp); } /// @brief Simple heap implementation for memory-optimal sparse array of @@ -87,8 +89,7 @@ namespace util { std::memset(found, 0, entrySize); return found; } - this->free(found); - return allocate(index, size); + free(found); } for (size_t i = 0; i < entriesCount; i++) { auto data = buffer.data() + offset; @@ -110,9 +111,12 @@ namespace util { entriesCount++; auto data = buffer.data() + offset; - *reinterpret_cast(data) = dataio::h2le(index); + + Tindex indexTmp = dataio::h2le(index); + Tsize sizeTmp = dataio::h2le(size); + std::memcpy(data, &indexTmp, sizeof(Tindex)); data += sizeof(Tindex); - *reinterpret_cast(data) = dataio::h2le(size); + std::memcpy(data, &sizeTmp, sizeof(Tsize)); return data + sizeof(Tsize); } @@ -147,7 +151,8 @@ namespace util { ubyte* dst = out.data(); const ubyte* src = buffer.data(); - *reinterpret_cast(dst) = dataio::h2le(entriesCount); + Tindex countTmp = dataio::h2le(entriesCount); + std::memcpy(dst, &countTmp, sizeof(Tindex)); dst += sizeof(Tindex); std::memcpy(dst, src, buffer.size()); From a8ed3cb36f92ddf8c8b8033ad8a0ecf912187531 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 7 Jul 2026 17:42:24 +0300 Subject: [PATCH 40/92] fix macos build (#859) * update macos.yml * update macos.yml * update macos.yml * update macos.yml * update macos.yml * update macos.yml * update workflows * update test * fix unused function warning * update test * fixes * update test * update test * update macos.yml * Revert "update workflows" This reverts commit ae78ff3ca24e845364776c9ff14c93e47991c4ce. --- src/util/SmallHeap.hpp | 8 ++++---- src/window/detail/GLFWWindow.cpp | 3 +++ test/util/SmallHeap.cpp | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/util/SmallHeap.hpp b/src/util/SmallHeap.hpp index 213d36958..6ff8cf238 100644 --- a/src/util/SmallHeap.hpp +++ b/src/util/SmallHeap.hpp @@ -11,9 +11,9 @@ namespace util { template - inline T read_int_le(const uint8_t* src, ptrdiff_t offset=0) { + inline T read_int_le(const uint8_t* src) { T tmp; - std::memcpy(&tmp, src + offset * sizeof(T), sizeof(T)); + std::memcpy(&tmp, src, sizeof(T)); return dataio::le2h(tmp); } @@ -126,7 +126,7 @@ namespace util { if (ptr == nullptr) { return 0; } - return read_int_le(ptr, -1); + return read_int_le(ptr - sizeof(Tsize)); } /// @return number of entries @@ -177,7 +177,7 @@ namespace util { ) : buffer(buffer), index(index), offset(offset) {} Tsize size() const { - return read_int_le(buffer.data() + offset, -1); + return read_int_le(buffer.data() + offset - sizeof(Tsize)); } bool operator!=(const const_iterator& o) const { diff --git a/src/window/detail/GLFWWindow.cpp b/src/window/detail/GLFWWindow.cpp index db3d67241..394472783 100644 --- a/src/window/detail/GLFWWindow.cpp +++ b/src/window/detail/GLFWWindow.cpp @@ -33,6 +33,7 @@ static void init_gl_extensions_list() { } } +[[maybe_unused]] static bool is_gl_extension_supported(const char *extension) { if (!extension || !*extension) { return false; @@ -40,6 +41,7 @@ static bool is_gl_extension_supported(const char *extension) { return supported_gl_extensions.find(extension) != supported_gl_extensions.end(); } +#ifndef __APPLE__ static const char* gl_error_name(int error) { switch (error) { case GL_DEBUG_TYPE_ERROR: return "ERROR"; @@ -80,6 +82,7 @@ static void GLAPIENTRY gl_message_callback( logger.warning() << "GL:" << gl_error_name(type) << ":" << gl_severity_name(severity) << ": " << message; } +#endif static bool initialize_gl(int width, int height) { glewExperimental = GL_TRUE; diff --git a/test/util/SmallHeap.cpp b/test/util/SmallHeap.cpp index c5b3adf28..a4ecab842 100644 --- a/test/util/SmallHeap.cpp +++ b/test/util/SmallHeap.cpp @@ -8,9 +8,13 @@ TEST(SmallHeap, Allocation) { auto index = 0; auto size = 4; + std::cout << "step 1" << std::endl; SmallHeap map; + std::cout << "step 2" << std::endl; auto ptr = map.allocate(index, size); + std::cout << "ptr: " << (void*)ptr << " le2h(1): " << dataio::le2h(1) << " h2le(1): " << dataio::h2le(1) << std::endl; EXPECT_EQ(map.sizeOf(ptr), size); + std::cout << "step 4" << std::endl; EXPECT_EQ(ptr, map.find(index)); } From 9dbf78d57c6b730a2a414b4a78bc04a6a3849881 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 15 Jul 2026 00:54:39 +0300 Subject: [PATCH 41/92] fix docs typo --- doc/ru/scripting/builtins/libinput.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ru/scripting/builtins/libinput.md b/doc/ru/scripting/builtins/libinput.md index 7d04747a9..17df1f8aa 100644 --- a/doc/ru/scripting/builtins/libinput.md +++ b/doc/ru/scripting/builtins/libinput.md @@ -21,7 +21,7 @@ input.get_bindings() -> table input.get_binding_text(bindname: string) -> string -- Проверяет активность привязки. -input.is_active(bindname: string) -> string +input.is_active(bindname: string) -> boolean -- Включает/выключает привязку до выхода из мира. input.set_enabled(bindname: string, flag: boolean) @@ -74,4 +74,4 @@ input.add_callback("key:escape", function () print("NO") return true -- предотвращает вызов назначенных ранее функций end, document.root) -``` \ No newline at end of file +``` From f29f3b1bbadc66b620f4db1a0273123b052577d1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 15 Jul 2026 22:21:18 +0300 Subject: [PATCH 42/92] add advanced entities.raycast overload --- src/graphics/render/Decorator.cpp | 12 +- src/logic/PlayerController.cpp | 21 +- src/logic/scripting/lua/libs/libblock.cpp | 20 +- src/logic/scripting/lua/libs/libentity.cpp | 221 ++++++++++++++------- src/objects/Entities.cpp | 15 +- src/objects/Entities.hpp | 28 ++- src/voxels/Chunks.cpp | 5 +- src/voxels/Chunks.hpp | 7 +- src/voxels/blocks_agent.cpp | 23 ++- src/voxels/blocks_agent.hpp | 11 +- 10 files changed, 235 insertions(+), 128 deletions(-) diff --git a/src/graphics/render/Decorator.cpp b/src/graphics/render/Decorator.cpp index b988898ce..1fa2abd0b 100644 --- a/src/graphics/render/Decorator.cpp +++ b/src/graphics/render/Decorator.cpp @@ -22,6 +22,7 @@ #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" +#include "voxels/blocks_agent.hpp" #include "window/Camera.hpp" #include "world/Level.hpp" #include "WorldRenderer.hpp" @@ -98,7 +99,7 @@ void Decorator::updateAcoustics(const Camera& camera) { auto& chunks = *player.chunks; const auto& start = camera.position; - float rayLength = 40.0f; + float length = 40.0f; auto& blocks = level.content.getIndices()->blocks; @@ -107,7 +108,7 @@ void Decorator::updateAcoustics(const Camera& camera) { float averageDistance = 0.0f; float averageAbsorption = 0.0f; float speedOfSound = 300.0f; - float minDistance = rayLength; + float minDistance = length; for (int i = 0; i < rays; i++) { float u1 = random.randFloat(); float u2 = random.randFloat(); @@ -121,12 +122,13 @@ void Decorator::updateAcoustics(const Camera& camera) { glm::vec3 end; glm::ivec3 norm; glm::ivec3 iend; - auto vox = chunks.rayCast(start, dir, rayLength, end, norm, iend); + blocks_agent::RaycastSettings raycast {}; + auto vox = chunks.rayCast(start, dir, length, end, norm, iend, raycast); if (vox == nullptr) { continue; } auto distance = glm::distance(start, end); - if (distance >= rayLength * 0.98f) { + if (distance >= length * 0.98f) { continue; } if (distance < minDistance) { @@ -149,7 +151,7 @@ void Decorator::updateAcoustics(const Camera& camera) { averageAbsorption /= hit; } - float decayTime = 20.0f * (averageDistance / rayLength); + float decayTime = 20.0f * (averageDistance / length); float escapeRatio = static_cast(rays - hit) / static_cast(rays); acoustics.reverbGain = glm::pow(hit / static_cast(rays), 3.0f); diff --git a/src/logic/PlayerController.cpp b/src/logic/PlayerController.cpp index 909ca3778..03abc07b2 100644 --- a/src/logic/PlayerController.cpp +++ b/src/logic/PlayerController.cpp @@ -200,8 +200,13 @@ void CameraControl::update( auto castRay = [](Player& player, Camera& camera, const glm::vec3& front) -> glm::vec3 { auto blockEnd = player.chunks->rayCastToObstacle(camera.position, front, 3.0f); + + Entities::RaycastSettings raycast {}; + raycast.ignoredUid = player.getEntity(); + raycast.solidEntitiesOnly = true; + auto entityRay = player.getLevel().entities->rayCast( - camera.position, front, 3.0f, player.getEntity(), true + camera.position, front, 3.0f, raycast ); if (entityRay.has_value() && entityRay->distance < glm::distance(camera.position, blockEnd)) { @@ -350,7 +355,13 @@ voxel* PlayerController::updateSelection(float maxDistance) { glm::ivec3 iend; glm::ivec3 norm; voxel* vox = chunks.rayCast( - camera->position, camera->front, maxDistance, end, norm, iend + camera->position, + camera->front, + maxDistance, + end, + norm, + iend, + blocks_agent::RaycastSettings {} ); if (vox) { maxDistance = glm::distance(camera->position, end); @@ -358,8 +369,12 @@ voxel* PlayerController::updateSelection(float maxDistance) { auto prevEntity = selection.entity; selection.entity = ENTITY_NONE; selection.actualPosition = iend; + + Entities::RaycastSettings raycast {}; + raycast.ignoredUid = player.getEntity(); + if (auto result = level.entities->rayCast( - camera->position, camera->front, maxDistance, player.getEntity() + camera->position, camera->front, maxDistance, raycast )) { selection.entity = result->entity; selection.hitPosition = diff --git a/src/logic/scripting/lua/libs/libblock.cpp b/src/logic/scripting/lua/libs/libblock.cpp index 6c9550dde..fc26ced38 100644 --- a/src/logic/scripting/lua/libs/libblock.cpp +++ b/src/logic/scripting/lua/libs/libblock.cpp @@ -543,13 +543,16 @@ static int l_destruct(lua::State* L) { } static int l_raycast(lua::State* L) { - auto& level = require_level(); + const auto& level = require_level(); auto start = lua::tovec<3>(L, 1); auto dir = lua::tovec<3>(L, 2); auto maxDistance = lua::tonumber(L, 3); - bool includeNonSelectable = false; + + blocks_agent::RaycastSettings raycast {}; std::set filteredBlocks {}; + raycast.filter = &filteredBlocks; + const int luaStackSize = lua::gettop(L); if (luaStackSize >= 5) { if (lua::istable(L, 5)) { @@ -568,21 +571,14 @@ static int l_raycast(lua::State* L) { } } if (luaStackSize >= 6) { - includeNonSelectable = lua::toboolean(L, 6); + raycast.includeNonSelectable = lua::toboolean(L, 6); } glm::vec3 end; glm::ivec3 normal; glm::ivec3 iend; + if (auto voxel = blocks_agent::raycast( - *level.chunks, - start, - dir, - maxDistance, - end, - normal, - iend, - filteredBlocks, - includeNonSelectable + *level.chunks, start, dir, maxDistance, end, normal, iend, raycast )) { if (luaStackSize >= 4 && !lua::isnil(L, 4)) { lua::pushvalue(L, 4); diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index d2c0c4bc5..ebf70d485 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -17,6 +17,8 @@ #include "voxels/blocks_agent.hpp" #include "window/Camera.hpp" +#include + using namespace scripting; static const EntityDef* require_entity_def(lua::State* L) { @@ -145,34 +147,58 @@ static int l_get_all_in_radius(lua::State* L) { return 1; } -static int l_raycast(lua::State* L) { - auto start = lua::tovec<3>(L, 1); - auto dir = lua::tovec<3>(L, 2); - auto maxDistance = lua::tonumber(L, 3); - auto ignoreEntityId = lua::tointeger(L, 4); - bool includeNonSelectable = false; - std::set filteredBlocks {}; - const int luaStackSize = lua::gettop(L); - if (luaStackSize >= 6) { - if (lua::istable(L, 6)) { - int addLen = lua::objlen(L, 6); - for (int i = 0; i < addLen; i++) { - lua::rawgeti(L, i + 1, 6); - auto blockName = std::string(lua::tostring(L, -1)); - const Block* block = content->blocks.find(blockName); - if (block != nullptr) { - filteredBlocks.insert(block->rt.id); - } - lua::pop(L); - } - } else { - throw std::runtime_error("table expected for filter"); - } +static int l_push_ray( + lua::State* L, + const std::optional& ray, + blockid_t block, + const glm::vec3& start, + const glm::vec3& dir, + const glm::vec3& end, + const glm::ivec3& iend, + const glm::ivec3& normal +) { + if (!ray.has_value() && block == BLOCK_VOID) { + return 0; } - if (luaStackSize >= 7) { - includeNonSelectable = lua::toboolean(L, 6); + if (lua::gettop(L) >= 5 && lua::istable(L, 5)) { + lua::pushvalue(L, 5); + } else { + lua::createtable(L, 0, 6); } + lua::pushvec3(L, ray ? (start + dir * ray->distance) : end); + lua::setfield(L, "endpoint"); + + lua::pushvec3(L, ray ? ray->normal : normal); + lua::setfield(L, "normal"); + + lua::pushnumber(L, glm::distance(start, end)); + lua::setfield(L, "length"); + + lua::pushvec3(L, iend); + lua::setfield(L, "iendpoint"); + + lua::pushinteger(L, block); + lua::setfield(L, "block"); + + if (ray.has_value()) { + lua::pushinteger(L, ray->entity); + lua::setfield(L, "entity"); + } else if (block != BLOCK_VOID) { + lua::pushinteger(L, block); + lua::setfield(L, "block"); + } + return 1; +} + +static int l_raycast( + lua::State* L, + const glm::vec3& start, + const glm::vec3& dir, + float maxDistance, + const blocks_agent::RaycastSettings& voxelRaycast, + const Entities::RaycastSettings& entityRaycast +) { glm::vec3 end; glm::ivec3 normal; glm::ivec3 iend; @@ -187,61 +213,108 @@ static int l_raycast(lua::State* L) { end, normal, iend, - filteredBlocks, - includeNonSelectable + voxelRaycast )) { maxDistance = glm::distance(start, end); block = voxel->id; } - if (auto ray = - level->entities->rayCast(start, dir, maxDistance, ignoreEntityId)) { - if (luaStackSize >= 5 && !lua::isnil(L, 5)) { - lua::pushvalue(L, 5); - } else { - lua::createtable(L, 0, 6); + auto ray = level->entities->rayCast(start, dir, maxDistance, entityRaycast); + return l_push_ray(L, ray, block, start, dir, end, iend, normal); +} + +static void load_blocks_filter(lua::State* L, int idx, std::set& dst) { + int count = lua::objlen(L, idx); + for (int i = 0; i < count; i++) { + lua::rawgeti(L, i + 1, idx); + auto blockName = std::string(lua::tostring(L, -1)); + const Block* block = content->blocks.find(blockName); + if (block != nullptr) { + dst.insert(block->rt.id); } - - lua::pushvec3(L, start + dir * ray->distance); - lua::setfield(L, "endpoint"); - - lua::pushvec3(L, ray->normal); - lua::setfield(L, "normal"); - - lua::pushnumber(L, glm::distance(start, end)); - lua::setfield(L, "length"); - - lua::pushvec3(L, iend); - lua::setfield(L, "iendpoint"); - - lua::pushinteger(L, block); - lua::setfield(L, "block"); - - lua::pushinteger(L, ray->entity); - lua::setfield(L, "entity"); - return 1; - } else if (block != BLOCK_VOID) { - if (lua::gettop(L) >= 5 && !lua::isnil(L, 5)) { - lua::pushvalue(L, 5); - } else { - lua::createtable(L, 0, 5); - } - lua::pushvec3(L, end); - lua::setfield(L, "endpoint"); - - lua::pushvec3(L, normal); - lua::setfield(L, "normal"); - - lua::pushnumber(L, glm::distance(start, end)); - lua::setfield(L, "length"); - - lua::pushvec3(L, iend); - lua::setfield(L, "iendpoint"); - - lua::pushinteger(L, block); - lua::setfield(L, "block"); - return 1; + lua::pop(L); } - return 0; +} + +static void load_entities_filter(lua::State* L, int idx, std::set& dst) { + int count = lua::objlen(L, idx); + for (int i = 0; i < count; i++) { + lua::rawgeti(L, i + 1, idx); + auto entityName = std::string(lua::tostring(L, -1)); + const EntityDef* entity = content->entities.find(entityName); + if (entity != nullptr) { + dst.insert(entity->rt.id); + } + lua::pop(L); + } +} + +static int l_raycast(lua::State* L) { + blocks_agent::RaycastSettings blocksRaycast {}; + Entities::RaycastSettings entityRaycast {}; + + std::set filteredBlocks {}; + std::set filteredEntities {}; + + glm::vec3 start, dir; + float maxDistance; + + const int luaStackSize = lua::gettop(L); + if (luaStackSize == 1) { + if (!lua::istable(L, 1)) { + throw std::runtime_error("invalid function overload"); + } + + lua::requirefield(L, "start", 1); + start = lua::tovec3(L, -1); + + lua::requirefield(L, "dir", 1); + dir = lua::tovec3(L, -1); + + lua::requirefield(L, "distance", 1); + maxDistance = lua::tonumber(L, -1); + + lua::pop(L, 3); + + if (lua::getfield(L, "ignore_uid", 1)) { + entityRaycast.ignoredUid = lua::tointeger(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "ignore_entities")) { + load_entities_filter(L, -1, filteredEntities); + lua::pop(L); + } + if (lua::getfield(L, "entities_exclusion")) { + entityRaycast.entityFilterExcludeMode = lua::toboolean(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "filter_blocks", 1)) { + load_blocks_filter(L, -1, filteredBlocks); + lua::pop(L); + } + } else { + start = lua::tovec<3>(L, 1); + dir = lua::tovec<3>(L, 2); + maxDistance = lua::tonumber(L, 3); + + entityRaycast.ignoredUid = lua::tointeger(L, 4); + if (luaStackSize >= 6) { + if (lua::istable(L, 6)) { + load_blocks_filter(L, 6, filteredBlocks); + } else { + throw std::runtime_error("table expected for filter"); + } + } + if (luaStackSize >= 7) { + blocksRaycast.includeNonSelectable = lua::toboolean(L, 6); + } + } + if (!filteredBlocks.empty()) { + blocksRaycast.filter = &filteredBlocks; + } + if (!filteredEntities.empty()) { + blocksRaycast.filter = &filteredEntities; + } + return l_raycast(L, start, dir, maxDistance, blocksRaycast, entityRaycast); } static int l_reload_component(lua::State* L) { diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index 61b312a24..fafd99fe5 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -166,8 +166,7 @@ std::optional Entities::rayCast( glm::vec3 start, glm::vec3 dir, float maxDistance, - entityid_t ignore, - bool solidOnly + const RaycastSettings& settings ) { Ray ray(start, dir); auto view = registry->view(); @@ -176,10 +175,18 @@ std::optional Entities::rayCast( glm::ivec3 foundNormal; for (auto [entity, eid, transform, body] : view.each()) { - if (eid.uid == ignore || !body.enabled || (solidOnly && !eid.def.solid)) { + if (eid.uid == settings.ignoredUid || !body.enabled || + (settings.solidEntitiesOnly && !eid.def.solid)) { continue; } - auto& hitbox = body.hitbox; + if (settings.entitiesFilter) { + bool matches = settings.entitiesFilter->find(eid.def.rt.id) != + settings.entitiesFilter->end(); + if (matches == settings.entityFilterExcludeMode) { + continue; + } + } + const auto& hitbox = body.hitbox; glm::ivec3 normal; double distance; if (ray.intersectAABB( diff --git a/src/objects/Entities.hpp b/src/objects/Entities.hpp index 33786118a..0d003ffb1 100644 --- a/src/objects/Entities.hpp +++ b/src/objects/Entities.hpp @@ -5,6 +5,7 @@ #include #include +#include "constants.hpp" #include "physics/Hitbox.hpp" #include "Transform.hpp" #include "Rigidbody.hpp" @@ -46,12 +47,6 @@ class Entities final { ); void preparePhysics(float delta); public: - struct RaycastResult { - entityid_t entity; - glm::ivec3 normal; - float distance; - }; - Entities(Level& level); ~Entities(); @@ -82,20 +77,33 @@ public: std::optional get(entityid_t id); + struct RaycastSettings { + /// @param ignore Ignored entity uid + entityid_t ignoredUid; + /// @param solidEntitiesOnly If true, only entities with solid hitboxes + /// will be checked + bool solidEntitiesOnly = false; + bool entityFilterExcludeMode = false; + const std::set* entitiesFilter = nullptr; + }; + + struct RaycastResult { + entityid_t entity; + glm::ivec3 normal; + float distance; + }; + /// @brief Entities raycast. No blocks check included, use combined with /// Chunks.rayCast /// @param start Ray start /// @param dir Ray direction normalized vector /// @param maxDistance Max ray length - /// @param ignore Ignored entity ID - /// @param solidOnly If true, only entities with solid hitboxes will be checked /// @return An optional structure containing entity, normal and distance std::optional rayCast( glm::vec3 start, glm::vec3 dir, float maxDistance, - entityid_t ignore = -1, - bool solidOnly = false + const RaycastSettings& settings ); void loadEntities(dv::value map); diff --git a/src/voxels/Chunks.cpp b/src/voxels/Chunks.cpp index 2a85b38bd..615ed2fbb 100644 --- a/src/voxels/Chunks.cpp +++ b/src/voxels/Chunks.cpp @@ -215,11 +215,10 @@ voxel* Chunks::rayCast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const blocks_agent::RaycastSettings& settings ) const { return blocks_agent::raycast( - *this, start, dir, maxDist, end, norm, iend, std::move(filter), includeNonSelectable + *this, start, dir, maxDist, end, norm, iend, settings ); } diff --git a/src/voxels/Chunks.hpp b/src/voxels/Chunks.hpp index 9fdebff1c..8b94026cb 100644 --- a/src/voxels/Chunks.hpp +++ b/src/voxels/Chunks.hpp @@ -24,6 +24,10 @@ class VoxelsVolume; template class StaticVoxelsVolume; +namespace blocks_agent { + struct RaycastSettings; +} + /// Player-centred chunks matrix class Chunks { LevelEvents* events; @@ -112,8 +116,7 @@ public: glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter = {}, - bool includeNonSelectable = false + const blocks_agent::RaycastSettings& settings ) const; glm::vec3 rayCastToObstacle( diff --git a/src/voxels/blocks_agent.cpp b/src/voxels/blocks_agent.cpp index 436372457..eb96c5e2a 100644 --- a/src/voxels/blocks_agent.cpp +++ b/src/voxels/blocks_agent.cpp @@ -223,8 +223,7 @@ static inline voxel* raycast_blocks( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ) { const auto& blocks = chunks.getContentIndices().blocks; float px = start.x; @@ -259,16 +258,20 @@ static inline voxel* raycast_blocks( float tzMax = (tzDelta < infinity) ? tzDelta * zdist : infinity; int steppedIndex = -1; - + bool includeNonSelectable = settings.includeNonSelectable; + while (t <= maxDist) { voxel* voxel = get(chunks, ix, iy, iz); if (voxel == nullptr) { return nullptr; } + auto filter = settings.filter; const auto& def = blocks.require(voxel->id); - if (voxel->id != BLOCK_AIR && (def.selectable || includeNonSelectable) && - (filter.empty() || filter.find(def.rt.id) == filter.end())) { + if (voxel->id != BLOCK_AIR + && (def.selectable || includeNonSelectable) + && (filter == nullptr || filter->empty() || + filter->find(def.rt.id) == filter->end())) { end.x = px + t * dx; end.y = py + t * dy; end.z = pz + t * dz; @@ -365,10 +368,9 @@ voxel* blocks_agent::raycast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ) { - return raycast_blocks(chunks, start, dir, maxDist, end, norm, iend, filter, includeNonSelectable); + return raycast_blocks(chunks, start, dir, maxDist, end, norm, iend, settings); } voxel* blocks_agent::raycast( @@ -379,10 +381,9 @@ voxel* blocks_agent::raycast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ) { - return raycast_blocks(chunks, start, dir, maxDist, end, norm, iend, filter, includeNonSelectable); + return raycast_blocks(chunks, start, dir, maxDist, end, norm, iend, settings); } // reduce nesting on next modification diff --git a/src/voxels/blocks_agent.hpp b/src/voxels/blocks_agent.hpp index a86b2e25b..3ac4c74d5 100644 --- a/src/voxels/blocks_agent.hpp +++ b/src/voxels/blocks_agent.hpp @@ -412,6 +412,11 @@ inline void set_rotation( } } +struct RaycastSettings { + const std::set* filter = nullptr; + bool includeNonSelectable = false; +}; + /// @brief Cast ray to a selectable block with filter based on id. /// @param chunks chunks matrix /// @param start ray start position @@ -431,8 +436,7 @@ voxel* raycast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ); /// @brief Cast ray to a selectable block with filter based on id. @@ -454,8 +458,7 @@ voxel* raycast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ); void get_voxels( From baa952a1725943aae4026bdb7cbc95a22432e6ce Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 16 Jul 2026 00:44:44 +0300 Subject: [PATCH 43/92] add 'nonselect_blocks' parameter --- src/logic/scripting/lua/libs/libentity.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index ebf70d485..3e47e2432 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -291,6 +291,10 @@ static int l_raycast(lua::State* L) { load_blocks_filter(L, -1, filteredBlocks); lua::pop(L); } + if (lua::getfield(L, "nonselect_blocks", 1)) { + blocksRaycast.includeNonSelectable = lua::toboolean(L, -1); + lua::pop(L); + } } else { start = lua::tovec<3>(L, 1); dir = lua::tovec<3>(L, 2); From ad6b72f87ccece5d7a2966e3849033bb10bbb437 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 16 Jul 2026 21:52:32 +0300 Subject: [PATCH 44/92] add world.raycast --- res/scripts/stdlib.lua | 2 + src/logic/scripting/lua/libs/libentity.cpp | 135 ++++++++++++--------- 2 files changed, 78 insertions(+), 59 deletions(-) diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 093501d75..b658fe096 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -312,6 +312,8 @@ entities.get_all = function(uids) return stdcomp.get_all(uids) end end +world.raycast = entities.__world_raycast +entities.__world_raycast = nil __vc_scripts_registry = require "core:internal/scripts_registry" diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 3e47e2432..50711cf01 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -197,7 +197,8 @@ static int l_raycast( const glm::vec3& dir, float maxDistance, const blocks_agent::RaycastSettings& voxelRaycast, - const Entities::RaycastSettings& entityRaycast + const Entities::RaycastSettings& entityRaycast, + bool entities ) { glm::vec3 end; glm::ivec3 normal; @@ -218,7 +219,10 @@ static int l_raycast( maxDistance = glm::distance(start, end); block = voxel->id; } - auto ray = level->entities->rayCast(start, dir, maxDistance, entityRaycast); + auto ray = + entities + ? level->entities->rayCast(start, dir, maxDistance, entityRaycast) + : std::nullopt; return l_push_ray(L, ray, block, start, dir, end, iend, normal); } @@ -248,7 +252,7 @@ static void load_entities_filter(lua::State* L, int idx, std::set } } -static int l_raycast(lua::State* L) { +static int l_world_raycast(lua::State* L) { blocks_agent::RaycastSettings blocksRaycast {}; Entities::RaycastSettings entityRaycast {}; @@ -257,68 +261,80 @@ static int l_raycast(lua::State* L) { glm::vec3 start, dir; float maxDistance; + bool entities = true; + + if (!lua::istable(L, 1)) { + throw std::runtime_error("invalid function overload"); + } + + lua::requirefield(L, "start", 1); + start = lua::tovec3(L, -1); + + lua::requirefield(L, "dir", 1); + dir = lua::tovec3(L, -1); + + lua::requirefield(L, "distance", 1); + maxDistance = lua::tonumber(L, -1); + + lua::pop(L, 3); + + if (lua::getfield(L, "entities", 1)) { + entities = lua::toboolean(L, -1); + lua::pop(L); + } + + if (lua::getfield(L, "ignore_uid", 1)) { + entityRaycast.ignoredUid = lua::tointeger(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "ignore_entities")) { + load_entities_filter(L, -1, filteredEntities); + blocksRaycast.filter = &filteredEntities; + lua::pop(L); + } + if (lua::getfield(L, "entities_exclusion")) { + entityRaycast.entityFilterExcludeMode = lua::toboolean(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "filter_blocks", 1)) { + load_blocks_filter(L, -1, filteredBlocks); + blocksRaycast.filter = &filteredBlocks; + lua::pop(L); + } + if (lua::getfield(L, "nonselect_blocks", 1)) { + blocksRaycast.includeNonSelectable = lua::toboolean(L, -1); + lua::pop(L); + } + return l_raycast( + L, start, dir, maxDistance, blocksRaycast, entityRaycast, entities + ); +} + +static int l_raycast(lua::State* L) { + blocks_agent::RaycastSettings blocksRaycast {}; + Entities::RaycastSettings entityRaycast {}; + std::set filteredBlocks {}; const int luaStackSize = lua::gettop(L); - if (luaStackSize == 1) { - if (!lua::istable(L, 1)) { - throw std::runtime_error("invalid function overload"); - } + glm::vec3 start = lua::tovec<3>(L, 1); + glm::vec3 dir = lua::tovec<3>(L, 2); + float maxDistance = lua::tonumber(L, 3); - lua::requirefield(L, "start", 1); - start = lua::tovec3(L, -1); - - lua::requirefield(L, "dir", 1); - dir = lua::tovec3(L, -1); - - lua::requirefield(L, "distance", 1); - maxDistance = lua::tonumber(L, -1); - - lua::pop(L, 3); - - if (lua::getfield(L, "ignore_uid", 1)) { - entityRaycast.ignoredUid = lua::tointeger(L, -1); - lua::pop(L); - } - if (lua::getfield(L, "ignore_entities")) { - load_entities_filter(L, -1, filteredEntities); - lua::pop(L); - } - if (lua::getfield(L, "entities_exclusion")) { - entityRaycast.entityFilterExcludeMode = lua::toboolean(L, -1); - lua::pop(L); - } - if (lua::getfield(L, "filter_blocks", 1)) { - load_blocks_filter(L, -1, filteredBlocks); - lua::pop(L); - } - if (lua::getfield(L, "nonselect_blocks", 1)) { - blocksRaycast.includeNonSelectable = lua::toboolean(L, -1); - lua::pop(L); - } - } else { - start = lua::tovec<3>(L, 1); - dir = lua::tovec<3>(L, 2); - maxDistance = lua::tonumber(L, 3); - - entityRaycast.ignoredUid = lua::tointeger(L, 4); - if (luaStackSize >= 6) { - if (lua::istable(L, 6)) { - load_blocks_filter(L, 6, filteredBlocks); - } else { - throw std::runtime_error("table expected for filter"); - } - } - if (luaStackSize >= 7) { - blocksRaycast.includeNonSelectable = lua::toboolean(L, 6); + entityRaycast.ignoredUid = lua::tointeger(L, 4); + if (luaStackSize >= 6) { + if (lua::istable(L, 6)) { + load_blocks_filter(L, 6, filteredBlocks); + blocksRaycast.filter = &filteredBlocks; + } else { + throw std::runtime_error("table expected for filter"); } } - if (!filteredBlocks.empty()) { - blocksRaycast.filter = &filteredBlocks; + if (luaStackSize >= 7) { + blocksRaycast.includeNonSelectable = lua::toboolean(L, 6); } - if (!filteredEntities.empty()) { - blocksRaycast.filter = &filteredEntities; - } - return l_raycast(L, start, dir, maxDistance, blocksRaycast, entityRaycast); + return l_raycast( + L, start, dir, maxDistance, blocksRaycast, entityRaycast, true + ); } static int l_reload_component(lua::State* L) { @@ -355,6 +371,7 @@ const luaL_Reg entitylib[] = { {"get_all_in_box", lua::wrap}, {"get_all_in_radius", lua::wrap}, {"raycast", lua::wrap}, + {"__world_raycast", lua::wrap}, {"reload_component", lua::wrap}, {nullptr, nullptr} }; From 00d6e28208b176e663f9fb3c695ceb32dde8739b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 16 Jul 2026 21:55:35 +0300 Subject: [PATCH 45/92] cleanup --- src/voxels/blocks_agent.cpp | 49 +++++++++++++------------------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/src/voxels/blocks_agent.cpp b/src/voxels/blocks_agent.cpp index eb96c5e2a..0ffd92276 100644 --- a/src/voxels/blocks_agent.cpp +++ b/src/voxels/blocks_agent.cpp @@ -239,9 +239,8 @@ static inline voxel* raycast_blocks( int iy = std::floor(py); int iz = std::floor(pz); - int stepx = (dx > 0.0f) ? 1 : -1; - int stepy = (dy > 0.0f) ? 1 : -1; - int stepz = (dz > 0.0f) ? 1 : -1; + glm::ivec3 step { + (dx > 0.0f) ? 1 : -1, (dy > 0.0f) ? 1 : -1, (dz > 0.0f) ? 1 : -1}; constexpr float infinity = std::numeric_limits::infinity(); constexpr float epsilon = 1e-6f; // 0.000001 @@ -249,9 +248,9 @@ static inline voxel* raycast_blocks( float tyDelta = (std::fabs(dy) < epsilon) ? infinity : std::fabs(1.0f / dy); float tzDelta = (std::fabs(dz) < epsilon) ? infinity : std::fabs(1.0f / dz); - float xdist = (stepx > 0) ? (ix + 1 - px) : (px - ix); - float ydist = (stepy > 0) ? (iy + 1 - py) : (py - iy); - float zdist = (stepz > 0) ? (iz + 1 - pz) : (pz - iz); + float xdist = (step.x > 0) ? (ix + 1 - px) : (px - ix); + float ydist = (step.y > 0) ? (iy + 1 - py) : (py - iy); + float zdist = (step.z > 0) ? (iz + 1 - pz) : (pz - iz); float txMax = (txDelta < infinity) ? txDelta * xdist : infinity; float tyMax = (tyDelta < infinity) ? tyDelta * ydist : infinity; @@ -272,12 +271,8 @@ static inline voxel* raycast_blocks( && (def.selectable || includeNonSelectable) && (filter == nullptr || filter->empty() || filter->find(def.rt.id) == filter->end())) { - end.x = px + t * dx; - end.y = py + t * dy; - end.z = pz + t * dz; - iend.x = ix; - iend.y = iy; - iend.z = iz; + end = start + t * dir; + iend = {ix, iy, iz}; if (!def.rt.solid) { const std::vector& hitboxes = @@ -312,51 +307,41 @@ static inline voxel* raycast_blocks( if (hit) return voxel; } else { - iend.x = ix; - iend.y = iy; - iend.z = iz; - - norm.x = norm.y = norm.z = 0; - if (steppedIndex == 0) norm.x = -stepx; - if (steppedIndex == 1) norm.y = -stepy; - if (steppedIndex == 2) norm.z = -stepz; + iend = {ix, iy, iz}; + norm = {0, 0, 0}; + norm[steppedIndex] = -step[steppedIndex]; return voxel; } } if (txMax < tyMax) { if (txMax < tzMax) { - ix += stepx; + ix += step.x; t = txMax; txMax += txDelta; steppedIndex = 0; } else { - iz += stepz; + iz += step.z; t = tzMax; tzMax += tzDelta; steppedIndex = 2; } } else { if (tyMax < tzMax) { - iy += stepy; + iy += step.y; t = tyMax; tyMax += tyDelta; steppedIndex = 1; } else { - iz += stepz; + iz += step.z; t = tzMax; tzMax += tzDelta; steppedIndex = 2; } } } - iend.x = ix; - iend.y = iy; - iend.z = iz; - - end.x = px + t * dx; - end.y = py + t * dy; - end.z = pz + t * dz; - norm.x = norm.y = norm.z = 0; + iend = {ix, iy, iz}; + end = start + t * dir; + norm = {0, 0, 0}; return nullptr; } From 605b0d63c34510694e506f1bc867d6b7b860e581 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 16 Jul 2026 22:22:22 +0300 Subject: [PATCH 46/92] update world.raycast params & add blocks_exclusion parameter --- src/logic/scripting/lua/libs/libentity.cpp | 6 +++++- src/voxels/blocks_agent.cpp | 8 ++++---- src/voxels/blocks_agent.hpp | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 50711cf01..7e68ac5ff 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -287,7 +287,7 @@ static int l_world_raycast(lua::State* L) { entityRaycast.ignoredUid = lua::tointeger(L, -1); lua::pop(L); } - if (lua::getfield(L, "ignore_entities")) { + if (lua::getfield(L, "filter_entities")) { load_entities_filter(L, -1, filteredEntities); blocksRaycast.filter = &filteredEntities; lua::pop(L); @@ -301,6 +301,10 @@ static int l_world_raycast(lua::State* L) { blocksRaycast.filter = &filteredBlocks; lua::pop(L); } + if (lua::getfield(L, "blocks_exclusion")) { + blocksRaycast.blocksFilterExcludeMode = lua::toboolean(L, -1); + lua::pop(L); + } if (lua::getfield(L, "nonselect_blocks", 1)) { blocksRaycast.includeNonSelectable = lua::toboolean(L, -1); lua::pop(L); diff --git a/src/voxels/blocks_agent.cpp b/src/voxels/blocks_agent.cpp index 0ffd92276..81a752a70 100644 --- a/src/voxels/blocks_agent.cpp +++ b/src/voxels/blocks_agent.cpp @@ -267,10 +267,10 @@ static inline voxel* raycast_blocks( auto filter = settings.filter; const auto& def = blocks.require(voxel->id); - if (voxel->id != BLOCK_AIR - && (def.selectable || includeNonSelectable) - && (filter == nullptr || filter->empty() || - filter->find(def.rt.id) == filter->end())) { + if (voxel->id != BLOCK_AIR && + (def.selectable || includeNonSelectable) && + (filter == nullptr || (filter->find(def.rt.id) == filter->end()) == + settings.blocksFilterExcludeMode)) { end = start + t * dir; iend = {ix, iy, iz}; diff --git a/src/voxels/blocks_agent.hpp b/src/voxels/blocks_agent.hpp index 3ac4c74d5..22144c618 100644 --- a/src/voxels/blocks_agent.hpp +++ b/src/voxels/blocks_agent.hpp @@ -414,6 +414,7 @@ inline void set_rotation( struct RaycastSettings { const std::set* filter = nullptr; + bool blocksFilterExcludeMode = true; bool includeNonSelectable = false; }; From f6e1f23c841f4fe98dce071a6d5465f1283436de Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 16 Jul 2026 22:24:47 +0300 Subject: [PATCH 47/92] update doc/*/scripting/builtins/libworld.md --- doc/en/scripting/builtins/libworld.md | 41 ++++++++++++++++++++++++++ doc/ru/scripting/builtins/libworld.md | 42 ++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/doc/en/scripting/builtins/libworld.md b/doc/en/scripting/builtins/libworld.md index b2453b34b..54b44fe15 100644 --- a/doc/en/scripting/builtins/libworld.md +++ b/doc/en/scripting/builtins/libworld.md @@ -70,4 +70,45 @@ world.save_chunk_data( -- compressed chunk data data: Bytearray ) + +-- Casts a ray from the start point in the dir direction. +world.raycast( + params: table { + [==[ required parameters ]==] + -- starting point + start: vec3, + -- ray direction vector + dir: vec3, + -- maximum distance + distance: number, + + [==[ additional parameters (entities) ]==] + -- consider entities + entities: bool = true, + -- uid of the ignored entity + ignore_uid: number = 0 (ENTITY_NONE), + -- id of the entity types used for filtering + filter_entities: table = nil, + -- exclusion filtering mode: + -- if true, filter_entities defines the list of ignored entity types + entities_exclusion: bool = false, + + [==[ additional parameters (blocks) ]==] + -- id of the block types used for filtering + filter_blocks: table = nil, + -- exclusion filtering mode: + -- if true, filter_blocks defines the list Ignored + blocks_exclusion: bool = true, + -- include blocks with selectable=false + nonselect_blocks: bool = false; + } +) -> { + block: int or nil, -- block id + entity: int or nil, -- entity uid + endpoint: vec3, -- ray touchpoint + iendpoint: vec3, -- position of the block touched by the ray + length: number, -- ray length + normal: vec3, -- normal vector of the surface touched by the ray +} or nil if the ray did not touch a block or entity +``` ``` diff --git a/doc/ru/scripting/builtins/libworld.md b/doc/ru/scripting/builtins/libworld.md index 215329df3..6b945aa76 100644 --- a/doc/ru/scripting/builtins/libworld.md +++ b/doc/ru/scripting/builtins/libworld.md @@ -69,4 +69,44 @@ world.save_chunk_data( -- сжатые данные чанка data: Bytearray ) -``` \ No newline at end of file + +-- Бросает луч из точки start в направлении dir. +world.raycast( + params: table { + [==[ обязательные параметры ]==] + -- стартовая точка + start: vec3, + -- вектор направления луча + dir: vec3, + -- максимальное расстояние + distance: number, + + [==[ дополнительные параметры (сущности) ]==] + -- учитывать сущности + entities: bool = true, + -- uid игнорируемой сущности + ignore_uid: number = 0 (ENTITY_NONE), + -- id типов сущностей используемых для фильтрации + filter_entities: table = nil, + -- исключающий режим фильтрации: + -- при значении (true) filter_entities определяет список игнорируемых + entities_exclusion: bool = false, + + [==[ дополнительные параметры (блоки) ]==] + -- id типов блоков используемых для фильтрации + filter_blocks: table = nil, + -- исключающий режим фильтрации: + -- при значении (true) filter_blocks определяет список игнорируемых + blocks_exclusion: bool = true, + -- учитывать блоки с selectable=false + nonselect_blocks: bool = false; + } +) -> { + block: int или nil, -- id блока + entity: int или nil, -- uid сущности + endpoint: vec3, -- точка касания луча + iendpoint: vec3, -- позиция блока, которого касается луч + length: number, -- длина луча + normal: vec3, -- вектор нормали поверхности, которой касается луч +} или nil если луч не коснулся блока или сущности +``` From b2ed5122e79718588c6c725702e1a15aaa75e298 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 16 Jul 2026 23:37:08 +0300 Subject: [PATCH 48/92] fix particles docs --- doc/en/particles.md | 9 +++++---- doc/ru/particles.md | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/en/particles.md b/doc/en/particles.md index 66d3858d5..ecded4096 100644 --- a/doc/en/particles.md +++ b/doc/en/particles.md @@ -9,14 +9,14 @@ Particles are a table, all fields of which are optional. | lighting | Lighting. | true | | collision | Collision detection. | true | | max_distance | Maximum distance from the camera at which particles may spawn. | 16.0 | -| spawn_interval | Particle spawn interval in seconds. | 1.0 | +| spawn_interval | Particle spawn interval in seconds. Also initial spawn delay | 1.0 | | lifetime | Average lifetime of particles in seconds. | 5.0 | | lifetime_spread | Maximum deviation of particle lifetime (from 0.0 to 1.0). | 0.2 | | velocity | Initial linear velocity of particles. | {0, 0, 0} | | acceleration | Particles acceleration. | {0, -16, 0} | | explosion | Force of particles explosion on spawn. | {2, 2, 2} | | size | Size of particles. | {0.1, 0.1, 0.1} | -| size_spread | Maximum particle size spread over time. | 0.2 | +| size_spread | Maximum particle size spread. | 0.2 | | angle_spread | Maximum initial rotation angle spread (0 to 1) | 0.0 | | min_angular_vel | Minimum angular velocity (radians per sec). Non-negative. | 0.0 | | max_angular_vel | Maximum angular velocity (radians per sec). Non-negative. | 0.0 | @@ -35,7 +35,8 @@ gfx.particles.emit( count: int, -- particle settings table preset: table, - -- additional particle settings table + -- particle settings table overriding 'preset' parameters + -- use it instead of 'presets' table copying for modification [optional] extension: table ) -> int ``` @@ -63,7 +64,7 @@ Returns the static position or uid of the entity the emitter is bound to. Returns nil if the emitter does not exist. -``lua +```lua gfx.particles.set_origin(id: int, origin: vec3 | int) ``` diff --git a/doc/ru/particles.md b/doc/ru/particles.md index 445ded593..e1e9fb34a 100644 --- a/doc/ru/particles.md +++ b/doc/ru/particles.md @@ -10,14 +10,14 @@ | lighting | Освещение. | true | | collision | Обнаружение столкновений. | true | | max_distance | Максимальная дистанция от камеры, при которой происходит спавн частиц. | 16.0 | -| spawn_interval | Интервал спавна частиц в секундах. | 1.0 | +| spawn_interval | Интервал спавна частиц в секундах, также задержка перед спавном первой. | 1.0 | | lifetime | Среднее время жизни частиц в секундах. | 5.0 | | lifetime_spread | Максимальное отклонение времени жизни частицы (от 0.0 до 1.0). | 0.2 | | velocity | Начальная линейная скорость частиц. | {0, 0, 0} | | acceleration | Ускорение частиц. | {0, -16, 0} | | explosion | Сила разлёта частиц при спавне. | {2, 2, 2} | | size | Размер частиц. | {0.1, 0.1, 0.1} | -| size_spread | Максимальное отклонение времени размера частиц. | 0.2 | +| size_spread | Максимальное отклонение размера частиц. | 0.2 | | angle_spread | Максимальное отклонение начального угла поворота (от 0 до 1) | 0.0 | | min_angular_vel | Минимальная угловая скорость (радианы в сек.). Неотрицательное. | 0.0 | | max_angular_vel | Максимальная угловая скорость (радианы в сек.). Неотрицательное. | 0.0 | @@ -36,7 +36,8 @@ gfx.particles.emit( count: int, -- таблица настроек частиц preset: table, - -- дополнительная таблица настроек частиц + -- таблица настроек частиц перезаписывающая значения параметров + -- используется вместо копирования и модификации 'preset' [опционально] extension: table ) -> int ``` From 39a42365534ebd4c4c0be690e9ea0646e1459a7b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 17 Jul 2026 00:23:16 +0300 Subject: [PATCH 49/92] fix integer raycast filters & add simple raycast test --- dev/tests/raycast.lua | 47 ++++++++++++++++++++++ res/modules/tests_util.lua | 21 ++++++++++ src/logic/scripting/lua/libs/libentity.cpp | 24 +++++++---- src/voxels/blocks_agent.cpp | 2 +- 4 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 dev/tests/raycast.lua diff --git a/dev/tests/raycast.lua b/dev/tests/raycast.lua new file mode 100644 index 000000000..6016989d8 --- /dev/null +++ b/dev/tests/raycast.lua @@ -0,0 +1,47 @@ +local util = require "core:tests_util" + +-- Create world and prepare settings +util.create_demo_world("core:default") +app.set_setting("chunks.load-distance", 3) +app.set_setting("chunks.load-speed", 1) + +-- Create player +local pid = player.create("Xerxes") +player.set_spawnpoint(pid, 0, 100, 0) +player.set_pos(pid, 0, 100, 0) +app.sleep_until(function () return block.get(0, 0, 0) ~= -1 end) +app.sleep_until(function () return block.get(-5, 8, 2) ~= -1 end) + +local sand_idx = block.index("base:sand") +block.set(0, 1, 0, sand_idx) + +local start = {-5, 8, 2} +local target = {0.5, 1.5, 0.5} +local ray = world.raycast({ + start = start, + dir = vec3.normalize(vec3.sub(target, start)), + distance = 10, +}) +debug.print(ray) + +assert(ray, "ray didn't hit") +util.check_int(sand_idx, ray.block, "ray.block") +util.check_vec3({0, 1, 0}, ray.normal, "ray.normal") +util.check_vec3({0.0769, 2, 0.6153}, ray.endpoint, "ray.endpoint") +util.check_vec3({0, 1, 0}, ray.iendpoint, "ray.iendpoint") +util.check_float(7.9807, ray.length, "ray.length") + +ray = world.raycast({ + start = start, + dir = vec3.normalize(vec3.sub(target, start)), + distance = 10, + filter_blocks = {sand_idx}, +}) +debug.print(ray) + +assert(ray, "ray didn't hit") +util.check_int(block.index("core:obstacle"), ray.block, "ray.block") +util.check_vec3({0, 1, 0}, ray.normal, "ray.normal") +util.check_vec3({0.923, 1, 0.3846}, ray.endpoint, "ray.endpoint") +util.check_vec3({0, 0, 0}, ray.iendpoint, "ray.iendpoint") +util.check_float(9.3108, ray.length, "ray.length") diff --git a/res/modules/tests_util.lua b/res/modules/tests_util.lua index 1e2207c24..e4ab73bc3 100644 --- a/res/modules/tests_util.lua +++ b/res/modules/tests_util.lua @@ -5,4 +5,25 @@ function util.create_demo_world(generator) app.new_world("demo", "2019", generator or "core:default") end +function util.check_vec3(expected, fact, name, delta) + delta = delta or 1e-4 + for i=1,3 do + assert(math.abs(fact[i] - expected[i]) < delta, + name.."["..i.."] expected: "..expected[i]..", fact: "..fact[i]) + end +end + +function util.check_float(expected, fact, name, delta) + delta = delta or 1e-4 + local actual_delta = math.abs(fact - expected) + assert(actual_delta < delta, + name.." expected: "..expected..", fact: "..fact.. + " (delta: "..actual_delta.." > "..delta..")") +end + +function util.check_int(expected, fact, name) + assert(fact == expected, + name.." expected: "..expected..", fact: "..fact) +end + return util diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 7e68ac5ff..64e8e126e 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -230,10 +230,14 @@ static void load_blocks_filter(lua::State* L, int idx, std::set& dst) int count = lua::objlen(L, idx); for (int i = 0; i < count; i++) { lua::rawgeti(L, i + 1, idx); - auto blockName = std::string(lua::tostring(L, -1)); - const Block* block = content->blocks.find(blockName); - if (block != nullptr) { - dst.insert(block->rt.id); + if (lua::isnumber(L, -1)) { + dst.insert(lua::touinteger(L, -1)); + } else { + auto blockName = std::string(lua::tostring(L, -1)); + const Block* block = content->blocks.find(blockName); + if (block != nullptr) { + dst.insert(block->rt.id); + } } lua::pop(L); } @@ -243,10 +247,14 @@ static void load_entities_filter(lua::State* L, int idx, std::set int count = lua::objlen(L, idx); for (int i = 0; i < count; i++) { lua::rawgeti(L, i + 1, idx); - auto entityName = std::string(lua::tostring(L, -1)); - const EntityDef* entity = content->entities.find(entityName); - if (entity != nullptr) { - dst.insert(entity->rt.id); + if (lua::isnumber(L, -1)) { + dst.insert(lua::touinteger(L, -1)); + } else { + auto entityName = std::string(lua::tostring(L, -1)); + const EntityDef* entity = content->entities.find(entityName); + if (entity != nullptr) { + dst.insert(entity->rt.id); + } } lua::pop(L); } diff --git a/src/voxels/blocks_agent.cpp b/src/voxels/blocks_agent.cpp index 81a752a70..9d1f0cc17 100644 --- a/src/voxels/blocks_agent.cpp +++ b/src/voxels/blocks_agent.cpp @@ -269,7 +269,7 @@ static inline voxel* raycast_blocks( const auto& def = blocks.require(voxel->id); if (voxel->id != BLOCK_AIR && (def.selectable || includeNonSelectable) && - (filter == nullptr || (filter->find(def.rt.id) == filter->end()) == + ((filter == nullptr || filter->find(def.rt.id) == filter->end()) == settings.blocksFilterExcludeMode)) { end = start + t * dir; iend = {ix, iy, iz}; From 3bd297f56279eb46c0f55c6f027a9b3ab44ac44e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 17 Jul 2026 01:46:11 +0300 Subject: [PATCH 50/92] update doc/*/scripting/builtins/libentities.md, libblock.md --- doc/en/scripting/builtins/libblock.md | 2 ++ doc/en/scripting/builtins/libentities.md | 2 ++ doc/ru/scripting/builtins/libblock.md | 4 +++- doc/ru/scripting/builtins/libentities.md | 4 +++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/en/scripting/builtins/libblock.md b/doc/en/scripting/builtins/libblock.md index db407e92a..4513d57cf 100644 --- a/doc/en/scripting/builtins/libblock.md +++ b/doc/en/scripting/builtins/libblock.md @@ -159,6 +159,8 @@ The function returns a table with the results or nil if the ray does not hit any The result will use the destination table instead of creating a new one if the optional argument specified. +Use `world.raycast(...)` for more detailed configuration. + ## Model and physics ```lua diff --git a/doc/en/scripting/builtins/libentities.md b/doc/en/scripting/builtins/libentities.md index d65d95f05..e34cd7a53 100644 --- a/doc/en/scripting/builtins/libentities.md +++ b/doc/en/scripting/builtins/libentities.md @@ -71,3 +71,5 @@ entities.raycast(start: vec3, dir: vec3, max_distance: number, ignore: int, [opt The function is an extended version of [block.raycast](libblock.md#raycast). Returns a table with the results if the ray touches a block or entity. Accordingly, this will affect the presence of the *entity* and *block* fields. + +Use `world.raycast(...)` for more detailed configuration. diff --git a/doc/ru/scripting/builtins/libblock.md b/doc/ru/scripting/builtins/libblock.md index b7af3e462..9725d2b3d 100644 --- a/doc/ru/scripting/builtins/libblock.md +++ b/doc/ru/scripting/builtins/libblock.md @@ -115,6 +115,8 @@ block.raycast(start: vec3, dir: vec3, max_distance: number, [опциональ Для результата будет использоваться целевая (dest) таблица вместо создания новой, если указан опциональный аргумент. +Для более тонкой настройки используйте `world.raycast(...)`. + ## Вращение ```lua @@ -209,4 +211,4 @@ block.get_field( name: string, [опционально] index: int = 0 ) -> any | nil -``` \ No newline at end of file +``` diff --git a/doc/ru/scripting/builtins/libentities.md b/doc/ru/scripting/builtins/libentities.md index f37dc1b32..27d8d03b1 100644 --- a/doc/ru/scripting/builtins/libentities.md +++ b/doc/ru/scripting/builtins/libentities.md @@ -69,4 +69,6 @@ entities.raycast(start: vec3, dir: vec3, max_distance: number, ignore: int, [opt Функция является расширенным вариантом [block.raycast](libblock.md#raycast). Возвращает таблицу с результатами если луч касается блока, либо сущности. -Соответственно это повлияет на наличие полей *entity* и *block*. \ No newline at end of file +Соответственно это повлияет на наличие полей *entity* и *block*. + +Для более тонкой настройки используйте `world.raycast(...)`. From 279fb92b94a0f8b8d28ed141b54b160450ec91f7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 13:14:38 +0300 Subject: [PATCH 51/92] fix canvas:encode produces flipped images for png --- src/coders/png.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coders/png.cpp b/src/coders/png.cpp index 030844352..18cc0a474 100644 --- a/src/coders/png.cpp +++ b/src/coders/png.cpp @@ -49,9 +49,9 @@ static util::Buffer write_to_memory(uint width, uint height, const ubyte* png_write_info(png_ptr, info_ptr); auto row = std::make_unique(pixsize * width); - for (uint y = 0; y < height; y++) { - for (uint x = 0; x < width; x++) { - for (uint i = 0; i < pixsize; i++) { + for (int y = height-1; y >= 0; y--) { + for (int x = 0; x < width; x++) { + for (int i = 0; i < pixsize; i++) { row[x * pixsize + i] = (png_byte)data[(y * width + x) * pixsize + i]; } From a5596ed5cd65bc62b983ec9dff3263beeced2dd7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 13:25:04 +0300 Subject: [PATCH 52/92] fix canvas:line lines clipping --- src/graphics/core/ImageData.cpp | 121 +++++++++++++----- .../lua/usertypes/lua_type_canvas.cpp | 11 +- 2 files changed, 93 insertions(+), 39 deletions(-) diff --git a/src/graphics/core/ImageData.cpp b/src/graphics/core/ImageData.cpp index 6875d7ab9..5f7761188 100644 --- a/src/graphics/core/ImageData.cpp +++ b/src/graphics/core/ImageData.cpp @@ -1,5 +1,7 @@ #include "ImageData.hpp" +#include "debug/Logger.hpp" + #include #include #include @@ -7,6 +9,8 @@ #include #include +static debug::Logger logger("image-data"); + ImageData::ImageData(ImageFormat format, uint width, uint height) : format(format), width(width), height(height) { size_t pixsize; @@ -106,47 +110,83 @@ std::unique_ptr ImageData::cropped(int x, int y, int width, int heigh return subImage; } -static bool clip_line(int& x1, int& y1, int& x2, int& y2, int width, int height) { - const int left = 0; - const int right = width; - const int bottom = 0; - const int top = height; +static bool clip_line( + int& x0, int& y0, int& x1, int& y1, int width, int height +) { + if (width <= 0 || height <= 0) { + return false; + } + constexpr int INSIDE = 0; + constexpr int LEFT = 1; + constexpr int RIGHT = 2; + constexpr int BOTTOM = 4; + constexpr int TOP = 8; - int dx = x2 - x1; - int dy = y2 - y1; + const int xmin = 0; + const int ymin = 0; + const int xmax = width - 1; + const int ymax = height - 1; - float t0 = 0.0f; - float t1 = 1.0f; + auto outcode = [&](int x, int y) { + int code = INSIDE; - auto clip = [](int p, int q, float& t0, float& t1) { - if (p == 0) { - return q >= 0; - } - float t = static_cast(q) / p; - if (p < 0) { - if (t > t1) return false; - if (t > t0) t0 = t; - } else { - if (t < t0) return false; - if (t < t1) t1 = t; - } - return true; + if (x < xmin) + code |= LEFT; + else if (x > xmax) + code |= RIGHT; + + if (y < ymin) + code |= BOTTOM; + else if (y > ymax) + code |= TOP; + + return code; }; - if (!clip(-dx, x1 - left, t0, t1)) return false; - if (!clip( dx, right - x1, t0, t1)) return false; - if (!clip(-dy, y1 - bottom, t0, t1)) return false; - if (!clip( dy, top - y1, t0, t1)) return false; + int c0 = outcode(x0, y0); + int c1 = outcode(x1, y1); - if (t1 < 1.0f) { - x2 = x1 + static_cast(std::round(t1 * dx)); - y2 = y1 + static_cast(std::round(t1 * dy)); + while (true) { + if (!(c0 | c1)) { + return true; + } + if (c0 & c1) { + return false; + } + + const int out = c0 ? c0 : c1; + + double x = 0.0; + double y = 0.0; + + if (out & TOP) { + y = ymax; + x = x0 + (x1 - x0) * static_cast(ymax - y0) / (y1 - y0); + } else if (out & BOTTOM) { + y = ymin; + x = x0 + (x1 - x0) * static_cast(ymin - y0) / (y1 - y0); + } else if (out & RIGHT) { + x = xmax; + y = y0 + (y1 - y0) * static_cast(xmax - x0) / (x1 - x0); + } else { // LEFT + x = xmin; + y = y0 + (y1 - y0) * static_cast(xmin - x0) / (x1 - x0); + } + + if (out == c0) { + x0 = std::clamp(static_cast(std::lround(x)), xmin, xmax); + y0 = std::clamp(static_cast(std::lround(y)), ymin, ymax); + c0 = outcode(x0, y0); + } else { + x1 = std::clamp(static_cast(std::lround(x)), xmin, xmax); + y1 = std::clamp(static_cast(std::lround(y)), ymin, ymax); + c1 = outcode(x1, y1); + } } - if (t0 > 0.0f) { - x1 = x1 + static_cast(std::round(t0 * dx)); - y1 = y1 + static_cast(std::round(t0 * dy)); - } - return true; +} + +static bool is_point_outside(int x, int y, int width, int height) { + return x < 0 || y < 0 || x >= width || y >= height; } template @@ -155,11 +195,24 @@ static void draw_line(ImageData& image, int x1, int y1, int x2, int y2, const gl uint width = image.getWidth(); uint height = image.getHeight(); + glm::ivec4 init {x1, y1, x2, y2}; + if ((x1 < 0 || x1 >= width || x2 < 0 || x2 >= width || y1 < 0 || y1 >= height || y2 < 0 || y2 >= height) && !clip_line(x1, y1, x2, y2, width, height)) { return; } + // spam info for bug report + if (is_point_outside(x1, y1, width, height) || is_point_outside(x2, y2, width, height)) { + logger.warning() << "clip_line fault: [" << init.x << ", " << init.y + << "] - [" << init.z << ", " << init.w << "] --> [" + << x1 << ", " << y1 << "] - [" << x2 << ", " << y2 + << "]"; + x1 = glm::clamp(x1, 0, static_cast(width) - 1); + y1 = glm::clamp(y1, 0, static_cast(height) - 1); + x2 = glm::clamp(x2, 0, static_cast(width) - 1); + y2 = glm::clamp(y2, 0, static_cast(height) - 1); + } int dx = std::abs(x2 - x1); int dy = -std::abs(y2 - y1); diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp index 37c53abd9..c670e23df 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp @@ -81,16 +81,17 @@ union RGBA { uint8_t arr[4]; uint32_t rgba; }; +static_assert(sizeof(RGBA) == 4); -static RGBA* get_at(const ImageData& data, uint index) { - if (index >= data.getWidth() * data.getHeight()) { - return nullptr; - } +static RGBA* get_at(const ImageData& data, size_t index) { return reinterpret_cast(data.getData() + index * sizeof(RGBA)); } static RGBA* get_at(const ImageData& data, uint x, uint y) { - return get_at(data, y * data.getWidth() + x); + if (x >= data.getWidth() || y >= data.getHeight()) { + return nullptr; + } + return get_at(data, static_cast(y) * data.getWidth() + x); } static RGBA* get_at(State* L, uint x, uint y) { From 586ab3948323e2160ac10198c97528d891ab05d8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 13:26:49 +0300 Subject: [PATCH 53/92] add canvas comparsion to asserts.equals --- res/modules/internal/asserts.lua | 73 +++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/res/modules/internal/asserts.lua b/res/modules/internal/asserts.lua index 2317bb7ce..5c52de875 100644 --- a/res/modules/internal/asserts.lua +++ b/res/modules/internal/asserts.lua @@ -1,8 +1,77 @@ local this = {} +local app = __vc_app -function this.equals(expected, fact) +local function diff_canvas(expected, fact) + this.equals(expected.width, fact.width, "image width") + this.equals(expected.width, fact.width, "image height") + + local diff = Canvas({expected.width, expected.height}) + + local mismatches = 0 + + for y = 0, expected.height - 1 do + for x = 0, expected.width - 1 do + local ca = expected:at(x, y) + local cb = fact:at(x, y) + + if ca == cb then + diff:set(x, y, ca) + else + diff:set(x, y, 0xff0000ff) + mismatches = mismatches + 1 + end + end + end + + return diff, mismatches +end + +local TEST_ARTIFACTS_PATH = "export:test-results" + +local function get_test_artifact_path(name) + local path = TEST_ARTIFACTS_PATH + if not file.isdir(path) then + file.mkdirs(path) + end + return file.join(path, name) +end + +function this.equals(expected, fact, prefix) + local mt = getmetatable(expected) + if mt == Canvas then + local diff, mismatches = diff_canvas(expected, fact) + if mismatches > 0 then + local test_name = app.script or random.uuid() + local path_diff = get_test_artifact_path( + string.format("diff_%s.png", test_name) + ) + local path_fact = get_test_artifact_path( + string.format("fact_%s.png", test_name) + ) + local path_expected = get_test_artifact_path( + string.format("expected_%s.png", test_name) + ) + file.write_bytes(path_diff, diff:encode('png')) + file.write_bytes(path_fact, fact:encode('png')) + file.write_bytes(path_expected, expected:encode('png')) + local total_pixels = expected.width * expected.height + assert(mismatches == 0, string.format( + "%s pixels of %s (%s%%) are different, diff saved as %s", + mismatches, + total_pixels, + math.round(mismatches / total_pixels * 100), + string.escape(path_diff)) + ) + end + return + end + if prefix then + prefix = prefix .. " " + else + prefix = "" + end assert(fact == expected, string.format( - "(fact == expected) assertion failed\n Expected: %s\n Fact: %s", + prefix .. "(fact == expected) assertion failed\n Expected: %s\n Fact: %s", expected, fact )) end From 917806a7ef8f67dee14a6a258ab3e9ae357a89d3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 13:27:27 +0300 Subject: [PATCH 54/92] add canvas lines test --- dev/tests/attachments/line_clipping_test.png | Bin 0 -> 1523 bytes dev/tests/canvas_lines.lua | 51 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 dev/tests/attachments/line_clipping_test.png create mode 100644 dev/tests/canvas_lines.lua diff --git a/dev/tests/attachments/line_clipping_test.png b/dev/tests/attachments/line_clipping_test.png new file mode 100644 index 0000000000000000000000000000000000000000..bb20d18415b55895486fc5ec65ebfcb54c2d6b6f GIT binary patch literal 1523 zcmVK^PVmfgrKJFx)a61v5=>Eu2#vd395Ypb#UO$uAVZ0toAT&1Kdw=%Z zb6$G%-#yQqCyU1~JbRD*?7jc;-uu4q>6~Ngd)B6N9aBHO{M=nS`tR4DOUt|C*N{EO z-Z{qzoWc*Le+W`UOZlzl{>^^6|3wM}R=(^!kMyVKKiGr7%9owFY5oNOY(rq>%g(!V zyjcLS4uO>~J4+|{Zvj9Yg7JoxFF2pB?iK;SG6Hw$7_Qq7&T?#GF6}=8fO!Nq?8{E< zx=s`Tctl|3%g&lby+3Q7I3VaZtQcDIWbKM>^auby@BEZ2hL*J1vYa0c;C|RQ1oE== zUQQH%xjEku$jR37Ap*eC3E$5@9XI`BOPcH{AK>eJ_Ws>3$MeAO--m+70T^M}d<#CbUTVn{;C)X|aEC9d|tVz!O7TExRA&5!t%{Ey9 zfFW2)iJey20e~TxOQ~&kMF4;yxKGJFmc;;oA%OB5Y>NVbwh@#r9|Qn^HW5fDAA|sa zwh%}u9|Qq_HV{ZEAA|vbx)De#9|Qt`IuS@LAA|ycx)4Y$9|Qw{IuJ-MAA|z{-3X+Y z4+;Q)P6Qgt2PFVN7XmHigCYQ+1A(UUK^Xv$jX+!Zpb!AaM4+*JPznHKA<$YrCAgHN)s0;uci=ejhp+W#~41yZV zhe`oJS_HM04;2G|Gze-gA1Vg``w-M#J~RLTwjpStd}ss!tV7UB`Opvmh(pj)`Op{u zSVquV`OqK$m`BiJ`Oqi;ctp@@`S9W6;-jMZHC;? z;CBSjdi;A|d*|G`Zw;#{Vc73nhojvopviN9=Q&@(*0NG*4#2w;zE%-51>n=!ev1g2 z0T6qs*BXK*0Ia>vZwW#50Jbjjw1S{&0DD*XT0l@OfKCfLyn9YeP^4fZX@)?AP@~eYxyA> z5Qqh!z5EdA2t)#?rTh@d2*d%Xt^5$F2t)y>wfqo?2*d!Wz5Ebq2t)vArTh>{2#Nz} zt^5!v2#Nw|wfqnX2#Nt{z5I~U2#Nr(@*h(YL3RLE{(MRy$O^#9Urq@G*#KDiamkG! z3jiyBU2-Bg9DtR-Ex8aJ3c$+Wm%|aPX9gO7#KFb&T}JkG5}V7p4=mVA^LIersX74 zfR&#&{rALp8-TcvieEY>OYnUGtbr&miofq*JP!=laq;#4^nD0l-h03G`vSUu!_d@E zn;tJAo$J&df9E>J8w}TRb_D)(HRhb#J_XpYFC$?(c)mJ_y%gVc5@_Yi%JbF1nsu1f z0IYnOxks>ON?$bvSo!iZHytb`dIYfYW#@j_@2>HWiy8r}eA#)P^QXR#9tA!Eu<~W+ z-3ebTf<*vUzU+)Q7SHx$5WEFo<;#w{bPN%5{U-#o0IYo3S-an23Bd~hD_?fDZnk-c z-~oV@FFSj8TFoK&1z_dNPO5Eo&j?On Date: Sat, 18 Jul 2026 13:54:50 +0300 Subject: [PATCH 55/92] update asserts --- res/modules/internal/asserts.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/modules/internal/asserts.lua b/res/modules/internal/asserts.lua index 5c52de875..b84d5d37f 100644 --- a/res/modules/internal/asserts.lua +++ b/res/modules/internal/asserts.lua @@ -41,15 +41,15 @@ function this.equals(expected, fact, prefix) if mt == Canvas then local diff, mismatches = diff_canvas(expected, fact) if mismatches > 0 then - local test_name = app.script or random.uuid() + prefix = prefix or app.script or base64.encode_urlsafe(random.bytes(6)) local path_diff = get_test_artifact_path( - string.format("diff_%s.png", test_name) + string.format("%s.diff.png", prefix) ) local path_fact = get_test_artifact_path( - string.format("fact_%s.png", test_name) + string.format("%s.fact.png", prefix) ) local path_expected = get_test_artifact_path( - string.format("expected_%s.png", test_name) + string.format("%s.expected.png", prefix) ) file.write_bytes(path_diff, diff:encode('png')) file.write_bytes(path_fact, fact:encode('png')) @@ -59,7 +59,7 @@ function this.equals(expected, fact, prefix) "%s pixels of %s (%s%%) are different, diff saved as %s", mismatches, total_pixels, - math.round(mismatches / total_pixels * 100), + math.round(mismatches / total_pixels * 100, 2), string.escape(path_diff)) ) end From e4a11f182efd7096bd32bf772a370f2a4c398764 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 19:09:43 +0300 Subject: [PATCH 56/92] add on_player_set entity component event --- res/scripts/components/mob.lua | 6 ++++-- res/scripts/components/player.lua | 12 ++++-------- src/logic/LevelController.cpp | 3 +-- src/logic/scripting/scripting.hpp | 1 + src/logic/scripting/scripting_entities.cpp | 10 ++++++++++ src/objects/Entity.cpp | 7 ++++++- src/objects/Player.cpp | 2 +- src/objects/Player.hpp | 2 +- src/objects/ScriptComponents.hpp | 1 + 9 files changed, 29 insertions(+), 15 deletions(-) diff --git a/res/scripts/components/mob.lua b/res/scripts/components/mob.lua index 92c2a3723..9f2dc53b7 100644 --- a/res/scripts/components/mob.lua +++ b/res/scripts/components/mob.lua @@ -142,14 +142,16 @@ end function is_flight() return flight end -function set_flight(flag) flight = flag end +function set_flight(flag) + flight = flag + body:set_gravity_scale(flight and 0.0 or props.gravity_scale) +end local prev_angle = (vec2.angle({dir[3], dir[1]})) % 360 function on_physics_update(delta) local grounded = body:is_grounded() body:set_vdamping(flight) - body:set_gravity_scale(flight and 0.0 or props.gravity_scale) body:set_linear_damping( (flight or not grounded) and props.air_damping or props.ground_damping ) diff --git a/res/scripts/components/player.lua b/res/scripts/components/player.lua index 4f2ec53a8..e0f8cb900 100644 --- a/res/scripts/components/player.lua +++ b/res/scripts/components/player.lua @@ -58,16 +58,12 @@ local function process_player_inputs(pid, rot, delta) end end -local prepared = false -body:set_enabled(false) -mob.set_flight(true) +function on_player_set(pid) + mob.set_flight(player.is_flight(pid)) + body:set_body_type(player.is_noclip(pid) and "kinematic" or "dynamic") +end function on_physics_update(delta) - if not prepared then - prepared = true - body:set_enabled(true) - end - local pid = entity:get_player() if pid == -1 then return diff --git a/src/logic/LevelController.cpp b/src/logic/LevelController.cpp index ae0fa6798..704949851 100644 --- a/src/logic/LevelController.cpp +++ b/src/logic/LevelController.cpp @@ -82,7 +82,6 @@ void LevelController::update(float delta, bool pause) { continue; } player->rotationInterpolation.updateTimer(delta); - player->updateEntity(); glm::vec3 position = player->getPosition(); player->chunks->configure( glm::floor(position.x), @@ -96,9 +95,9 @@ void LevelController::update(float delta, bool pause) { *player, player.get() == clientPlayer ); + player->updateEntity(); } if (!pause) { - // update all objects that needed blocks->update(delta, settings.chunks.padding.get()); level->entities->update(delta); for (const auto& [_, player] : *level->players) { diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index 9d29528b8..4c71a3aa6 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -142,6 +142,7 @@ namespace scripting { void on_entity_grounded(const Entity& entity, float force); void on_entity_fall(const Entity& entity); void on_entity_save(const Entity& entity); + void on_entity_player_set(const Entity& entity, int64_t pid); void on_entities_update(int tps, int parts, int part); void on_entities_physics_update(float delta); void on_entities_render(float delta); diff --git a/src/logic/scripting/scripting_entities.cpp b/src/logic/scripting/scripting_entities.cpp index 375b382a9..ca9ed6db1 100644 --- a/src/logic/scripting/scripting_entities.cpp +++ b/src/logic/scripting/scripting_entities.cpp @@ -71,6 +71,7 @@ static void init_component_events( funcsset.on_aim_off = lua::hasfield(L, "on_aim_off"); funcsset.on_attacked = lua::hasfield(L, "on_attacked"); funcsset.on_used = lua::hasfield(L, "on_used"); + funcsset.on_player_set = lua::hasfield(L, "on_player_set"); lua::pop(L); } @@ -238,6 +239,15 @@ void scripting::on_entity_save(const Entity& entity) { ); } +void scripting::on_entity_player_set(const Entity& entity, int64_t pid) { + process_entity_callback( + entity, + "on_player_set", + &EntityFuncsSet::on_player_set, + [pid](auto L) { return lua::pushinteger(L, pid); } + ); +} + void scripting::on_sensor_enter( const Entity& entity, size_t index, entityid_t oid ) { diff --git a/src/objects/Entity.cpp b/src/objects/Entity.cpp index 0bf898351..6629d2981 100644 --- a/src/objects/Entity.cpp +++ b/src/objects/Entity.cpp @@ -113,6 +113,11 @@ int64_t Entity::getPlayer() const { } void Entity::setPlayer(int64_t id) { - registry.get(entity).player = id; + auto& eid = registry.get(entity); + if (eid.player == id) { + return; + } + eid.player = id; + scripting::on_entity_player_set(*this, id); } diff --git a/src/objects/Player.cpp b/src/objects/Player.cpp index fec65d7de..c0fe61744 100644 --- a/src/objects/Player.cpp +++ b/src/objects/Player.cpp @@ -81,7 +81,7 @@ void Player::updateEntity() { } } -Hitbox* Player::getHitbox() { +Hitbox* Player::getHitbox() const { if (auto entity = level.entities->get(eid)) { return &entity->getRigidbody().hitbox; } diff --git a/src/objects/Player.hpp b/src/objects/Player.hpp index a095ccfb7..54ed70023 100644 --- a/src/objects/Player.hpp +++ b/src/objects/Player.hpp @@ -135,7 +135,7 @@ public: bool isCurrentCameraBuiltin() const; - Hitbox* getHitbox(); + Hitbox* getHitbox() const; void setSpawnPoint(glm::vec3 point); glm::vec3 getSpawnPoint() const; diff --git a/src/objects/ScriptComponents.hpp b/src/objects/ScriptComponents.hpp index 2711d4823..92c21d49f 100644 --- a/src/objects/ScriptComponents.hpp +++ b/src/objects/ScriptComponents.hpp @@ -17,6 +17,7 @@ struct EntityFuncsSet { bool on_aim_off; bool on_attacked; bool on_used; + bool on_player_set; }; struct UserComponent { From b99be08e88770bff77e19f22424f1449e4956895 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 19:46:21 +0300 Subject: [PATCH 57/92] update doc/*/scripting/ecs.md --- doc/en/scripting/ecs.md | 6 ++++++ doc/ru/scripting/ecs.md | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index 878452928..52be70363 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -264,3 +264,9 @@ function on_used(playerid: int) ``` Called when an entity is used (RMB by entity). The player ID is passed as an argument. + +```lua +function on_player_set(playerid: int) +``` + +Called when an entity is attached to a player. diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index 1a5f6ccf3..d659f342d 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -265,3 +265,8 @@ function on_used(playerid: int) Вызывается при использовании сущности (ПКМ по сущности). ID игрока передается в качестве аргумента. +```lua +function on_player_set(playerid: int) +``` + +Вызывается при прикреплении сущности к игроку. From 8164a16db830ee1a22ab2bd922c664a43fb95dcb Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 19 Jul 2026 19:41:52 +0300 Subject: [PATCH 58/92] add counter-based manual gc trigger for FFIBytearray --- res/modules/internal/bytearray.lua | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 739bb7cea..bc0907869 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -12,18 +12,34 @@ FFI.cdef[[ } bytearray_t; ]] -local malloc = FFI.C.malloc -local free = FFI.C.free local FFIBytearray local bytearray_type +local allocated_bytes = 0 +local GC_THRESHOLD = 50 * 1024 * 1024 + +local function malloc(size) + local raw = FFI.C.malloc(size) + if raw == nil then + return nil + end + + allocated_bytes = allocated_bytes + size + + if allocated_bytes >= GC_THRESHOLD then + collectgarbage("step", 200) + allocated_bytes = 0 + end + + return FFI.gc(raw, FFI.C.free) +end + local function grow_buffer(self, elems) local new_capacity = math.ceil(self.capacity / 0.75 + elems) local prev = self.bytes self.bytes = malloc(new_capacity) FFI.copy(self.bytes, prev, self.size) self.capacity = new_capacity - free(prev) end local function trim_buffer(self) @@ -35,7 +51,6 @@ local function trim_buffer(self) self.bytes = malloc(size) FFI.copy(self.bytes, prev, self.size) self.capacity = size - free(prev) end local function count_elements(b) @@ -114,7 +129,6 @@ local function reserve(self, new_capacity) self.bytes = malloc(new_capacity) FFI.copy(self.bytes, prev, self.size) self.capacity = new_capacity - free(prev) end local function get_capacity(self) @@ -173,9 +187,7 @@ local bytearray_mt = { __len = function(self) return tonumber(self.size) end, - __gc = function(self) - free(self.bytes) - end, + __gc = function(self) end, __ipairs = function(self) local i = 0 return function() From 46b4aab6dce9e7c195b5c29584168f8e5b8fbb52 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 19 Jul 2026 22:20:23 +0300 Subject: [PATCH 59/92] fix newly created raycast bug --- src/voxels/blocks_agent.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/voxels/blocks_agent.cpp b/src/voxels/blocks_agent.cpp index 9d1f0cc17..d72ddcceb 100644 --- a/src/voxels/blocks_agent.cpp +++ b/src/voxels/blocks_agent.cpp @@ -309,7 +309,9 @@ static inline voxel* raycast_blocks( } else { iend = {ix, iy, iz}; norm = {0, 0, 0}; - norm[steppedIndex] = -step[steppedIndex]; + if (steppedIndex != -1) { + norm[steppedIndex] = -step[steppedIndex]; + } return voxel; } } From 8e719eace65bae6578849fd57c50895c19d0f05f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 19 Jul 2026 22:43:50 +0300 Subject: [PATCH 60/92] add missing inventory function --- doc/en/scripting/builtins/libinventory.md | 14 +++++++++++++- doc/ru/scripting/builtins/libinventory.md | 16 ++++++++++++++-- src/items/ItemStack.hpp | 10 ++++++++++ src/logic/scripting/lua/libs/libinventory.cpp | 9 +++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/doc/en/scripting/builtins/libinventory.md b/doc/en/scripting/builtins/libinventory.md index 8cf2d7c3e..172ec63e6 100644 --- a/doc/en/scripting/builtins/libinventory.md +++ b/doc/en/scripting/builtins/libinventory.md @@ -162,7 +162,19 @@ inventory.get_all_data( invid: int, -- slot index slot: int, -) -> table +) -> table | nil + +-- Sets the values ​​of all local item properties. +inventory.set_all_data( + -- inventory id + invid: int, + -- slot index + slot: int, + -- property:value table + fields: table, + -- clear other property values + [optional] clear: boolean = false +) ``` ```lua diff --git a/doc/ru/scripting/builtins/libinventory.md b/doc/ru/scripting/builtins/libinventory.md index 50ed8dccb..4a836f83b 100644 --- a/doc/ru/scripting/builtins/libinventory.md +++ b/doc/ru/scripting/builtins/libinventory.md @@ -170,7 +170,19 @@ inventory.get_all_data( invid: int, -- индекс слота slot: int, -) -> table +) -> table | nil + +-- Устанавливает значения локальных cвойств предмета. +inventory.set_all_data( + -- id инвентаря + invid: int, + -- индекс слота + slot: int, + -- таблица cвойство:значение + fields: table, + -- удалить остальные значения свойств + [опционально] clear: boolean = false +) ``` ```lua @@ -222,4 +234,4 @@ inventory.use( -- индекс слота slot: int ) -``` \ No newline at end of file +``` diff --git a/src/items/ItemStack.hpp b/src/items/ItemStack.hpp index 91f2d3e74..4896bc39e 100644 --- a/src/items/ItemStack.hpp +++ b/src/items/ItemStack.hpp @@ -55,6 +55,16 @@ public: return fields; } + void setFields(dv::value&& table, bool clear) { + if (clear) { + fields = std::move(table); + return; + } + for (auto&& [key, value] : table.asObject()) { + fields[std::move(key)] = std::move(value); + } + } + bool hasFields() const { return fields != nullptr; } diff --git a/src/logic/scripting/lua/libs/libinventory.cpp b/src/logic/scripting/lua/libs/libinventory.cpp index 3ec9d2ca3..2aea52394 100644 --- a/src/logic/scripting/lua/libs/libinventory.cpp +++ b/src/logic/scripting/lua/libs/libinventory.cpp @@ -237,6 +237,14 @@ static int l_get_all_data(lua::State* L, ItemStack& stack) { return lua::pushvalue(L, stack.getFields()); } +static int l_set_all_data(lua::State* L, ItemStack& stack) { + if (!lua::istable(L, 3)) { + throw std::runtime_error("table expected as argument 1"); + } + stack.setFields(lua::tovalue(L, 3), lua::toboolean(L, 4)); + return 0; +} + static int l_has_data(lua::State* L, ItemStack& stack) { auto key = lua::tostring(L, 3); if (key == nullptr) { @@ -267,6 +275,7 @@ const luaL_Reg inventorylib[] = { {"get_data", lua::wrap>}, {"set_data", lua::wrap>}, {"get_all_data", lua::wrap>}, + {"set_all_data", lua::wrap>}, {"has_data", lua::wrap>}, {"create", lua::wrap}, {"remove", lua::wrap}, From d55b494bb0a2c21e3f95f4f232c51ff1509abf20 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 20 Jul 2026 00:05:50 +0300 Subject: [PATCH 61/92] update bytearray,lua --- res/modules/internal/bytearray.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index bc0907869..58b33a520 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -12,6 +12,8 @@ FFI.cdef[[ } bytearray_t; ]] +local free = FFI.C.free + local FFIBytearray local bytearray_type @@ -31,7 +33,7 @@ local function malloc(size) allocated_bytes = 0 end - return FFI.gc(raw, FFI.C.free) + return raw end local function grow_buffer(self, elems) @@ -40,6 +42,7 @@ local function grow_buffer(self, elems) self.bytes = malloc(new_capacity) FFI.copy(self.bytes, prev, self.size) self.capacity = new_capacity + free(prev) end local function trim_buffer(self) @@ -51,6 +54,7 @@ local function trim_buffer(self) self.bytes = malloc(size) FFI.copy(self.bytes, prev, self.size) self.capacity = size + free(prev) end local function count_elements(b) @@ -129,6 +133,7 @@ local function reserve(self, new_capacity) self.bytes = malloc(new_capacity) FFI.copy(self.bytes, prev, self.size) self.capacity = new_capacity + free(prev) end local function get_capacity(self) @@ -187,7 +192,9 @@ local bytearray_mt = { __len = function(self) return tonumber(self.size) end, - __gc = function(self) end, + __gc = function(self) + free(self.bytes) + end, __ipairs = function(self) local i = 0 return function() From a0b7e81a94d8a4f2750fbbd507ba6bd2e688f8b4 Mon Sep 17 00:00:00 2001 From: Onran Date: Wed, 1 Jul 2026 20:34:35 +0900 Subject: [PATCH 62/92] fix Skeleton::setConfig --- src/objects/rigging.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/objects/rigging.cpp b/src/objects/rigging.cpp index f686bfbcf..f47f7c733 100644 --- a/src/objects/rigging.cpp +++ b/src/objects/rigging.cpp @@ -88,12 +88,22 @@ void Skeleton::deserialize(const dv::value& root) { void Skeleton::setConfig(std::shared_ptr rigConfig) { config = std::move(rigConfig); + + const auto& bones = config->getBones(); + pose.matrices.resize( - config->getBones().size(), glm::mat4(1.0f) + bones.size(), glm::mat4(1.0f) ); calculated.matrices.resize( - config->getBones().size(), glm::mat4(1.0f) + bones.size(), glm::mat4(1.0f) ); + + modelOverrides.resize(bones.size()); + flags.resize(bones.size()); + + for (size_t i = 0; i < bones.size(); i++) { + flags[i].visible = true; + } } static void get_all_nodes(std::vector& nodes, Bone* node) { From a1a5d390c4207f7b108259161438338a99f130e6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:41:03 +0300 Subject: [PATCH 63/92] add math.sign --- res/modules/internal/extensions/math.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua index d906e94a2..df0eadf72 100644 --- a/res/modules/internal/extensions/math.lua +++ b/res/modules/internal/extensions/math.lua @@ -35,3 +35,7 @@ function math.sum(...) return sum end + +function math.sign(x) + return (x > 0) and 1 or (x < 0 and -1 or 0) +end From bed71ea607da4804061ea229970dec296ed424f0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:43:50 +0300 Subject: [PATCH 64/92] update doc/ru/scripting/extensions.md --- doc/ru/scripting/extensions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index 868210056..e7b0fe292 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -156,6 +156,8 @@ math.round(num: number, [опционально] places: number) -> number -- Возвращает сумму всех принимаемых аргументов. Если в качестве аргумента была передана таблица, метод вернёт сумму всех её элементов. math.sum(x: number, ... | t: table) -> number +-- Возвращает целое число, указывающее знак числа (-1/0/1) +math.sign(x: number) -> int ``` ## Расширения для bit @@ -200,4 +202,4 @@ await(co: coroutine) -> result, error -- Константа, в которой хранится PID текущего инстанса движка. os.pid -> number -``` \ No newline at end of file +``` From 2d1e783a64a0e352c2cb2d3c4e05b9de001d51f5 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:49:10 +0300 Subject: [PATCH 65/92] add entities.def_solid --- src/logic/scripting/lua/libs/libentity.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 64e8e126e..110aee123 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -50,6 +50,13 @@ static int l_def_hitbox(lua::State* L) { return 0; } +static int l_def_solid(lua::State* L) { + if (auto def = require_entity_def(L)) { + return lua::pushboolean(L, def->solid); + } + return 0; +} + static int l_defs_count(lua::State* L) { return lua::pushinteger(L, indices->entities.count()); } @@ -373,6 +380,7 @@ const luaL_Reg entitylib[] = { {"def_index", lua::wrap}, {"def_name", lua::wrap}, {"def_hitbox", lua::wrap}, + {"def_solid", lua::wrap}, {"get_def", lua::wrap}, {"defs_count", lua::wrap}, {"spawn", lua::wrap}, From e829bf7912d6776f2db2173cbb3b481aa0369d19 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:49:38 +0300 Subject: [PATCH 66/92] update doc/*/scripting/builtins/libentities.md --- doc/en/scripting/builtins/libentities.md | 3 +++ doc/ru/scripting/builtins/libentities.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/doc/en/scripting/builtins/libentities.md b/doc/en/scripting/builtins/libentities.md index e34cd7a53..375c52660 100644 --- a/doc/en/scripting/builtins/libentities.md +++ b/doc/en/scripting/builtins/libentities.md @@ -30,6 +30,9 @@ entities.def_hitbox(id: int) -> vec3 -- Returns entity definition name by index (string ID). entities.def_name(id: int) -> str +-- Returns true if the entity is a solid obstacle. +entities.def_solid(id: int) -> boolean + -- Returns entity definition index by name (integer ID). entities.def_index(name: str) -> int diff --git a/doc/ru/scripting/builtins/libentities.md b/doc/ru/scripting/builtins/libentities.md index 27d8d03b1..d14ef7ea6 100644 --- a/doc/ru/scripting/builtins/libentities.md +++ b/doc/ru/scripting/builtins/libentities.md @@ -30,6 +30,9 @@ entities.def_name(id: int) -> string -- Возвращает значение свойства 'hitbox' сущности entities.def_hitbox(id: int) -> vec3 +-- Возвращает true если сущность является осязаемым препятствием. +entities.def_solid(id: int) -> boolean + -- Возвращает индекс определения сущности по имени (числовой ID). entities.def_index(name: string) -> int From 3c7b7a931890cdd55941d27f33fa05de7e3b71af Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:53:22 +0300 Subject: [PATCH 67/92] update doc/*/scripting/builtins/libentities.md --- doc/en/scripting/builtins/libentities.md | 4 ++-- doc/ru/scripting/builtins/libentities.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/en/scripting/builtins/libentities.md b/doc/en/scripting/builtins/libentities.md index 375c52660..9e653ba78 100644 --- a/doc/en/scripting/builtins/libentities.md +++ b/doc/en/scripting/builtins/libentities.md @@ -54,12 +54,12 @@ entities.get_all() -> table -- Returns a table of loaded entities based on the passed list of UIDs entities.get_all(uids: array) -> table --- Returns a list of UIDs of entities inside the rectangular area +-- Returns a list of UIDs of entities with origin inside the rectangular area -- pos - minimal area corner -- size - area size entities.get_all_in_box(pos: vec3, size: vec3) -> array --- Returns a list of UIDs of entities inside the radius +-- Returns a list of UIDs of entities with origin inside the radius -- center - center of the area -- radius - radius of the area entities.get_all_in_radius(center: vec3, radius: number) -> array diff --git a/doc/ru/scripting/builtins/libentities.md b/doc/ru/scripting/builtins/libentities.md index d14ef7ea6..9753c7e15 100644 --- a/doc/ru/scripting/builtins/libentities.md +++ b/doc/ru/scripting/builtins/libentities.md @@ -53,12 +53,12 @@ entities.get_all() -> table -- Возвращает таблицу загруженных сущностей по переданному списку UID entities.get_all(uids: table) -> table --- Возвращает список UID сущностей, попадающих в прямоугольную область +-- Возвращает список UID сущностей, позиции которых попадают в прямоугольную область -- pos - минимальный угол области -- size - размер области entities.get_all_in_box(pos: vec3, size: vec3) -> table --- Возвращает список UID сущностей, попадающих в радиус +-- Возвращает список UID сущностей, позиции которых попадают в радиус -- center - центр области -- radius - радиус области entities.get_all_in_radius(center: vec3, radius: number) -> table From c023697bccc0fddc94eef79a85ec51860757c704 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 23:13:09 +0300 Subject: [PATCH 68/92] update doc/*/scripting/ecs.md --- doc/en/scripting/ecs.md | 11 +++++++++++ doc/ru/scripting/ecs.md | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index 52be70363..9636cf763 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -38,6 +38,17 @@ entity:set_enabled(name: str, enable: bool) entity:get_player() -> int or nil ``` +## Custom Components + +A component is defined as a script in `{pack}/scripts/components/{name}.lua`. +The component will be available for use in the entity definition as `{pack}:{name}`. +Example: `core:scripts/components/pathfinding.lua` -> `core:pathfinding`. + +For **each** entity containing a component, a **separate instance** is created with **its own namespace** within the pack namespace. +Global variables declared in it act as public fields of the component (same for global functions). + +Entity components are specified via the ["components" list](../entity-properties.md#components) + ## Built-in components ### Transform diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index d659f342d..cb4a79bec 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -180,6 +180,17 @@ rig:get_color() -> vec3 rig:set_color(color: vec3) ``` +## Пользовательские компоненты + +Компонент описывается в виде скрипта `{пак}/scripts/components/{имя}.lua`. +Компонент будет доступен для использования в описании сущности как `{пак}:{имя}`. +Пример: `core:scripts/components/pathfinding.lua` -> `core:pathfinding`. + +Для **каждой** сущности, содержащей компонент, создаётся **отдельный экземпляр** со **своим пространством имен** внутри пространства имён пака. +Глобальные переменные, объявляемые в нём, играют роль публичных полей компонента (как и глобальные функции). + +Компоненты сущности указываются через [список "components"](../entity-properties.md#cписок-компонентов---components) + > [!WARNING] > При выходе сущности за пределы зоны прогрузки она удаляется, вызывая события. > При повторном попадании в зону прогрузки сущность спавнится заново. From c3794191e226a4cd587ec0b846ddb0aae1865370 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 00:00:41 +0300 Subject: [PATCH 69/92] document 'script-name' propertyfor items --- doc/en/item-properties.md | 7 ++++++- doc/ru/item-properties.md | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/en/item-properties.md b/doc/en/item-properties.md index 7bef07664..d3ae82e7d 100644 --- a/doc/en/item-properties.md +++ b/doc/en/item-properties.md @@ -60,13 +60,18 @@ Property used via [inventory.use](scripting/builtins/libinventory.md). Property status is displayed in the inventory interface. Display method is defined via `uses-display`. -### Display of uses - `uses-display` +### Display of remaining uses count - `uses-display` - `none` - display disabled - `number` - number - `relation` - current value to initial value (x/y) - `vbar` - vertical scale (used by default) +### *script-name* + +Defines the name of the script containing the item's event handlers. By default, equals to the item's string id (name). +Allows you to reuse a single script for multiple items. + ## Tags Tags allow you to designate general properties of items. Names should be formatted as `prefix:tag_name`. diff --git a/doc/ru/item-properties.md b/doc/ru/item-properties.md index cb45827a1..7453789fb 100644 --- a/doc/ru/item-properties.md +++ b/doc/ru/item-properties.md @@ -66,6 +66,10 @@ - `relation` - отношение текущего значения к изначальному (x/y) - `vbar` - вертикальная шкала (используется по-умолчанию) +### Имя скрипта - `script-name` + +Определяет имя скрипта, содержащего обработчики событий предмета. По-умолчанию соответствует строковому id (имени) предмета. +Свойство обеспечивает возможность использования одного скрипта для нескольких предметов. ## Теги - *tags* From da6a36a76c9208e94469f1e314bc019a501aaf19 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 00:33:36 +0300 Subject: [PATCH 70/92] update scripting docs --- doc/en/scripting.md | 55 +++++++++++++++++++++++++++++++++++++++--- doc/en/scripting/ui.md | 2 +- doc/ru/scripting.md | 54 ++++++++++++++++++++++++++++++++++++++++- doc/ru/scripting/ui.md | 2 +- 4 files changed, 107 insertions(+), 6 deletions(-) diff --git a/doc/en/scripting.md b/doc/en/scripting.md index a21b959ed..7cd22ed8a 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -58,9 +58,58 @@ not part of Lua syntax. - quat - array of four numbers - quaternion - matrix - array of 16 numbers - matrix -## Core functions +## Namespaces + +Currently, the engine uses a namespace hierarchy that minimizes conflicts between individual modules, scripts, and even packs. + +The following structure is relevant: + +- Global namespace (_G), which is the root namespace. Writing to it outside of engine core modules is highly undesirable, as it can waste hours of debugging time. + - Pack namespace - created each time content is loaded for each pack included in the configuration. This namespace is used by modules, as well as item and block scripts. + - [Component namespace](scripting/ecs.md#built-in-components). + - [UI document namespace](scripting/ui.md). +- Isolated world generator namespace. + +## Modules + +A module is a global object with a lifetime limited to the lifetime of the loaded content, used both for interaction between different packages and as a substitute for global variables within the package itself. + +The module must be located in `contentpack/modules/**` (subfolders are allowed, but you will need to specify them) ```lua -require "packid:module_name" -- load Lua module from pack-folder/modules/ --- no extension included, just name +local module_name = require "contentpack:module_name" -- imports the module + +-- If the module is in the same pack in which it is imported, you can use the shortcut: +local module_name = require "module_name" + +-- When using nested folders: +local module_name = require "contentpack:path/to/module_name" -- the path does not include `modules` +local module_name = require "path/to/module_name" -- if in the same pack ``` + +The module will have the namespace of the folder in which it is located, regardless of the namespace from which the first import was performed. + +When creating a module, you should follow the following approach, which is what *require* expects: + +```lua +local this = { + variable_name = ... -- public variables +} + +-- private module variables +local variable_name = ... + +-- private module functions +local function function_name(...) + ... +end + +-- public module functions +function this.function_name(...) + ... +end + +return this -- the result that require will return and cache +``` + +When reimported, the module will not be reloaded; instead, require will return the cached result. diff --git a/doc/en/scripting/ui.md b/doc/en/scripting/ui.md index 04784b449..2d338a5aa 100644 --- a/doc/en/scripting/ui.md +++ b/doc/en/scripting/ui.md @@ -1,7 +1,7 @@ # UI properties and methods UI elements in scripts are accessed through a Document instance -(*document* variable) by id specified in xml. +(global *document* variable) by id specified in xml. Example: print the pos property of an element with id: "worlds-panel" to the console: ```lua diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index cd8a3b00a..db3fcd921 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -60,6 +60,58 @@ - quat - массив из четырех чисел - кватернион - matrix - массив из 16 чисел - матрица +## Пространства имён + +В настоящее время, движок использует иерархию пространств имён, минимизирующую конфликты между отдельными модулями, скриптами, да и паками. + +Актуальна следующая структура: + +- Глобальное пространство (оно же - _G), являющееся корневым, запись в которое, вне модулей ядра движка, является крайне нежелательным для вашего же времени, что может уйти на лишние часы отладки. + - Пространство пака - создаётся каждый раз при загрузке контента для каждого, включённого в конфигурацию, пака. Это пространство используют модули, а также скрипты предметов и блоков. + - Пространство [компонента](scripting/ecs.md#пользовательские-компоненты). + - Пространство [UI-документа](scripting/ui.md) +- Изолированное пространство имён генератора мира. + +## Модули + +Модуль - глобальный объект со сроком жизни, ограниченным сроком жизни контента, используемый как для взаимодействия разных паков, так и для вместо глобальных переменных в пределах самого пака. + +Модуль должен находиться в `контентпак/modules/**` (допускаются вложенные папки, что нужно будет указывать) + ```lua -require "контентпак:имя_модуля" -- загружает lua модуль из папки modules (расширение не указывается) +local имя_модуля = require "контентпак:имя_модуля" -- импортирует модуль + +-- если модуль находится в том же паке, в котором импортируется, можно использовать сокращённый вариант: +local имя_модуля = require "имя_модуля" + +-- при использовании вложенных папок: +local имя_модуля = require "контентпак:путь/к/имя_модуля" -- путь не включает `modules` +local имя_модуля = require "путь/к/имя_модуля" -- если в том же паке ``` + +Модуль будет иметь пространство имён того пака, в котором находится, независимо от пространства имён, из которого был выполнен первый импорт. + +При создании модуля следует придерживаться следующего подхода, которого и ожидает *require*: + +```lua +local this = { + имя_переменной = ... -- публичные переменные +} + +-- приватные переменные модуля +local имя_переменной = ... + +-- приватные функции модуля +local function имя_функции(...) + ... +end + +-- публичные функции модуля +function this.имя_функции(...) + ... +end + +return this -- результат, который и будет возвращать и кешировать require +``` + +При повторном импорте модуль не будет перезагружен - вместо этого require вернёт кешированный результат. diff --git a/doc/ru/scripting/ui.md b/doc/ru/scripting/ui.md index 6cff70e2e..631d5e00f 100644 --- a/doc/ru/scripting/ui.md +++ b/doc/ru/scripting/ui.md @@ -1,7 +1,7 @@ # Свойства и методы UI элементов Обращение к UI элементов в скриптах произодится через экземпляр Document -(переменная document) по id, указанному в xml. +(глобальная переменная document) по id, указанному в xml. Пример: вывод в консоль свойства pos элемента с id: "worlds-panel": ```lua From c7ba237933310866a7f7945e5a2d92cb777cd503 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 21:55:53 +0300 Subject: [PATCH 71/92] add xml library --- src/coders/xml.cpp | 8 +++ src/coders/xml.hpp | 6 ++ src/logic/scripting/lua/libs/api_lua.hpp | 1 + src/logic/scripting/lua/libs/libxml.cpp | 90 ++++++++++++++++++++++++ src/logic/scripting/lua/lua_engine.cpp | 1 + 5 files changed, 106 insertions(+) create mode 100644 src/logic/scripting/lua/libs/libxml.cpp diff --git a/src/coders/xml.cpp b/src/coders/xml.cpp index 93e1c8031..b40ee7700 100644 --- a/src/coders/xml.cpp +++ b/src/coders/xml.cpp @@ -528,3 +528,11 @@ std::string xml::stringify( return ss.str(); } + +std::string xml::stringify( + const Node& element, bool nice, const std::string& indentStr +) { + std::stringstream ss; + stringifyElement(ss, element, nice, indentStr, 0); + return ss.str(); +} diff --git a/src/coders/xml.hpp b/src/coders/xml.hpp index 4954caa17..1a8a94ca5 100644 --- a/src/coders/xml.hpp +++ b/src/coders/xml.hpp @@ -119,6 +119,12 @@ namespace xml { const std::string& indentStr = " " ); + std::string stringify( + const Node& element, + bool nice = true, + const std::string& indentStr = " " + ); + /// @brief Read XML Document from string /// @param filename file name will be shown in error messages /// @param source xml source code string diff --git a/src/logic/scripting/lua/libs/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp index e304d6dc1..6d135c100 100644 --- a/src/logic/scripting/lua/libs/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -53,6 +53,7 @@ extern const luaL_Reg vec3lib[]; // vecn.cpp extern const luaL_Reg vec4lib[]; // vecn.cpp extern const luaL_Reg weatherlib[]; // gfx.weather extern const luaL_Reg worldlib[]; +extern const luaL_Reg xmllib[]; extern const luaL_Reg yamllib[]; // Components diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp new file mode 100644 index 000000000..e2121465d --- /dev/null +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -0,0 +1,90 @@ +#include "coders/xml.hpp" +#include "api_lua.hpp" + +static int push_xml(lua::State* L, const xml::xmlelement& elem) { + if (elem.isText()) { + return lua::pushlstring(L, elem.getInnerText()); + } + + const auto& attrs = elem.getAttributes(); + const auto& elems = elem.getElements(); + const auto& tag = elem.getTag(); + + lua::createtable(L, elem.size(), attrs.size() + 1); + lua::pushlstring(L, tag); + lua::setfield(L, "@"); + + for (int i = 0; i < elems.size(); i++) { + push_xml(L, *elems[i]); + lua::rawseti(L, i + 1); + } + + for (const auto& [key, value] : attrs) { + lua::pushlstring(L, value.getText()); + lua::setfield(L, key); + } + return 1; +} + +static std::unique_ptr toxml(lua::State* L) { + lua::getfield(L, "@"); + auto tag = lua::require_lstring(L, -1); + lua::pop(L); + + auto elem = std::make_unique(std::string(tag)); + int length = lua::objlen(L, -1); + + for (int i = 0; i < length; i++) { + lua::rawgeti(L, i + 1); + elem->add(toxml(L)); + lua::pop(L); + } + + lua::pushnil(L); + while (lua::next(L, -2)) { + lua::pushvalue(L, -2); + do { + if (!lua::isstring(L, -1)) { + break; + } + auto key = lua::require_lstring(L, -1); + if (key == "@") { + break; + } + auto value = lua::require_lstring(L, -2); + elem->set(std::string(key), std::string(value)); + } while (false); + + lua::pop(L, 2); + } + return elem; +} + +static int l_tostring(lua::State* L) { + bool nice = lua::isboolean(L, 2) ? lua::toboolean(L, 2) : true; + lua::pushvalue(L, 1); + auto xml = toxml(L); + lua::pop(L); + return lua::pushstring(L, xml::stringify(*xml, nice)); +} + +static int l_parse(lua::State* L) { + auto string = lua::require_string(L, 1); + auto document = xml::parse("[string]", string); + return push_xml(L, *document->getRoot()); +} + + +static int l_parse_vcd(lua::State* L) { + auto string = lua::require_string(L, 1); + auto rootTag = lua::tostring(L, 2); + auto document = xml::parse_vcm("[string]", string, rootTag ? rootTag : "root"); + return push_xml(L, *document->getRoot()); +} + +const luaL_Reg xmllib[] = { + {"tostring", lua::wrap}, + {"parse", lua::wrap}, + {"parse_vcd", lua::wrap}, + {nullptr, nullptr} +}; diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index afb2f42eb..276dbaa74 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -66,6 +66,7 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "vec2", vec2lib); openlib(L, "vec3", vec3lib); openlib(L, "vec4", vec4lib); + openlib(L, "xml", xmllib); openlib(L, "yaml", yamllib); openlib(L, "__vc_app", applib); From cd53a33d7bc3ca5a8976bf1f37ad80e695f74b6d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 22:26:44 +0300 Subject: [PATCH 72/92] replace tag attribute key '@' with '#' --- src/logic/scripting/lua/libs/libxml.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp index e2121465d..ff52b6321 100644 --- a/src/logic/scripting/lua/libs/libxml.cpp +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -1,6 +1,9 @@ #include "coders/xml.hpp" #include "api_lua.hpp" +static const char* TAG_ATTR = "#"; +static const char* DEFAULT_ROOT_TAG = "root"; + static int push_xml(lua::State* L, const xml::xmlelement& elem) { if (elem.isText()) { return lua::pushlstring(L, elem.getInnerText()); @@ -12,7 +15,7 @@ static int push_xml(lua::State* L, const xml::xmlelement& elem) { lua::createtable(L, elem.size(), attrs.size() + 1); lua::pushlstring(L, tag); - lua::setfield(L, "@"); + lua::setfield(L, TAG_ATTR); for (int i = 0; i < elems.size(); i++) { push_xml(L, *elems[i]); @@ -27,7 +30,7 @@ static int push_xml(lua::State* L, const xml::xmlelement& elem) { } static std::unique_ptr toxml(lua::State* L) { - lua::getfield(L, "@"); + lua::getfield(L, TAG_ATTR); auto tag = lua::require_lstring(L, -1); lua::pop(L); @@ -78,7 +81,9 @@ static int l_parse(lua::State* L) { static int l_parse_vcd(lua::State* L) { auto string = lua::require_string(L, 1); auto rootTag = lua::tostring(L, 2); - auto document = xml::parse_vcm("[string]", string, rootTag ? rootTag : "root"); + auto document = xml::parse_vcm( + "[string]", string, rootTag ? rootTag : DEFAULT_ROOT_TAG + ); return push_xml(L, *document->getRoot()); } From 13984539f580b12d189960463b9c890d7cecef3a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 22:38:43 +0300 Subject: [PATCH 73/92] update doc/*/scripting/filesystem.md --- doc/en/scripting/filesystem.md | 53 ++++++++++++++++++++++++++++++++++ doc/ru/scripting/filesystem.md | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/doc/en/scripting/filesystem.md b/doc/en/scripting/filesystem.md index 8341e9665..03976a7f8 100644 --- a/doc/en/scripting/filesystem.md +++ b/doc/en/scripting/filesystem.md @@ -52,6 +52,59 @@ yaml.parse(code: str) -> table Parses a YAML string into a table. +## XML Library + +The library contains functions for serializing and deserializing XML: + +Further, XML as a data type denotes a Lua table with the following structure: +- Element tag is accessible by the key `#` +- XML ​​attributes are accessible as key = value pairs +- Inner elements are accessible by index + +The table is used both as a dictionary and an array. + +Example: + +```xml + + + +``` + +Equivalent Lua table: + +```lua +{ + ['#'] = "panel", + size = "400", + color = "0", + interval = "1", + context = "menu", + { + ['#'] = "button", + onclick = 'menu.page="worlds"', + { + "@Worlds" + } + } +} +``` + +Consequently, the button element is accessible as `panel_element[1]`. + +```lua +-- Serializes a tree in XML format +-- multiline - use multiline formatting +xml.tostring(data: XML, multiline: bool = true) -> str + +-- Parses XML +xml.parse(code: str) -> XML + +-- Parses the VCD (VoxelCore Declaration) format used by the VCM format. +-- root_tag - root element tag (the format does not require an explicit root) +xml.parse(code: str, root_tag: str = "root") -> XML +``` + ## *bjson* library The library contains functions for working with the binary data exchange format [vcbjson](../../specs/binary_json_spec.md). diff --git a/doc/ru/scripting/filesystem.md b/doc/ru/scripting/filesystem.md index 0e060d94c..6334b5e90 100644 --- a/doc/ru/scripting/filesystem.md +++ b/doc/ru/scripting/filesystem.md @@ -52,6 +52,59 @@ yaml.parse(code: str) -> table Парсит YAML строку в таблицу. +## Библиотека xml + +Библиотека содержит функции для сериализации и десериализации XML: + +Далее XML как тип данных обозначает Lua-таблицу со следующей структурой: +- Тег элемента доступен по ключу `#` +- XML-аттрибуты доступны как пары ключ = значение +- Вложенные элементы доступны по индексу + +Таблица используется как словарь, так и массив. + +Пример: + +```xml + + + +``` + +Эквивалентная Lua-таблица: + +```lua +{ + ['#'] = "panel", + size = "400", + color = "0", + interval = "1", + context = "menu", + { + ['#'] = "button", + onclick = 'menu.page="worlds"', + { + "@Worlds" + } + } +} +``` + +Соответственно, элемент button доступен как `элемент_panel[1]`. + +```lua +-- Сериализует дерево в формате XML +-- multiline - использовать ли многострочное форматирование +xml.tostring(data: XML, multiline: bool = true) -> str + +-- Парсит XML +xml.parse(code: str) -> XML + +-- Парсит формат VCD (VoxelCore Declaration), используемый форматом VCM. +-- root_tag - тег корневого элемента (формат не требует наличия явного) +xml.parse(code: str, root_tag: str = "root") -> XML +``` + ## Библиотека bjson Библиотека содержит функции для работы с двоичным форматом обмена данными [vcbjson](../../specs/binary_json_spec.md). From 420d0daf28853e1db1a985f9a74ec38f3d978ddc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 22:48:16 +0300 Subject: [PATCH 74/92] fix: missing font usage produces fatal error --- src/graphics/core/FontMetics.hpp | 4 ++-- src/graphics/ui/elements/Label.cpp | 3 +++ src/graphics/ui/elements/TextBox.cpp | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/graphics/core/FontMetics.hpp b/src/graphics/core/FontMetics.hpp index a835a495b..d62a105d9 100644 --- a/src/graphics/core/FontMetics.hpp +++ b/src/graphics/core/FontMetics.hpp @@ -9,8 +9,8 @@ class Font; struct FontMetrics { std::optional> font; - int lineHeight; - int yoffset; + int lineHeight = 0; + int yoffset = 0; int _glyphInterval = 8; ~FontMetrics(); diff --git a/src/graphics/ui/elements/Label.cpp b/src/graphics/ui/elements/Label.cpp index 14cbeea2d..effb79830 100644 --- a/src/graphics/ui/elements/Label.cpp +++ b/src/graphics/ui/elements/Label.cpp @@ -207,6 +207,9 @@ uint Label::getLinesNumber() const { void Label::draw(const DrawContext& pctx, const Assets& assets) { auto batch = pctx.getBatch2D(); auto font = assets.getShared(fontName); + if (font == nullptr) { + return; + } cache.prepare( font, font->getMetrics(), diff --git a/src/graphics/ui/elements/TextBox.cpp b/src/graphics/ui/elements/TextBox.cpp index d04ca5786..f37673fc1 100644 --- a/src/graphics/ui/elements/TextBox.cpp +++ b/src/graphics/ui/elements/TextBox.cpp @@ -353,7 +353,9 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) { void TextBox::drawBackground(const DrawContext& pctx, const Assets& assets) { auto font = assets.getShared(label->getFontName()); - rawTextCache.prepare(font, font->getMetrics(), label->getSize().x); + if (font != nullptr) { + rawTextCache.prepare(font, font->getMetrics(), label->getSize().x); + } glm::vec2 pos = calcPos(); From e9221af4074f36c4c6088ccda8a78789839bd424 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 23:29:27 +0300 Subject: [PATCH 75/92] update xml.parse_vcd --- doc/en/scripting/filesystem.md | 5 +++-- doc/ru/scripting/filesystem.md | 5 +++-- src/logic/scripting/lua/libs/libxml.cpp | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/doc/en/scripting/filesystem.md b/doc/en/scripting/filesystem.md index 03976a7f8..12a190daa 100644 --- a/doc/en/scripting/filesystem.md +++ b/doc/en/scripting/filesystem.md @@ -101,8 +101,9 @@ xml.tostring(data: XML, multiline: bool = true) -> str xml.parse(code: str) -> XML -- Parses the VCD (VoxelCore Declaration) format used by the VCM format. --- root_tag - root element tag (the format does not require an explicit root) -xml.parse(code: str, root_tag: str = "root") -> XML +-- root_tag - a root element with the specified tag will be implicitly created, +-- containing all root elements +xml.parse_vcd(code: str, root_tag: str = "root") -> XML ``` ## *bjson* library diff --git a/doc/ru/scripting/filesystem.md b/doc/ru/scripting/filesystem.md index 6334b5e90..a3bee78a6 100644 --- a/doc/ru/scripting/filesystem.md +++ b/doc/ru/scripting/filesystem.md @@ -101,8 +101,9 @@ xml.tostring(data: XML, multiline: bool = true) -> str xml.parse(code: str) -> XML -- Парсит формат VCD (VoxelCore Declaration), используемый форматом VCM. --- root_tag - тег корневого элемента (формат не требует наличия явного) -xml.parse(code: str, root_tag: str = "root") -> XML +-- root_tag - будет неявно создан корневой элемент с указанным тегом, +-- содержащий все корневые элементы +xml.parse_vcd(code: str, root_tag: str | nil = nil) -> XML ``` ## Библиотека bjson diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp index ff52b6321..70ccc36bb 100644 --- a/src/logic/scripting/lua/libs/libxml.cpp +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -82,9 +82,19 @@ static int l_parse_vcd(lua::State* L) { auto string = lua::require_string(L, 1); auto rootTag = lua::tostring(L, 2); auto document = xml::parse_vcm( - "[string]", string, rootTag ? rootTag : DEFAULT_ROOT_TAG + "[string]", string, rootTag ? rootTag : "" ); - return push_xml(L, *document->getRoot()); + const auto& root = *document->getRoot(); + if (rootTag != nullptr) { + return push_xml(L, root); + } + int rootElements = root.size(); + if (rootElements != 1) { + throw std::runtime_error( + "one root element expected, got " + std::to_string(rootElements) + ); + } + return push_xml(L, *root.getElements()[0]); } const luaL_Reg xmllib[] = { From d3eb7a10bea6825c0ff2da9f459a1b528a6a410f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Jul 2026 19:29:44 +0300 Subject: [PATCH 76/92] fix texture-faces reading --- src/content/loading/BlockLoader.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/content/loading/BlockLoader.cpp b/src/content/loading/BlockLoader.cpp index f44d3a9ca..56903db79 100644 --- a/src/content/loading/BlockLoader.cpp +++ b/src/content/loading/BlockLoader.cpp @@ -53,6 +53,9 @@ static void load_variant( } } else if (root.has("texture-faces")) { const auto& texarr = root["texture-faces"]; + if (texarr.length() < 6) { + throw std::runtime_error("texture-faces requires 6 elements"); + } for (uint i = 0; i < 6; i++) { variant.textureFaces[i] = texarr[i].asString(); } From 900f118f9483430d2fb0c27c9ceba06485b50b17 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Jul 2026 19:59:31 +0300 Subject: [PATCH 77/92] feat: verbose error messages for parsing functions --- src/coders/commons.cpp | 4 ++++ src/coders/commons.hpp | 2 ++ src/coders/xml.cpp | 18 +++++++++++++++--- src/logic/scripting/lua/libs/libjson.cpp | 9 +++++++-- src/logic/scripting/lua/libs/libtoml.cpp | 9 +++++++-- src/logic/scripting/lua/libs/libxml.cpp | 23 ++++++++++++++++------- src/logic/scripting/lua/libs/libyaml.cpp | 9 +++++++-- 7 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/coders/commons.cpp b/src/coders/commons.cpp index 273f74f51..aeba0c851 100644 --- a/src/coders/commons.cpp +++ b/src/coders/commons.cpp @@ -57,3 +57,7 @@ std::string parsing_error::errorLog() const { ss << "^"; return ss.str(); } + +std::runtime_error parsing_error::toRuntimeError() const { + return std::runtime_error("parsing error: " + errorLog()); +} diff --git a/src/coders/commons.hpp b/src/coders/commons.hpp index d374cd92b..d1612cfd8 100644 --- a/src/coders/commons.hpp +++ b/src/coders/commons.hpp @@ -73,4 +73,6 @@ public: uint linestart ); std::string errorLog() const; + + std::runtime_error toRuntimeError() const; }; diff --git a/src/coders/xml.cpp b/src/coders/xml.cpp index b40ee7700..a907b49db 100644 --- a/src/coders/xml.cpp +++ b/src/coders/xml.cpp @@ -385,10 +385,22 @@ public: } if (c == '(') { nextChar(); - // TODO: replace with array parsing after moving to dv::value's - std::string value = std::string(readUntil(')')); + int depth = 1; + size_t start = pos; + while (hasNext()) { + char c = nextChar(); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + goBack(1); + break; + } + } + } expect(')'); - return value; + return std::string(source.substr(start, pos - start - 1)); } return std::string(readUntilWhitespace()); } diff --git a/src/logic/scripting/lua/libs/libjson.cpp b/src/logic/scripting/lua/libs/libjson.cpp index a0caa4c33..a4354ea0c 100644 --- a/src/logic/scripting/lua/libs/libjson.cpp +++ b/src/logic/scripting/lua/libs/libjson.cpp @@ -1,4 +1,5 @@ #include "coders/json.hpp" +#include "coders/commons.hpp" #include "api_lua.hpp" static int l_json_stringify(lua::State* L) { @@ -12,8 +13,12 @@ static int l_json_stringify(lua::State* L) { static int l_json_parse(lua::State* L) { auto string = lua::require_string(L, 1); - auto element = json::parse("[string]", string); - return lua::pushvalue(L, element); + try { + auto element = json::parse("[string]", string); + return lua::pushvalue(L, element); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } const luaL_Reg jsonlib[] = { diff --git a/src/logic/scripting/lua/libs/libtoml.cpp b/src/logic/scripting/lua/libs/libtoml.cpp index 4626ee3b9..947496d08 100644 --- a/src/logic/scripting/lua/libs/libtoml.cpp +++ b/src/logic/scripting/lua/libs/libtoml.cpp @@ -1,4 +1,5 @@ #include "coders/toml.hpp" +#include "coders/commons.hpp" #include "api_lua.hpp" using namespace scripting; @@ -16,8 +17,12 @@ static int l_toml_stringify(lua::State* L) { static int l_toml_parse(lua::State* L) { auto string = lua::require_string(L, 1); - auto element = toml::parse("[string]", string); - return lua::pushvalue(L, element); + try { + auto element = toml::parse("[string]", string); + return lua::pushvalue(L, element); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } const luaL_Reg tomllib[] = { diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp index 70ccc36bb..139e0ee35 100644 --- a/src/logic/scripting/lua/libs/libxml.cpp +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -1,8 +1,8 @@ #include "coders/xml.hpp" +#include "coders/commons.hpp" #include "api_lua.hpp" static const char* TAG_ATTR = "#"; -static const char* DEFAULT_ROOT_TAG = "root"; static int push_xml(lua::State* L, const xml::xmlelement& elem) { if (elem.isText()) { @@ -73,17 +73,26 @@ static int l_tostring(lua::State* L) { static int l_parse(lua::State* L) { auto string = lua::require_string(L, 1); - auto document = xml::parse("[string]", string); - return push_xml(L, *document->getRoot()); + try { + auto document = xml::parse("[string]", string); + return push_xml(L, *document->getRoot()); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } - static int l_parse_vcd(lua::State* L) { auto string = lua::require_string(L, 1); auto rootTag = lua::tostring(L, 2); - auto document = xml::parse_vcm( - "[string]", string, rootTag ? rootTag : "" - ); + + std::unique_ptr document; + try { + document = xml::parse_vcm( + "[string]", string, rootTag ? rootTag : "" + ); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } const auto& root = *document->getRoot(); if (rootTag != nullptr) { return push_xml(L, root); diff --git a/src/logic/scripting/lua/libs/libyaml.cpp b/src/logic/scripting/lua/libs/libyaml.cpp index 42c9906c4..f951727af 100644 --- a/src/logic/scripting/lua/libs/libyaml.cpp +++ b/src/logic/scripting/lua/libs/libyaml.cpp @@ -1,4 +1,5 @@ #include "coders/yaml.hpp" +#include "coders/commons.hpp" #include "api_lua.hpp" static int l_stringify(lua::State* L) { @@ -9,8 +10,12 @@ static int l_stringify(lua::State* L) { static int l_parse(lua::State* L) { auto string = lua::require_string(L, 1); - auto element = yaml::parse("[string]", string); - return lua::pushvalue(L, element); + try { + auto element = yaml::parse("[string]", string); + return lua::pushvalue(L, element); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } const luaL_Reg yamllib[] = { From e0bf82a71e15731aa0a4a6e8670bcf0c9cf392b7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Jul 2026 20:08:42 +0300 Subject: [PATCH 78/92] add filename info to content unit loading --- src/content/ContentLoader.cpp | 11 ++++++++++- src/io/io.cpp | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index 5120f5c59..14da690a2 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -178,7 +178,16 @@ void ContentUnitLoader::loadUnit( ) { auto folder = pack.folder; auto configFile = folder / (defsDir + "/" + name + ".json"); - if (io::exists(configFile)) loadUnit(def, full, configFile); + if (io::exists(configFile)) { + try { + loadUnit(def, full, configFile); + } catch (const std::runtime_error& err) { + throw std::runtime_error( + "file " + util::quote(configFile.string()) + ": " + + std::string(err.what()) + ); + } + } } void ContentLoader::loadBlockMaterial( diff --git a/src/io/io.cpp b/src/io/io.cpp index 2f4eab11c..638108587 100644 --- a/src/io/io.cpp +++ b/src/io/io.cpp @@ -173,8 +173,12 @@ bool io::write_binary_json( } dv::value io::read_json(const path& filename) { - std::string text = io::read_string(filename); - return json::parse(filename.string(), text); + auto text = io::read_string(filename); + try { + return json::parse(filename.string(), text); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } dv::value io::read_binary_json(const path& file) { @@ -184,7 +188,12 @@ dv::value io::read_binary_json(const path& file) { } dv::value io::read_toml(const path& file) { - return toml::parse(file.string(), io::read_string(file)); + auto text = io::read_string(file); + try { + return toml::parse(file.string(), text); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } std::vector io::read_list(const io::path& filename) { From 7d148ae2cf476368e64b086a987d336288c54675 Mon Sep 17 00:00:00 2001 From: Onran Date: Fri, 24 Jul 2026 03:14:47 +0900 Subject: [PATCH 79/92] all camelCase occurences in io_stream changed to snake_case --- res/modules/io_stream.lua | 212 +++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 104 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 98a37529c..522e6ecb7 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -25,13 +25,13 @@ local ALL_FLUSH_MODES = { local CR = string.byte('\r') local LF = string.byte('\n') -local function readFully(result, readFunc) +local function read_fully(result, read_func) local isTable = type(result) == "table" local buf repeat - buf = readFunc(MAX_BUFFER_SIZE) + buf = read_func(MAX_BUFFER_SIZE) if isTable then for i = 1, #buf do @@ -59,29 +59,29 @@ ioLib - I/O library. Should include the following functions: close(descriptor: int) --]] -function io_stream.new(descriptor, binaryMode, ioLib, mode, flushMode) +function io_stream.new(descriptor, binary_mode, io_lib, mode, flush_mode) mode = mode or DEFAULT_MODE - flushMode = flushMode or FLUSH_MODE_ALL + flush_mode = flush_mode or FLUSH_MODE_ALL local self = setmetatable({}, io_stream) self.descriptor = descriptor - self.binaryMode = binaryMode - self.maxBufferSize = MAX_BUFFER_SIZE - self.ioLib = ioLib + self.binary_mode = binary_mode + self.max_buffer_size = MAX_BUFFER_SIZE + self.io_lib = io_lib self:set_mode(mode) - self:set_flush_mode(flushMode) + self:set_flush_mode(flush_mode) return self end function io_stream:is_binary_mode() - return self.binaryMode + return self.binary_mode end -function io_stream:set_binary_mode(binaryMode) - self.binaryMode = binaryMode ~= nil +function io_stream:set_binary_mode(binary_mode) + self.binary_mode = binary_mode ~= nil end function io_stream:get_mode() @@ -93,52 +93,52 @@ function io_stream:set_mode(mode) error("invalid stream mode: "..mode) end - if self.writeBuffer then - self.writeBuffer:clear() - self.writeBuffer = nil + if self.write_buffer then + self.write_buffer:clear() + self.write_buffer = nil end - if self.readBuffer then - self.readBuffer:clear() - self.readBuffer = nil + if self.read_buffer then + self.read_buffer:clear() + self.read_buffer = nil end if mode == BUFFERED_MODE then - self.writeBuffer = Bytearray(self.maxBufferSize) - self.readBuffer = Bytearray(self.maxBufferSize) + self.write_buffer = Bytearray(self.max_buffer_size) + self.read_buffer = Bytearray(self.max_buffer_size) end self.mode = mode end function io_stream:get_flush_mode() - return self.flushMode + return self.flush_mode end -function io_stream:set_flush_mode(flushMode) - if not table.has(ALL_FLUSH_MODES, flushMode) then - error("invalid flush mode: "..flushMode) +function io_stream:set_flush_mode(flush_mode) + if not table.has(ALL_FLUSH_MODES, flush_mode) then + error("invalid flush mode: " .. flush_mode) end - self.flushMode = flushMode + self.flush_mode = flush_mode end function io_stream:get_max_buffer_size() - return self.maxBufferSize + return self.max_buffer_size end -function io_stream:set_max_buffer_size(maxBufferSize) - self.maxBufferSize = maxBufferSize +function io_stream:set_max_buffer_size(max_buffer_size) + self.max_buffer_size = max_buffer_size - self.writeBuffer = Bytearray(self.maxBufferSize) - self.readBuffer = Bytearray(self.maxBufferSize) + self.write_buffer = Bytearray(self.max_buffer_size) + self.read_buffer = Bytearray(self.max_buffer_size) end function io_stream:available(length) - local available = self.ioLib.available and self.ioLib.available(self.descriptor) or 0 + local available = self.io_lib.available and self.io_lib.available(self.descriptor) or 0 if self.mode == BUFFERED_MODE then - available = available + #self.readBuffer + available = available + #self.read_buffer end if not length then @@ -148,86 +148,86 @@ function io_stream:available(length) end end -function io_stream:__read(length, fromReadFully) +function io_stream:__read(length, from_read_fully) if self.mode == YIELD_MODE then - if fromReadFully then - return self.ioLib.read(self.descriptor, length) + if from_read_fully then + return self.io_lib.read(self.descriptor, length) end local buffer = Bytearray() while #buffer < length do - buffer:append(self.ioLib.read(self.descriptor, length - #buffer)) + buffer:append(self.io_lib.read(self.descriptor, length - #buffer)) if #buffer < length then coroutine.yield() end end return buffer elseif self.mode == BUFFERED_MODE then - local bufLen = #self.readBuffer + local buf_len = #self.read_buffer if bufLen < length then - self.readBuffer:append( - self.ioLib.read(self.descriptor, self.maxBufferSize - bufLen) + self.read_buffer:append( + self.io_lib.read(self.descriptor, self.max_buffer_size - buf_len) ) end - bufLen = #self.readBuffer + bufLen = #self.read_buffer - length = math.min(bufLen, length) + length = math.min(buf_len, length) - local copy = self.readBuffer:slice(1, length) + local copy = self.read_buffer:slice(1, length) if bufLen == length then - self.readBuffer:clear() + self.read_buffer:clear() else - self.readBuffer:remove(1, length) + self.read_buffer:remove(1, length) end return copy elseif self.mode == DEFAULT_MODE then - return self.ioLib.read(self.descriptor, length) + return self.io_lib.read(self.descriptor, length) end end function io_stream:__write(data) if self.mode == BUFFERED_MODE then - local dataLength = #data + local data_length = #data - if #self.writeBuffer + dataLength > self.maxBufferSize then + if #self.write_buffer + data_length > self.max_buffer_size then self:flush() end - if dataLength > self.maxBufferSize then - local toWrite = math.floor(dataLength / self.maxBufferSize) * self.maxBufferSize - local toSave = dataLength - toWrite + if data_length > self.max_buffer_size then + local to_write = math.floor(data_length / self.max_buffer_size) * self.max_buffer_size + local to_save = data_length - to_write - self.ioLib.write(self.descriptor, data:slice(1, toWrite)) + self.io_lib.write(self.descriptor, data:slice(1, to_write)) self:flush() - self.writeBuffer = data:slice(toWrite + 1, toSave) - else self.writeBuffer:append(data) end + self.write_buffer = data:slice(to_write + 1, to_save) + else self.write_buffer:append(data) end elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then - return self.ioLib.write(self.descriptor, data) + return self.io_lib.write(self.descriptor, data) end end -function io_stream:read_fully(useTable) - if self.binaryMode then - local result = useTable and Bytearray() or { } +function io_stream:read_fully(use_table) + if self.binary_mode then + local result = use_table and { } or Bytearray() local avail = self:available() if avail == 0 then - avail = self.maxBufferSize + avail = self.max_buffer_size end - readFully(result, function() return self:__read(avail, true) end) + read_fully(result, function() return self:__read(avail, true) end) return result else - if useTable then + if use_table then local lines = { } local line @@ -242,7 +242,7 @@ function io_stream:read_fully(useTable) else local result = Bytearray() - readFully(result, function() return self:__read(self.maxBufferSize) end) + read_fully(result, function() return self:__read(self.max_buffer_size) end) return utf8.tostring(result) end @@ -284,57 +284,61 @@ function io_stream:write_line(str) self:__write(utf8.tobytes(str .. "\n")) end -function io_stream:read(arg, useTable) - local argType = type(arg) +function io_stream:read(arg, use_table) + local arg_type = type(arg) - if self.binaryMode then - local byteArr + if self.binary_mode then + local byte_arr - if argType == "number" then + if arg_type == "number" then -- using 'arg' as length - byteArr = self:__read(arg) + byte_arr = self:__read(arg) - if useTable == true then + if use_table == true then local t = { } - for i = 1, #byteArr do - t[i] = byteArr[i] + for i = 1, #byte_arr do + t[i] = byte_arr[i] end return t else - return byteArr + return byte_arr end - elseif argType == "string" then + elseif arg_type == "string" then return byteutil.unpack( arg, self:__read(byteutil.get_size(arg)) ) - elseif argType == nil then + elseif arg_type == "nil" then error( "in binary mode the first argument must be a string data format".. " for the library \"byteutil\" or the number of bytes to read" ) else - error("unknown argument type: "..argType) + error("unknown argument type: "..arg_type) end else if not arg then return self:read_line() else - local linesCount = arg - local trimLastEmptyLines = useTable or true + local lines_count = arg + local trim_last_empty_lines = use_table - if linesCount < 0 then error "count of lines to read must be positive" end + if use_table == nil then + trim_last_empty_lines = true + end + + if lines_count < 0 then error "count of lines to read must be positive" end local result = { } - for i = 1, linesCount do + for i = 1, lines_count do result[i] = self:read_line() end - if trimLastEmptyLines then + if trim_last_empty_lines then local i = #result while i >= 0 do @@ -362,53 +366,53 @@ function io_stream:read(arg, useTable) end function io_stream:write(arg, ...) - local argType = type(arg) + local arg_type = type(arg) - if self.binaryMode then - local byteArr + if self.binary_mode then + local byte_arr - if argType ~= "string" then + if arg_type ~= "string" then -- using arg as bytes table/bytearray - if argType == "table" then - byteArr = Bytearray(arg) + if arg_type == "table" then + byte_arr = Bytearray(arg) else - byteArr = arg + byte_arr = arg end else - byteArr = byteutil.pack(arg, ...) + byte_arr = byteutil.pack(arg, ...) end - self:__write(byteArr) + self:__write(byte_arr) else - if argType == "string" then + if arg_type == "string" then self:write_line(arg) elseif argType == "table" then for i = 1, #arg do self:write_line(arg[i]) end - else error("unknown argument type: "..argType) end + else error("unknown argument type: "..arg_type) end end end function io_stream:seek(mode, offset) - if not self.ioLib.seek then + if not self.io_lib.seek then error("cannot seek this stream") end - self.ioLib.seek(self.descriptor, mode, offset) + self.io_lib.seek(self.descriptor, mode, offset) end function io_stream:tell() - if not self.ioLib.tell then + if not self.io_lib.tell then error("cannot tell this stream") end - return self.ioLib.tell(self.descriptor) + return self.io_lib.tell(self.descriptor) end function io_stream:is_alive() - return self.ioLib.is_alive(self.descriptor) + return self.io_lib.is_alive(self.descriptor) end function io_stream:is_closed() @@ -417,24 +421,24 @@ end function io_stream:close() if self.mode == BUFFERED_MODE then - self.readBuffer:clear() - self.writeBuffer:clear() + self.read_buffer:clear() + self.write_buffer:clear() end - return self.ioLib.close(self.descriptor) + return self.io_lib.close(self.descriptor) end function io_stream:flush() - if self.mode == BUFFERED_MODE and #self.writeBuffer > 0 then - self.ioLib.write(self.descriptor, self.writeBuffer) - self.writeBuffer:clear() + if self.mode == BUFFERED_MODE and #self.write_buffer > 0 then + self.io_lib.write(self.descriptor, self.write_buffer) + self.write_buffer:clear() end - if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then - if self.ioLib.flush then - self.ioLib.flush(self.descriptor) + if self.flush_mode ~= FLUSH_MODE_ONLY_BUFFER then + if self.io_lib.flush then + self.io_lib.flush(self.descriptor) elseif self:is_closed() then error("stream is closed") end end end -return io_stream +return io_stream \ No newline at end of file From 2bbcb77bcf8dbe953702137b41ba716d36a3e540 Mon Sep 17 00:00:00 2001 From: Onran Date: Fri, 24 Jul 2026 03:29:02 +0900 Subject: [PATCH 80/92] camelCase in some stream providers changed to snake_case --- .../internal/stream_providers/bytearray.lua | 70 +++++++++---------- .../internal/stream_providers/socket.lua | 30 ++++---- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua index 0f168c851..e377ee784 100644 --- a/res/modules/internal/stream_providers/bytearray.lua +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -5,73 +5,73 @@ local lib = { } local buffers = { } local positions = { } -local nextDescriptor = 0 +local next_descriptor = 0 -local function openDescriptor(buffer) - nextDescriptor = nextDescriptor + 1 +local function open_descriptor(buffer) + next_descriptor = next_descriptor + 1 - buffers[nextDescriptor] = buffer - positions[nextDescriptor] = 1 + buffers[next_descriptor] = buffer + positions[next_descriptor] = 1 - return nextDescriptor + return next_descriptor end -local function requireDescriptor(descriptor) +local function require_descriptor(descriptor) if not buffers[descriptor] then error("unknown descriptor") end end function lib.read(descriptor, length) - requireDescriptor(descriptor) + require_descriptor(descriptor) local buf = buffers[descriptor] - local bufLength = #buf + local buf_length = #buf local pos = positions[descriptor] - local toRead = math.min(bufLength - pos + 1, length) + local to_read = math.min(buf_length - pos + 1, length) - if toRead <= 0 then + if to_read <= 0 then return Bytearray() end - local segment = buf:slice(pos, toRead) + local segment = buf:slice(pos, to_read) - positions[descriptor] = pos + toRead + positions[descriptor] = pos + to_read return segment end function lib.write(descriptor, data) - requireDescriptor(descriptor) + require_descriptor(descriptor) local buf = buffers[descriptor] local pos = positions[descriptor] - local bufLength = #buf - local dataLength = #data + local buf_length = #buf + local data_length = #data - local endPos = pos + dataLength - 1 + local end_pos = pos + data_length - 1 -- size ensuring - if endPos > bufLength then - for i = bufLength + 1, endPos do + if endPos > buf_length then + for i = buf_length + 1, end_pos do buf[i] = 0 end end - for i = 1, dataLength do + for i = 1, data_length do buf[i + pos - 1] = data[i] end - positions[descriptor] = pos + dataLength + positions[descriptor] = pos + data_length end function lib.seek(descriptor, mode, offset) - requireDescriptor(descriptor) + require_descriptor(descriptor) local buf = buffers[descriptor] - local bufLength = #buf + local buf_length = #buf local base @@ -80,26 +80,26 @@ function lib.seek(descriptor, mode, offset) elseif mode == 'c' then base = positions[descriptor] elseif mode == 'e' then - base = bufLength + 1 + base = buf_length + 1 else error('invalid seek mode') end - local newPos = base + offset + local new_pos = base + offset - if newPos < 1 then + if new_pos < 1 then error('failed to seek stream') end - positions[descriptor] = newPos + positions[descriptor] = new_pos end function lib.tell(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) return positions[descriptor] end function lib.available(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) local buf = buffers[descriptor] local pos = positions[descriptor] @@ -112,20 +112,20 @@ function lib.is_alive(descriptor) end function lib.close(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) buffers[descriptor] = nil positions[descriptor] = nil end -return function(buffer, binaryMode) - if binaryMode == nil then - binaryMode = true +return function(buffer, binary_mode) + if binary_mode == nil then + binary_mode = true end return io_stream.new( - openDescriptor(buffer), - binaryMode, + open_descriptor(buffer), + binary_mode, lib ) end \ No newline at end of file diff --git a/res/modules/internal/stream_providers/socket.lua b/res/modules/internal/stream_providers/socket.lua index 0faedd695..c9a728a8b 100644 --- a/res/modules/internal/stream_providers/socket.lua +++ b/res/modules/internal/stream_providers/socket.lua @@ -4,36 +4,36 @@ local lib = { } local sockets = { } -local nextDescriptor = 0 +local next_descriptor = 0 -local function openDescriptor(socket) - nextDescriptor = nextDescriptor + 1 +local function open_descriptor(socket) + next_descriptor = next_descriptor + 1 - sockets[nextDescriptor] = socket + sockets[next_descriptor] = socket - return nextDescriptor + return next_descriptor end -local function requireDescriptor(descriptor) +local function require_descriptor(descriptor) if not sockets[descriptor] then error("unknown descriptor") end end function lib.read(descriptor, length) - requireDescriptor(descriptor) + require_descriptor(descriptor) return sockets[descriptor]:recv(length) end function lib.write(descriptor, data) - requireDescriptor(descriptor) + require_descriptor(descriptor) sockets[descriptor]:send(data) end function lib.available(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) return sockets[descriptor]:available() end @@ -45,20 +45,20 @@ function lib.is_alive(descriptor) end function lib.close(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) sockets[descriptor]:close() sockets[descriptor] = nil end -return function(socket, binaryMode) - if binaryMode == nil then - binaryMode = true +return function(socket, binary_mode) + if binary_mode == nil then + binary_mode = true end return io_stream.new( - openDescriptor(socket), - binaryMode, + open_descriptor(socket), + binary_mode, lib ) end \ No newline at end of file From f539ddf9e0ffd954e46f63793e9ae64182aa3bb3 Mon Sep 17 00:00:00 2001 From: Onran Date: Fri, 24 Jul 2026 03:34:28 +0900 Subject: [PATCH 81/92] docs update --- doc/ru/scripting/builtins/libnetwork.md | 2 +- doc/ru/scripting/io_stream.md | 44 ++++++++++++------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index 5bc5eaafd..46a3eded8 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -94,7 +94,7 @@ socket:recv_async( -- Оборачивает сокет в io_stream (см. ../io_stream.md) socket:as_stream( - [опционально] binaryMode: boolean=true + [опционально] binary_mode: boolean=true ) -> io_stream -- Закрывает соединение diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index 57f447643..e54ca1fdb 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -18,7 +18,7 @@ Поток имеет три различных вида режима: - Режим общего поведения (`general` / `mode`) -- Режим сброса (`flush` / `flushMode`) +- Режим сброса (`flush` / `flush_mode`) - Двоичный режим (`binary`) @@ -27,34 +27,34 @@ Определяет, как поток обрабатывает чтение и запись, имеет три подрежима: -| Режим | Описание | -|---------------|--------| -| `"default"` | Прямой режим. `read` может вернуть меньше байт, чем запрошено. `write` сразу отправляет данные в низкоуровневый дескриптор. Нет буферизации. | +| Режим | Описание | +|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| `"default"` | Прямой режим. `read` может вернуть меньше байт, чем запрошено. `write` сразу отправляет данные в низкоуровневый дескриптор. Нет буферизации. | | `"yield"` | Как `default`, но при нехватке данных в `read(n)` поток будет вызывать `coroutine.yield()`, пока не соберёт ровно `n` байт. Удобно для корутин. | -| `"buffered"` | Включает внутренние буферы чтения и записи. `read` берёт данные из буфера, `write` — складывает в буфер. При превышении `maxBufferSize` — ошибка `buffer overflow`. | +| `"buffered"` | Включает внутренние буферы чтения и записи. `read` берёт данные из буфера , `write` — складывает в буфер. | ### flush Работает только в режиме `"buffered"`, имеет два подрежима: -| Режим | Что делает `flush()` | -|------------------|----------------------| -| `"all"` (по умолчанию) | Сначала сбрасывает буфер записи → низкоуровневый `write`, затем вызывает `ioLib.flush(descriptor)` | -| `"buffer"` | Сбрасывает только буфер записи, без вызова системного `flush` | +| Режим | Что делает `flush()` | +|------------------------|-----------------------------------------------------------------------------------------------------| +| `"all"` (по умолчанию) | Сначала сбрасывает буфер записи → низкоуровневый `write`, затем вызывает `io_lib.flush(descriptor)` | +| `"buffer"` | Сбрасывает только буфер записи, без вызова системного `flush` | ### binary **Независимый флаг** (включается через `set_binary_mode(true)` или при создании потока). Определяет, в каком виде методы `read` и `write` принимают и возвращают данные: -| binary = true | binary = false (по умолчанию) | +| binary = true | binary = false (по умолчанию) | |------------------------------------------------------------|----------------------------------------| | Данные — это **байты** (`Bytearray`, таблица чисел 0..255) | Данные — это **текстовые строки** | | `read(n)` → `Bytearray` или `table` | `read()` → одна строка | -| `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице | +| `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице | | `write(Bytearray)` → запись байтов | `write("hello")` → строка + `\n` | -| `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n`| +| `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n` | `read_line` / `write_line` — работают как в текстовом режиме @@ -90,17 +90,17 @@ io_stream:set_flush_mode(string) --[[ Читает данные из потока В двоичном режиме: - Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true + Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если use_table = true Если arg - string, то функция интерпретирует arg как шаблон для byteutil. Прочитает кол-во байт, которое определено шаблоном, передаст их в byteutil.unpack и вернёт результат В текстовом режиме: - Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы + Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trim_empty_lines = true, то удаляет пустые строки с начала и конца из итоговой таблицы Если arg не определён, то читает одну строку с окончанием CRLF/LF и возвращает её. --]] io_stream:read( [опционально] arg: int | string, - [опционально] useTable | trimEmptyLines: boolean + [опционально] use_table = false | trim_empty_lines = true: boolean ) -> Bytearray | table | string | table | ... --[[ @@ -128,14 +128,14 @@ io_stream:write_line(string) --[[ В двоичном режиме: - Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если useTable = true + Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если use_table = true В текстовом режиме: - Читает все доступные строки из потока в table если useTable = true, или в одну строку вместе с окончаниями, если нет + Читает все доступные строки из потока в table если use_table = true, или в одну строку вместе с окончаниями, если нет --]] io_stream:read_fully( - [опционально] useTable: boolean + [опционально] use_table: boolean ) -> Bytearray | table | table | string --[[ @@ -203,15 +203,15 @@ io_stream.wrap_bytearray( -- по-умолчанию равен true, поскольку функция из вводных аргументов -- будет использоваться преимущественно для работы с двоичными данными. - [опционально] binaryMode: boolean = true + [опционально] binary_mode: boolean = true ) -> io_stream -- Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua) io_stream.new( descriptor: int, - binaryMode: boolean, - ioLib: table, + binary_mode: boolean, + io_lib: table, [опционально] mode: string = "default", - [опционально] flushMode: string = "all" + [опционально] flush_mode: string = "all" ) -> io_stream ``` \ No newline at end of file From 19237b60dce3737cb9aef4bbc5ae8eccd767cd40 Mon Sep 17 00:00:00 2001 From: Onran Date: Fri, 24 Jul 2026 04:01:33 +0900 Subject: [PATCH 82/92] fixes --- res/modules/internal/stream_providers/bytearray.lua | 2 +- res/modules/io_stream.lua | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua index e377ee784..a10b11cdf 100644 --- a/res/modules/internal/stream_providers/bytearray.lua +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -54,7 +54,7 @@ function lib.write(descriptor, data) local end_pos = pos + data_length - 1 -- size ensuring - if endPos > buf_length then + if end_pos > buf_length then for i = buf_length + 1, end_pos do buf[i] = 0 end diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 522e6ecb7..8776ca810 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -166,19 +166,19 @@ function io_stream:__read(length, from_read_fully) elseif self.mode == BUFFERED_MODE then local buf_len = #self.read_buffer - if bufLen < length then + if buf_len < length then self.read_buffer:append( self.io_lib.read(self.descriptor, self.max_buffer_size - buf_len) ) end - bufLen = #self.read_buffer + buf_len = #self.read_buffer length = math.min(buf_len, length) local copy = self.read_buffer:slice(1, length) - if bufLen == length then + if buf_len == length then self.read_buffer:clear() else self.read_buffer:remove(1, length) @@ -387,7 +387,7 @@ function io_stream:write(arg, ...) else if arg_type == "string" then self:write_line(arg) - elseif argType == "table" then + elseif arg_type == "table" then for i = 1, #arg do self:write_line(arg[i]) end From 86fdcb289b1cfa510c861c677731599b4f0f0519 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 19:19:07 +0300 Subject: [PATCH 83/92] fix the margin --- res/layouts/pages/new_world.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/layouts/pages/new_world.xml b/res/layouts/pages/new_world.xml index 84a9519fa..fc13d5084 100644 --- a/res/layouts/pages/new_world.xml +++ b/res/layouts/pages/new_world.xml @@ -11,7 +11,7 @@ @World generator From 6910a7e029bd039415cd28a85ee13167e5806ad3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 19:57:40 +0300 Subject: [PATCH 84/92] add math.noise --- res/modules/internal/extensions/math.lua | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua index df0eadf72..1d168a377 100644 --- a/res/modules/internal/extensions/math.lua +++ b/res/modules/internal/extensions/math.lua @@ -39,3 +39,35 @@ end function math.sign(x) return (x > 0) and 1 or (x < 0 and -1 or 0) end + +local noise_period = 3021 +local noise_rands = {} + +for i=1,noise_period do + table.insert(noise_rands, math.random() * 2.0 - 1.0) +end + +local function smoothstep(t) + return t * t * (3 - 2 * t) +end + +local function sample_noise(x) + x = x % noise_period + local left_cell = math.floor(x) + local right_cell = (left_cell + 1) % noise_period + local t = smoothstep(x - left_cell) + return noise_rands[left_cell + 1] * (1.0 - t) + + noise_rands[right_cell + 1] * t +end + +function math.noise(x, octaves) + octaves = octaves or 1 + local value = 0.0 + local mul = 1.0 + for i=1,octaves do + value = value * (1.0 - mul) + sample_noise(x + 37 * i) * mul + x = x * 2.0 + mul = mul * 0.5 + end + return value +end From 192dbd9b6ca85025c4e982dcd45bd24f9a48530d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 20:01:14 +0300 Subject: [PATCH 85/92] add math.noise2d --- res/modules/internal/extensions/math.lua | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua index 1d168a377..97bbc33a6 100644 --- a/res/modules/internal/extensions/math.lua +++ b/res/modules/internal/extensions/math.lua @@ -71,3 +71,49 @@ function math.noise(x, octaves) end return value end + +local function lerp(a, b, t) + return a + (b - a) * t +end + +local function rand_2d(x, y) + return noise_rands[math.floor(x * 317 + y) % noise_period + 1] +end + +local function sample_noise2d(x, y) + local x0 = math.floor(x) + local y0 = math.floor(y) + local x1 = x0 + 1 + local y1 = y0 + 1 + + local tx = x - x0 + local ty = y - y0 + + local sx = smoothstep(tx) + local sy = smoothstep(ty) + + local c00 = rand_2d(x0, y0) + local c10 = rand_2d(x1, y0) + local c01 = rand_2d(x0, y1) + local c11 = rand_2d(x1, y1) + + local bottom = lerp(c00, c10, sx) + local top = lerp(c01, c11, sx) + + return lerp(bottom, top, sy) +end + + +function math.noise2d(x, y, octaves) + octaves = octaves or 1 + local value = 0.0 + local mul = 1.0 + for i=1,octaves do + value = value * (1.0 - mul) + + sample_noise2d(x + 37 * i, y + 73 * i) * mul + x = x * 2.0 + y = y * 2.0 + mul = mul * 0.5 + end + return value +end From e09eb41cc7120149cb2d479215a7e35ae5d5b346 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 20:27:42 +0300 Subject: [PATCH 86/92] update doc/*/scripting/extensions.md --- doc/ru/scripting/extensions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index e7b0fe292..f51ffcb07 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -158,6 +158,12 @@ math.sum(x: number, ... | t: table) -> number -- Возвращает целое число, указывающее знак числа (-1/0/1) math.sign(x: number) -> int + +-- Одномерный шум с значениями в диапазоне [-1..1]. +math.noise(x: number, [опционально] octaves: int = 1) -> number + +-- Двумерный шум с значениями в диапазоне [-1..1]. +math.noise2d(x: number, y: number [опционально] octaves: int = 1) -> number ``` ## Расширения для bit From 6b090e20c1beeb4a2bfdcc4b8c75c2322a589142 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 20:33:18 +0300 Subject: [PATCH 87/92] ignore negative noise arguments --- res/modules/internal/extensions/math.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua index 97bbc33a6..f653bf80e 100644 --- a/res/modules/internal/extensions/math.lua +++ b/res/modules/internal/extensions/math.lua @@ -61,6 +61,7 @@ local function sample_noise(x) end function math.noise(x, octaves) + x = math.abs(x) octaves = octaves or 1 local value = 0.0 local mul = 1.0 @@ -81,6 +82,9 @@ local function rand_2d(x, y) end local function sample_noise2d(x, y) + x = math.abs(x) + y = math.abs(y) + local x0 = math.floor(x) local y0 = math.floor(y) local x1 = x0 + 1 From 28385f236c3e3d2ccf020b116f08a8f6d33ec1a3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 15:27:38 +0300 Subject: [PATCH 88/92] fix user component initialization exception handling & fix fatal error on uninitialized component access --- src/logic/scripting/scripting_entities.cpp | 10 +++++++++- src/objects/Entity.cpp | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/logic/scripting/scripting_entities.cpp b/src/logic/scripting/scripting_entities.cpp index ca9ed6db1..2de177562 100644 --- a/src/logic/scripting/scripting_entities.cpp +++ b/src/logic/scripting/scripting_entities.cpp @@ -148,7 +148,12 @@ void scripting::on_entity_spawn( lua::call(L, 1); } for (auto& component : components) { - create_component(L, -1, *component, args, saved); + try { + create_component(L, -1, *component, args, saved); + } catch (const std::runtime_error& err) { + logger.error() << "could not to initialize user component " + << component->name << ": " << err.what(); + } } for (auto& [packid, pack] : content->getPacks()) { @@ -166,6 +171,9 @@ static void process_entity_callback( const std::string& name, std::function args ) { + if (env == nullptr) { + return; + } auto L = lua::get_main_state(); lua::pushenv(L, *env); if (lua::hasfield(L, "__disabled")) { diff --git a/src/objects/Entity.cpp b/src/objects/Entity.cpp index 6629d2981..46664e24e 100644 --- a/src/objects/Entity.cpp +++ b/src/objects/Entity.cpp @@ -71,6 +71,9 @@ dv::value Entity::serialize() const { if (!scripts.components.empty()) { auto& compsMap = root.object("comps"); for (auto& comp : scripts.components) { + if (comp->env == nullptr) { + continue; + } auto data = scripting::get_component_value(comp->env, SAVED_DATA_VARNAME); compsMap[comp->name] = data; From ea0d23831f06d8050b3efed951d12adbb98918e0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 15:57:41 +0300 Subject: [PATCH 89/92] add required components check --- src/content/Content.hpp | 1 + src/content/ContentLoader.cpp | 15 ++++++++++++++- src/logic/scripting/scripting.cpp | 3 ++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/content/Content.hpp b/src/content/Content.hpp index dd0e30bf8..27596c4ac 100644 --- a/src/content/Content.hpp +++ b/src/content/Content.hpp @@ -188,6 +188,7 @@ public: ContentUnitDefs generators; std::unique_ptr const drawGroups; ResourceIndicesSet resourceIndices {}; + std::set components {}; Content( std::unique_ptr indices, diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index 14da690a2..3c45c4438 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -506,6 +506,7 @@ void ContentLoader::loadScripts(Content& content) { scripting::on_scripts_loading(); load_scripts(content, content.blocks); load_scripts(content, content.items); + auto& tmpContent = content; for (const auto& [packid, runtime] : content.getPacks()) { auto env = runtime->getEnvironment(); @@ -516,7 +517,7 @@ void ContentLoader::loadScripts(Content& content) { // Load entity components io::path componentsDir = folder / "scripts/components"; - foreach_file(componentsDir, [&pack, env](const io::path& file) { + foreach_file(componentsDir, [&pack, env, &tmpContent](const io::path& file) { auto name = pack.id + ":" + file.stem(); scripting::load_entity_component( env, @@ -524,9 +525,21 @@ void ContentLoader::loadScripts(Content& content) { file, pack.id + ":scripts/components/" + file.name() ); + tmpContent.components.insert(name); }); } + for (const auto& [eid, def] : content.entities.getDefs()) { + for (const auto& instance : def->components) { + if (content.components.find(instance.component) == content.components.end()) { + throw std::runtime_error( + "component " + instance.component + + " is not available (required by entity " + eid + ")" + ); + } + } + } + scripting::on_content_loaded(); } diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 6954ee6b8..8c039fa32 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -744,9 +744,10 @@ void scripting::load_entity_component( const io::path& file, const std::string& fileName ) { + logger.info() << "script (component) " << file.string(); + auto L = lua::get_main_state(); std::string src = io::read_string(file); - logger.info() << "script (component) " << file.string(); lua::loadbuffer(L, *env, src, fileName); lua::store_in_registry(L, lua::CHUNKS_TABLE, name); } From 3de2a41092de93b27559c34f5199720a4fe5d989 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 16:39:54 +0300 Subject: [PATCH 90/92] add 3d text 'font' property --- src/graphics/core/Font.cpp | 4 +++- src/graphics/render/TextsRenderer.cpp | 6 +++++- src/presets/NotePreset.cpp | 2 ++ src/presets/NotePreset.hpp | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/graphics/core/Font.cpp b/src/graphics/core/Font.cpp index 7aaa67e80..9f1f0a434 100644 --- a/src/graphics/core/Font.cpp +++ b/src/graphics/core/Font.cpp @@ -162,6 +162,8 @@ static inline void draw_text( const FontStylesScheme* styles, size_t styleMapOffset ) { + bool is3d = std::is_same(); + static FontStylesScheme defStyles {{{}}, {0}}; if (styles == nullptr) { @@ -200,7 +202,7 @@ static inline void draw_text( if (charpage == page){ batch.texture(font.getPage(charpage)); draw_glyph( - batch, pos, glm::vec2(x, y - yOffset / static_cast(font.getLineHeight())), c, right, up, interval, style + batch, pos, glm::vec2(x, y - yOffset * (is3d ? -1 : 1) / static_cast(font.getLineHeight())), c, right, up, interval, style ); } else if (charpage > page && charpage < next){ diff --git a/src/graphics/render/TextsRenderer.cpp b/src/graphics/render/TextsRenderer.cpp index ac93d1e43..b5965dcc4 100644 --- a/src/graphics/render/TextsRenderer.cpp +++ b/src/graphics/render/TextsRenderer.cpp @@ -58,7 +58,10 @@ void TextsRenderer::renderNote( } opacity = preset.xrayOpacity; } - auto& font = assets.require(FONT_DEFAULT); + + auto specifiedFont = assets.get(preset.font); + auto& font = + specifiedFont ? *specifiedFont : assets.require(FONT_DEFAULT); glm::vec3 xvec = note.getAxisX(); glm::vec3 yvec = note.getAxisY(); @@ -118,6 +121,7 @@ void TextsRenderer::renderNote( } auto color = preset.color; batch.setColor(glm::vec4(color.r, color.g, color.b, color.a * opacity)); + font.draw( batch, text, diff --git a/src/presets/NotePreset.cpp b/src/presets/NotePreset.cpp index 59865e80a..a1d68994d 100644 --- a/src/presets/NotePreset.cpp +++ b/src/presets/NotePreset.cpp @@ -13,6 +13,7 @@ dv::value NotePreset::serialize() const { {"render_distance", renderDistance}, {"xray_opacity", xrayOpacity}, {"perspective", perspective}, + {"font", font}, }); } @@ -27,4 +28,5 @@ void NotePreset::deserialize(const dv::value& src) { src.at("render_distance").get(renderDistance); src.at("xray_opacity").get(xrayOpacity); src.at("perspective").get(perspective); + src.at("font").get(font); } diff --git a/src/presets/NotePreset.hpp b/src/presets/NotePreset.hpp index fecdaf18d..e8d030c78 100644 --- a/src/presets/NotePreset.hpp +++ b/src/presets/NotePreset.hpp @@ -28,6 +28,7 @@ struct NotePreset : public Serializable { float renderDistance = 32.0f; float xrayOpacity = 0.0f; float perspective = 1.0f; + std::string font {}; dv::value serialize() const override; void deserialize(const dv::value& src) override; From 4837e1de027e738bd59c4ba9c91b654e5d866f9a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 17:46:48 +0300 Subject: [PATCH 91/92] fix projected 3d text for non-monospace fonts --- src/graphics/core/Font.cpp | 17 +++++++++++++++-- src/graphics/render/TextsRenderer.cpp | 5 ++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/graphics/core/Font.cpp b/src/graphics/core/Font.cpp index 9f1f0a434..4843387e4 100644 --- a/src/graphics/core/Font.cpp +++ b/src/graphics/core/Font.cpp @@ -196,13 +196,26 @@ static inline void draw_text( float advance = baseAdvance; if (auto glyph = font.getGlyph(c)) { yOffset = glyph->yOffset; - advance = glyph->xAdvance; + advance = glyph->xAdvance / + static_cast(font.getLineHeight()) * 2.0f * + baseAdvance; } uint charpage = c >> 8; if (charpage == page){ batch.texture(font.getPage(charpage)); draw_glyph( - batch, pos, glm::vec2(x, y - yOffset * (is3d ? -1 : 1) / static_cast(font.getLineHeight())), c, right, up, interval, style + batch, + pos, + glm::vec2( + x, + y - yOffset * (is3d ? -1 : 1) / + static_cast(font.getLineHeight()) + ), + c, + right, + up, + interval, + style ); } else if (charpage > page && charpage < next){ diff --git a/src/graphics/render/TextsRenderer.cpp b/src/graphics/render/TextsRenderer.cpp index b5965dcc4..8c3afb6e3 100644 --- a/src/graphics/render/TextsRenderer.cpp +++ b/src/graphics/render/TextsRenderer.cpp @@ -79,7 +79,10 @@ void TextsRenderer::renderNote( yvec = camera.up; } float scale = - (1.0f - preset.perspective) * glm::pow(glm::distance(camera.position, pos), 1.0f-preset.perspective); + (1.0f - preset.perspective) * + glm::pow( + glm::distance(camera.position, pos), 1.0f - preset.perspective + ); xvec *= 1.0f + scale; yvec *= 1.0f + scale; } From 135f351374965a4ebffab9221d9fc195157daa82 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 17:54:29 +0300 Subject: [PATCH 92/92] update doc/*/3d-text.md --- doc/en/3d-text.md | 17 +++++++++-------- doc/ru/3d-text.md | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/en/3d-text.md b/doc/en/3d-text.md index b0c9fd1f6..be4a2757f 100644 --- a/doc/en/3d-text.md +++ b/doc/en/3d-text.md @@ -4,14 +4,15 @@ The appearance of 3D text, is configured via a table, like [particles](particles.md). All fields are optional. -| Field | Description | Default | -| --------------- | ---------------------------- | ---------------- | -| display | Display format | static_billboard | -| color | Text color | {1, 1, 1, 1} | -| scale | Text scale | 1 | -| render_distance | Text rendering distance | 32 | -| xray_opacity | Visibility through obstacles | 0 | -| perspective | Perspective coefficient | 1 | +| Field | Description | Default | +| --------------- | ---------------------------------------- | ---------------- | +| display | Display format | static_billboard | +| color | Text color | {1, 1, 1, 1} | +| scale | Text scale | 1 | +| render_distance | Text rendering distance | 32 | +| xray_opacity | Visibility through obstacles | 0 | +| perspective | Perspective coefficient | 1 | +| font | Font. If absent, the default one is used | "" | Available display formats: diff --git a/doc/ru/3d-text.md b/doc/ru/3d-text.md index c2d209434..548289701 100644 --- a/doc/ru/3d-text.md +++ b/doc/ru/3d-text.md @@ -12,6 +12,7 @@ | render_distance | Дистанция отрисовки текста | 32 | | xray_opacity | Коэффициент видимости через препятствия (просвечивание) | 0 | | perspective | Коэффициент перспективы | 1 | +| font | Шрифт. При отсутствии используется основной | "" | Доступные форматы отображения: