diff --git a/doc/en/scripting/filesystem.md b/doc/en/scripting/filesystem.md
index 8341e9665..12a190daa 100644
--- a/doc/en/scripting/filesystem.md
+++ b/doc/en/scripting/filesystem.md
@@ -52,6 +52,60 @@ 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 - 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
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..a3bee78a6 100644
--- a/doc/ru/scripting/filesystem.md
+++ b/doc/ru/scripting/filesystem.md
@@ -52,6 +52,60 @@ 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_vcd(code: str, root_tag: str | nil = nil) -> XML
+```
+
## Библиотека bjson
Библиотека содержит функции для работы с двоичным форматом обмена данными [vcbjson](../../specs/binary_json_spec.md).
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/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();
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..70ccc36bb
--- /dev/null
+++ b/src/logic/scripting/lua/libs/libxml.cpp
@@ -0,0 +1,105 @@
+#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());
+ }
+
+ 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, TAG_ATTR);
+
+ 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, TAG_ATTR);
+ 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 : ""
+ );
+ 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[] = {
+ {"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);