From c7ba237933310866a7f7945e5a2d92cb777cd503 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 21:55:53 +0300 Subject: [PATCH 1/5] add xml library --- src/coders/xml.cpp | 8 +++ src/coders/xml.hpp | 6 ++ src/logic/scripting/lua/libs/api_lua.hpp | 1 + src/logic/scripting/lua/libs/libxml.cpp | 90 ++++++++++++++++++++++++ src/logic/scripting/lua/lua_engine.cpp | 1 + 5 files changed, 106 insertions(+) create mode 100644 src/logic/scripting/lua/libs/libxml.cpp diff --git a/src/coders/xml.cpp b/src/coders/xml.cpp index 93e1c8031..b40ee7700 100644 --- a/src/coders/xml.cpp +++ b/src/coders/xml.cpp @@ -528,3 +528,11 @@ std::string xml::stringify( return ss.str(); } + +std::string xml::stringify( + const Node& element, bool nice, const std::string& indentStr +) { + std::stringstream ss; + stringifyElement(ss, element, nice, indentStr, 0); + return ss.str(); +} diff --git a/src/coders/xml.hpp b/src/coders/xml.hpp index 4954caa17..1a8a94ca5 100644 --- a/src/coders/xml.hpp +++ b/src/coders/xml.hpp @@ -119,6 +119,12 @@ namespace xml { const std::string& indentStr = " " ); + std::string stringify( + const Node& element, + bool nice = true, + const std::string& indentStr = " " + ); + /// @brief Read XML Document from string /// @param filename file name will be shown in error messages /// @param source xml source code string diff --git a/src/logic/scripting/lua/libs/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp index e304d6dc1..6d135c100 100644 --- a/src/logic/scripting/lua/libs/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -53,6 +53,7 @@ extern const luaL_Reg vec3lib[]; // vecn.cpp extern const luaL_Reg vec4lib[]; // vecn.cpp extern const luaL_Reg weatherlib[]; // gfx.weather extern const luaL_Reg worldlib[]; +extern const luaL_Reg xmllib[]; extern const luaL_Reg yamllib[]; // Components diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp new file mode 100644 index 000000000..e2121465d --- /dev/null +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -0,0 +1,90 @@ +#include "coders/xml.hpp" +#include "api_lua.hpp" + +static int push_xml(lua::State* L, const xml::xmlelement& elem) { + if (elem.isText()) { + return lua::pushlstring(L, elem.getInnerText()); + } + + const auto& attrs = elem.getAttributes(); + const auto& elems = elem.getElements(); + const auto& tag = elem.getTag(); + + lua::createtable(L, elem.size(), attrs.size() + 1); + lua::pushlstring(L, tag); + lua::setfield(L, "@"); + + for (int i = 0; i < elems.size(); i++) { + push_xml(L, *elems[i]); + lua::rawseti(L, i + 1); + } + + for (const auto& [key, value] : attrs) { + lua::pushlstring(L, value.getText()); + lua::setfield(L, key); + } + return 1; +} + +static std::unique_ptr toxml(lua::State* L) { + lua::getfield(L, "@"); + auto tag = lua::require_lstring(L, -1); + lua::pop(L); + + auto elem = std::make_unique(std::string(tag)); + int length = lua::objlen(L, -1); + + for (int i = 0; i < length; i++) { + lua::rawgeti(L, i + 1); + elem->add(toxml(L)); + lua::pop(L); + } + + lua::pushnil(L); + while (lua::next(L, -2)) { + lua::pushvalue(L, -2); + do { + if (!lua::isstring(L, -1)) { + break; + } + auto key = lua::require_lstring(L, -1); + if (key == "@") { + break; + } + auto value = lua::require_lstring(L, -2); + elem->set(std::string(key), std::string(value)); + } while (false); + + lua::pop(L, 2); + } + return elem; +} + +static int l_tostring(lua::State* L) { + bool nice = lua::isboolean(L, 2) ? lua::toboolean(L, 2) : true; + lua::pushvalue(L, 1); + auto xml = toxml(L); + lua::pop(L); + return lua::pushstring(L, xml::stringify(*xml, nice)); +} + +static int l_parse(lua::State* L) { + auto string = lua::require_string(L, 1); + auto document = xml::parse("[string]", string); + return push_xml(L, *document->getRoot()); +} + + +static int l_parse_vcd(lua::State* L) { + auto string = lua::require_string(L, 1); + auto rootTag = lua::tostring(L, 2); + auto document = xml::parse_vcm("[string]", string, rootTag ? rootTag : "root"); + return push_xml(L, *document->getRoot()); +} + +const luaL_Reg xmllib[] = { + {"tostring", lua::wrap}, + {"parse", lua::wrap}, + {"parse_vcd", lua::wrap}, + {nullptr, nullptr} +}; diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index afb2f42eb..276dbaa74 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -66,6 +66,7 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "vec2", vec2lib); openlib(L, "vec3", vec3lib); openlib(L, "vec4", vec4lib); + openlib(L, "xml", xmllib); openlib(L, "yaml", yamllib); openlib(L, "__vc_app", applib); From cd53a33d7bc3ca5a8976bf1f37ad80e695f74b6d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 22:26:44 +0300 Subject: [PATCH 2/5] replace tag attribute key '@' with '#' --- src/logic/scripting/lua/libs/libxml.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp index e2121465d..ff52b6321 100644 --- a/src/logic/scripting/lua/libs/libxml.cpp +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -1,6 +1,9 @@ #include "coders/xml.hpp" #include "api_lua.hpp" +static const char* TAG_ATTR = "#"; +static const char* DEFAULT_ROOT_TAG = "root"; + static int push_xml(lua::State* L, const xml::xmlelement& elem) { if (elem.isText()) { return lua::pushlstring(L, elem.getInnerText()); @@ -12,7 +15,7 @@ static int push_xml(lua::State* L, const xml::xmlelement& elem) { lua::createtable(L, elem.size(), attrs.size() + 1); lua::pushlstring(L, tag); - lua::setfield(L, "@"); + lua::setfield(L, TAG_ATTR); for (int i = 0; i < elems.size(); i++) { push_xml(L, *elems[i]); @@ -27,7 +30,7 @@ static int push_xml(lua::State* L, const xml::xmlelement& elem) { } static std::unique_ptr toxml(lua::State* L) { - lua::getfield(L, "@"); + lua::getfield(L, TAG_ATTR); auto tag = lua::require_lstring(L, -1); lua::pop(L); @@ -78,7 +81,9 @@ static int l_parse(lua::State* L) { static int l_parse_vcd(lua::State* L) { auto string = lua::require_string(L, 1); auto rootTag = lua::tostring(L, 2); - auto document = xml::parse_vcm("[string]", string, rootTag ? rootTag : "root"); + auto document = xml::parse_vcm( + "[string]", string, rootTag ? rootTag : DEFAULT_ROOT_TAG + ); return push_xml(L, *document->getRoot()); } From 13984539f580b12d189960463b9c890d7cecef3a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 22:38:43 +0300 Subject: [PATCH 3/5] update doc/*/scripting/filesystem.md --- doc/en/scripting/filesystem.md | 53 ++++++++++++++++++++++++++++++++++ doc/ru/scripting/filesystem.md | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/doc/en/scripting/filesystem.md b/doc/en/scripting/filesystem.md index 8341e9665..03976a7f8 100644 --- a/doc/en/scripting/filesystem.md +++ b/doc/en/scripting/filesystem.md @@ -52,6 +52,59 @@ yaml.parse(code: str) -> table Parses a YAML string into a table. +## XML Library + +The library contains functions for serializing and deserializing XML: + +Further, XML as a data type denotes a Lua table with the following structure: +- Element tag is accessible by the key `#` +- XML ​​attributes are accessible as key = value pairs +- Inner elements are accessible by index + +The table is used both as a dictionary and an array. + +Example: + +```xml + + + +``` + +Equivalent Lua table: + +```lua +{ + ['#'] = "panel", + size = "400", + color = "0", + interval = "1", + context = "menu", + { + ['#'] = "button", + onclick = 'menu.page="worlds"', + { + "@Worlds" + } + } +} +``` + +Consequently, the button element is accessible as `panel_element[1]`. + +```lua +-- Serializes a tree in XML format +-- multiline - use multiline formatting +xml.tostring(data: XML, multiline: bool = true) -> str + +-- Parses XML +xml.parse(code: str) -> XML + +-- Parses the VCD (VoxelCore Declaration) format used by the VCM format. +-- root_tag - root element tag (the format does not require an explicit root) +xml.parse(code: str, root_tag: str = "root") -> XML +``` + ## *bjson* library The library contains functions for working with the binary data exchange format [vcbjson](../../specs/binary_json_spec.md). diff --git a/doc/ru/scripting/filesystem.md b/doc/ru/scripting/filesystem.md index 0e060d94c..6334b5e90 100644 --- a/doc/ru/scripting/filesystem.md +++ b/doc/ru/scripting/filesystem.md @@ -52,6 +52,59 @@ yaml.parse(code: str) -> table Парсит YAML строку в таблицу. +## Библиотека xml + +Библиотека содержит функции для сериализации и десериализации XML: + +Далее XML как тип данных обозначает Lua-таблицу со следующей структурой: +- Тег элемента доступен по ключу `#` +- XML-аттрибуты доступны как пары ключ = значение +- Вложенные элементы доступны по индексу + +Таблица используется как словарь, так и массив. + +Пример: + +```xml + + + +``` + +Эквивалентная Lua-таблица: + +```lua +{ + ['#'] = "panel", + size = "400", + color = "0", + interval = "1", + context = "menu", + { + ['#'] = "button", + onclick = 'menu.page="worlds"', + { + "@Worlds" + } + } +} +``` + +Соответственно, элемент button доступен как `элемент_panel[1]`. + +```lua +-- Сериализует дерево в формате XML +-- multiline - использовать ли многострочное форматирование +xml.tostring(data: XML, multiline: bool = true) -> str + +-- Парсит XML +xml.parse(code: str) -> XML + +-- Парсит формат VCD (VoxelCore Declaration), используемый форматом VCM. +-- root_tag - тег корневого элемента (формат не требует наличия явного) +xml.parse(code: str, root_tag: str = "root") -> XML +``` + ## Библиотека bjson Библиотека содержит функции для работы с двоичным форматом обмена данными [vcbjson](../../specs/binary_json_spec.md). From 420d0daf28853e1db1a985f9a74ec38f3d978ddc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 22:48:16 +0300 Subject: [PATCH 4/5] fix: missing font usage produces fatal error --- src/graphics/core/FontMetics.hpp | 4 ++-- src/graphics/ui/elements/Label.cpp | 3 +++ src/graphics/ui/elements/TextBox.cpp | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/graphics/core/FontMetics.hpp b/src/graphics/core/FontMetics.hpp index a835a495b..d62a105d9 100644 --- a/src/graphics/core/FontMetics.hpp +++ b/src/graphics/core/FontMetics.hpp @@ -9,8 +9,8 @@ class Font; struct FontMetrics { std::optional> font; - int lineHeight; - int yoffset; + int lineHeight = 0; + int yoffset = 0; int _glyphInterval = 8; ~FontMetrics(); diff --git a/src/graphics/ui/elements/Label.cpp b/src/graphics/ui/elements/Label.cpp index 14cbeea2d..effb79830 100644 --- a/src/graphics/ui/elements/Label.cpp +++ b/src/graphics/ui/elements/Label.cpp @@ -207,6 +207,9 @@ uint Label::getLinesNumber() const { void Label::draw(const DrawContext& pctx, const Assets& assets) { auto batch = pctx.getBatch2D(); auto font = assets.getShared(fontName); + if (font == nullptr) { + return; + } cache.prepare( font, font->getMetrics(), diff --git a/src/graphics/ui/elements/TextBox.cpp b/src/graphics/ui/elements/TextBox.cpp index d04ca5786..f37673fc1 100644 --- a/src/graphics/ui/elements/TextBox.cpp +++ b/src/graphics/ui/elements/TextBox.cpp @@ -353,7 +353,9 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) { void TextBox::drawBackground(const DrawContext& pctx, const Assets& assets) { auto font = assets.getShared(label->getFontName()); - rawTextCache.prepare(font, font->getMetrics(), label->getSize().x); + if (font != nullptr) { + rawTextCache.prepare(font, font->getMetrics(), label->getSize().x); + } glm::vec2 pos = calcPos(); From e9221af4074f36c4c6088ccda8a78789839bd424 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 23:29:27 +0300 Subject: [PATCH 5/5] update xml.parse_vcd --- doc/en/scripting/filesystem.md | 5 +++-- doc/ru/scripting/filesystem.md | 5 +++-- src/logic/scripting/lua/libs/libxml.cpp | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/doc/en/scripting/filesystem.md b/doc/en/scripting/filesystem.md index 03976a7f8..12a190daa 100644 --- a/doc/en/scripting/filesystem.md +++ b/doc/en/scripting/filesystem.md @@ -101,8 +101,9 @@ xml.tostring(data: XML, multiline: bool = true) -> str xml.parse(code: str) -> XML -- Parses the VCD (VoxelCore Declaration) format used by the VCM format. --- root_tag - root element tag (the format does not require an explicit root) -xml.parse(code: str, root_tag: str = "root") -> XML +-- root_tag - a root element with the specified tag will be implicitly created, +-- containing all root elements +xml.parse_vcd(code: str, root_tag: str = "root") -> XML ``` ## *bjson* library diff --git a/doc/ru/scripting/filesystem.md b/doc/ru/scripting/filesystem.md index 6334b5e90..a3bee78a6 100644 --- a/doc/ru/scripting/filesystem.md +++ b/doc/ru/scripting/filesystem.md @@ -101,8 +101,9 @@ xml.tostring(data: XML, multiline: bool = true) -> str xml.parse(code: str) -> XML -- Парсит формат VCD (VoxelCore Declaration), используемый форматом VCM. --- root_tag - тег корневого элемента (формат не требует наличия явного) -xml.parse(code: str, root_tag: str = "root") -> XML +-- root_tag - будет неявно создан корневой элемент с указанным тегом, +-- содержащий все корневые элементы +xml.parse_vcd(code: str, root_tag: str | nil = nil) -> XML ``` ## Библиотека bjson diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp index ff52b6321..70ccc36bb 100644 --- a/src/logic/scripting/lua/libs/libxml.cpp +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -82,9 +82,19 @@ static int l_parse_vcd(lua::State* L) { auto string = lua::require_string(L, 1); auto rootTag = lua::tostring(L, 2); auto document = xml::parse_vcm( - "[string]", string, rootTag ? rootTag : DEFAULT_ROOT_TAG + "[string]", string, rootTag ? rootTag : "" ); - return push_xml(L, *document->getRoot()); + const auto& root = *document->getRoot(); + if (rootTag != nullptr) { + return push_xml(L, root); + } + int rootElements = root.size(); + if (rootElements != 1) { + throw std::runtime_error( + "one root element expected, got " + std::to_string(rootElements) + ); + } + return push_xml(L, *root.getElements()[0]); } const luaL_Reg xmllib[] = {