From b2ed4b472f9b93f796381300859f58dcd12aac4f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 1 Jun 2026 01:14:30 +0300 Subject: [PATCH 01/22] 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 02/22] 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 03/22] 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 04/22] 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 05/22] 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 06/22] 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 07/22] 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 08/22] 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 09/22] 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 10/22] 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 11/22] 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 12/22] 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 13/22] 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 14/22] 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 15/22] 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 16/22] 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 17/22] 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 18/22] 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 19/22] 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 20/22] 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 21/22] 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 22/22] 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)